Linux PREEMPT_RT 详解(5.10.268-rt164)

Linux PREEMPT_RT 详解(5.10.268-rt164)

基准:patch-5.10.268-rt164,328 个拆分补丁,打在主线 v5.10.268 上。

材料:patches/early-5.10/patch-5.10.268-rt164.patch.xz(合并包)、patches/early-5.10/series/(逐文件)。

打开 CONFIG_PREEMPT_RT=y 后,内核从「吞吐优先、最坏延迟无上界」变为「最坏调度延迟可控」。本补丁在树外补齐锁、迁移、softirq、RCU、printk、内存映射等全部规则翻转。下文按知识点展开,每点都追到源码与下一层原理。


1. 调度延迟、抢占、睡眠、优先级反转

1.1 调度延迟与抖动

调度延迟 :从「事件已发生、高优先级任务该跑」到「该任务真正占用 CPU」之间的时间。

抖动:同一次延迟的波动(例如 40µs / 60µs / 900µs)。

实时系统验收看 worst-case,不看平均值。平均 50µs、偶尔 2ms,对硬周期控制仍失败。

延迟几乎都堆在内核路径,而不是用户态谁 nice 更小:

来源 机制 为何抬延迟
关中断 local_irq_disable / spin_lock_irq 定时器与唤醒进不来
持自旋锁 非 RT spinlock_t 同 CPU 不可抢占,临界区多长就等多久
softirq 硬中断返回路径 插在实时任务前,且非普通进程优先级模型
不可抢占内核段 PREEMPT_NONE 等 低优先级陷在内核里,高优先级进不来
长临界打印 console UART 轮询 无上界持锁/关抢占
复制代码
时刻 T0: 硬件事件 / 定时器到期
时刻 T0~T1: CPU 可能在关中断、持锁、跑 softirq、不可抢占段
时刻 T1: 才切换到高优先级任务
延迟 = T1 − T0

1.2 抢占与睡眠(决定锁能不能用)

概念 含义 典型语境
抢占 更高优先级打断当前任务占 CPU 用户态通常可抢;内核看配置与锁
睡眠 主动让出 CPU,进等待队列 schedule()、等互斥锁
不能睡眠 必须跑完当前段,禁止 schedule 硬中断、关中断、持真正自旋锁

判定口诀:

若当前路径「不能睡眠」,则禁止调用任何可能睡眠的 API(含 RT 上的普通 spinlock_t)。

反之,RT 把普通 spinlock_t 改成可睡眠后,持锁路径可以 被抢占、竞争时可以睡,因此绝不能在硬中断/关中断里再拿它。

1.3 优先级反转与 PI

复制代码
Lo(低)拿到锁 L
Hi(高)也要 L → 阻塞,等 Lo
Mid(中)可运行 → 抢占 Lo(Lo 未放锁)
结果:Hi 的有效进展被 Mid 拖死 = 优先级反转

优先级继承(PI): 持锁的 Lo 临时把自己的调度优先级抬到等待者 Hi 的水平,尽快跑完临界区放锁;Mid 插不进这条关键路径。

实现载体是 rt_mutex 。本补丁里:可睡眠 spinlock_t、RT mutex、部分 RCU boost、futex PI,都建立在同一套 PI 等待链上。

1.4 CONFIG_PREEMPT_RT 改写的总表

维度 非 RT PREEMPT_RT(本补丁生效后)
优化目标 吞吐、代码好写 最坏调度延迟
spinlock_t 自旋 + 关抢占 rt_mutex,可睡、可抢、有 PI
raw_spinlock_t 硬锁 仍硬锁,只许极短临界区
spin_lock_irq 关中断 + 自旋 退化成 spin_lock(见 §4.4)
关中断大段 驱动常用 延迟毒药;能去掉就去掉
IRQ 重活 常在硬中断上下文 线程化,按优先级竞争
softirq 易插队 ksoftirqd + 可抢占 BH
per-CPU 保护 常靠 preempt/irq-off migrate_disable / local_lock
RCU 核心 可用 softirq 强制 rcuc 线程 + 默认 BOOST
printk 同步打控制台风险高 异步 kthread + write_atomic

2. 非 RT 契约(对照基线)

2.1 非 RT spinlock_t 实际做了什么

  1. preempt_count 加上 PREEMPT_LOCK_OFFSET(通常等于关抢占)
  2. 忙等直到拿到锁(持锁者在别的 CPU 时空转;同 CPU 因关抢占不会出现「持锁者被抢走」)
  3. 持锁期间禁止睡眠

同 CPU 更高优先级任务:在解锁前进不来。临界区 100µs,延迟至少 100µs。

2.2 为何总写 local_irq_disable + spin_lock

c 复制代码
spin_lock_irq(&lock);
/* 访问共享数据 */
spin_unlock_irq(&lock);
  • 正确性: 本 CPU 中断处理若再抢同一把锁 → 自死锁。关中断堵住这条路径。
  • 减抖滥用: 有人用关中断包住「希望别被打断」的大段更新(显示管线、串口 console)。减抖不是同步语义;RT 上常被拆掉。

2.3 softirq

硬中断返回可跑 softirq(NET_RX_SOFTIRQ 等)。它:

  • 不是用户可见的普通进程
  • 不走你设的 FIFO 优先级
  • 可长时间占 CPU

