CHAPTER 09

对内核线程与用户态线程的影响

前面八章讲的是机制。这一章换成"系统里能看见什么"的视角:RT 内核上多出了哪些线程、它们排在优先级表的什么位置,内核线程和用户态线程的行为各自变了什么,以及应用该怎样写才能真正拿到 RT 带来的确定性。

9.1一句话:执行上下文变成了"线程"

非 RT 内核里,CPU 上的时间被三类东西瓜分:硬中断、软中断、任务。前两者不归调度器管,优先级事实上高于任何任务——哪怕是 SCHED_FIFO 99。

PREEMPT_RT 把绝大部分硬中断处理和全部软中断处理都塞进了内核线程(第 5 章、第 6 章),把 RCU 回调、printk 输出、非 HARD 的 irq_work 也挪进了线程(第 7 章)。结果是:

核心结论

在 RT 内核上,几乎所有"会花时间的事情"都变成了有名字、有调度策略、有优先级、可以绑核的线程。系统的实时行为不再由"上下文的硬性等级"决定,而由你给这些线程排的优先级决定。

这既是 RT 的全部价值,也是它最大的使用门槛:优先级排错了,RT 内核照样会出高延迟,甚至把系统饿死。

非 RT:三个"层级" PREEMPT_RT:一张优先级表 硬中断 handler 关中断执行,不可调度 软中断 / tasklet(irq_exit 时就地跑) 借用 current,不可调度 任务(调度器管辖) SCHED_FIFO 99 … 1(用户 RT 任务) SCHED_OTHER:ksoftirqd、kworker… idle 总是先于 FIFO 99 全部归调度器管辖 残留硬中断:IPI、arch timer、GIC 本身(极短) FIFO 99…51:你的关键 RT 任务 FIFO 50:irq/N-xxx 中断线程(含其中的软中断) FIFO 49…2:次要 RT 任务 FIFO 1:rcuc、rcub、rcu_preempt、irq_work OTHER:ksoftirqd、kworker、pr/legacy、普通进程 idle
CPU 时间的归属:非 RT 是"层级制",硬/软中断天然压在所有任务之上;RT 下几乎一切都进入同一张优先级表,层级由你来排。

9.2RT 内核上的线程清单

在 RT 内核上跑 ps -eLo pid,cls,rtprio,ni,psr,comm,能看到一批非 RT 内核没有(或不重要)的线程。下表每一项的策略和默认优先级都来自 6.12 源码,括号里是设置位置。

