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 实际做了什么
preempt_count加上PREEMPT_LOCK_OFFSET(通常等于关抢占)- 忙等直到拿到锁(持锁者在别的 CPU 时空转;同 CPU 因关抢占不会出现「持锁者被抢走」)
- 持锁期间禁止睡眠
同 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)- 显式改写:如
0202drain_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() */
}
要点:
- per-CPU
softirq_ctrl.cnt+ 任务softirq_disable_cnt: BH 关闭区可被抢占;per-CPU 计数避免无意义地唤醒 ksoftirqd。 local_lock序列化: 同一 CPU 上 BH 关闭与 softirq 处理互斥,但可睡、可抢。- 首次进入补
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);
实现演变知识点:
- 初版类似
msleep→ 依赖 TIMER softirq;若在 softirq 里 chill 可能等不到 ksoftirqd → 死锁 - 改 hrtimer /
schedule_hrtimeout - 注意信号与 nanosleep restart block,避免脏用户指针
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,对照该节源码再读一遍。