因此「用户态 SCHED_FIFO 99」仍可能被 softirq 拖死------这是非 RT 最坏延迟难谈的核心原因之一。

2.4 隐含契约(整包补丁要推翻的东西)

持有 spinlock_t 或关着中断 ⇒ 处于原子上下文:不会睡眠、同 CPU 不可抢占、且常常隐含处于 RCU 读侧(因为关抢占 ⇒ RCU 认为读者不会在读侧被抢到休眠)。

驱动里 irqs_disabled() 断言、in_atomic() 判断、在 spinlock 临界区里当 RCU 读侧用,全都建立在这条契约上。RT 翻转锁语义后,这些断言与隐含必须逐条重建。


3. 补丁材料与主题地图

复制代码
官方:
  https://cdn.kernel.org/pub/linux/kernel/projects/rt/5.10/
  patch-5.10.268-rt164.patch.xz
  patches-5.10.268-rt164.tar.xz

本仓:
  patches/early-5.10/patch-5.10.268-rt164.patch.xz
  patches/early-5.10/series/0001-....patch ... 0328-....patch
  patches/early-5.10/series/series          # 应用顺序清单

合并包 ≈ 一次 patch -p1;series/ 便于按 Subject 精读单点。共 328 个生效补丁,约 413 个文件级 diff。

3.1 按机制阅读顺序(不要按文件名死记)

复制代码
0010+  migrate_disable
  → 0047~0062  highmem / kmap_local / kmap_atomic
  → 0071~0073  RCU BOOST / rcuc / normal_after_boot
  → 0067        timer TIMER_IRQSAFE
  → 0080~0108  printk / console / write_atomic
  → 0124~0151  softirq 重排与 RT 化
  → 0170~0175  可睡眠锁全家桶接线
  → 0194~0211  local_lock 与子系统迁移
  → 0228        持锁 = RCU 读侧
  → 0234        cpu_chill
  → 0258~0263  lazy preemption
  → 0256+       ARCH_SUPPORTS_RT
  → 0286/0287   sysfs / localversion
  → 0308        irq_work 线程化
主题 代表编号 一句话
禁迁移 0010 可抢占但钉在本 CPU
highmem 0048, 0062 local 映射;atomic 改 migrate_disable
RCU 0071, 0072, 0073 BOOST;强制 rcuc;禁 post-boot expedited
timer 0067 IRQSAFE 删定时器不能睡 expiry_lock
printk 0099, 0105 write_atomic;每 console 一打印线程
softirq 0131, 0149 唤醒 ksoftirqd;BH 可抢占
锁 0170~0175, 0228 spinlock=rt_mutex;补 RCU+migrate
local_lock 0194, 0207 per-CPU 睡锁原语
cpu_chill 0234 重试环让出,防活锁
lazy 0258 仅 CFS↔CFS 推迟抢占
架构 0256, 0273, 0274, 0280 选 ARCH_SUPPORTS_RT
标识 0286, 0287 /sys/kernel/realtime,-rt164
irq_work 0308 HARD vs irq_workd

其余大量补丁是 mm、drm、net、tty、block 等服从上表机制的适配,不发明新规则。


4. 锁子系统(核心)

4.1 类型定义:spinlock_t 内嵌 rt_mutex

补丁: 0170-locking-rtmutex-add-sleeping-lock-implementation.patch

新文件: include/linux/spinlock_types_rt.h、include/linux/spinlock_rt.h

实现: kernel/locking/rtmutex.c 增补 rt_spin_*

c 复制代码
/* PREEMPT_RT: spinlocks - an RT mutex plus lock-break field */
typedef struct spinlock {
	struct rt_mutex		lock;
	unsigned int		break_lock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map	dep_map;
#endif
} spinlock_t;
c 复制代码
#if !defined(CONFIG_PREEMPT_RT)
#define PREEMPT_LOCK_OFFSET	PREEMPT_DISABLE_OFFSET
#else
#define PREEMPT_LOCK_OFFSET	0   /* 持锁不再抬 preempt_count */
#endif
非 RT spinlock_t RT spinlock_t
存储 arch 自旋票/票据等 struct rt_mutex
竞争 忙等 可睡眠等待 + PI
持锁 关抢占 可被更高优先级抢占
might_sleep 持锁路径不应睡 获取路径调用 might_sleep_no_state_check()
原子语境 可用 禁止(硬中断/关中断/NMI)

为何仍叫 spinlock: API 名 spin_lock() 不变,海量驱动不用改标识符;语义却从「自旋」变为「可睡互斥」。这是源码兼容、契约不兼容。

raw_spinlock_t: 真正自旋,仍关抢占。调度器、IRQ 芯片、rt_mutex 内部的 wait_lock 等必须用 raw。驱动把大段业务塞进 raw = 亲手制造无界延迟。

break_lock: 长临界区可标记/参与 lock-break,让出给更高优先级,缓解「虽可睡但仍长时间占着逻辑锁」的问题。

4.2 加锁快慢路径(源码级)