线程名作用默认策略 / 优先级源码依据
irq/<N>-<name>每个线程化中断一个;RT 下强制线程化的中断在这里跑驱动 handler,以及 handler 里 raise 的软中断SCHED_FIFO 50irq_thread() 调 sched_set_fifo(current) kernel/irq/manage.c:1310;sched_set_fifo 取 MAX_RT_PRIO / 2 = 50 kernel/sched/syscalls.c:856
irq/<N>-s-<name>驱动本来就有 primary+thread 两段时,强制线程化后原 thread_fn 进入这个"secondary"线程SCHED_FIFO 50kernel/irq/manage.c:1466
ksoftirqd/<N>兜底执行软中断:硬中断(未线程化的那部分)raise 的、以及没在任务上下文里被消化掉的软中断SCHED_OTHER,nice 0smpboot 线程没有 setup 回调 kernel/softirq.c:996;kthread 创建时重置为 SCHED_NORMAL kernel/kthread.c:370
rcu_preemptRCU grace-period kthread(RT 下必然是 PREEMPT_RCU,名字是 rcu_preempt)SCHED_FIFO 1kthread_prio 在 RCU_BOOST 下默认 1 kernel/rcu/tree.c:168,GP 线程按它设 FIFO kernel/rcu/tree.c:5393
rcuc/<N>每 CPU 执行 RCU 回调(RT 下 use_softirq=false,不再走 RCU_SOFTIRQ)SCHED_FIFO 1kernel/rcu/tree.c:106、kernel/rcu/tree.c:2939、rcu_cpu_kthread_setup() kernel/rcu/tree_plugin.h:1122
rcub/<N>RCU priority boost:给被抢占的 RCU 读者做 PI(每个叶子 rcu_node 一个)SCHED_FIFO 1kernel/rcu/tree_plugin.h:1320
rcuog/<N>、rcuop/<N>开 rcu_nocbs= 后,把回调从隔离核卸载出去的线程(GP 等待线程 / 回调执行线程)SCHED_FIFO 1(rcuop 依赖 RCU_NOCB_CPU_CB_BOOST,RT 下默认 y)kernel/rcu/tree_nocb.h:1399、kernel/rcu/tree_nocb.h:1422、kernel/rcu/Kconfig:282
irq_work/<N>只在 RT 下创建:执行没有标 IRQ_WORK_HARD_IRQ 的 irq_work(lazy list)SCHED_FIFO 1irq_workd_setup() → sched_set_fifo_low kernel/irq_work.c:317;if (IS_ENABLED(CONFIG_PREEMPT_RT)) 才注册 kernel/irq_work.c:330
pr/legacyRT 下 legacy console(6.12 里所有串口驱动都还是 legacy)的打印线程SCHED_OTHER,nice −20kernel/printk/printk.c:3640、:3652
pr/<console>nbcon console 的打印线程(6.12 里还没有转成 nbcon 的串口驱动,通常看不到)SCHED_OTHER,nice −20kernel/printk/nbcon.c:1335、:1347
migration/<N>stop_machine / 迁移 / softlockup 检测的执行者stop 调度类(高于一切 FIFO/DL)sched_set_stop_task() kernel/stop_machine.c:540
cpuhp/<N>CPU 热插拔状态机SCHED_OTHERkernel/cpu.c:1198
kworker/…workqueue 工作者SCHED_OTHER(highpri 池 nice −20)kernel/workqueue.c
三个容易误会的点
  • ksoftirqd 是 SCHED_OTHER。在 6.12 RT 内核里,大部分软中断是在 irq 线程(FIFO 50)里通过 local_bh_enable() 顺带执行的;落到 ksoftirqd 的那部分(例如 TIMER_SOFTIRQ、HRTIMER_SOFTIRQ —— 它们由仍在硬中断里的 arch timer raise)跑在普通优先级上。如果 CPU 被 FIFO 任务占满,soft hrtimer 和 timer_list 回调会被饿住。专门的 ktimers 线程是 6.12 之后才有的,见 第 10 章。
  • 没有 watchdog/N 线程。softlockup 检测借 stop_one_cpu_nowait() 在 migration/N 上跑 softlockup_fn kernel/watchdog.c:665。
  • 所有 RCU 线程默认只有 FIFO 1。它们高于所有普通进程,但低于你几乎所有的 RT 任务。只要某个 CPU 被 FIFO ≥ 2 的任务长期占满,这个 CPU 上的 RCU 回调就无法执行、grace period 也可能被拖住——这正是隔离核要配 rcu_nocbs 的原因。

优先级地图

Linux 内部用一个统一的 prio 数值(越小越优先)来比较不同调度类的任务:MAX_RT_PRIO = 100 include/linux/sched/prio.h:16,RT 任务的内部 prio = 99 − rt_priority,普通任务的 prio = 120 + nice。而调度类之间则是严格分层:stop > deadline > rt > fair > idle。

调度类(从上到下严格优先) stop class migration/N 不可被任何任务抢占;stop_machine、迁移、softlockup 探测 deadline class SCHED_DEADLINE 任务;以及 6.12 新增的 fair_server(每 CPU 默认 50ms/1000ms 给 CFS 兜底) rt class FIFO / RR 1–99 99 1 建议:最关键的应用 RT 线程(如 80–95) timerlat/N 95、cyclictest -p 应用所依赖的中断线程:手动 chrt 抬高 50 ── irq/N-xxx、irq/N-s-xxx(默认全部同级) 次要 RT 任务(不依赖网络/存储中断的) 1 ── rcu_preempt、rcuc/N、rcub/N、rcuog/rcuop、irq_work/N fair class nice −20: pr/legacy、highpri kworker nice 0: ksoftirqd/N、kworker、cpuhp、普通进程 idle class swapper/N
PREEMPT_RT 内核(6.12)上的优先级地图。橙色是中断线程,黄色是 RCU/irq_work 线程,绿色是"你的应用应该放在哪"。
6.12 的新变量:fair_server

