CHAPTER 07

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)。

根 rcu_node qsmask = 0b10 raw_spinlock_t lock 叶子 rcu_node 0 qsmask = 0b0000(已清空) 已向根报告 叶子 rcu_node 1 qsmask = 0b0000,但 blkd_tasks 非空 → 不能向上报告 卡住 CPU0 ✓ CPU1 ✓ CPU2 ✓ CPU3 ✓ CPU4 ✓ CPU5 ✓ blkd_tasks 链表 任务 T(nice 10) 在读侧被抢占 rcu_read_lock_nesting=1 非抢占 RCU:所有 CPU ✓ 即可结束 GP。 PREEMPT_RCU:还要等 blkd_tasks 上的读者跑完 —— 这就是 RT 付出的代价。
rcu_node 树:所有 CPU 都已静止,但叶子 1 上挂着一个在读侧临界区里被抢占的任务,GP 只能继续等

7.2非 RT 下 RCU 的两个延迟问题

  1. 读侧临界区 = 关抢占区。在 PREEMPT_NONE/VOLUNTARY 下 rcu_read_lock() 甚至编译成零条指令,但它依然意味着"这段代码不可被抢占"。遍历一个长链表的读者,就是一个与链表长度成正比的不可抢占区。
  2. 回调在 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,见下)。

读侧被抢占:挂入 blkd_tasks PREEMPT_RT · ARM64
  1. 低优先级任务 T:rcu_read_lock() → 遍历链表rcu_read_lock_nesting = 1
  2. el1h_64_irq → … → hrtimer_interrupt → hrtimer_wakeup某个 RT 任务到点,被唤醒
  3. __el1_irq() → arm64_preempt_schedule_irq()entry-common.c:619 / :628preempt_count == 0:T 可被抢占
  4. preempt_schedule_irq() → __schedule(SM_PREEMPT)kernel/sched/core.c
  5. rcu_note_context_switch(preempt=true)kernel/sched/core.c:6637 → tree_plugin.h:323rcu_preempt_depth() > 0
  6. raw_spin_lock_rcu_node(rnp)rcu_node 锁必须是 raw
  7. rcu_preempt_ctxt_queue(rnp, rdp)tree_plugin.h:162T 挂到 rnp→blkd_tasks,可能成为 gp_tasks
  8. 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 可以结束
    
RCU boost:rcub 线程通过"代理加锁"让被抢占的读者继承它的优先级

提升到多高?由 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 boost 路径 PREEMPT_RT · RCU_BOOST
  1. rcu_gp_kthread()(线程名 rcu_preempt)→ rcu_gp_fqs_loop() → rcu_gp_fqs() → force_qs_rnp()tree.c:2022 / :1986周期性 force quiescent state 扫描
  2. rcu_initiate_boost(rnp)tree_plugin.h:1262jiffies 超过 rnp→boost_time(GP 开始 + 500ms)
  3. wake_up_process(rnp→boost_kthread_task)
  4. …… rcub/N 运行(SCHED_FIFO kthread_prio)……
  5. rcu_boost_kthread()tree_plugin.h:1220
  6. rcu_boost(rnp)tree_plugin.h:1157
  7. rt_mutex_init_proxy_locked(&rnp→boost_mtx.rtmutex, t)tree_plugin.h:1206伪造 owner = 被抢占的读者
  8. rt_mutex_lock(&rnp→boost_mtx) → rt_mutex_slowlock()task_blocks_on_rt_mutex → rt_mutex_adjust_prio(t)
  9. rt_mutex_setprio(t, rcub)kernel/sched/core.ct 从 fair 类切到 rt 类,优先级 = rcub 的
  10. …… t 运行到最外层 rcu_read_unlock ……
  11. rcu_read_unlock_special() → rcu_preempt_deferred_qs_irqrestore():725 / :477
  12. 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)不 raise RCU_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 隔离。
非 RT(PREEMPT_NONE/VOLUNTARY)
  • rcu_read_lock() = preempt_disable()
  • 静止态只需观察 CPU
  • 回调在 RCU_SOFTIRQ 中批量执行
  • expedited GP 用 IPI 加速