c 复制代码
static inline void rt_spin_lock_fastlock(struct rt_mutex *lock,
					 void (*slowfn)(struct rt_mutex *lock))
{
	might_sleep_no_state_check();

	if (likely(rt_mutex_cmpxchg_acquire(lock, NULL, current)))
		return;          /* 快路径:无人持有,直接占有 */
	else
		slowfn(lock);    /* 慢路径:排队、PI、可能 schedule */
}

慢路径里还有 adaptive spinning :若持锁者正在别的 CPU 上跑(owner->on_cpu),先短自旋;持锁者已睡眠再真正睡。这保留了一部分「锁争用短」时的吞吐,但不改变「可以睡」的语义。

might_sleep_no_state_check():允许在「任务 state 已被特殊用途占用」时仍做睡眠注解/重调度检查------睡锁获取路径需要,而不能用普通 might_sleep() 的全部检查。

4.3 加锁后必须补齐的两件事(0228)

补丁: 0228-locking-Make-spinlock_t-and-rwlock_t-a-RCU-section-o.patch

非 RT:持 spinlock ⇒ 关抢占 ⇒ 隐含 RCU 读侧。

大量代码在 spin_lock() 区内当 RCU 读者用,自己不再写 rcu_read_lock()。

RT:持锁可抢占 ⇒ 隐含消失 ⇒ 必须显式补上,并钉住 CPU:

c 复制代码
void __lockfunc rt_spin_lock(spinlock_t *lock)
{
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	rt_spin_lock_fastlock(&lock->lock, rt_spin_lock_slowlock);
	rcu_read_lock();       /* 补偿隐含 RCU 读侧 */
	migrate_disable();     /* 补偿「像关抢占一样钉在本 CPU」的 per-CPU 假设 */
}

void __lockfunc rt_spin_unlock(spinlock_t *lock)
{
	spin_release(&lock->dep_map, _RET_IP_);
	migrate_enable();
	rcu_read_unlock();
	rt_spin_lock_fastunlock(&lock->lock, rt_spin_lock_slowunlock);
}

rwlock 的读写锁路径同样补 rcu_read_lock + migrate_disable(同补丁改 rwlock-rt.c)。

三件套(必须能默写):

复制代码
拿到 RT spinlock = rt_mutex 语义 + rcu_read_lock + migrate_disable

铁律:

复制代码
已处于:关中断 / 硬中断 / 关抢占 / RCU 读侧(含持 RT spinlock)
    ↓
禁止再:spin_lock(普通 spinlock_t)   ← 可能 schedule
    ↓
结果:scheduling while atomic

下一层: RT 持锁时中断往往仍开着,irqs_disabled() 为假;in_atomic() 也可能为假;但 rcu_preempt_depth() 因上面的 rcu_read_lock 非零。凡「能不能睡/能不能 schedule_timeout」的判定,必须把 RCU depth 算进去,不能只查 irq/atomic。

4.4 spin_lock_irq 在 RT 上的语义翻转(极易踩坑)

0170 的 spinlock_rt.h:

c 复制代码
#define spin_lock(lock)			rt_spin_lock(lock)
#define spin_lock_irq(lock)		spin_lock(lock)
#define spin_unlock_irq(lock)		spin_unlock(lock)

#define spin_lock_bh(lock)			\
	do {					\
		local_bh_disable();		\
		rt_spin_lock(lock);		\
	} while (0)
API 非 RT RT
spin_lock_irq 关中断 + 自旋 只做睡锁,不关中断
spin_lock_irqsave 保存/关中断 + 自旋 睡锁路径;flags 语义变为占位(与 local_lock 的 flags=0 同类思路)
spin_lock_bh 关 BH + 自旋 关 BH + 睡锁(BH 关闭本身在 RT 上也可抢占,见 §7)

背后知识点: 非 RT 下「外面 local_irq_disable,里面 spin_lock」≈ spin_lock_irq。

RT 下二者不再等价 :外面关中断后里面睡锁 = 非法。必须改成真正的 spin_lock_irq* API(在 RT 上它不再关中断,但表达了「锁与中断上下文关系已按 RT 规则处理」),或取消不必要的 irq-off。

4.5 mutex / rwsem / rwlock 接线

补丁 内容
0172 mutex_rt.h / mutex-rt.c:mutex 基于 rtmutex
0173 rwsem 基于 rtmutex
0174 rwlock 基于 rtmutex(bias 方案,类 rwsem)
0175 #ifdef CONFIG_PREEMPT_RT 头文件分流;构建改链 mutex-rt.o 等;PREEMPT_RT select RT_MUTEXES
c 复制代码
#ifdef CONFIG_PREEMPT_RT
# include <linux/mutex_rt.h>
#else
/* 原 mutex 实现 */
#endif

rwsem 与 PI 的深层矛盾(0173):

写者无法在「多个读者」上同时阻塞并继承每一个人的优先级/带宽。早期限制单读者会痛(mmap 读路径、精心规避死锁的驱动)。本系列 rwsem 策略要点:

  • 允许并发读者
  • 写者等到最后一个读者离开;这段等待不对多读者做 PI
  • 读者被写者挡住时,仍按常規对写者做 PI

代价:可能对写者不公平(写者饥饿)。典型 RT 负载很少依赖会饿死写者的 mmap_sem 写路径;真正的 RT 任务也不该走 mmap() 写锁这类无界延迟的 syscall。