6.12 合入了用 deadline server 给 CFS 任务兜底的机制:每个 CPU 一个 rq->fair_server,默认 runtime 50 ms / period 1000 ms kernel/sched/deadline.c:1839,可在 /sys/kernel/debug/sched/fair_server/cpuN/{runtime,period} 调整 kernel/sched/debug.c:481。它和传统的 RT throttling(sched_rt_runtime_us = 950000 kernel/sched/rt.c:21)并存。两者的共同含义:一个死循环的 FIFO 任务不会把 CPU 彻底吃死,普通任务每秒仍能分到几十毫秒——这对救命有用,但对需要 100% CPU 的实时任务就是一个"周期性被打断"的延迟源,需要在第 10 章里有意识地处理。

9.3对内核线程的影响

持锁可被抢占,并可能被 boost

非 RT 下,内核线程拿着 spinlock_t 时是不可抢占的;RT 下这把锁是 rtmutex(第 3 章),持有者随时可能被更高优先级任务抢占。于是一个 SCHED_OTHER 的 kworker 拿着某驱动的锁被抢占,若 FIFO 80 的任务恰好也要这把锁,kworker 会被临时 boost 到 FIFO 80(第 4 章),在 ps/top 里你可能短暂看到一个 kworker 跑在 RT 优先级上——这是 PI 在工作,不是 bug。

"上下文判断"的语义变了

写法非 RT 下的含义RT 下的现实
in_interrupt() / in_hardirq()在硬中断/软中断里线程化后的驱动 handler 运行时 in_hardirq() 为假——它在 irq 线程里
in_softirq()preempt_count 的 softirq 位非 0来自 current->softirq_disable_cnt,只对当前任务有意义(第 6 章)
in_atomic()持 spinlock 时为真持 spinlock_t 时为假(只是 migrate_disable),持 raw_spinlock_t 时为真
irqs_disabled()spin_lock_irqsave 后为真spin_lock_irqsave 不关中断,flags 恒为 0 include/linux/spinlock_rt.h:96

结论:不要用这些谓词做"保护"或"选择 GFP 标志"。需要的是显式的锁和明确的调用约定。

新的禁区:原子区里不能拿 spinlock_t

在 preempt_disable()、local_irq_disable()、raw_spin_lock() 区间里调用任何最终会取 spinlock_t 的函数——包括 kmalloc(GFP_ATOMIC)、wake_up() 的某些路径之外的大量通用 API——在 RT 下都会撞上 "sleeping function called from invalid context" kernel/sched/core.c:8671。非 RT 内核上开 CONFIG_PROVE_RAW_LOCK_NESTING 能提前查出一部分 lib/Kconfig.debug:1398。

所有中断线程默认同为 FIFO 50

这是一个刻意"中立"的默认:内核不知道网卡、磁盘、串口、CAN 控制器哪个对你的应用更重要。后果是:

  • 所有中断线程之间按 FIFO 规则"先来先跑、跑完再让",一个处理大量网络包的 irq/NN-eth0 可以连续占 CPU 很久,挡住同核的 irq/MM-can0。
  • 你的关键中断(如运动控制的 CAN、编码器 GPIO)应该用 chrt -f -p 9x <pid> 抬上去;不重要的大流量中断可以压下去或绑到别的核。
  • irq 线程会跟随中断亲和性迁移:irq_thread_check_affinity() kernel/irq/manage.c:1061 在线程每次被唤醒时检查 /proc/irq/N/smp_affinity 的变化并调整自己的 cpumask,所以改中断亲和性就同时改了线程跑在哪。

软中断"跟着 raise 它的线程走"

RT 下软中断由 local_bh_enable() 在当前任务上下文执行(第 6 章)。强制线程化的 handler 由 irq_forced_thread_fn() kernel/irq/manage.c:1191 包上一对 local_bh_disable()/local_bh_enable(),于是网卡 handler 里 raise 的 NET_RX_SOFTIRQ 会在 irq/NN-eth0 线程、以 FIFO 50 的优先级执行。这意味着:

  • 网络协议栈的耗时被算到了网卡中断线程头上——top 里你会看到 irq/NN-eth0 很忙,而 ksoftirqd 相对清闲。
  • 给网卡中断线程调优先级,等于同时调了网络软中断的优先级。

9.4对用户态线程的影响

好处与危险:用户 RT 线程现在能抢占中断处理

非 RT

FIFO 99 的用户线程也挡不住硬中断和软中断。一次收包风暴里 net_rx_action() 可以连续跑 2 ms(MAX_SOFTIRQ_TIME kernel/softirq.c:500),你的控制循环就晚 2 ms。

