RCU、printk 与内存分配
锁、中断、软中断改完之后,还有三块"基础设施"在非 RT 内核里依赖关抢占或长时间的原子执行:RCU 读侧就是关抢占,printk 在锁里同步刷串口,内存分配器用关中断保护 per-CPU 缓存。本章看 PREEMPT_RT 如何分别处理它们,以及由此产生的一条最重要的编码红线——原子上下文里不能分配内存。
7.1RCU 一屏回顾
RCU(Read-Copy-Update)为"读多写少"的数据(路由表、进程列表、文件系统挂载点……)设计,目标是读侧零开销:不写任何共享变量、不做原子操作。写者用"复制—修改—原子替换指针"发布新版本,然后等一个 grace period(宽限期):保证在替换之前已经开始的读者全部退出后,再释放旧版本。
/* 写者 */
new = kmalloc(...); *new = *old; new->x = 1;
rcu_assign_pointer(gp, new); /* 原子发布 */
synchronize_rcu(); /* 或 call_rcu(&old->rcu, free_fn) 异步 */
kfree(old);
/* 读者 */
rcu_read_lock();
p = rcu_dereference(gp); /* 要么旧版本、要么新版本,都是完整的 */
use(p);
rcu_read_unlock();
难点在于"怎么知道老读者都走了"而又不让读者留痕迹。非抢占 RCU 的答案是一个逆否命题:rcu_read_lock() 就是 preempt_disable()(include/linux/rcupdate.h:91),所以
读者存在 ⟹ 该 CPU 不会发生上下文切换
(逆否)该 CPU 发生了上下文切换 / 进入 idle / 在用户态被 tick 打断 ⟹ 该 CPU 上没有老读者
这类"能证明没有读者"的时刻叫静止态(quiescent state)。RCU 只追踪 CPU 而非读者:每个 CPU 至少经历一次静止态,grace period 就结束。为了让几百个 CPU 汇报时不争同一把锁,RCU 把 CPU 组织成一棵 rcu_node 树(kernel/rcu/tree.h:41),每个节点的 qsmask 位图记录"还欠着静止态的孩子",CPU 清自己那一位,节点清空时再向父节点报告,根节点清空即 GP 结束(rcu_report_qs_rnp(),kernel/rcu/tree.c:2297)。
7.2非 RT 下 RCU 的两个延迟问题
- 读侧临界区 = 关抢占区。在
PREEMPT_NONE/VOLUNTARY下rcu_read_lock()甚至编译成零条指令,但它依然意味着"这段代码不可被抢占"。遍历一个长链表的读者,就是一个与链表长度成正比的不可抢占区。 - 回调在 softirq 里批量执行。
call_rcu()登记的回调(大量是kfree)在 GP 结束后由RCU_SOFTIRQ的rcu_core()→rcu_do_batch()(kernel/rcu/tree.c:2498)批量执行。一次rm -rf大目录或者网络对象大量释放,就能堆出成千上万个回调,在 softirq 里成批执行——就是 6.2 节说的"隐形最高优先级"。
7.3RT 的解法:可抢占 RCU + 优先级提升
读侧不再关抢占
RT 选择了 PREEMPTION(kernel/Kconfig.preempt:73),而 PREEMPT_RCU 是 default y if PREEMPTION,于是读侧换成了一个 per-task 的嵌套计数:
void __rcu_read_lock(void)
{
rcu_preempt_read_enter(); /* current->rcu_read_lock_nesting++ */
...
barrier(); /* critical section after entry code. */
}
void __rcu_read_unlock(void)
{
struct task_struct *t = current;
barrier(); // critical section before exit code.
if (rcu_preempt_read_exit() == 0) { /* 最外层退出 */
barrier();
if (unlikely(READ_ONCE(t->rcu_read_unlock_special.s)))
rcu_read_unlock_special(t); /* 只有"出过事"才走慢路径 */
}
...
}
快路径依然很便宜:读写一个 per-task 计数器、一次很少成立的分支。代价是那个逆否命题失效了——读者可以被抢占、被调度走,"CPU 上发生了上下文切换"不再意味着"老读者都走了"。RCU 只好在读者被抢占的那一刻把它记下来。
被抢占的读者:进入 blkd_tasks
void rcu_note_context_switch(bool preempt) /* 由 __schedule() 调用 */
{
...
WARN_ONCE(!preempt && rcu_preempt_depth() > 0,
"Voluntary context switch within RCU read-side critical section!");
if (rcu_preempt_depth() > 0 &&
!t->rcu_read_unlock_special.b.blocked) {
/* Possibly blocking in an RCU read-side critical section. */
rnp = rdp->mynode;
raw_spin_lock_rcu_node(rnp);
t->rcu_read_unlock_special.b.blocked = true;
t->rcu_blocked_node = rnp;
...
rcu_preempt_ctxt_queue(rnp, rdp); /* 挂到 rnp->blkd_tasks */
} else {
rcu_preempt_deferred_qs(t);
}
...
}
注意那条 WARN_ONCE:读侧临界区里被抢占是允许的,但主动睡眠仍然不行(那是 SRCU 的领域)。RT 下的例外是 spinlock_t:它在 RT 下会"睡眠",但走的是 TASK_RTLOCK_WAIT,调度器把它当作抢占处理(参见第 3 章),所以在 RCU 读侧里拿 spinlock_t 在 RT 下依然合法。
rcu_preempt_ctxt_queue()(kernel/rcu/tree_plugin.h:162)把任务插到叶子节点的 blkd_tasks 链表(kernel/rcu/tree.h:88)里一个精心选择的位置,并用 gp_tasks 指针标出"阻塞当前 GP 的第一个任务"。从此,rcu_report_qs_rnp() 在清空 qsmask 后还要检查:
if (rnp->qsmask != 0 || rcu_preempt_blocked_readers_cgp(rnp)) {
/* 还有 CPU 没静止,或者还有被抢占的读者没退出:到此为止 */
raw_spin_unlock_irqrestore_rcu_node(rnp, flags);
return;
}
读者最终执行最外层 rcu_read_unlock() 时,发现 rcu_read_unlock_special.b.blocked 被置位,进入 rcu_read_unlock_special()(kernel/rcu/tree_plugin.h:725)→ rcu_preempt_deferred_qs_irqrestore()(:477):把自己从 blkd_tasks 摘下;如果它是这个节点上最后一个挡着 GP 的,就调用 rcu_report_unblock_qs_rnp() 继续向上报告;如果它曾被 boost,则释放 boost 锁(:585,见下)。
- 低优先级任务 T:rcu_read_lock() → 遍历链表rcu_read_lock_nesting = 1
- el1h_64_irq → … → hrtimer_interrupt → hrtimer_wakeup某个 RT 任务到点,被唤醒
- __el1_irq() → arm64_preempt_schedule_irq()entry-common.c:619 / :628preempt_count == 0:T 可被抢占
- preempt_schedule_irq() → __schedule(SM_PREEMPT)kernel/sched/core.c
- rcu_note_context_switch(preempt=true)kernel/sched/core.c:6637 → tree_plugin.h:323rcu_preempt_depth() > 0
- raw_spin_lock_rcu_node(rnp)rcu_node 锁必须是 raw
- rcu_preempt_ctxt_queue(rnp, rdp)tree_plugin.h:162T 挂到 rnp→blkd_tasks,可能成为 gp_tasks
- context_switch() → cpu_switch_to()arch/arm64/kernel/entry.SRT 任务开始运行
RCU_BOOST:用一把"伪造的 rtmutex"提升读者
新的风险随之而来:如果被抢占的读者 T 优先级很低,而 CPU 一直被 RT 任务占着,T 永远回不来,GP 就永远结束不了,call_rcu() 的回调越积越多,内存最终耗尽。这就是"RCU 读者的优先级反转"。RT 因此强制打开 RCU_BOOST:
config RCU_BOOST
bool "Enable RCU priority boosting"
depends on (RT_MUTEXES && PREEMPT_RCU && RCU_EXPERT) || PREEMPT_RT
default y if PREEMPT_RT
config RCU_BOOST_DELAY
int "Milliseconds to delay boosting after RCU grace-period start"
range 0 3000
default 500
boost 的实现极其巧妙——它没有发明新的提升机制,而是直接复用 rtmutex 的优先级继承:
/*
* We boost task t by manufacturing an rt_mutex that appears to
* be held by task t. We leave a pointer to that rt_mutex where
* task t can find it, and task t will release the mutex when it
* exits its outermost RCU read-side critical section. Then
* simply acquiring this artificial rt_mutex will boost task
* t's priority. (Thanks to tglx for suggesting this approach!)
*/
t = container_of(tb, struct task_struct, rcu_node_entry);
rt_mutex_init_proxy_locked(&rnp->boost_mtx.rtmutex, t); /* 伪造:锁的主人是 t */
raw_spin_unlock_irqrestore_rcu_node(rnp, flags);
/* Lock only for side effect: boosts task t's priority. */
rt_mutex_lock(&rnp->boost_mtx); /* rcub 阻塞在这把锁上 → PI 把 t 提到 rcub 的优先级 */
rt_mutex_unlock(&rnp->boost_mtx); /* Then keep lockdep happy. */
sequenceDiagram
participant T as 读者 T nice 10
participant RT as RT 任务 FIFO 80
participant GP as rcu_preempt GP 线程
participant B as rcub/N FIFO 1
participant M as rnp boost_mtx
T->>T: rcu_read_lock
RT-->>T: 抢占 T, T 挂入 blkd_tasks
Note over GP: 所有 CPU 都已静止, 但 blkd_tasks 非空
GP->>B: GP 已开始超过 RCU_BOOST_DELAY 500ms, 唤醒 rcub
B->>M: rt_mutex_init_proxy_locked, 伪造 owner 为 T
B->>M: rt_mutex_lock 阻塞
M-->>T: PI 把 T 提升到 FIFO 1, 高于所有 SCHED_OTHER
Note over T: T 得以运行, 但 RT 任务 FIFO 80 仍优先
T->>T: rcu_read_unlock 最外层
T->>M: rcu_read_unlock_special 释放 boost_mtx, 去提升
M->>B: rcub 获得锁并立即释放
T->>GP: rcu_report_unblock_qs_rnp, GP 可以结束
提升到多高?由 kthread_prio 决定:static int kthread_prio = IS_ENABLED(CONFIG_RCU_BOOST) ? 1 : 0;(kernel/rcu/tree.c:168),即默认 SCHED_FIFO 1。rcub 线程用它(tree_plugin.h:1319),GP 线程 rcu_preempt 也用它(tree.c:5390 附近)。FIFO 1 足以压过所有 SCHED_OTHER 负载,但不会干扰任何真正的 RT 任务——这正是设计意图:RCU 不应抢 RT 应用的 CPU。如果 RT 任务把某个 CPU 100% 占满,被困在那里的读者依然会拖住 GP;对此可以用启动参数 rcutree.kthread_prio= 调高(见第 10 章)。
- rcu_gp_kthread()(线程名 rcu_preempt)→ rcu_gp_fqs_loop() → rcu_gp_fqs() → force_qs_rnp()tree.c:2022 / :1986周期性 force quiescent state 扫描
- rcu_initiate_boost(rnp)tree_plugin.h:1262jiffies 超过 rnp→boost_time(GP 开始 + 500ms)
- wake_up_process(rnp→boost_kthread_task)
- …… rcub/N 运行(SCHED_FIFO kthread_prio)……
- rcu_boost_kthread()tree_plugin.h:1220
- rcu_boost(rnp)tree_plugin.h:1157
- rt_mutex_init_proxy_locked(&rnp→boost_mtx.rtmutex, t)tree_plugin.h:1206伪造 owner = 被抢占的读者
- rt_mutex_lock(&rnp→boost_mtx) → rt_mutex_slowlock()task_blocks_on_rt_mutex → rt_mutex_adjust_prio(t)
- rt_mutex_setprio(t, rcub)kernel/sched/core.ct 从 fair 类切到 rt 类,优先级 = rcub 的
- …… t 运行到最外层 rcu_read_unlock ……
- rcu_read_unlock_special() → rcu_preempt_deferred_qs_irqrestore():725 / :477
- rt_mutex_futex_unlock(&rnp→boost_mtx.rtmutex)tree_plugin.h:585释放 → 去提升,唤醒 rcub
回调与核心处理:从 softirq 搬到线程
static bool use_softirq = !IS_ENABLED(CONFIG_PREEMPT_RT); /* RT:不用 RCU_SOFTIRQ */
static int rcu_normal_after_boot = IS_ENABLED(CONFIG_PREEMPT_RT); /* RT:开机后禁用 expedited */
- rcuc/N 线程:
use_softirq为假时invoke_rcu_core()(kernel/rcu/tree.c:2877)不 raiseRCU_SOFTIRQ,而是唤醒 per-CPU 的rcuc/%u线程(tree.c:2939),在里面跑rcu_core()和回调。它在rcu_cpu_kthread_setup()里被设成SCHED_FIFO kthread_prio(tree_plugin.h:1115)。回调执行因此变成一个可抢占、有明确优先级的线程。 - 禁用 expedited GP:
synchronize_rcu_expedited()通过 IPI 强迫所有 CPU 立即汇报静止态。对吞吐友好,但对 RT 是延迟灾难——一个 IPI 打到正在跑控制环的隔离核上。rcu_normal_after_boot让开机完成后所有 expedited 请求退化为普通 GP。同理RCU_EXP_KTHREAD在 RT 上默认关闭(kernel/rcu/Kconfig:224)。 - 回调卸载(NOCB):
CONFIG_RCU_NOCB_CPU+rcu_nocbs=可以把指定 CPU 的回调交给rcuog/N、rcuo%c/N线程(kernel/rcu/tree_nocb.h:1399 / :1412)在别的 CPU 上执行,配合nohz_full做 CPU 隔离。
rcu_read_lock()=preempt_disable()- 静止态只需观察 CPU
- 回调在 RCU_SOFTIRQ 中批量执行
- expedited GP 用 IPI 加速
rcu_read_lock()= per-task 计数,可抢占- 还要追踪被抢占的读者(blkd_tasks)
- RCU_BOOST:rcub 用代理 rtmutex 把读者提到 FIFO 1
- 回调在 rcuc/N(FIFO 1)或 NOCB 线程执行;开机后禁用 expedited
RT 要读侧可抢占 → "关抢占 ≡ 读侧"的等价关系破裂 → 必须逐个追踪被抢占的读者 → 读者可能饿死、GP 无限延长 → 必须 boost → boost 借用 rtmutex 的 PI(第 4 章)。RT 的各个部件就是这样一环扣一环,最后都回到锁与优先级继承这块地基上。
7.4printk:RT 进主线前的最后一块拼图
老 printk 为什么是延迟杀手
传统 printk 的路径是:把消息写进 ringbuffer → 尝试 console_trylock() → 成功者在 console_unlock() 里同步调用每个 console 的 ->write(),把所有积压消息一个字符一个字符写到硬件,而且写的时候关着中断(printk_safe_enter_irqsave())。
ARM64 板子最常见的控制台是 PL011(ttyAMA0),115200 波特、8N1 每字符 10 bit:
1 字符 ≈ 10 / 115200 s ≈ 87 µs
一行 80 字符 ≈ 7 ms
一次 WARN 的栈回溯(~40 行)≈ 280 ms ← 这么长时间关中断、不可抢占
更糟的是 "console_lock 接力":谁拿到锁谁负责把所有 CPU 积压的消息都打完——一个 RT 线程里随手的 pr_info(),可能让它替整个系统还几百毫秒的"打印债"。6.12 合入的 printk 重构(printk-for-6.12)就是为了根治这件事,Thomas Gleixner 称它是 RT 进主线前的最后一个前提。
nbcon:新的 console 模型
6.12 引入了 "no-BKL console"(nbcon,kernel/printk/nbcon.c)。它的核心是一个塞进一个 int 的原子状态,替代全局的 console_lock:
struct nbcon_state {
union {
unsigned int atom;
struct {
unsigned int prio : 2; /* 当前 owner 的优先级 */
unsigned int req_prio : 2; /* 请求接管者的优先级 */
unsigned int unsafe : 1; /* owner 正处于不可接管区 */
unsigned int unsafe_takeover : 1; /* 曾发生过强行接管 */
unsigned int cpu : 24; /* owner 所在 CPU */
};
};
};
enum nbcon_prio {
NBCON_PRIO_NONE = 0,
NBCON_PRIO_NORMAL, /* 普通打印 */
NBCON_PRIO_EMERGENCY, /* WARN / OOPS */
NBCON_PRIO_PANIC, /* panic */
NBCON_PRIO_MAX,
};
stateDiagram-v2
[*] --> Free
Free --> Owned: try_acquire_direct, cmpxchg 成功
Owned --> Free: release
Owned --> HandoverReq: 更高优先级请求, 设置 req_prio
HandoverReq --> Owned2: owner 在安全点让出, handover
HandoverReq --> Owned2: 超时且 prio 为 PANIC, hostile takeover
Owned2 --> Free: release
state Owned {
[*] --> Safe
Safe --> Unsafe: enter_unsafe 操作硬件寄存器
Unsafe --> Safe: exit_unsafe
}
对应的三个获取函数:nbcon_context_try_acquire_direct()(nbcon.c:238)、_handover()(:426)、_hostile()(:542)。驱动则提供两个回调(include/linux/console.h:384 / :415):
write_thread():在专属 printer 线程里调用,可以睡眠,用device_lock()和 tty 层串行化;write_atomic():只在 emergency/panic 时使用,必须能在任何上下文工作,在 unsafe 区段里操作硬件。
每个 nbcon console 有一个线程 pr/<name><index>(nbcon.c:1335),调度策略是 sched_set_normal(..., -20)(nbcon.c:1347),即 SCHED_OTHER nice −20——高于普通进程,但低于任何 RT 任务。
6.12 的真实情况:串口仍是 legacy console
在 6.12.110 源码里 grep -rn write_atomic drivers/ 结果为空——没有任何串口驱动(包括 PL011、8250)实现了 nbcon 接口,它们都还是 legacy console。nbcon 框架在 6.12 已经就位,驱动的转换是之后版本的事。所以在 6.12 RT 上真正起作用的是另一条路:legacy console 的强制线程化。
#ifdef CONFIG_PREEMPT_RT
# define force_legacy_kthread() (true)
#else
# define force_legacy_kthread() (false)
#endif
bool is_printk_legacy_deferred(void)
{
return (force_legacy_kthread() || /* RT:永远推迟 */
this_cpu_read(printk_context) ||
in_nmi() ||
is_printk_cpu_sync_owner());
}
printk_get_console_flush_type()(kernel/printk/internal.h:233)据此给出决策:在 NORMAL 和 EMERGENCY 优先级下,legacy console 都走 legacy_offload;只有 PANIC 时才直接打印。vprintk_emit()(printk.c:2342)于是只做 defer_console_output()(printk.c:4571):排一个 lazy irq_work(IRQ_WORK_INIT_LAZY,printk.c:4516),它在 RT 下由 irq_work/N 线程执行,去唤醒 pr/legacy 线程(printk.c:3640,同样是 nice −20,:3652)。真正写串口发生在这个线程里,而且不再关中断:
if (force_legacy_kthread() && !panic_in_progress()) {
/*
* With forced threading this function is in a task context
* (either legacy kthread or get_init_console_seq()). There
* is no need for concern about printk reentrance, handovers,
* or lockdep complaints.
*/
con->write(con, outbuf, pmsg.outbuf_len); /* 普通任务上下文,可被抢占 */
con->seq = pmsg.seq + 1;
} else {
printk_safe_enter_irqsave(flags); /* 非 RT:关中断写串口 */
console_lock_spinning_enable();
...
con->write(con, outbuf, pmsg.outbuf_len);
...
}
- 某 RT 线程调用 pr_warn()
- _printk() → vprintk() → vprintk_default() → vprintk_emit()printk.c:2342
- vprintk_store()写 ringbuffer,无锁
- printk_get_console_flush_type()internal.h:233legacy_offload = true(is_printk_legacy_deferred)
- defer_console_output() → irq_work_queue(wake_up_klogd_work)printk.c:4571LAZY → lazy_list,返回调用者
- …… 稍后 ……
- irq_work/N:run_irq_workd() → wake_up_klogd_work_func()irq_work.c:309 / printk.cforce_legacy_kthread → wake_up_interruptible(&legacy_wait)
- …… pr/legacy 被调度 ……
- legacy_kthread_func()printk.c:3619
- console_flush_all() → console_emit_next_record()printk.c:3230
- con→write() = pl011_console_write()任务上下文;端口锁是 spinlock_t(RT 下可睡眠)
WARN/OOPS 路径会用 nbcon_cpu_emergency_enter()(nbcon.c:1652)把本 CPU 标记为 EMERGENCY 优先级,nbcon console 会立即用 write_atomic() 刷出。但如上所述,6.12 RT 上的 legacy 串口在 EMERGENCY 下依然走线程——所以在 6.12 RT 系统上看到的 WARN 输出可能会比非 RT 晚一点出现,panic 除外。
7.5内存分配:per-CPU 缓存的保护方式变了
SLUB:local_lock 保护 cpu_slab
SLUB 给每个 kmem_cache 配一个 per-CPU 的 kmem_cache_cpu(当前 slab 和空闲链表),用一把 local_lock_t 保护:
struct kmem_cache_cpu {
union {
struct {
void **freelist; /* Pointer to next available object */
unsigned long tid; /* Globally unique transaction id */
};
freelist_aba_t freelist_tid;
};
struct slab *slab; /* The slab from which we are allocating */
...
local_lock_t lock; /* Protects the fields above */
};
源码的锁说明(mm/slub.c:120 起)把 RT 差异说得很直白:
* On PREEMPT_RT, the local lock neither disables interrupts nor preemption
* which means the lockless fastpath cannot be used as it might interfere with
* an in-progress slow path operations. In this case the local lock is always
* taken but it still utilizes the freelist for the common operations.
#ifndef CONFIG_PREEMPT_RT
#define slub_get_cpu_ptr(var) get_cpu_ptr(var) /* preempt_disable */
#define USE_LOCKLESS_FAST_PATH() (true)
#else
#define slub_get_cpu_ptr(var) ({ migrate_disable(); this_cpu_ptr(var); })
#define USE_LOCKLESS_FAST_PATH() (false) /* RT:没有无锁快路径 */
#endif
非 RT 下,local_lock_irqsave(&s->cpu_slab->lock) 就是关中断,快路径用 tid + cmpxchg128 完全无锁。RT 下 local_lock_irqsave 变成 migrate_disable() + 一把 per-CPU spinlock_t(也就是 rtmutex),而快路径也被关掉,每次分配都要拿这把锁:
- kmalloc(size, GFP_KERNEL) → __kmalloc_noprof()mm/slub.c:4380
- __do_kmalloc_node() → slab_alloc_node()mm/slub.c:4209
- __slab_alloc_node()mm/slub.c:4014!USE_LOCKLESS_FAST_PATH() → 直接走 __slab_alloc
- __slab_alloc() → slub_get_cpu_ptr()mm/slub.c:3993migrate_disable
- ___slab_alloc()mm/slub.c:3741
- local_lock_irqsave(&s→cpu_slab→lock, flags)mm/slub.c:3788RT:spin_lock → rt_spin_lock,争用时睡眠 + PI;不关中断
- 从 c→freelist 取对象 / 必要时向 partial 链表或页分配器要新 slab
页分配器与其他 mm 统计
- per-CPU 页链表(pcp):非 RT 用
preempt_disable()钉住 CPU,RT 用migrate_disable()(mm/page_alloc.c:117 起的pcpu_task_pin()),注释直说原因:"otherwise RT spinlock usage is interfered with and a high priority task cannot preempt the allocator"。pcp 本身用spin_trylock获取,失败就绕开 pcp 走 zone 锁。 - vmstat 计数:RT 下
local_lock_irq只禁迁移,而计数更新是读-改-写,所以显式加preempt_disable_nested()(mm/vmstat.c:348 起的注释)——一个刻意保留的、极短的不可抢占区。 - memcg:统计更新路径同样在 RT 下补了 preempt_disable 访问器(mm/memcontrol.c:535)。
由此得出的红线:原子上下文不能分配(也不能释放)内存
既然分配器内部用的是 spinlock_t(RT 下会睡眠),那么在以下上下文中调用分配器在 RT 下都是错误的:
- 持有
raw_spinlock_t时; local_irq_disable()/preempt_disable()区间内;- 真正的硬中断上下文(
IRQF_NO_THREADhandler、HARD hrtimer 回调、HARD irq_work)。
GFP_ATOMIC 也不例外——它只表示"不要为了回收内存而睡眠",并不能让分配器内部的 spinlock_t 不睡眠。kfree() 同样可能拿锁(把对象还给非本 CPU 的 slab、把空 slab 还给页分配器)。
raw_spin_lock_irqsave(&dev->raw_lock, flags);
buf = kmalloc(64, GFP_ATOMIC); /* 内部 rt_spin_lock 可能睡眠 */
...
raw_spin_unlock_irqrestore(&dev->raw_lock, flags);
buf = kmalloc(64, GFP_KERNEL); /* 先在可睡眠上下文分配 */
raw_spin_lock_irqsave(&dev->raw_lock, flags);
list_add(&buf->node, &dev->list);
raw_spin_unlock_irqrestore(&dev->raw_lock, flags);
/* 或者:预分配对象池 / 把 raw 锁降为 spinlock_t */
这条红线影响深远。BPF 是最典型的受害者:BPF 程序可以挂在任意 tracepoint、甚至关中断的路径上,所以在 RT 下它的 map 元素分配被迫全部改走预分配的 bpf_mem_alloc(kernel/bpf/bpf_local_storage.c:800–804 的注释:"In PREEMPT_RT, kmalloc(GFP_ATOMIC) is still not safe in non preemptible context"),bpf_timer 在 RT 下直接被 verifier 拒绝(kernel/bpf/verifier.c:7977)。lockdep 的 wait-type 检查(第 3 章)能在非 RT 内核上就把这类违规以 "Invalid wait context" 报出来。
7.6本章小结
| 子系统 | 非 RT 的不可抢占来源 | RT 的做法 | 新代价 / 约束 |
|---|---|---|---|
| RCU 读侧 | rcu_read_lock() = 关抢占 | per-task 计数;被抢占读者入 blkd_tasks | GP 可能被低优先级读者拖长 → 需要 boost |
| RCU 回调 | RCU_SOFTIRQ 批量执行 | rcuc/N、NOCB 线程(FIFO 1) | 回调执行可能被 RT 任务推迟,内存回收变慢 |
| RCU GP | expedited IPI | rcu_normal_after_boot | synchronize_rcu_expedited() 不再快 |
| printk | console_lock 内关中断同步写串口 | 6.12:legacy console 强制线程化(pr/legacy);nbcon 框架就位 | 输出延迟;panic 前的日志可能来不及刷出 |
| SLUB | 关中断 / 无锁快路径 | local_lock = per-CPU rtmutex,无快路径 | 分配变慢;原子上下文禁止分配 |
| 页分配器 | preempt_disable 钉 CPU | migrate_disable + spin_trylock | 同上 |
这三个子系统的改造遵循同一个模式:把"关抢占/关中断"这种隐式保护换成显式的、可被抢占的、带 PI 的锁或计数;当这样做会让某些东西(GP、回调、日志)被饿死时,再用 boost 或专属线程把它们拉回来。