4.6 PI 等待链(锁、futex、RCU boost 同源)

复制代码
任务 Hi 阻塞在 rt_mutex
  → 内核把 Hi 链到持有者 Lo 的 PI 等待树
  → Lo 的有效优先级抬到 max(自身, 等待者)
  → Lo 若又阻塞在另一把 rt_mutex,优先级沿链传播

同一骨架服务:内核睡锁、用户态 FUTEX_LOCK_PI、RCU_BOOST。理解 PI 链,才能理解「为何 RCU 读侧被抢占过久要用 boost 拉起来」。


5. migrate_disable

5.1 接口与计数

补丁: 0010-sched-Add-migrate_disable.patch(后续 0011~0019、0046、0299 等修 affinity / hotplug / 与 RT-DL 平衡)

c 复制代码
void migrate_disable(void)
{
	if (current->migration_disabled++)
		return;
	barrier();
	/* 首次进入:与唤醒路径配合,把任务钉在当前 CPU */
}

void migrate_enable(void)
{
	struct task_struct *p = current;
	if (--p->migration_disabled)
		return;
	barrier();
	if (p->cpus_ptr == &p->cpus_mask)
		return;
	__set_cpus_allowed_ptr(p, &p->cpus_mask, SCA_MIGRATE_ENABLE);
}

嵌套计数:多次 disable 需配对 enable。首次进入与末次退出才改亲和/指针。

5.2 与 preempt_disable 对比

preempt_disable migrate_disable
抢占 禁止 允许
迁移到其它 CPU 一并禁止 禁止
区内拿 RT spinlock_t 非法 合法
对延迟 直接造成不可抢占窗口 不直接关抢占,但可造成「多任务堆单核」

5.3 为何「强烈不想要却必须有」(补丁头注释)

理论损害:

SCHED_FIFO / SCHED_DEADLINE 假定:有足够可运行任务时,M 个 CPU 跑 M 个最高优先级任务 (work-conserving)。

migrate_disable 可嵌套且不 等于优先级天花板:任务在禁迁移区被抢占后,更高优先级任务继续进来,最终可能全部堆在同一 CPU。最坏可用带宽塌成单核;实时可调度性分析被迫退回单 CPU。

为何仍引入:

PREEMPT_RT 让大量原语可抢占 ⇒ 也可迁移 ⇒ 打破「per-CPU 数据只在本 CPU 碰」的隐含假设。migrate_disable 是恢复钉核的过渡手段。正确长期方向:显式 per-CPU 锁,或极短 preempt_disable,最终消灭 migrate_disable。

实现约束: disable/enable 都不能阻塞 。不能用 cpus_read_lock 简单对抗热插拔;enable 时若有 pending 亲和掩码变更,迁移逻辑极刁钻(补丁内注释甚至写 smells like something putrid,后续补丁持续修)。

5.4 谁在用

  • 每一个 RT rt_spin_lock 成功路径(§4.3)
  • local_lock(§6)
  • kmap_atomic 在 RT 上(§10)
  • 显式改写:如 0202 drain_local_pages_wq 用 migrate_disable 替代 preempt_disable

6. local_lock

6.1 数据结构与 RT 语义

补丁: 0194-rt-Add-local-irq-locks.patch

文件: include/linux/local_lock_internal.h

c 复制代码
#ifdef CONFIG_PREEMPT_RT
typedef struct {
	spinlock_t              lock;     /* 即 rt_mutex 睡锁 */
	struct task_struct      *owner;
	int                     nestcnt;
} local_lock_t;

static inline void local_lock_acquire(local_lock_t *l)
{
	if (l->owner != current) {
		spin_lock(&l->lock);
		l->owner = current;
	}
	l->nestcnt++;
}
c 复制代码
#define __local_lock(lock)					\
	do {							\
		migrate_disable();				\
		local_lock_acquire(this_cpu_ptr(lock));		\
	} while (0)

#define __local_lock_irqsave(lock, flags)			\
	do {							\
		migrate_disable();				\
		flags = 0;   /* RT:不再真正关中断 */		\
		local_lock_acquire(this_cpu_ptr(lock));		\
	} while (0)
构建 local_lock / local_lock_irq*
非 RT ≈ preempt_disable / local_irq_disable
RT 本 CPU 睡锁 + migrate_disable;不关中断;同任务可递归

6.2 解决什么死结

复制代码
旧:local_irq_save → 访问 this_cpu 变量 → 区内再 spin_lock(spinlock_t)
RT:关中断后 spin_lock 可睡 → 非法
新:local_lock_irqsave → 同一串行化,但可睡、可抢、钉核

6.3 落地例:memcg

0207-mm-memcontrol-Provide-a-local-lock-for-per-CPU-memcg.patch

diff 复制代码
 struct memcg_stock_pcp {
+	local_lock_t lock;
 	...
 };

-	local_irq_save(flags);
+	local_lock_irqsave(&memcg_stock.lock, flags);
 	/* 操作 per-CPU stock */
-	local_irq_restore(flags);
+	local_unlock_irqrestore(&memcg_stock.lock, flags);

同类:0276 PowerPC pseries IOMMU 的 per-CPU TCE 页指针。