好处是"不可能饿死中断":中断处理总能拿到 CPU。

PREEMPT_RT

FIFO 80 的用户线程在跑时,同核的 irq/NN-eth0(FIFO 50)根本得不到 CPU。网卡的硬件中断只执行几微秒的 primary(mask + 唤醒线程),协议栈处理被推迟到你的线程让出 CPU 为止。

危险是对称的:如果你的线程恰恰在等这个中断的数据(比如等一个 UDP 包),而它又在同核忙等,那它在等一个永远轮不到 CPU 的线程——自造的优先级反转,PI 救不了(中间没有锁)。

优先级设计原则:依赖链在上

一个 RT 线程依赖的所有执行实体(它等的中断线程、它用到的内核线程、它的生产者线程)都必须优先级不低于它,或者不在同一个 CPU 上。否则就会出现"高优先级等低优先级、低优先级永远轮不到"的局面。

系统调用几乎全程可抢占

用户线程进入内核(svc → el0_svc),在 RT 下这段内核路径里的 spinlock_t 都是可睡眠的 rtmutex:

  • 对高优先级线程:它在系统调用里也可以随时抢占低优先级任务——哪怕低优先级任务正拿着某把内核 spinlock。最坏延迟从"最长不可抢占区"缩短到"最长 raw 区 + PI 传递时间"。
  • 对它自己:它在内核里等锁时是睡眠的,持锁者被 boost 到它的优先级。所以一个 FIFO 90 的线程调 write() 到一个被低优先级任务争用的文件系统路径,会把那个任务临时拉到 FIFO 90。
  • 代价:争用时多了上下文切换,系统调用吞吐下降;sched_switch 事件数量明显上升。

定时睡眠:hrtimer 自动走 HARD

周期性 RT 线程最常用的是 clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, …)。RT 下 hrtimer 默认被挪到软中断(第 6 章)——如果唤醒 RT 线程的定时器也被挪到 SCHED_OTHER 的 ksoftirqd 里,那 RT 内核反而更差。__hrtimer_init_sleeper() 专门处理了这件事 kernel/time/hrtimer.c:2041:

if (IS_ENABLED(CONFIG_PREEMPT_RT)) {
	if (rt_or_dl_task_policy(current) && !(mode & HRTIMER_MODE_SOFT))
		mode |= HRTIMER_MODE_HARD;
}

只有 调用时已经是 RT/DL 策略 的线程才拿到 HARD 定时器。注释里解释了为什么不对所有人开放:非特权用户可以造大量线程在同一时刻到期,在硬中断里一次唤醒成千上万个线程本身就是延迟尖峰。推论:先 sched_setscheduler,再进入睡眠循环——顺序反了,第一次睡眠可能走 soft 路径。

用户 RT 线程的周期睡眠与唤醒 PREEMPT_RT · ARM64
  1. clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next)glibc → svc #0
  2. el0t_64_sync_handler()arch/arm64/kernel/entry-common.c:858
  3. el0_svc() → do_el0_svc() → el0_svc_common() → invoke_syscall()entry-common.c:839 · syscall.c:149
  4. __arm64_sys_clock_nanosleep()kernel/time/posix-timers.c:1396
  5. common_nsleep() → hrtimer_nanosleep()posix-timers.c:1368 · hrtimer.c:2133
  6. hrtimer_init_sleeper_on_stack() → __hrtimer_init_sleeper()hrtimer.c:2019当前是 FIFO 任务 → 加上 HRTIMER_MODE_HARD
  7. do_nanosleep() → schedule()hrtimer.c:2085TASK_INTERRUPTIBLE,让出 CPU
  8. …… 到期时刻:arch timer PPI 触发 ……
  9. CNTP/CNTV 定时器到期 → GICv3 PPI → el0t_64_irq / el1h_64_irqpercpu 中断,IRQF_TIMER ⇒ 不线程化
  10. gic_handle_irq() → … → handle_percpu_devid_irq()
  11. arch_timer_handler_phys() / _virt()drivers/clocksource/arm_arch_timer.c:681
  12. hrtimer_interrupt()kernel/time/hrtimer.c:1849
  13. __hrtimer_run_queues(…, HRTIMER_ACTIVE_HARD) → __run_hrtimer()hrtimer.c:1785 · 1710
  14. hrtimer_wakeup() → wake_up_process() → try_to_wake_up()hrtimer.c:1981 · sched/core.c:4142在硬中断里直接唤醒,不经过任何线程
  15. ttwu_do_activate() → wakeup_preempt() → resched_curr()本 CPU 当前任务优先级更低 → 置 TIF_NEED_RESCHED
  16. …… 中断返回路径上的抢占点 ……
  17. __el1_irq → arm64_preempt_schedule_irq() → preempt_schedule_irq()entry-common.c:270 · sched/core.c:7040若被打断的是内核态
  18. 或 exit_to_user_mode → do_notify_resume() → schedule()entry-common.c:130若被打断的是用户态
  19. __schedule() → context_switch() → cpu_switch_tosched/core.c:6613 · 5296 · arch/arm64/kernel/entry.S:825
  20. 从 do_nanosleep() 返回 → 系统调用返回 → ERET 回 EL0控制循环开始本周期的工作