PREEMPT_RT
  • 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 所有权状态机(简化):每个 console 自己一把原子"锁",高优先级上下文可以请求交接,panic 时甚至可以强行接管

对应的三个获取函数: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 PREEMPT_RT(6.12,legacy 串口) 任意上下文 printk() 写入 ringbuffer(无锁) preempt_disable + console_trylock console_unlock:关中断逐字符写 PL011 替所有 CPU 还"打印债",毫秒级 调用者被拖住;关中断窗口不可控 任意上下文 printk() 写入 ringbuffer → defer_console_output lazy irq_work → irq_work/N(FIFO 1) pr/legacy(nice −20)唤醒 任务上下文写 PL011,可被抢占 调用者立即返回;输出延后、可能被 RT 任务推迟 只有 panic 时才同步直写
printk 的两条路径:RT 把"写硬件"从调用者身上彻底剥离
RT:RT 线程里的 printk 与真正的输出 PREEMPT_RT · 6.12 · legacy console
  1. 某 RT 线程调用 pr_warn()
  2. _printk() → vprintk() → vprintk_default() → vprintk_emit()printk.c:2342
  3. vprintk_store()写 ringbuffer,无锁
  4. printk_get_console_flush_type()internal.h:233legacy_offload = true(is_printk_legacy_deferred)
  5. defer_console_output() → irq_work_queue(wake_up_klogd_work)printk.c:4571LAZY → lazy_list,返回调用者
  6. …… 稍后 ……
  7. irq_work/N:run_irq_workd() → wake_up_klogd_work_func()irq_work.c:309 / printk.cforce_legacy_kthread → wake_up_interruptible(&legacy_wait)
  8. …… pr/legacy 被调度 ……
  9. legacy_kthread_func()printk.c:3619
  10. console_flush_all() → console_emit_next_record()printk.c:3230
  11. con→write() = pl011_console_write()任务上下文;端口锁是 spinlock_t(RT 下可睡眠)
emergency section

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),而快路径也被关掉,每次分配都要拿这把锁:

RT 下的 kmalloc PREEMPT_RT · mm/slub.c
  1. kmalloc(size, GFP_KERNEL) → __kmalloc_noprof()mm/slub.c:4380
  2. __do_kmalloc_node() → slab_alloc_node()mm/slub.c:4209
  3. __slab_alloc_node()mm/slub.c:4014!USE_LOCKLESS_FAST_PATH() → 直接走 __slab_alloc
  4. __slab_alloc() → slub_get_cpu_ptr()mm/slub.c:3993migrate_disable
  5. ___slab_alloc()mm/slub.c:3741
  6. local_lock_irqsave(&s→cpu_slab→lock, flags)mm/slub.c:3788RT:spin_lock → rt_spin_lock,争用时睡眠 + PI;不关中断
  7. 从 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_THREAD handler、HARD hrtimer 回调、HARD irq_work)。

GFP_ATOMIC 也不例外——它只表示"不要为了回收内存而睡眠",并不能让分配器内部的 spinlock_t 不睡眠。kfree() 同样可能拿锁(把对象还给非本 CPU 的 slab、把空 slab 还给页分配器)。

在 RT 下是 bug
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_tasksGP 可能被低优先级读者拖长 → 需要 boost
RCU 回调RCU_SOFTIRQ 批量执行rcuc/N、NOCB 线程(FIFO 1)回调执行可能被 RT 任务推迟,内存回收变慢
RCU GPexpedited IPIrcu_normal_after_bootsynchronize_rcu_expedited() 不再快
printkconsole_lock 内关中断同步写串口6.12:legacy console 强制线程化(pr/legacy);nbcon 框架就位输出延迟;panic 前的日志可能来不及刷出
SLUB关中断 / 无锁快路径local_lock = per-CPU rtmutex,无快路径分配变慢;原子上下文禁止分配
页分配器preempt_disable 钉 CPUmigrate_disable + spin_trylock同上
一句话

这三个子系统的改造遵循同一个模式:把"关抢占/关中断"这种隐式保护换成显式的、可被抢占的、带 PI 的锁或计数;当这样做会让某些东西(GP、回调、日志)被饿死时,再用 boost 或专属线程把它们拉回来。