6.4 关中断 / 锁选型决策树

复制代码
这段代码为何关中断?
│
├─ 只为减抖、注释写明非同步
│     → RT 上直接去掉 local_irq_disable(例:0251 i915)
│
└─ 为保护数据
      ├─ 普通跨 CPU 共享
      │     → spin_lock_irq*(注意 RT 上 irq 变体不再关中断)
      ├─ 本 CPU per-CPU 数据
      │     → local_lock* (0194 语义)
      └─ 调度器/IRQ 芯片级极短硬路径
            → raw_spinlock_t,并保证临界区极短

7. 中断、softirq、tasklet、irq_work

7.1 硬中断 vs 线程化 IRQ

复制代码
非 RT 倾向:
  硬件中断 → 硬中断处理函数里做较多工作 → softirq → 用户任务

RT 倾向:
  硬件中断 → 极短硬入口(ack、唤醒线程)→ irq/NNN-* 线程(FIFO)
            → 与实时任务按优先级竞争

启动参数 threadirqs: 强制多数 IRQ 走线程(force_irqthreads)。

睡锁要求:中断重活不能在硬中断上下文跑------线程化是锁模型的前提。

0069-genirq-Move-prio-assignment-into-the-newly-created-t.patch:

把 sched_set_fifo(current) 挪到新 IRQ 线程刚跑起来时执行,而不是在创建路径持着锁设优先级------避免 lockdep 报告的锁顺序环(nouveau / cpuset / mmap 一类)。

下一层: 线程化不是消灭硬中断;硬入口仍必须短。延迟预算里要单独计量「硬入口最长」与「IRQ 线程被更高优先级挡住」。

7.2 softirq:可抢占的 BH 与 ksoftirqd

0149-softirq-Make-softirq-control-and-processing-RT-aware.patch

c 复制代码
struct softirq_ctrl {
	local_lock_t	lock;
	int		cnt;
};
static DEFINE_PER_CPU(struct softirq_ctrl, softirq_ctrl);

void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
	WARN_ON_ONCE(in_hardirq());

	/* 任务首次进入 BH 关闭区:拿 softirq local_lock + rcu_read_lock */
	if (!current->softirq_disable_cnt) {
		if (preemptible()) {
			local_lock(&softirq_ctrl.lock);
			rcu_read_lock();
		}
	}
	newcnt = __this_cpu_add_return(softirq_ctrl.cnt, cnt);
	current->softirq_disable_cnt = newcnt;  /* 任务侧映像,供 softirq_count() */
}

要点:

  1. per-CPU softirq_ctrl.cnt + 任务 softirq_disable_cnt: BH 关闭区可被抢占;per-CPU 计数避免无意义地唤醒 ksoftirqd。
  2. local_lock 序列化: 同一 CPU 上 BH 关闭与 softirq 处理互斥,但可睡、可抢。
  3. 首次进入补 rcu_read_lock: 满足 RCU bottom-half 相关假设。

0131-smp-Wake-ksoftirqd-on-PREEMPT_RT-instead-do_softirq.patch:

diff 复制代码
- do_softirq();
+ wake_up_process(__this_cpu_read(ksoftirqd));

例如 migration_cpu_stop 等路径:RT 无「在当前上下文直接 __do_softirq」,改为唤醒 ksoftirqd,把何时跑 softirq 交给调度器。

另有 0220 等:RT 上禁用 softirq 专用栈(与可抢占模型一致)。0124 把 softirq 相关代码聚拢,便于套 RT 分支。

7.3 tasklet

tasklet 挂在 softirq 上,继承 §7.2 的约束。

RT 目标:tasklet_disable() 可睡眠。仍卡在原子语境的调用点用 tasklet_disable_in_atomic() 过渡(例:0141 ath9k 在 spin_lock_bh 路径上)。

7.4 irq_work:HARD 与线程

0308-irq_work-Handle-some-irq_work-in-a-per-CPU-thread-on.patch

类型 宏/标志 路径 用途
硬 IRQ_WORK_INIT_HARD / IRQ_WORK_HARD_IRQ 硬中断上下文 NO_HZ 等不可睡
懒/默认 无 HARD 标志 per-CPU irq_workd 可睡锁、kfree

把「可能睡」的工作从硬路径拆出,避免 irq_work 回调里踩睡锁铁律。


8. RCU

8.1 强制 rcuc 线程

0072-rcu-Unconditionally-use-rcuc-threads-on-PREEMPT_RT.patch

c 复制代码
static bool use_softirq = !IS_ENABLED(CONFIG_PREEMPT_RT);
#ifndef CONFIG_PREEMPT_RT
module_param(use_softirq, bool, 0444);
#endif
  • 非 RT:可用 RCU_SOFTIRQ,也可用引导参数 rcutree.use_softirq=0 改走 rcuc。
  • RT:强制 use_softirq=false,参数不可覆盖。

原因: softirq 里跑 RCU 核心/回调,会在「实时代码执行期间到来的中断」返回时插入,打穿时间关键段。rcuc 线程把控制权还给调度器。

8.2 默认 RCU_BOOST

0071-rcu-Make-RCU_BOOST-default-on-CONFIG_PREEMPT_RT.patch