硬件硬中断软中断内核态任务用户态锁

这条路径就是 cyclictest 和 rtla timerlat 测量的东西:从 hrtimer 到期时刻到线程真正运行的间隔。整条路径上只有硬中断那几帧是不可抢占的,其余都由优先级决定——这就是 RT 内核上 cyclictest 最大值能做到几十微秒量级的原因。

内存:缺页是 RT 线程的天敌

RT 内核让内核路径可抢占,但不会让缺页变快:一次 major fault 要读盘,minor fault 也要走 mm 的锁和页分配器(RT 下这些都是可睡眠锁,见 第 7 章)。所以 RT 应用的固定套路是:

  • mlockall(MCL_CURRENT | MCL_FUTURE):锁住所有现有和将来的映射,防止被换出;
  • 预触页:在进入实时循环之前,把栈(如 memset 一个大的局部数组)和堆(预分配并逐页写一遍)都摸一遍,把 minor fault 全部提前触发;
  • 实时循环里不调 malloc/free、不打开文件、不创建线程。

锁:必须用 PI mutex

用户态线程之间共享数据用 pthread_mutex,默认协议是 PTHREAD_PRIO_NONE——没有优先级继承,经典的三任务反转会原样出现。设成 PTHREAD_PRIO_INHERIT 后,glibc 走 FUTEX_LOCK_PI,内核用和 spinlock 相同的 rtmutex 做 PI(第 4 章)。条件变量配 PI mutex 时,glibc 会使用 requeue-PI。注意:sem_t、pthread_rwlock 的读锁、pthread_spinlock 都没有 PI。

普通 SCHED_OTHER 进程

  • 吞吐下降:更多的抢占、更多的上下文切换、锁争用时睡眠而非自旋(虽然 rtmutex 有自适应自旋缓解)。CPU 密集的批处理任务可能慢几个百分点到十几个百分点,网络/存储密集型负载受影响更大。
  • 6.12 里没有 PREEMPT_LAZY:RT 内核对 SCHED_OTHER 任务也是"立刻抢占",不会推迟到 tick 边界。lazy preemption 是 6.13 之后用来挽回这部分吞吐的。
  • 与 RT 任务共享 CPU 时,普通进程靠 RT throttling 和 fair_server 才能保证每秒拿到最低限度的 CPU。

9.5端到端:用户 RT 线程等设备数据

一个典型场景:用户态控制线程(FIFO 80)阻塞在 read() 上等一个 SPI/GPIO 设备的数据就绪中断。RT 内核上从中断到用户代码运行的完整链路:

sequenceDiagram
  autonumber
  participant Dev as 设备
  participant GIC as GICv3
  participant CPU as CPU 硬中断上下文
  participant IT as irq/N-dev 线程 FIFO 50+
  participant App as 用户线程 FIFO 80
  App->>App: read() 进入内核,wait_event_interruptible 睡眠
  Dev->>GIC: 拉高 SPI
  GIC->>CPU: 判优,投递到目标 CPU,进入 el1h_64_irq 或 el0t_64_irq
  CPU->>CPU: gic_handle_irq 读 ICC_IAR1_EL1,handle_fasteoi_irq
  CPU->>GIC: ONESHOT 先 mask 该 SPI,再 EOI
  CPU->>IT: irq_default_primary_handler 返回 IRQ_WAKE_THREAD,唤醒线程
  CPU->>CPU: 中断返回,抢占点检查 need_resched
  IT->>IT: irq_forced_thread_fn 调用驱动 handler,读设备、填缓冲
  IT->>App: wake_up_interruptible,try_to_wake_up
  Note over IT,App: App 优先级更高,同核则立刻抢占 IT
  App->>App: 从 wait_event 返回,copy_to_user,ERET 回 EL0
  IT->>GIC: 线程处理完毕,irq_finalize_oneshot 解除 mask
