对内核线程与用户态线程的影响
前面八章讲的是机制。这一章换成"系统里能看见什么"的视角:RT 内核上多出了哪些线程、它们排在优先级表的什么位置,内核线程和用户态线程的行为各自变了什么,以及应用该怎样写才能真正拿到 RT 带来的确定性。
9.1一句话:执行上下文变成了"线程"
非 RT 内核里,CPU 上的时间被三类东西瓜分:硬中断、软中断、任务。前两者不归调度器管,优先级事实上高于任何任务——哪怕是 SCHED_FIFO 99。
PREEMPT_RT 把绝大部分硬中断处理和全部软中断处理都塞进了内核线程(第 5 章、第 6 章),把 RCU 回调、printk 输出、非 HARD 的 irq_work 也挪进了线程(第 7 章)。结果是:
在 RT 内核上,几乎所有"会花时间的事情"都变成了有名字、有调度策略、有优先级、可以绑核的线程。系统的实时行为不再由"上下文的硬性等级"决定,而由你给这些线程排的优先级决定。
这既是 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 50 | irq_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 50 | kernel/irq/manage.c:1466 |
ksoftirqd/<N> | 兜底执行软中断:硬中断(未线程化的那部分)raise 的、以及没在任务上下文里被消化掉的软中断 | SCHED_OTHER,nice 0 | smpboot 线程没有 setup 回调 kernel/softirq.c:996;kthread 创建时重置为 SCHED_NORMAL kernel/kthread.c:370 |
rcu_preempt | RCU grace-period kthread(RT 下必然是 PREEMPT_RCU,名字是 rcu_preempt) | SCHED_FIFO 1 | kthread_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 1 | kernel/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 1 | kernel/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 1 | irq_workd_setup() → sched_set_fifo_low kernel/irq_work.c:317;if (IS_ENABLED(CONFIG_PREEMPT_RT)) 才注册 kernel/irq_work.c:330 |
pr/legacy | RT 下 legacy console(6.12 里所有串口驱动都还是 legacy)的打印线程 | SCHED_OTHER,nice −20 | kernel/printk/printk.c:3640、:3652 |
pr/<console> | nbcon console 的打印线程(6.12 里还没有转成 nbcon 的串口驱动,通常看不到) | SCHED_OTHER,nice −20 | kernel/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_OTHER | kernel/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_fnkernel/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。
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 线程现在能抢占中断处理
FIFO 99 的用户线程也挡不住硬中断和软中断。一次收包风暴里 net_rx_action() 可以连续跑 2 ms(MAX_SOFTIRQ_TIME kernel/softirq.c:500),你的控制循环就晚 2 ms。
好处是"不可能饿死中断":中断处理总能拿到 CPU。
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 路径。
- clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next)glibc →
svc #0 - el0t_64_sync_handler()arch/arm64/kernel/entry-common.c:858
- el0_svc() → do_el0_svc() → el0_svc_common() → invoke_syscall()entry-common.c:839 · syscall.c:149
- __arm64_sys_clock_nanosleep()kernel/time/posix-timers.c:1396
- common_nsleep() → hrtimer_nanosleep()posix-timers.c:1368 · hrtimer.c:2133
- hrtimer_init_sleeper_on_stack() → __hrtimer_init_sleeper()hrtimer.c:2019当前是 FIFO 任务 → 加上 HRTIMER_MODE_HARD
- do_nanosleep() → schedule()hrtimer.c:2085TASK_INTERRUPTIBLE,让出 CPU
- …… 到期时刻:arch timer PPI 触发 ……
- CNTP/CNTV 定时器到期 → GICv3 PPI → el0t_64_irq / el1h_64_irqpercpu 中断,IRQF_TIMER ⇒ 不线程化
- gic_handle_irq() → … → handle_percpu_devid_irq()
- arch_timer_handler_phys() / _virt()drivers/clocksource/arm_arch_timer.c:681
- hrtimer_interrupt()kernel/time/hrtimer.c:1849
- __hrtimer_run_queues(…, HRTIMER_ACTIVE_HARD) → __run_hrtimer()hrtimer.c:1785 · 1710
- hrtimer_wakeup() → wake_up_process() → try_to_wake_up()hrtimer.c:1981 · sched/core.c:4142在硬中断里直接唤醒,不经过任何线程
- ttwu_do_activate() → wakeup_preempt() → resched_curr()本 CPU 当前任务优先级更低 → 置 TIF_NEED_RESCHED
- …… 中断返回路径上的抢占点 ……
- __el1_irq → arm64_preempt_schedule_irq() → preempt_schedule_irq()entry-common.c:270 · sched/core.c:7040若被打断的是内核态
- 或 exit_to_user_mode → do_notify_resume() → schedule()entry-common.c:130若被打断的是用户态
- __schedule() → context_switch() → cpu_switch_tosched/core.c:6613 · 5296 · arch/arm64/kernel/entry.S:825
- 从 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
这张图里最值得琢磨的是最后两步的顺序。用户线程 FIFO 80 高于中断线程,所以一旦被唤醒,同核上它立刻抢走 CPU,中断线程的收尾工作——包括 irq_finalize_oneshot() 解除屏蔽——要等用户线程下一次睡眠才继续。在这段时间里该中断线保持 masked,设备的下一个中断会被 GIC 挂起(pending)而不是丢失。对周期性设备这通常无害,但如果你的用户线程处理一次数据要很久,就要考虑把中断线程优先级抬到用户线程之上,或者把两者放在不同核上。
- kthread() → irq_thread()kernel/irq/manage.c:1300FIFO 50(或你 chrt 后的值)
- irq_forced_thread_fn()kernel/irq/manage.c:1191local_bh_disable() 包住 handler
- mydev_irq_handler()驱动原来的 handler,现在可以拿 spinlock_t
- spin_lock_irqsave(&dev->lock) → rt_spin_lock()kernel/locking/spinlock_rt.c:54不关中断;争用时睡眠 + PI
- wake_up_interruptible(&dev->wq) → __wake_up()kernel/sched/wait.c:124
- __wake_up_common_lock() → __wake_up_common()wait.c:99 · 73
- default_wake_function() → try_to_wake_up()sched/core.c:7060 · 4142
- ttwu_queue() → ttwu_do_activate() → wakeup_preempt() → resched_curr()sched/core.c:3958 · 3668 · 2140 · 1083用户线程 FIFO 80 > 当前 FIFO 50 ⇒ 立刻需要重调度;若目标在别的 CPU,经 IPI_RESCHEDULE(SGI)通知
- 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 是"高于默认中断线程 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 时就地;超时后 ksoftirqd | raise 它的任务的 local_bh_enable();硬中断 raise 的走 ksoftirqd(OTHER) |
| RCU 回调 | RCU_SOFTIRQ | rcuc/N FIFO 1(或 nocb 的 rcuop) |
| printk 输出 | 调用者上下文同步刷 console | pr/legacy 线程,nice −20;emergency/panic 时才同步 |
| 非 HARD 的 irq_work | 硬中断 / tick | irq_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 这类"救命但会打断你"的机制——下一章讲怎么配。