kconfig 复制代码
config RCU_BOOST
	depends on (RT_MUTEXES && PREEMPT_RCU && RCU_EXPERT) || PREEMPT_RT
	default y if PREEMPT_RT

PREEMPT_RT 下读者可被抢占、rcuc 也可被抢占 → grace period 拖很久 → 回调堆积 → 内存耗尽。BOOST:对挡住当前宽限期的抢占读者抬优先级(rt_mutex PI 族)。

8.3 boot 后禁止 expedited

0073-rcu-Enable-rcu_normal_after_boot-unconditionally-for.patch

synchronize_rcu_expedited() 向非 idle CPU 发 IPI,干扰实时任务。RT 上等价强制 rcu_normal_after_boot:boot 之后 expedited 当普通 synchronize_rcu()。boot 早期仍可用 expedited(实时应用通常尚未跑)。

8.4 两维对照

机制 解决什么
0228 持锁时 rcu_read_lock 语义:补齐「自旋锁隐含读侧」
rcuc + BOOST + normal_after_boot 进度与干扰:回调谁跑、宽限期会不会饿死、会不会 IPI 扫核

9. 定时器

0067-timers-Don-t-block-on-expiry_lock-for-TIMER_IRQSAFE.patch

RT 上删除/同步等待定时器时,可能在 expiry_lock 上睡眠(与睡锁模型一致)。

TIMER_IRQSAFE 定时器允许在 IRQ 上下文运行回调,删除常在 IRQ-off 路径 → 不能睡 → 跳过对 expiry_lock 的阻塞等待。

复制代码
删定时器时问:
  当前能不能睡?
    ├─ 能 → 可走 expiry_lock 等待跑完
    └─ 不能(IRQSAFE + irq-off)→ 不能阻塞在 expiry_lock

hrtimer: RT 上多数可在 softirq 上下文跑;必须硬上下文的用 HRTIMER_MODE_*_HARD。

0300:wait_task_inactive 改用 HRTIMER_MODE_REL_HARD,避免 boot 阶段依赖尚未就绪的 softirq/ksoftirqd 导致死锁。


10. highmem / kmap

10.1 kmap_local(0048)

映射索引存在 task_struct。调度出去时卸映射,调度进来时恢复。

  • 用 migrate_disable 保持「返回的虚拟地址仍指向本 CPU 上的映射」
  • 不 禁抢占、不禁缺页
  • 可从可抢占或原子语境调用

不能无脑替换全部 kmap_atomic:许多调用点依赖「关抢占」串行化,或依赖隐含 pagefault_disable。

10.2 kmap_atomic 在 RT(0062)

c 复制代码
static inline void *kmap_atomic(struct page *page)
{
	if (IS_ENABLED(CONFIG_PREEMPT_RT))
		migrate_disable();
	else
		preempt_disable();
	pagefault_disable();
	return __kmap_local_page_prot(page, kmap_prot);
}
属性 非 RT kmap_atomic RT kmap_atomic
抢占 禁止 允许(只禁迁移)
缺页 禁止 仍禁止
区内睡锁 非法 可以(这正是改动动机)

11. printk / console

11.1 无界延迟从哪来

复制代码
printk → 进入控制台路径 → 持锁 → UART 逐字节轮询

串口慢、持锁时间无上界 → 一次日志可制造毫秒级尖刺。实时内核不能让「调试打印」变成最坏延迟来源。

11.2 write_atomic(0099)

c 复制代码
struct console {
	void (*write)(struct console *, const char *, unsigned);
	void (*write_atomic)(struct console *co, const char *s, unsigned int count);
};
  • write:可睡(8250 拿 port 锁)
  • write_atomic:NMI/panic 等要求有界、原子;配合 console_atomic_lock(cpulock:按 CPU 可重入)
  • 0100:8250 实现 atomic 写

11.3 每 console 一个 kthread(0105)

c 复制代码
struct console {
	atomic64_t printk_seq;
	struct task_struct *thread;
};
  • 普通打印:异步到该 console 的线程,不再 在 console_unlock 里同步刷屏
  • console_lock/unlock:回归真正的加锁/解锁
  • 例外:boot console;kernel 进入 sync 模式且存在 atomic console 时

同系列约 0080~0108:去掉/削弱 logbuf_lock、限制递归、改造 syslog/kmsg_dump、引入 sync 模式等,目标都是去掉打印路径上的无界临界区。


12. lazy preemption 与 cpu_chill

12.1 lazy preemption(0258 + 各 arch)

问题: 非 RT 持自旋锁隐含关抢占,wakee 不会立刻抢走 waker。

RT 上锁可抢占:SCHED_OTHER 的 A 唤醒 SCHED_OTHER 的 B,B 立刻抢占 A,而 A 还握着 B 马上要抢的锁 → B 立刻再睡 → 抖动 + 吞吐崩。

做法:

  • 任务增加 preempt_lazy_count(与 migrate_disable 等耦合增减)
  • 仅 SCHED_OTHER 抢占 SCHED_OTHER 时置 TIF_NEED_RESCHED_LAZY,让 waker 先走出持锁区
  • SCHED_FIFO/RR/DEADLINE 仍置普通 NEED_RESCHED,立即抢占