RT 内核上"设备中断 → 中断线程 → 用户线程"的完整时序。注意第 9–10 步:用户线程优先级高于中断线程,被唤醒时会立刻抢占中断线程,中断线程剩下的收尾(unmask)要等用户线程让出 CPU。

这张图里最值得琢磨的是最后两步的顺序。用户线程 FIFO 80 高于中断线程,所以一旦被唤醒,同核上它立刻抢走 CPU,中断线程的收尾工作——包括 irq_finalize_oneshot() 解除屏蔽——要等用户线程下一次睡眠才继续。在这段时间里该中断线保持 masked,设备的下一个中断会被 GIC 挂起(pending)而不是丢失。对周期性设备这通常无害,但如果你的用户线程处理一次数据要很久,就要考虑把中断线程优先级抬到用户线程之上,或者把两者放在不同核上。

唤醒侧:中断线程里的驱动唤醒用户线程 PREEMPT_RT
  1. kthread() → irq_thread()kernel/irq/manage.c:1300FIFO 50(或你 chrt 后的值)
  2. irq_forced_thread_fn()kernel/irq/manage.c:1191local_bh_disable() 包住 handler
  3. mydev_irq_handler()驱动原来的 handler,现在可以拿 spinlock_t
  4. spin_lock_irqsave(&dev->lock) → rt_spin_lock()kernel/locking/spinlock_rt.c:54不关中断;争用时睡眠 + PI
  5. wake_up_interruptible(&dev->wq) → __wake_up()kernel/sched/wait.c:124
  6. __wake_up_common_lock() → __wake_up_common()wait.c:99 · 73
  7. default_wake_function() → try_to_wake_up()sched/core.c:7060 · 4142
  8. ttwu_queue() → ttwu_do_activate() → wakeup_preempt() → resched_curr()sched/core.c:3958 · 3668 · 2140 · 1083用户线程 FIFO 80 > 当前 FIFO 50 ⇒ 立刻需要重调度;若目标在别的 CPU,经 IPI_RESCHEDULE(SGI)通知
  9. local_bh_enable() → 执行本线程 raise 的软中断若 handler raise 过软中断

9.6标准用户态 RT 程序骨架

下面是一份在 RT 内核上写周期性实时线程的最小骨架。每一步都对应前面章节的一个内核机制,注释里标出了原因。

#define _GNU_SOURCE
#include <pthread.h>
#include <sched.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>

#define PERIOD_NS   1000000L      /* 1 ms 周期 */
#define PRE_STACK   (256 * 1024)  /* 预触栈大小 */

static pthread_mutex_t shared_lock;

static void prefault_stack(void)
{
	volatile unsigned char buf[PRE_STACK];
	memset((void *)buf, 0, sizeof(buf));     /* ① 把栈页全部摸一遍 */
}

static void *rt_thread(void *arg)
{
	struct timespec next;

	prefault_stack();
	clock_gettime(CLOCK_MONOTONIC, &next);
	for (;;) {
		next.tv_nsec += PERIOD_NS;
		if (next.tv_nsec >= 1000000000L) {
			next.tv_nsec -= 1000000000L;
			next.tv_sec++;
		}
		/* ② 绝对时间睡眠:没有累积漂移;RT/DL 任务自动用 HARD hrtimer */
		clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);

		pthread_mutex_lock(&shared_lock);   /* ③ PI mutex → FUTEX_LOCK_PI */
		/* ... 本周期的控制计算,不分配内存、不做阻塞 IO ... */
		pthread_mutex_unlock(&shared_lock);
	}
	return NULL;
}