默认开启;CONFIG_SCHED_DEBUG 下可通过 sched_feature NO_PREEMPT_LAZY / PREEMPT_LAZY 开关。

架构入口:0260 x86、0261 arm、0262 powerpc、0263 arm64。

12.2 cpu_chill(0234)

非 RT 重试环:cpu_relax() 空转。

RT:修改方可能被抢占,空转方永远等不到 → 活锁。

c 复制代码
/* 非 RT:cpu_relax();RT:短睡,让出 CPU */
void cpu_chill(void);

实现演变知识点:

  1. 初版类似 msleep → 依赖 TIMER softirq;若在 softirq 里 chill 可能等不到 ksoftirqd → 死锁
  2. 改 hrtimer / schedule_hrtimeout
  3. 注意信号与 nanosleep restart block,避免脏用户指针
  4. PF_NOFREEZE 等,避免冻结路径持锁睡觉报 BUG

13. 架构使能与运行时标识

13.1 Kconfig 门闩

PREEMPT_RT 依赖 EXPERT && ARCH_SUPPORTS_RT。

补丁 作用
0256 x86 允许 RT
0272 x86 32-bit 亦允许
0273 ARM 允许
0274 ARM64:select ARCH_SUPPORTS_RT if HAVE_POSIX_CPU_TIMERS_TASK_WORK
0280 PowerPC 允许

ARM64 还涉及 HAVE_POSIX_CPU_TIMERS_TASK_WORK(与 KVM 等条件相关)。

仅打开 ARCH_SUPPORTS_RT 不够:还需 lazy 入口、信号路径(如 0321)、FPU/KVM 限制等 per-arch 补丁,运行时才稳。

13.2 标识

0286:

c 复制代码
#if defined(CONFIG_PREEMPT_RT)
static ssize_t realtime_show(...)
{
	return sprintf(buf, "%d\n", 1);
}
KERNEL_ATTR_RO(realtime);
#endif

/sys/kernel/realtime 仅 RT 构建存在,值恒为 1。

0287 + rebase: localversion-rt → uname -r 含 -rt164。


14. 驱动适配:完整范式与源码例

14.1 范式总表

范式 做法 代表
A. 去掉减抖 irq-off if (!IS_ENABLED(CONFIG_PREEMPT_RT)) local_irq_disable() 0251 i915
B. irq-off 与 spin 拆开 → 一体 API spin_lock_irqsave 0266 omap serial
C. per-CPU irq-off → local_lock local_lock_irqsave 0207 memcg、0276 iommu
D. atomic map → local map io_mapping_map_local_wc 等 0059 i915
E. 强制线程化后少关中断 force_irqthreads 则不必 irqsave 0254 i915 gt
F. 不安全则关功能 RT 上禁 tracepoint 0252/0253 i915
G. 真正短更新改 raw stat_lock 等 0112 shmem
H. bit spinlock → 睡锁 zram 等 0281

14.2 例 A:0251 i915 原子更新

diff 复制代码
 void intel_pipe_update_start(...)
 {
-	local_irq_disable();
+	if (!IS_ENABLED(CONFIG_PREEMPT_RT))
+		local_irq_disable();
 	...
-		local_irq_enable();
+		if (!IS_ENABLED(CONFIG_PREEMPT_RT))
+			local_irq_enable();
 		timeout = schedule_timeout(timeout);  /* 睡眠点 */
-		local_irq_disable();
+		if (!IS_ENABLED(CONFIG_PREEMPT_RT))
+			local_irq_disable();
 }

补丁说明:关中断是为避免随机延迟,不是 保护/同步;区内会拿 spinlock_t。RT 上必须去掉。注意 schedule_timeout:关中断时本就不能睡,RT 路径保持开中断才合法。

14.3 例 B:0266 omap serial console

diff 复制代码
-	local_irq_save(flags);
-	spin_lock(&up->port.lock);
+	spin_lock_irqsave(&up->port.lock, flags);
 	...
-	spin_unlock(&up->port.lock);
-	local_irq_restore(flags);
+	spin_unlock_irqrestore(&up->port.lock, flags);

注释要点:锁在 RT 上是睡锁;local_irq_save + 单独 spin_lock 不是正确优化。

14.4 例 E:0254 i915 timeline

force_irqthreads 下 timeline 锁不会在硬 IRQ 里观察到 → 不必 local_irq_save。按「是否强制线程化」分支,避免无脑 irq-off。

14.5 例 F:trace

RT 上 trace 求参若调用会拿 spinlock_t 的路径,而 trace 点可能在关抢占语境 → 关 i915 相关 trace(0252/0253),用正确性换可观测性。


15. 机制如何合成为「可测的最坏延迟」

复制代码
┌─────────────────┐   ┌──────────────────┐   ┌────────────────────┐
│ 锁与 per-CPU    │   │ 中断与 BH        │   │ 进度与无界清零     │
│ 睡锁+PI+raw分界 │   │ IRQ 线程化       │   │ rcuc+BOOST         │
│ migrate_disable │   │ softirq→ksoftirqd│   │ 禁 expedited IPI   │
│ local_lock      │   │ irq_work 分流    │   │ printk 异步+atomic │
│                 │   │ tasklet 服从 BH  │   │ kmap 可与睡锁共存  │
│                 │   │ lazy 不碰 FIFO   │   │ cpu_chill 破活锁   │
└────────┬────────┘   └────────┬─────────┘   └─────────┬──────────┘
         └─────────────────────┴───────────────────────┘
                               ↓
                 最坏调度延迟:可测、可回归、可针对性缩短

部署层(绑核、isolcpus、关 idle 状态等)不改变上述规则,只减少干扰源;规则本身由 PREEMPT_RT + 本补丁保证。


16. 应用与确认

bash 复制代码
# 1. 源码树:v5.10.268
# 2. 打补丁
xzcat patches/early-5.10/patch-5.10.268-rt164.patch.xz | patch -p1
# 或:按 series/series 逐个 patch -p1 -i <patch>

# 3. 配置
#    CONFIG_EXPERT=y
#    架构已 ARCH_SUPPORTS_RT
#    CONFIG_PREEMPT_RT=y

# 4. 启动参数(常用)
#    threadirqs

# 5. 确认
uname -r                  # 应含 -rt164
cat /sys/kernel/realtime  # 1

用 cyclictest 等测量 worst-case;本补丁不含测试程序。


17. 知识点完备性对照

# 知识点 必须掌握的下一层 锚点
1 调度延迟/抖动 看 worst-case;来源在内核窗口 §1.1
2 抢占 vs 睡眠 决定能否用睡锁 §1.2
3 优先级反转 PI 链、rt_mutex 同源 §1.3 §4.6
4 非 RT spinlock 关抢占、忙等、隐含 RCU §2
5 spinlock=rt_mutex raw 分界、break_lock、API 名 §4.1 0170
6 快慢路径 cmpxchg、adaptive spin、might_sleep §4.2
7 持锁三件套 rcu_read_lock+migrate_disable §4.3 0228
8 铁律 + rcu depth 不能只查 irqs_disabled/in_atomic §4.3
9 spin_lock_irq 退化 RT 上不再关中断 §4.4
10 rwsem 与 PI 多读者 vs 写者继承;写者不公平可能 §4.5 0173
11 migrate_disable ≠ preempt;损 work-conserving;不能阻塞 §5 0010
12 local_lock 替 irq-off;flags=0;可递归 §6 0194
13 锁选型决策树 减抖/共享/per-CPU/raw §6.4
14 IRQ 线程化 threadirqs;prio 在线程内设 §7.1 0069
15 softirq_ctrl per-CPU+task 计数;local_lock;rcu §7.2 0149
16 禁直接 do_softirq 改唤醒 ksoftirqd §7.2 0131
17 tasklet softirq 客户;disable 可睡化 §7.3
18 irq_work HARD vs irq_workd §7.4 0308
19 rcuc 强制 禁 RCU_SOFTIRQ 插延迟 §8.1 0072
20 RCU_BOOST 防 GP 饿死→OOM §8.2 0071
21 normal_after_boot 禁 expedited IPI §8.3 0073
22 TIMER_IRQSAFE 删定时器能否睡 §9 0067
23 hrtimer HARD boot/原子路径 §9 0300
24 kmap_local/atomic 禁迁移≠禁抢占;仍禁缺页 §10
25 printk 异步线程 + write_atomic + cpulock §11
26 lazy preempt 仅 OTHER↔OTHER;FIFO 立即抢 §12.1 0258
27 cpu_chill 破活锁;忌 softirq 死锁 §12.2 0234
28 ARCH_SUPPORTS_RT 开门≠跑稳 §13
29 sysfs/localversion 确认 RT 构建 §13.2
30 驱动八范式 A~H 对应机制 §14

任一行说不清「下一层」或对不上补丁号:打开 patches/early-5.10/series/<编号>-*.patch,对照该节源码再读一遍。

相关推荐
螺蛳粉 螺蛳粉1 小时前
第五篇:Keepalived + LVS 四层负载均衡高可用实战:DR 模式全流程
运维·负载均衡·lvs·keepalived·高可用
Crazy________1 小时前
Linux内核核心机制全解析:从驱动到进程启动
运维
远牧1 小时前
Ubuntu 26 升级踩坑实录:hermes-agent venv 重建全过程
linux·ubuntu·ai·agent
眼不痛请看我1 小时前
Ubuntu_22.04_LTS虚拟机安装指南
linux·数据库·ubuntu
哎呦,帅小伙哦1 小时前
查看网卡配置连接工具组合——nmcli + nmtui
linux
xiaoye-duck1 小时前
《Linux 网络编程》从 0 手写 Reactor 反应堆(上):拆解 Reactor 架构 —— 连接抽象、Poller 封装与事件派发核心
linux·网络
脚踏实地,坚持不懈!1 小时前
Android 电池健康度检测全流程(驱动 → 内核 → HAL → Framework)
android·linux
IT技术分享社区1 小时前
在 Windows 上跑未改动的 Linux 程序:微软 LiteBox v0.1 把这件事变简单了
linux·运维·windows·microsoft·开源
Saniffer_SH1 小时前
【高清视频】一块 SSD,两条 PCIe 链路:Dual-Port NVMe SSD测试环境怎么搭?(一)
服务器·ssd测试·dual-port ssd·企业级ssd测试·dualport·ssd测试环境·dualport ssd测试