int main(void)
{
	pthread_mutexattr_t ma;
	pthread_attr_t attr;
	struct sched_param sp = { .sched_priority = 80 };
	cpu_set_t cpus;
	pthread_t tid;

	/* ④ 锁住全部内存,之后的映射也锁住:杜绝换出与大部分缺页 */
	if (mlockall(MCL_CURRENT | MCL_FUTURE))
		return 1;

	/* ⑤ 互斥量使用优先级继承协议 */
	pthread_mutexattr_init(&ma);
	pthread_mutexattr_setprotocol(&ma, PTHREAD_PRIO_INHERIT);
	pthread_mutex_init(&shared_lock, &ma);

	/* ⑥ 线程属性:显式调度策略 + 优先级(不继承创建者) */
	pthread_attr_init(&attr);
	pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
	pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
	pthread_attr_setschedparam(&attr, &sp);

	/* ⑦ 绑到隔离核(例如 isolcpus/nohz_full 里的 CPU 3) */
	CPU_ZERO(&cpus);
	CPU_SET(3, &cpus);
	pthread_attr_setaffinity_np(&attr, sizeof(cpus), &cpus);

	if (pthread_create(&tid, &attr, rt_thread, NULL))
		return 1;      /* 常见原因:没有 CAP_SYS_NICE / RLIMIT_RTPRIO */
	pthread_join(tid, NULL);
	return 0;
}
步骤防的是什么对应内核机制
① 预触栈 / ④ mlockall实时循环里的缺页(mm 锁、页分配器、可能的 IO)第 7 章 内存分配
② 绝对时间 clock_nanosleep相对睡眠的周期漂移;ksoftirqd 处理定时器的延迟__hrtimer_init_sleeper 自动 HARD,第 6 章
③ / ⑤ PTHREAD_PRIO_INHERIT用户态无界优先级反转futex PI + rtmutex,第 4 章
⑥ SCHED_FIFO 80,EXPLICIT_SCHED线程忘记继承策略而以 SCHED_OTHER 运行(最常见的坑)调度类分层,第 2 章
⑦ 绑核到隔离 CPU迁移开销、与其他负载/中断线程同核竞争isolcpus/nohz_full/rcu_nocbs,第 10 章
关于优先级 80

这里选 80 是"高于默认中断线程 50"的典型取值。如果这个线程依赖某个中断的数据(9.5 节的场景),要么把那个 irq/N 线程 chrt 到 81 以上,要么把中断亲和性设到另一个核。不要无脑把所有东西设成 99:FIFO 99 留给 watchdog 类的监控线程更合适,而且同优先级的 FIFO 线程之间不会互相抢占。

9.7线程行为对照总表

维度非 RT(PREEMPT / VOLUNTARY)PREEMPT_RT(6.12)
驱动中断 handler 在哪执行硬中断上下文,关中断irq/N-name 线程,FIFO 50,开中断、可抢占、可睡眠锁
软中断在哪执行irq_exit 时就地;超时后 ksoftirqdraise 它的任务的 local_bh_enable();硬中断 raise 的走 ksoftirqd(OTHER)
RCU 回调RCU_SOFTIRQrcuc/N FIFO 1(或 nocb 的 rcuop)
printk 输出调用者上下文同步刷 consolepr/legacy 线程,nice −20;emergency/panic 时才同步
非 HARD 的 irq_work硬中断 / tickirq_work/N FIFO 1
FIFO 99 用户线程能否被设备中断处理延迟能(硬中断 + 软中断都高于它)只能被残留硬中断(几微秒级 primary)延迟
持 spinlock_t 的任务能否被抢占否能,且会被 PI boost
内核里等 spinlock_t自旋自适应自旋后睡眠
clock_nanosleep 唤醒硬中断里唤醒RT/DL 任务:硬中断里唤醒;普通任务:经 soft hrtimer
能否在运行时切换抢占模型PREEMPT_DYNAMIC 可不可,编译期固定 kernel/Kconfig.preempt:98
普通进程吞吐基线下降(无 lazy preemption)

9.8小结

  • RT 把"上下文等级"换成了"线程优先级":irq/N 在 50,RCU 和 irq_work 在 1,ksoftirqd 和 printk 在普通优先级,migration/N 在 stop 类。
  • 内核线程:持锁可被抢占、会被 boost;in_atomic() 等谓词不再可靠;原子区内不能碰 spinlock_t;软中断随 raise 它的线程执行。
  • 用户线程:真正能压过中断处理,但必须按"依赖链在上"设计优先级;RT/DL 任务的定时睡眠自动走 HARD hrtimer;必须 mlockall + 预触页 + PI mutex + 绑核。
  • 系统层面要接受吞吐下降,并处理 RT throttling / fair_server 这类"救命但会打断你"的机制——下一章讲怎么配。