基于 Linux Kernel 源码深度分析(Linux 6.12+) 路径:
kernel/locking/|kernel/irq/|include/linux/preempt.h|include/linux/spinlock_rt.h
- 实时内核背景
- 内核抢占模型对比
- RT spinlock:rtmutex 替换
- rtmutex 与优先级继承
- 中断线程化
- Softirq 线程化与 local_bh_disable 语义变化
- RCU 在 RT 下
- 内存分配与实时性
- 高精度定时器与 tickless
- CPU 隔离
- 实时调度类
- 实时内核测试与延迟追踪
- PREEMPT_RT 合入主线进展
- 关键文件目录索引
实时系统的核心指标不是吞吐量,而是有界延迟(Bounded Latency)——即系统对外部事件做出响应的最坏情况时间(Worst-Case Response Time,WCRT)。
| 应用场景 | 可接受最大延迟 | 典型频率 |
|---|---|---|
| 工业运动控制 | < 100 μs | 1 kHz~10 kHz |
| 专业音频处理 | < 1 ms | 48 kHz |
| 机器人关节控制 | < 500 μs | 2 kHz |
| 汽车线控系统 | < 1 ms | 1 kHz |
| 医疗成像触发 | < 100 μs | 随机 |
| 5G 基带处理 | < 500 μs | 随机 |
标准 Linux 内核(vanilla kernel)在负载较大时调度延迟可达数毫秒甚至数十毫秒,完全无法满足上述需求。
影响 Linux 实时性的延迟来源可以分为四大类:
+------------------------------------------------------------------+
| 延迟来源分类 |
| |
| +-----------------+ +-----------------+ +-----------------+ |
| | 1. 中断禁止时间 | | 2. 自旋锁持有 | | 3. 内存分配 | |
| | | | | | | |
| | local_irq_save | | raw_spin_lock | | kmalloc(GFP_ | |
| | 典型: 1~50μs | | 典型: 1~200μs | | KERNEL) | |
| | 最坏: >1ms(驱动)| | 长临界区不可抢占| | 可能触发页回收 | |
| +-----------------+ +-----------------+ +-----------------+ |
| |
| +-----------------+ +-----------------+ +-----------------+ |
| | 4. 调度延迟 | | 5. SMI 固件中断 | | 6. 缓存 miss | |
| | | | | | | |
| | tick 粒度限制 | | BIOS 管理中断 | | TLB miss/ | |
| | 优先级反转 | | 完全不可见 | | cache eviction | |
| | 带宽限制节流 | | 典型: 10~200μs | | 典型: 1~50μs | |
| +-----------------+ +-----------------+ +-----------------+ |
+------------------------------------------------------------------+
中断禁止时间是最主要的延迟来源。当内核执行 local_irq_disable() 到 local_irq_enable() 之间时,任何中断(包括定时器)都无法触发,高优先级任务无法被唤醒。传统 spinlock 在持有期间会关闭抢占,形成另一个不可中断的时间窗口。
自旋锁持有时间的问题在于:标准 spin_lock() 关闭抢占后,即使高优先级任务已就绪,也必须等到 spinlock 释放才能运行。内核中有些 spinlock 临界区可能持续数百微秒(如网络栈的 socket 锁),直接导致 RT 任务的响应延迟飙升。
内存分配在 RT 上下文中尤其危险。kmalloc(GFP_KERNEL) 可能触发页面直接回收(direct reclaim),回收过程中会等待磁盘 I/O,延迟完全不可预测。页面错误(page fault)亦然——缺页处理可能等待 swap I/O,将实时任务阻塞数十毫秒。
调度延迟源于 Linux CFS 调度器的 tick 驱动模式:在默认配置(CONFIG_HZ=250)下,调度器每 4ms 检查一次是否需要切换任务。即使高优先级 RT 任务已就绪,最坏也要等待一个 tick 周期才被唤醒(若未使用 hrtimer)。
2004年 Ingo Molnar 与 Thomas Gleixner 开始 -rt 补丁开发
首批实现:RT mutex、优先级继承、中断线程化原型
2005年 补丁规模扩大,涵盖 spinlock 转换、softirq 线程化
首次在 Timesys、Red Hat 嵌入式产品中使用
2006年 hrtimer 子系统合入主线 (Linux 2.6.16)
这是 RT 补丁向主线迁移的第一块重要拼图
2009年 ftrace(函数追踪)合入主线,为延迟分析提供基础设施
2011年 NO_HZ(tickless kernel)改进,减少不必要的定时器中断
2021年 Linux 5.15 开始大批量 PREEMPT_RT 核心代码合入主线
- RT mutex 核心代码
- spinlock_rt.c 基础设施
2022年 Linux 6.1 (LTS)
- PREEMPT_RT Kconfig 选项正式出现
- softirq RT 路径合入
2023年 Linux 6.6 (LTS)
- 大量 RT 锁原语完善
- migrate_disable/enable 完善
2024年 Linux 6.12 (LTS) [重大里程碑]
PREEMPT_RT 完全合入主线,不再需要独立维护的外部补丁
Thomas Gleixner 在内核邮件列表宣告完成
Linux 内核支持多种抢占模型,通过 Kconfig 在编译时选择,抢占性越强,实时性越好,但代码复杂度也越高。
+-------------------+------------------------------------------+------------------+
| 配置选项 | 说明 | 典型最大延迟 |
+-------------------+------------------------------------------+------------------+
| CONFIG_PREEMPT_ | 不可抢占:内核代码运行时不可被打断 | 数十~数百 ms |
| NONE | 仅在系统调用返回时切换 | |
+-------------------+------------------------------------------+------------------+
| CONFIG_PREEMPT_ | 自愿抢占:在明确的抢占点 (cond_resched) | 数 ms~数十 ms |
| VOLUNTARY | 处检查是否需要调度 | |
+-------------------+------------------------------------------+------------------+
| CONFIG_PREEMPT | 完全抢占:持有自旋锁以外的任何内核代码 | 数百 μs~数 ms |
| (FULL) | 均可被抢占(自旋锁持有时仍关闭抢占) | |
+-------------------+------------------------------------------+------------------+
| CONFIG_PREEMPT_ | 懒惰抢占(6.6 新增):介于 FULL 和 RT | 数百 μs~2 ms |
| LAZY | 之间,减少不必要的抢占开销 | |
+-------------------+------------------------------------------+------------------+
| CONFIG_PREEMPT_ | 实时抢占:自旋锁转为睡眠锁,中断线程化 | < 100 μs (典型) |
| RT | 几乎所有内核代码均可被抢占 | < 20 μs (优化后) |
+-------------------+------------------------------------------+------------------+
内核用一个 32 位整数 preempt_count 跟踪当前执行上下文,定义于 include/linux/preempt.h。
include/linux/preempt.h,第 14-53 行
31 23-20 19-16 15-8 7-0
+-------+---------+--------+---------+---------+
| RESCD | NMI |HARDIRQ | SOFTIRQ | PREEMPT |
| (1bit)| (4 bits)|(4 bits)| (8 bits)| (8 bits)|
+-------+---------+--------+---------+---------+
PREEMPT_MASK: 0x000000ff -- 抢占禁用深度 (最大 256 层)
SOFTIRQ_MASK: 0x0000ff00 -- softirq 禁用深度
HARDIRQ_MASK: 0x000f0000 -- 硬中断嵌套层数
NMI_MASK: 0x00f00000 -- NMI 嵌套层数
PREEMPT_NEED_RESCHED: 0x80000000 -- 需要重新调度标志位
各字段的位宽由常量定义(include/linux/preempt.h,第 33-36 行):
#define PREEMPT_BITS 8
#define SOFTIRQ_BITS 8
#define HARDIRQ_BITS 4
#define NMI_BITS 4在 CONFIG_PREEMPT_RT 下,softirq 计数不再存放于全局 preempt_count,而改为存放于 current->softirq_disable_cnt(任务结构体中的 per-task 计数器),允许 softirq 禁用段被抢占。
include/linux/preempt.h,第 110-116 行
#ifdef CONFIG_PREEMPT_RT
# define softirq_count() (current->softirq_disable_cnt & SOFTIRQ_MASK)
# define irq_count() ((preempt_count() & (NMI_MASK | HARDIRQ_MASK)) | softirq_count())
#else
# define softirq_count() (preempt_count() & SOFTIRQ_MASK)
# define irq_count() (preempt_count() & (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_MASK))
#endif同理,in_task() 的判断也做了对应修改(第 129-133 行),使得持有 spinlock 的区段在 RT 下仍属于 task context,可以被调度器抢占:
#ifdef CONFIG_PREEMPT_RT
# define in_task() (!((preempt_count() & (NMI_MASK | HARDIRQ_MASK)) | in_serving_softirq()))
#else
# define in_task() (!(preempt_count() & (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET)))
#endifinclude/linux/preempt.h,第 155-160 行
#if !defined(CONFIG_PREEMPT_RT)
#define PREEMPT_LOCK_OFFSET PREEMPT_DISABLE_OFFSET
#else
/* Locks on RT do not disable preemption */
#define PREEMPT_LOCK_OFFSET 0
#endif这是 PREEMPT_RT 最核心的语义差异:在 RT 内核中,spin_lock() 不再增加 preempt_count,因此持有 spinlock 的任务完全可以被抢占。这一行改变了整个内核锁的语义。
include/linux/preempt.h,第 451-457 行
#define preempt_disable_nested() \
do { \
if (IS_ENABLED(CONFIG_PREEMPT_RT)) \
preempt_disable(); \
else \
lockdep_assert_preemption_disabled(); \
} while (0)这个宏用于那些在非 RT 内核中已经隐式禁用了抢占(因为持有 spinlock),但在 RT 内核中还需要显式禁用抢占的场景,例如 seqcount 写入临界区和 per-CPU 变量的 RMW 操作。
preempt.h 中包含大段注释(第 370-424 行)解释了 migrate_disable() 的设计哲学:
PREEMPT_RT 将若干原语变为可抢占,这同时允许了迁移(migration)。这破坏了大量 per-cpu 数据的访问假设。为此,所有这些原语都使用
migrate_disable()来恢复这一隐式假设。
migrate_disable() 是 spinlock 在 RT 下的"降级"替代物:
- 非 RT:
spin_lock()→preempt_disable()→ 禁止抢占 + 禁止迁移 - RT:
spin_lock()→migrate_disable()→ 仅禁止迁移,不禁止抢占
在 PREEMPT_RT 下,spinlock_t 的底层从一个简单的原子变量变为基于 rt_mutex_base 的睡眠锁。
include/linux/spinlock_rt.h,第 19-23 行
#define __spin_lock_init(slock, name, key, percpu) \
do { \
rt_mutex_base_init(&(slock)->lock); \
__rt_spin_lock_init(slock, name, key, percpu); \
} while (0)spin_lock_init() 最终初始化的是 rt_mutex_base,而非传统的原子计数器。
include/linux/spinlock_rt.h 提供了完整的 API 兼容层,使上层代码无需修改即可在 RT 下运行:
include/linux/spinlock_rt.h,第 42-130 行
// spin_lock 直接调用 rt_spin_lock
static __always_inline void spin_lock(spinlock_t *lock)
__acquires(lock)
{
rt_spin_lock(lock);
}
// spin_lock_irq 在 RT 下不禁止中断,直接调用 rt_spin_lock
static __always_inline void spin_lock_irq(spinlock_t *lock)
__acquires(lock)
{
rt_spin_lock(lock);
}
// spin_lock_irqsave 保存 flags 但实际上 flags 始终为 0
#define spin_lock_irqsave(lock, flags) \
do { \
typecheck(unsigned long, flags); \
flags = 0; \
spin_lock(lock); \
} while (0)
// spin_unlock
static __always_inline void spin_unlock(spinlock_t *lock)
__releases(lock)
{
rt_spin_unlock(lock);
}关键设计点:spin_lock_irqsave() 在 RT 下不再保存 IRQ 状态,而是将 flags 强制置为 0。这是因为在 RT 内核中,spinlock 不会禁止中断,IRQ 状态始终有效,无需保存/恢复。
kernel/locking/spinlock_rt.c,第 38-58 行
static __always_inline void __rt_spin_lock(spinlock_t *lock)
{
rtlock_might_resched();
rtlock_lock(&lock->lock); // 基于 rtmutex 的慢路径锁
rcu_read_lock(); // 替代原来 preempt_disable 对 RCU 的保护
migrate_disable(); // 替代原来 preempt_disable 对 per-CPU 数据的保护
}
void __sched rt_spin_lock(spinlock_t *lock) __acquires(RCU)
{
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
__rt_spin_lock(lock);
}
EXPORT_SYMBOL(rt_spin_lock);rtlock_lock() 的快路径(kernel/locking/spinlock_rt.c,第 38-44 行):
static __always_inline void rtlock_lock(struct rt_mutex_base *rtm)
{
lockdep_assert(!current->pi_blocked_on);
if (unlikely(!rt_mutex_cmpxchg_acquire(rtm, NULL, current)))
rtlock_slowlock(rtm); // 竞争时进入慢路径,睡眠等待
}kernel/locking/spinlock_rt.c,第 78-87 行
void __sched rt_spin_unlock(spinlock_t *lock) __releases(RCU)
{
spin_release(&lock->dep_map, _RET_IP_);
migrate_enable();
rcu_read_unlock();
if (unlikely(!rt_mutex_cmpxchg_release(&lock->lock, current, NULL)))
rt_mutex_slowunlock(&lock->lock);
}
EXPORT_SYMBOL(rt_spin_unlock);解锁顺序与加锁顺序相反:先 migrate_enable(),再 rcu_read_unlock(),最后释放底层 rtmutex。这保证了 per-CPU 数据访问和 RCU 保护的正确语义。
kernel/locking/spinlock_rt.c 开头注释(第 1-20 行)总结了 RT spinlock 与普通 rtmutex 的关键区别:
spinlocks and rwlocks on RT are based on rtmutexes, with a few twists to resemble the non RT semantics:
- Contrary to plain rtmutexes, spinlocks and rwlocks are state preserving. The task state is saved before blocking on the underlying rtmutex, and restored when the lock has been acquired. Regular wakeups during that time are redirected to the saved state so no wake up is missed.
即:RT spinlock 在阻塞等待时会保存任务的 state(TASK_RUNNING/TASK_INTERRUPTIBLE 等),获得锁后恢复原状态,不会丢失其他来源的唤醒信号。
RT rwlock 同样基于 rtmutex,通过 rwbase_rt.c 提供统一的读写锁基础设施:
kernel/locking/spinlock_rt.c,第 229-247 行
void __sched rt_read_lock(rwlock_t *rwlock) __acquires(RCU)
{
rtlock_might_resched();
rwlock_acquire_read(&rwlock->dep_map, 0, 0, _RET_IP_);
rwbase_read_lock(&rwlock->rwbase, TASK_RTLOCK_WAIT);
rcu_read_lock();
migrate_disable();
}
EXPORT_SYMBOL(rt_read_lock);
void __sched rt_write_lock(rwlock_t *rwlock) __acquires(RCU)
{
rtlock_might_resched();
rwlock_acquire(&rwlock->dep_map, 0, 0, _RET_IP_);
rwbase_write_lock(&rwlock->rwbase, TASK_RTLOCK_WAIT);
rcu_read_lock();
migrate_disable();
}
EXPORT_SYMBOL(rt_write_lock);+-------------------------+------------------------+------------------------+
| 特性 | 普通 spinlock | RT spinlock |
+-------------------------+------------------------+------------------------+
| 底层实现 | 原子变量 + 忙等 | rtmutex(睡眠锁) |
| 竞争时行为 | 自旋等待(占 CPU) | 睡眠阻塞(让出 CPU) |
| 是否禁止抢占 | 是(PREEMPT_LOCK) | 否(PREEMPT_LOCK=0) |
| 是否禁止中断 | spin_lock_irq 时禁止 | 永不禁止中断 |
| 优先级继承 | 无 | 有(通过 rtmutex PI) |
| API 兼容性 | 标准 | 完全兼容(宏替换) |
| 可在 RT 任务中使用 | 有延迟风险 | 安全,有界延迟 |
| lockdep 支持 | 完整 | 完整 |
+-------------------------+------------------------+------------------------+
include/linux/rtmutex.h,第 23-27 行
struct rt_mutex_base {
raw_spinlock_t wait_lock; // 保护本结构的原始自旋锁(不可睡眠)
struct rb_root_cached waiters; // 等待者红黑树(按优先级排序)
struct task_struct *owner; // 当前持有者(低位用于标志)
};完整的 rt_mutex 结构体:
struct rt_mutex {
struct rt_mutex_base rtmutex;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};owner 字段的低位编码了额外状态(kernel/locking/rtmutex.c,第 68-93 行):
owner 字段编码:
owner == NULL, bit0 == 0 --> 锁空闲,可快速获取(fast path cmpxchg)
owner == NULL, bit0 == 1 --> 锁空闲但有等待者正在尝试获取(过渡态)
owner == task_ptr, bit0 == 0 --> 锁被持有,可快速释放
owner == task_ptr, bit0 == 1 --> 锁被持有,且有等待者(HAS_WAITERS)
kernel/locking/rtmutex_common.h,第 32-59 行
// 辅助节点:在两棵红黑树中复用的排序键
struct rt_waiter_node {
struct rb_node entry;
int prio; // 等待者优先级
u64 deadline; // SCHED_DEADLINE 任务的截止时间
};
// 等待者控制结构(分配在被阻塞任务的内核栈上)
struct rt_mutex_waiter {
struct rt_waiter_node tree; // 挂入锁的等待者树(按 wait_lock 保护)
struct rt_waiter_node pi_tree; // 挂入锁持有者的 pi_waiters 树
struct task_struct *task; // 被阻塞的任务
struct rt_mutex_base *lock; // 阻塞在哪个锁上
unsigned int wake_state; // 唤醒状态
struct ww_acquire_ctx *ww_ctx; // WW mutex 上下文
};每个 rt_mutex_waiter 同时挂在两棵红黑树上:
rt_mutex_base.waiters(锁的等待者树)
|
+-- waiter1 (prio=90) <-- top waiter(最左节点)
+-- waiter2 (prio=80)
+-- waiter3 (prio=70)
task_struct.pi_waiters(锁持有者的 PI 等待者树)
|
+-- pi_waiter (来自最高优先级的等待者,用于 PI 计算)
PI 的核心问题是优先级反转(Priority Inversion):
任务优先级: H(高=90) > M(中=50) > L(低=10)
无 PI 的优先级反转:
t0: L(10) 持有 mutex
t1: H(90) 尝试获取 mutex --> 阻塞
t2: M(50) 抢占 L(因为 50 > 10)
M 长时间运行,不使用 mutex,但间接阻止了 L 释放锁
t3: L 无法运行 --> H 被 M 间接无限阻塞
结果: H 的等待时间 = L 剩余时间 + M 完整执行时间(无界!)
有 PI 的解决方案:
t1: H(90) 阻塞 --> PI 传播:L 的有效优先级提升为 90
t2: L(eff=90) > M(50),调度器选择 L 运行
t3: L 迅速完成临界区,释放 mutex,L 的优先级恢复为 10
H(90) 被唤醒,立即获得 CPU
结果: H 的等待时间 = L 的临界区时间(有界!)
PI 链传播是 rtmutex 最复杂的部分,实现于 kernel/locking/rtmutex.c:678。
kernel/locking/rtmutex.c,第 678-683 行
static int __sched rt_mutex_adjust_prio_chain(
struct task_struct *task,
enum rtmutex_chainwalk chwalk,
struct rt_mutex_base *orig_lock,
struct rt_mutex_base *next_lock,
struct rt_mutex_waiter *orig_waiter,
struct task_struct *top_task)PI 链传播步骤(kernel/locking/rtmutex.c,第 637-671 行注释):
函数参数说明:
[R] = 任务引用计数保护
[Pn] = task->pi_lock 持有
[L] = rtmutex->wait_lock 持有
传播循环(每次迭代最多持有两把锁,保证可抢占):
again:
loop_sanity_check() -- 检查链长度(max_lock_depth 限制)
retry:
[1] raw_spin_lock_irq(&task->pi_lock) -- 获取 [P1]
[2] waiter = task->pi_blocked_on -- 找到任务阻塞的 waiter
[3] check_exit_conditions_1() -- 检查退出条件
[4] lock = waiter->lock -- 找到对应的 rt_mutex
[5] if (!try_lock(lock->wait_lock)) -- 尝试获取 [L]
unlock(task->pi_lock); goto retry
[6] check_exit_conditions_2() -- [P1]+[L] 双锁保护下再次检查
[7] requeue_lock_waiter(lock, waiter) -- 按新优先级重新在锁等待树中排队
[8] unlock(task->pi_lock) -- 释放 [P1]
put_task_struct(task) -- 释放 [R]
[9] check_exit_conditions_3()
[10] task = owner(lock) -- 获取该锁的持有者
get_task_struct(task) -- 获取 [R]
lock(task->pi_lock) -- 获取 [P2]
[11] requeue_pi_waiter(task, waiters(lock))-- 更新持有者的 pi_waiters 树
[12] check_exit_conditions_4()
[13] unlock(task->pi_lock) -- 释放 [P2]
unlock(lock->wait_lock) -- 释放 [L]
goto again -- 继续向上传播
终止条件:链末端(持有者未阻塞在任何锁上)
每次迭代仅持有最多两把锁,循环体完全可抢占,避免长时间阻塞。
max_lock_depth(include/linux/rtmutex.h,第 21 行)控制最大链深度,防止环形死锁导致无限循环:
extern int max_lock_depth; // 默认值 1024kernel/locking/rtmutex.c,第 527-540 行
static __always_inline void rt_mutex_adjust_prio(struct rt_mutex_base *lock,
struct task_struct *p)
{
struct task_struct *pi_task = NULL;
lockdep_assert_held(&lock->wait_lock);
lockdep_assert(rt_mutex_owner(lock) == p);
lockdep_assert_held(&p->pi_lock);
if (task_has_pi_waiters(p))
pi_task = task_top_pi_waiter(p)->task; // 取红黑树最左节点
rt_mutex_setprio(p, pi_task); // 提升 p 的有效优先级
}kernel/locking/rtmutex.c,第 394-410 行
static __always_inline int rt_waiter_node_less(struct rt_waiter_node *left,
struct rt_waiter_node *right)
{
if (left->prio < right->prio)
return 1;
// 两个等待者都是 SCHED_DEADLINE 时,按截止时间(deadline)排序
if (dl_prio(left->prio))
return dl_time_before(left->deadline, right->deadline);
return 0;
}这确保了优先级继承对所有实时调度类(SCHED_FIFO/RR 和 SCHED_DEADLINE)均正确工作。
+---------------------------+------------------------------------------+
| 机制 | 说明 |
+---------------------------+------------------------------------------+
| Priority Inheritance (PI) | 动态:持有者临时获得最高等待者的优先级 |
| | 优点:精确,无需预先知道优先级 |
| | 缺点:链传播可能有延迟 |
| | Linux 实现:rt_mutex_adjust_prio_chain |
+---------------------------+------------------------------------------+
| Priority Ceiling (PC) | 静态:锁被分配一个固定的"天花板"优先级 |
| | 持有锁时临时提升到天花板优先级 |
| | 优点:无链传播,延迟固定 |
| | 缺点:需预先规划,可能过度提升 |
| | Linux:futex PI(FUTEX_LOCK_PI)近似实现|
+---------------------------+------------------------------------------+
| Immediate Priority Ceiling| PC 变体:立即提升(不只持有锁时) |
| (IPCP) | POSIX 的 PTHREAD_PRIO_PROTECT 策略 |
+---------------------------+------------------------------------------+
Linux 内核的 rt_mutex 实现的是 PI(Priority Inheritance),而非 PC。
三层嵌套锁场景:
任务: T_low(10) -- T_med(50) -- T_high(90)
锁: 锁 A 锁 B
时间线:
t0: T_low(10) 持有锁 A
t1: T_med(50) 持有锁 B,等待锁 A
t2: T_high(90) 等待锁 B
PI 链传播:
step1: T_high(90) 阻塞在锁 B
--> 锁 B 的等待者树插入 T_high
--> T_med 提升为 prio=90(因为 T_high 是 top waiter)
step2: T_med(eff=90) 阻塞在锁 A
--> 锁 A 的等待者树插入 T_med(以 prio=90 排序)
--> T_low 提升为 prio=90(因为 T_med 是 top waiter)
step3: T_low(eff=90) 运行,完成锁 A 的临界区
--> 释放锁 A,T_low 恢复 prio=10
--> T_med(eff=90) 被唤醒,获得锁 A
step4: T_med 完成锁 B 的临界区
--> 释放锁 B,T_med 恢复 prio=50
--> T_high(90) 被唤醒,获得锁 B
结果:T_high 等待时间 = T_low 在锁 A 的临界区 + T_med 在锁 B 的临界区(均有界)
传统硬中断(hard IRQ)直接在中断上下文运行,无法被抢占,持续时间越长,RT 任务等待越久。PREEMPT_RT 将每个中断处理函数转为独立的内核线程:
传统中断处理路径(非 RT):
硬中断触发 (HW)
|
v 关闭抢占,关闭其他中断
+------------------+
| 完整 ISR 处理 | 可能持续数十~数百 μs
| (atomic context) | 高优先级任务无法运行
+------------------+
|
v 恢复中断
返回之前任务
PREEMPT_RT 中断处理路径:
硬中断触发 (HW)
|
v 仍在 atomic context(极短)
+------------------+
| 最小化硬中断 ISR | < 几 μs
| 仅做: | ACK 中断控制器
| 1. ACK 控制器 | 设置 IRQTF_RUNTHREAD
| 2. 唤醒 irq线程 |
+------------------+
|
v irq_thread 被唤醒(SCHED_FIFO,prio=50)
+------------------+
| irq_thread | 可被高优先级 RT 任务抢占
| (task context) | 可使用睡眠锁
| 执行完整 ISR 逻辑 | 可被 prio>50 的任务抢占
+------------------+
kernel/irq/manage.c,第 1244-1286 行
static int irq_thread(void *data)
{
struct callback_head on_exit_work;
struct irqaction *action = data;
struct irq_desc *desc = irq_to_desc(action->irq);
irqreturn_t (*handler_fn)(struct irq_desc *desc,
struct irqaction *action);
irq_thread_set_ready(desc, action);
if (action->handler == irq_forced_secondary_handler)
sched_set_fifo_secondary(current);
else
sched_set_fifo(current); // 设置为 SCHED_FIFO,优先级 50
if (force_irqthreads() && test_bit(IRQTF_FORCED_THREAD,
&action->thread_flags))
handler_fn = irq_forced_thread_fn;
else
handler_fn = irq_thread_fn;
init_task_work(&on_exit_work, irq_thread_dtor);
task_work_add(current, &on_exit_work, TWA_NONE);
while (!irq_wait_for_interrupt(desc, action)) { // 阻塞等待中断触发
irqreturn_t action_ret;
action_ret = handler_fn(desc, action); // 执行实际处理函数
if (action_ret == IRQ_WAKE_THREAD)
irq_wake_secondary(desc, action);
wake_threads_waitq(desc);
}
task_work_cancel_func(current, irq_thread_dtor);
return 0;
}kernel/irq/manage.c,第 1158-1170 行
static irqreturn_t irq_forced_thread_fn(struct irq_desc *desc,
struct irqaction *action)
{
irqreturn_t ret;
local_bh_disable();
if (!IS_ENABLED(CONFIG_PREEMPT_RT))
local_irq_disable(); // 非 RT:禁止中断
ret = irq_thread_fn(desc, action);
if (!IS_ENABLED(CONFIG_PREEMPT_RT))
local_irq_enable();
local_bh_enable();
return ret;
}RT 内核中不禁止 IRQ,因为中断处理已在线程中运行,不存在重入问题。
不是所有中断都被线程化。带有 IRQF_TIMER 标志的定时器中断仍然在硬中断上下文运行,这是因为:
- 调度器 tick 必须在硬中断中运行,否则 RT 任务的时间片统计会混乱
- hrtimer 到期必须立即唤醒任务,不能等线程调度
中断线程化规则:
IRQF_TIMER 标志 --> 保留硬中断(不线程化)
IRQF_NO_THREAD 标志 --> 保留硬中断(驱动明确要求)
其他所有中断 --> PREEMPT_RT 强制线程化
通过 ps -eo pid,comm,policy,rtprio 可以看到系统中所有中断线程:
PID COMM POLICY RTPRIO
39 irq/16-ehci_hcd FF 50
40 irq/17-snd_hda FF 50
41 irq/18-xhci_hcd FF 50
42 irq/19-eth0 FF 50
在实际部署中,需要根据中断的实时性要求调整其线程优先级:
# 查看中断线程的默认优先级(SCHED_FIFO 50)
ps -eo pid,comm,policy,rtprio | grep "^.*FF"
# 提高网卡中断线程的优先级(适用于网络实时应用)
PID=$(pgrep "irq/19-eth0")
chrt -f -p 70 $PID
# 降低非关键中断的优先级(避免干扰 RT 任务)
PID=$(pgrep "irq/17-snd_hda")
chrt -f -p 20 $PID规则:RT 任务的优先级应高于其依赖的中断线程优先级,确保中断处理完成后能立即唤醒 RT 任务。
softirq 在中断返回路径或 local_bh_enable() 时被执行,运行在当前 CPU 上,不能被抢占(in_serving_softirq() 为真时)。常见的 softirq 处理包括:
NET_TX_SOFTIRQ/NET_RX_SOFTIRQ:网络包处理(可能持续 >100μs)BLOCK_SOFTIRQ:块设备 I/O 完成TIMER_SOFTIRQ:定时器回调
这些 softirq 的长时间执行会导致 RT 任务无法被调度,是延迟的重要来源。
kernel/softirq.c,第 106-127 行
#ifdef CONFIG_PREEMPT_RT
struct softirq_ctrl {
local_lock_t lock; // 基于 rtmutex,允许 BH 禁用段被抢占
int cnt; // per-CPU softirq 禁用计数
};
static DEFINE_PER_CPU(struct softirq_ctrl, softirq_ctrl) = {
.lock = INIT_LOCAL_LOCK(softirq_ctrl.lock),
};RT 内核引入了 per-CPU 的 softirq_ctrl 结构:
lock:基于local_lock_t(底层是 rtmutex),允许 BH 禁用段被抢占cnt:per-CPU softirq 禁用计数,与 per-task 的softirq_disable_cnt配合
lockdep_map bh_lock_map 的注释明确标记(第 135 行):
.wait_type_inner = LD_WAIT_CONFIG, /* PREEMPT_RT makes BH preemptible. */在非 RT 内核中:
// 非 RT:local_bh_disable 增加 preempt_count 的 SOFTIRQ 字段
// 效果:禁止抢占 + 禁止 softirq
local_bh_disable()
--> preempt_count += SOFTIRQ_DISABLE_OFFSET
--> 任务不可被抢占,softirq 不会在此 CPU 运行在 RT 内核中:
// RT:local_bh_disable 获取 per-CPU 的 softirq_ctrl.lock(rtmutex)
// 效果:阻止 softirq,但允许被抢占
local_bh_disable()
--> 尝试获取 local_lock(&softirq_ctrl.lock)
--> 若被抢占,高优先级任务可以先运行
--> 返回后继续持有 lock,softirq 不会运行这一语义变化的关键意义:在 RT 内核中,local_bh_disable() 区段可以被高优先级任务抢占。这解决了原本 BH 禁用段导致的延迟问题。
kernel/softirq.c,第 335 行
// RT 下 should_wake_ksoftirqd() 的实现:
// 只要 per-CPU 的 softirq_ctrl.cnt == 0 就允许唤醒
return !this_cpu_read(softirq_ctrl.cnt);在 RT 内核中,invoke_softirq() 不再直接执行 do_softirq(),而是只唤醒 ksoftirqd 线程:
非 RT 内核的 softirq 执行路径:
中断返回 --> irq_exit() --> invoke_softirq()
--> do_softirq() [在当前上下文直接执行,不可抢占]
RT 内核的 softirq 执行路径:
中断返回 --> irq_exit() --> invoke_softirq()
--> wakeup_softirqd() [只唤醒 ksoftirqd 线程]
--> ksoftirqd 在自己的线程上下文中执行 do_softirq()
--> 可被高优先级 RT 任务抢占
kernel/rcu/tree.c,第 114-118 行
/* By default, use RCU_SOFTIRQ instead of rcuc kthreads. */
static bool use_softirq = !IS_ENABLED(CONFIG_PREEMPT_RT);
#ifndef CONFIG_PREEMPT_RT
module_param(use_softirq, bool, 0444);
#endif在 RT 内核中,use_softirq 强制为 false,RCU 的 grace period 检测改由专用内核线程(rcuc/N、rcuog/N)完成,而非在 softirq 中进行。
传统(非 PREEMPT_RCU)实现中,rcu_read_lock() 通过 preempt_disable() 标记读侧临界区:
include/linux/rcupdate.h,第 93-117 行(非 PREEMPT_RCU 路径)
#else /* #ifdef CONFIG_PREEMPT_RCU */
static inline void __rcu_read_lock(void)
{
preempt_disable(); // 禁用抢占 = 不可被调度 = quiescent state 不会发生
}
static inline void __rcu_read_unlock(void)
{
if (IS_ENABLED(CONFIG_RCU_STRICT_GRACE_PERIOD))
rcu_read_unlock_strict();
preempt_enable();
}这意味着在 RCU 读侧临界区内,任务不可被抢占,即使高优先级 RT 任务已就绪。如果 RCU 读临界区很长(例如遍历一个大型链表),RT 任务的延迟将变得不可接受。
在 CONFIG_PREEMPT_RT 下,自动启用 CONFIG_PREEMPT_RCU,使 rcu_read_lock() 允许被抢占。
include/linux/rcupdate.h,第 80-91 行(PREEMPT_RCU 路径)
#ifdef CONFIG_PREEMPT_RCU
void __rcu_read_lock(void); // 实现在 kernel/rcu/tree_plugin.h
void __rcu_read_unlock(void);
// 读取嵌套深度(仅 PREEMPT_RCU 有效)
#define rcu_preempt_depth() READ_ONCE(current->rcu_read_lock_nesting)
PREEMPT_RCU 的 __rcu_read_lock() 实现(kernel/rcu/tree_plugin.h):
void __rcu_read_lock(void)
{
current->rcu_read_lock_nesting++;
// 注意:不调用 preempt_disable()
// 通过 rcu_read_lock_nesting 追踪嵌套深度
// 若被抢占,调度器记录该任务在 RCU 读侧临界区中
}允许抢占意味着 RCU grace period 的计算变复杂:被抢占出去的任务可能长时间持有 RCU 读侧临界区,导致 grace period 无法结束。
解决方案:RCU_BOOST
RCU_BOOST 机制:
1. RCU 监测到有任务长时间在读侧临界区中(被抢占)
2. 临时提升该任务的优先级(boost 到 RT 优先级)
3. 使该任务尽快被调度,完成读侧临界区,推进 grace period
相关内核线程:rcub/N(RCU boosting kthread)
优先级:CONFIG_RCU_BOOST_PRIO(默认值 1,可配置)
在 PREEMPT_RCU 下,每次上下文切换都需要通知 RCU,以便 RCU 记录可能的静止状态(quiescent state):
// kernel/sched/core.c 中的 __schedule() 调用
rcu_note_context_switch(preempt);如果被抢占的任务不在 RCU 读侧临界区中,则该上下文切换即为一个 quiescent state,推进 grace period。如果任务在读侧临界区中,RCU 将其加入 rnp->blkd_tasks 链表,等其退出临界区后再标记 quiescent state。
include/linux/rcupdate.h,第 387-395 行
#ifndef CONFIG_PREEMPT_RCU
// 非 PREEMPT_RCU:rcu_read_lock_sched 等同于 preempt_disable
static inline void rcu_read_lock_sched_notrace(void) { preempt_disable(); }
#else
// PREEMPT_RCU:rcu_read_lock_sched 也可被抢占(通过不同机制保护)RT 内核下 RCU 变体对比:
+---------------------------+------------------------------------------+
| RCU 变体 | RT 内核行为 |
+---------------------------+------------------------------------------+
| rcu_read_lock() | 可被抢占(PREEMPT_RCU),不能显式睡眠 |
| rcu_read_lock_bh() | 等同 rcu_read_lock() + local_bh_disable |
| rcu_read_lock_sched() | 等同 preempt_disable(),在 RT 下慎用 |
| srcu_read_lock() | 允许阻塞,适用于需要睡眠的读侧临界区 |
+---------------------------+------------------------------------------+
RT 内核建议:
- 需要长时间持有读锁时,优先使用 SRCU
- 避免在 RT 任务热路径中使用 rcu_read_lock_sched()
- RCU_BOOST 配置应高于 RT 任务优先级,避免 boost 失效
内存分配标志与 RT 实时性密切相关:
+--------------------+------------------------------------------+---------------------+
| 分配标志 | 行为 | RT 适用性 |
+--------------------+------------------------------------------+---------------------+
| GFP_KERNEL | 可睡眠:可以等待内存回收、磁盘 I/O | 不适用(延迟无界) |
| GFP_ATOMIC | 不可睡眠:分配失败直接返回 NULL | 有限适用(可能失败)|
| GFP_NOWAIT | 不可睡眠,不可等待 | 同 GFP_ATOMIC |
| GFP_NOIO | 可睡眠但不触发 I/O | 有限适用 |
| GFP_NOFS | 可睡眠但不触发文件系统 | 有限适用 |
+--------------------+------------------------------------------+---------------------+
在 RT 上下文(SCHED_FIFO/RR 任务)中使用 GFP_KERNEL 会有以下风险:
- 直接回收(direct reclaim):内存不足时触发页面扫描和回收,可能持续数毫秒
- OOM killer:极端情况下触发 OOM,杀死其他进程,延迟不可预测
- 压缩(compaction):内存碎片整理,可能持续数百毫秒
页面错误(page fault)是 RT 系统最常见的"隐藏延迟"来源:
页面错误类型与 RT 影响:
1. 次要缺页(minor fault):
页面在内存中但未映射(如 CoW)
延迟:~1~10μs(可接受)
2. 主要缺页(major fault):
页面不在内存中,需要从磁盘读取
延迟:~10ms~数百ms(完全不可接受!)
3. 栈增长缺页:
任务栈自动扩展
延迟:~1~50μs(需要关注)
4. mmap 文件缺页:
延迟依赖于 I/O,完全不可预测
解决页面错误的根本方法是使用 mlockall() 将所有内存页锁定在物理内存中:
#include <sys/mman.h>
// 在 RT 任务启动时调用
// MCL_CURRENT:锁定当前已分配的所有页面
// MCL_FUTURE:锁定未来分配的所有页面
if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) {
perror("mlockall failed");
// 注意:需要 CAP_IPC_LOCK 或 ulimit -l unlimited
}预触发栈页面:即使 mlockall(MCL_FUTURE) 锁定了未来分配,栈的每个新页面首次访问时仍会有 minor fault。解决方法是预先触发:
#define STACK_SIZE (8 * 1024 * 1024) // 8MB 预分配栈
static void prefault_stack(void)
{
volatile char stack_buf[STACK_SIZE];
// 每页写一个字节,触发所有栈页面的分配和锁定
for (int i = 0; i < STACK_SIZE; i += 4096)
stack_buf[i] = 0;
}普通 4KB 页面意味着每次访问新页面都需要 TLB 条目,TLB 容量有限(通常 642048 条),大内存工作集会导致频繁 TLB miss,每次 miss 的惩罚为数十数百 CPU 周期。
Huge Page(2MB 或 1GB 页面)减少 TLB 条目需求:
# 方法一:透明大页(THP)—— 不推荐用于 RT
# THP 的合并和拆分操作本身会带来延迟抖动
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 方法二:显式 HugeTLB 页面 —— 推荐
# 预分配大页内存池
echo 512 > /proc/sys/vm/nr_hugepages # 预分配 512 个 2MB 大页 = 1GB
# 应用程序使用 MAP_HUGETLB
void *buf = mmap(NULL, 1 << 21, // 2MB
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
# 方法三:通过 hugetlbfs 挂载
mount -t hugetlbfs none /mnt/hugepages大页减少 TLB miss 的效果:
假设工作集 = 256MB:
- 4KB 页面:需要 65536 个 TLB 条目(TLB 容量 2048,大量 miss)
- 2MB 大页:需要 128 个 TLB 条目(TLB 完全容纳,零 miss)
延迟改善:5~30μs(视工作集和访问模式)
在多 NUMA 节点服务器上,RT 任务访问远端 NUMA 节点内存会引入额外延迟:
# 将 RT 任务绑定到特定 NUMA 节点的 CPU 和内存
numactl --cpunodebind=0 --membind=0 chrt -f 90 ./rt_app
# 或在代码中使用 libnuma
#include <numa.h>
numa_set_preferred(0); // 优先从 node 0 分配内存
numa_set_localalloc(); // 从本地 NUMA 节点分配标准内核的 jiffies 定时器精度受 CONFIG_HZ 限制(通常 1001000 Hz),最小分辨率为 1ms10ms。hrtimer 基于硬件定时器直接编程,可实现纳秒级精度。
include/linux/hrtimer.h,第 26-55 行
enum hrtimer_mode {
HRTIMER_MODE_ABS = 0x00, // 绝对时间
HRTIMER_MODE_REL = 0x01, // 相对时间
HRTIMER_MODE_PINNED = 0x02, // 绑定到特定 CPU
HRTIMER_MODE_SOFT = 0x04, // 回调在 softirq 上下文执行
HRTIMER_MODE_HARD = 0x08, // 回调在硬中断上下文执行(即使 PREEMPT_RT)
HRTIMER_MODE_ABS_PINNED = HRTIMER_MODE_ABS | HRTIMER_MODE_PINNED,
HRTIMER_MODE_REL_PINNED = HRTIMER_MODE_REL | HRTIMER_MODE_PINNED,
HRTIMER_MODE_ABS_SOFT = HRTIMER_MODE_ABS | HRTIMER_MODE_SOFT,
HRTIMER_MODE_REL_SOFT = HRTIMER_MODE_REL | HRTIMER_MODE_SOFT,
HRTIMER_MODE_ABS_HARD = HRTIMER_MODE_ABS | HRTIMER_MODE_HARD,
HRTIMER_MODE_REL_HARD = HRTIMER_MODE_REL | HRTIMER_MODE_HARD,
HRTIMER_MODE_ABS_PINNED_HARD = HRTIMER_MODE_ABS_PINNED | HRTIMER_MODE_HARD,
HRTIMER_MODE_REL_PINNED_HARD = HRTIMER_MODE_REL_PINNED | HRTIMER_MODE_HARD,
};HRTIMER_MODE_HARD 注释说明:
Timer callback function will be executed in hard irq context even on PREEMPT_RT.
kernel/sched/rt.c,第 131-133 行
hrtimer_setup(&rt_b->rt_period_timer, sched_rt_period_timer, CLOCK_MONOTONIC,
HRTIMER_MODE_REL_HARD);RT 带宽控制定时器明确使用 HRTIMER_MODE_REL_HARD,保证即使在 PREEMPT_RT 下也运行于硬中断上下文,不受 softirq 线程化影响,确保 RT 任务的带宽限制准确执行。
include/linux/hrtimer.h,第 197-205 行
#ifdef CONFIG_PREEMPT_RT
void hrtimer_cancel_wait_running(const struct hrtimer *timer);
#else
static inline void hrtimer_cancel_wait_running(struct hrtimer *timer)
{
cpu_relax(); // 非 RT:简单忙等
}
#endif在 RT 内核中,hrtimer_cancel() 在等待正在运行的 hrtimer 完成时,必须主动调度(而非忙等),因为 hrtimer 回调可能持有 rtmutex,若调用者持有相同的锁则会死锁。hrtimer_cancel_wait_running() 在 RT 下通过 schedule() 让出 CPU。
用户实时任务 (SCHED_FIFO, prio=90) 调用 clock_nanosleep()
|
| 系统调用
v
+------------------+
| hrtimer_sleeper | -- 基于 hrtimer 实现的精确阻塞等待
| (纳秒精度) |
+------------------+
|
| 设置 hrtimer 到期时间,任务进入 TASK_INTERRUPTIBLE
v
+------------------+
| 调度器切换到 | -- 当前 CPU 运行其他任务
| 其他任务 |
+------------------+
|
| hrtimer 到期,触发硬中断
v
+------------------+
| hrtimer 硬中断 | -- 在硬中断上下文(< 1μs)
| 回调:唤醒任务 | -- wake_up_process(rt_task)
+------------------+
|
| 调度器检查:rt_task 的优先级 90 > 当前任务
v
+------------------+
| 立即抢占当前任务 | -- __preempt_schedule() 立即切换
| 运行 rt_task |
+------------------+
整个路径延迟(理想情况):
hrtimer 中断响应:< 1μs
任务唤醒到调度:1~5μs
总计:< 10μs(PREEMPT_RT x86 典型值)
CONFIG_HZ 影响分析:
CONFIG_HZ=100 (10ms tick):
- 不使用 hrtimer 的睡眠最大误差:10ms
- 使用 hrtimer 的睡眠:不受 HZ 影响(hrtimer 直接编程硬件)
- 调度器 load balancing 精度:10ms
CONFIG_HZ=250 (4ms tick):
- 标准服务器/桌面配置
- 适当的 RT 性能与调度开销平衡
CONFIG_HZ=1000 (1ms tick):
- 推荐用于 RT 系统
- 更精细的调度粒度
- 轻微增加调度开销(~0.1% CPU)
对 RT 任务而言,使用 clock_nanosleep() + hrtimer 时,
CONFIG_HZ 的值对唤醒延迟几乎没有影响。
决定性因素是:
1. 硬件定时器精度(通常 ~100ns)
2. 中断处理延迟(RT 下 < 5μs)
3. 调度器切换延迟(RT 下 < 5μs)
CONFIG_NO_HZ_FULL 允许特定 CPU 在只有一个可运行任务时关闭调度 tick:
# 内核命令行:将 CPU 2,3 设为 nohz_full
nohz_full=2,3
# 效果:
# - CPU 2,3 在 RT 任务单独运行时停止 tick(0 Hz)
# - 消除 1ms tick 导致的定期延迟抖动
# - RT 任务不会被周期性 tick 中断打断
# 注意事项:
# - nohz_full CPU 仍有 hrtimer 中断(定时器到期时)
# - 需配合 rcu_nocbs 避免 RCU 回调在 RT CPU 上运行
# - 调度 tick 停止意味着 load balance 不会在该 CPU 上触发与 rcu_nocbs= 配合使用,可以消除 RCU 回调导致的延迟:
nohz_full=2,3 rcu_nocbs=2,3 效果:
CPU 2,3 在 RT 任务运行期间:
- 无调度 tick(0 Hz)
- 无 RCU 回调执行
- 无 softirq 执行(已线程化)
- 唯一中断源:hrtimer 到期(来自 RT 任务自身的睡眠定时器)
- 最终效果:RT 任务几乎独占 CPU,延迟最小化
+----------------------------------------------------------------+
| CPU 隔离层次结构 |
| |
| 内核命令行参数(启动时配置): |
| |
| isolcpus=2,3 --> 从调度器域中移除 CPU 2,3 |
| 普通任务不会被调度到这些 CPU |
| |
| nohz_full=2,3 --> CPU 2,3 进入 tickless 模式 |
| 单任务时停止调度 tick |
| |
| rcu_nocbs=2,3 --> CPU 2,3 的 RCU 回调卸载到其他 CPU |
| 避免 RCU 回调打断 RT 任务 |
| |
| irqaffinity=0,1 --> 默认中断只分配到 CPU 0,1 |
| CPU 2,3 不被中断打扰 |
+----------------------------------------------------------------+
isolcpus= 是最直接的 CPU 隔离手段:
# GRUB 配置(/etc/default/grub)
GRUB_CMDLINE_LINUX="isolcpus=2,3,4,5 nohz_full=2,3,4,5 rcu_nocbs=2,3,4,5"
# 验证隔离效果
cat /sys/devices/system/cpu/isolated # 输出: 2-5
cat /sys/devices/system/cpu/nohz_full # 输出: 2-5
# 将 RT 任务绑定到隔离核
taskset -c 2 chrt -f 90 ./my_rt_app
# 将 irq 线程迁出隔离核
for pid in $(pgrep "irq/"); do
taskset -cp 0,1 $pid 2>/dev/null
done注意:isolcpus= 只影响调度器的自动分配,可以通过 taskset/sched_setaffinity() 强制将任务绑定到隔离核。
作用机制:
无 rcu_nocbs 时(CPU 2 上的 RT 任务):
RT 任务运行
--> RCU 回调到期
--> 当前 CPU 执行 RCU 回调(可能持续数十μs)
--> RT 任务被延迟
配置 rcu_nocbs=2,3 后:
RT 任务运行(CPU 2)
--> RCU 回调到期
--> 将回调加入队列,唤醒 rcuog/2 线程(在 CPU 0,1 上执行)
--> RT 任务不受影响
相关内核线程(rcuog = RCU offload gp):
rcuog/0 rcuog/1 -- 处理 CPU 0,1 的 RCU 回调
rcuog/2 rcuog/3 -- 处理 CPU 2,3 的 RCU 回调(在 CPU 0,1 上运行!)
irqaffinity= 设置未分配中断的默认亲和性掩码(位图):
# 将所有中断默认分配到 CPU 0,1(二进制 0011 = 3)
# 内核命令行:
irqaffinity=0,1
# 也可以在运行时调整
# 将中断 25(例如网卡 eth0)绑定到 CPU 0,1
echo 3 > /proc/irq/25/smp_affinity # 位图,3 = 0b11 = CPU 0+1
echo "0,1" > /proc/irq/25/smp_affinity_list # 列表形式
# 批量迁移所有中断
for irq in /proc/irq/*/; do
echo "0,1" > ${irq}smp_affinity_list 2>/dev/null
donecpuset 通过 cgroups 提供更灵活的 CPU 和内存节点隔离:
# 创建实时任务专用的 cpuset
mount -t cgroup -o cpuset none /sys/fs/cgroup/cpuset
mkdir /sys/fs/cgroup/cpuset/rt_tasks
echo "2,3" > /sys/fs/cgroup/cpuset/rt_tasks/cpuset.cpus
echo "0" > /sys/fs/cgroup/cpuset/rt_tasks/cpuset.mems
# 将任务加入 cpuset
echo $PID > /sys/fs/cgroup/cpuset/rt_tasks/cgroup.procs
# 排除系统任务(load balancer 不会将任务迁移到 rt_tasks 的 CPU)
echo 1 > /sys/fs/cgroup/cpuset/rt_tasks/cpuset.cpu_exclusive8 核服务器(CPU 0-7),CPU 0-3 用于系统,CPU 4-7 用于 RT 任务:
# /etc/default/grub
GRUB_CMDLINE_LINUX="isolcpus=4,5,6,7 \
nohz_full=4,5,6,7 \
rcu_nocbs=4,5,6,7 \
irqaffinity=0,1,2,3 \
processor.max_cstate=1 \
intel_idle.max_cstate=0"
# processor.max_cstate=1 禁止深度 C 状态(避免 CPU 唤醒延迟)
# intel_idle.max_cstate=0 对 Intel CPU 同样禁止深度 C 状态
# 启动后验证
cat /sys/devices/system/cpu/isolated # 4-7
cat /sys/devices/system/cpu/nohz_full # 4-7
# 启动 RT 应用
taskset -c 4 chrt -f 90 ./rt_appLinux 调度器是模块化的,调度类按优先级从高到低排列:
+------------------+ 优先级最高
| stop_sched_class | per-CPU 停止任务(migration/N)
+------------------+
| dl_sched_class | SCHED_DEADLINE(EDF)
+------------------+
| rt_sched_class | SCHED_FIFO(先进先出)
| | SCHED_RR(轮转)
| | 优先级范围:1~99(99 最高)
+------------------+
| fair_sched_class | SCHED_OTHER(CFS 公平调度)
| | SCHED_BATCH(批处理)
+------------------+
| idle_sched_class | SCHED_IDLE(空闲任务)
+------------------+ 优先级最低
kernel/sched/rt.c,第 10-24 行
int sched_rr_timeslice = RR_TIMESLICE;
// RT 任务的 CPU 使用带宽限制(防止 RT 任务饿死普通任务)
int sysctl_sched_rt_period = 1000000; // 默认 1 秒周期(μs)
int sysctl_sched_rt_runtime = 950000; // 默认 0.95 秒运行时间(μs)
// 含义:所有 RT 任务合计最多占 95% CPU,保留 5% 给普通任务
// 设置 runtime=-1 可禁用限制(生产 RT 系统常见配置,慎用!)RT 运行队列使用位图优先级队列(而非红黑树),实现 O(1) 选取最高优先级任务:
rt_prio_array(位图 + 链表数组):
bitmap[0..4] (MAX_RT_PRIO=100 个位) + 哨兵位
queue[0] --> list of prio-0 tasks
queue[1] --> list of prio-1 tasks
...
queue[99] --> list of prio-99 tasks
pick_next_task_rt():
idx = sched_find_first_bit(array->bitmap) // O(1),通常 1 个 CPU 指令
返回 queue[idx] 的链表头部任务
SCHED_FIFO:同优先级任务永不被抢占(除非显式让出)
SCHED_RR:同优先级任务按时间片轮转(默认 100ms)
SCHED_DEADLINE 基于**最早截止时间优先(EDF)**算法,提供最严格的实时保证,并有数学上的可调度性保证。
任务参数通过 sched_setattr() 系统调用设置:
struct sched_attr {
__u32 size;
__u32 sched_policy; // SCHED_DEADLINE
__u64 sched_flags;
__s32 sched_nice;
// 实时参数:
__u32 sched_priority; // 未使用
__u64 sched_runtime; // 每个周期最多运行多长时间 (ns)
__u64 sched_deadline; // 相对截止时间 (ns)
__u64 sched_period; // 任务周期 (ns)
};
// 示例: 10ms 周期任务,2ms 运行时间,5ms 截止时间
struct sched_attr attr = {
.size = sizeof(attr),
.sched_policy = SCHED_DEADLINE,
.sched_runtime = 2 * 1000000ULL, // 2ms
.sched_deadline = 5 * 1000000ULL, // 5ms
.sched_period = 10 * 1000000ULL, // 10ms
};
syscall(SYS_sched_setattr, 0, &attr, 0);可调度性条件(利用率检验):
n 个 SCHED_DEADLINE 任务 i (1 ≤ i ≤ n):
可调度条件:Σ(runtime_i / period_i) ≤ 1.0
例:
任务 A: runtime=2ms, period=10ms --> 利用率 20%
任务 B: runtime=3ms, period=15ms --> 利用率 20%
任务 C: runtime=1ms, period=5ms --> 利用率 20%
总利用率 = 60% ≤ 100% --> 可调度
注:Linux 内核会拒绝超过 RT 带宽限制的 SCHED_DEADLINE 任务
(kernel/sched/deadline.c 中的 admission control)
SCHED_DEADLINE 任务支持优先级继承,按截止时间排序:
kernel/locking/rtmutex.c,第 406-410 行
static __always_inline int rt_waiter_node_less(struct rt_waiter_node *left,
struct rt_waiter_node *right)
{
if (left->prio < right->prio)
return 1;
// 两个 DEADLINE 任务按截止时间排序(deadline 越早优先级越高)
if (dl_prio(left->prio))
return dl_time_before(left->deadline, right->deadline);
return 0;
}设置实时调度策略需要 CAP_SYS_NICE 能力:
#include <sched.h>
struct sched_param param = { .sched_priority = 90 };
// 方法一:sched_setscheduler(需要 CAP_SYS_NICE 或 root)
if (sched_setscheduler(0, SCHED_FIFO, ¶m) != 0) {
perror("sched_setscheduler");
// EPERM:权限不足
}
// 方法二:通过 ulimit 设置允许的最大实时优先级
// /etc/security/limits.conf:
// @realtime - rtprio 99
// 方法三:使用 libcap 在程序中提升自身权限
// cap_t caps = cap_get_proc();
// cap_set_flag(caps, CAP_EFFECTIVE, 1, cap_values, CAP_SET);RT 节流防止实时任务饿死普通任务:
# 查看当前 RT 带宽配置
cat /proc/sys/kernel/sched_rt_period_us # 默认: 1000000 (1秒)
cat /proc/sys/kernel/sched_rt_runtime_us # 默认: 950000 (950ms)
# 禁用 RT 节流(生产 RT 系统:保证 RT 任务不被限速)
echo -1 > /proc/sys/kernel/sched_rt_runtime_us
# 或更保守地,允许 RT 任务使用 99% CPU
echo 990000 > /proc/sys/kernel/sched_rt_runtime_us
# 通过 cgroup 对特定 RT 任务组限速
# /sys/fs/cgroup/cpu/<group>/cpu.rt_runtime_us
# /sys/fs/cgroup/cpu/<group>/cpu.rt_period_usRT 节流的实现(kernel/sched/rt.c,第 99-133 行):
static enum hrtimer_restart sched_rt_period_timer(struct hrtimer *timer)
{
struct rt_bandwidth *rt_b = ...;
// 每个周期(默认 1s)重置运行时间配额
// 若 RT 任务已耗尽配额,throttle_rt_entity()
// 被节流的 RT 任务从运行队列移除,直到下一个周期
}
hrtimer_setup(&rt_b->rt_period_timer, sched_rt_period_timer,
CLOCK_MONOTONIC, HRTIMER_MODE_REL_HARD);SCHED_DEADLINE 有独立的带宽控制,通过 admission control 在创建任务时检验:
# 查看 SCHED_DEADLINE 全局带宽限制
cat /proc/sys/kernel/sched_deadline_period_us # 默认: 1000000
cat /proc/sys/kernel/sched_deadline_runtime_us # 默认: 950000
# 增加允许的 DEADLINE 任务带宽
echo 999000 > /proc/sys/kernel/sched_deadline_runtime_uscyclictest 是 RT-Tests 套件中最核心的延迟测量工具,由 Thomas Gleixner 编写。
工作原理:
主测量循环(每个线程):
t_expected = t_start + interval
|
v
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, t_expected)
|
| <-- 实际被唤醒时刻 t_actual
v
latency = t_actual - t_expected // 超出预期的时间
|
v
更新统计: min(latency), max(latency), avg(latency), histogram
重复此循环,通常持续数分钟到数小时,
记录最大延迟(worst-case latency)
常用命令:
# 基本测量:FIFO 线程,优先级 99,测量 60 秒
cyclictest --mlockall \
--priority=99 \
--policy=fifo \
--interval=1000 \
--duration=60s \
--histogram=200 # 记录延迟直方图(μs)
# 多核测量(每个 CPU 一个线程,绑定 CPU 亲和性)
cyclictest -S -m -p99 -i200 -D 30m
# 在隔离核上测量(CPU 2,3)
cyclictest -a 2,3 -m -p99 -i200 -D 60m
# 带负载的测量(同时运行压力测试)
stress-ng --cpu 4 --io 2 --vm 1 --vm-bytes 256M &
cyclictest -S -m -p99 -i1000 -D 60m
# 典型输出(PREEMPT_RT x86 系统):
# T: 0 ( 1234) P:99 I:1000 C: 3600000 Min: 3 Act: 5 Avg: 5 Max: 47
# 最小(μs) 平均 最大(μs)延迟判定标准(x86 PREEMPT_RT):
< 20μs 优秀,适合硬实时应用
20~50μs 良好,适合大多数软实时应用
50~100μs 可接受,可能需要进一步调优
> 100μs 需要排查,通常有 SMI 或驱动问题
> 1ms 严重问题,不适合任何实时应用
rtla(Real-Time Linux Analysis)是 Linux 6.x 引入的官方实时分析工具套件,位于 tools/tracing/rtla/。
# rtla 子命令
rtla timerlat -- 测量定时器延迟(类似 cyclictest)
rtla osnoise -- 测量操作系统噪声(中断、softirq 等)
rtla hwnoise -- 测量硬件噪声(SMI 等)
# timerlat:测量定时器延迟,区分内核和用户态延迟
rtla timerlat top -p 95 -d 60s
# 输出示例:
# Timer Latency
# CPU IRQ Thread
# 0 min: 3us 5us
# 0 avg: 5us 8us
# 0 max: 45us 52us
# osnoise:测量每个 CPU 的噪声(非 RT 活动占用的时间)
rtla osnoise top -d 60s
# 输出:各 CPU 的噪声来源(IRQ、softIRQ、NMI、thread)
# hwnoise:检测硬件级别的延迟(SMI)
rtla hwnoise top -d 10s
# 若检测到 SMI,输出 SMI 的持续时间和频率ftrace 的多个 tracer 可以帮助定位延迟来源:
irqsoff tracer(追踪中断禁用延迟):
# 挂载 debugfs
mount -t debugfs debugfs /sys/kernel/debug
# 启用 irqsoff tracer
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 等待,然后读取最大延迟事件
cat /sys/kernel/debug/tracing/trace
# 典型输出(追踪到最长的 IRQ 禁用路径):
# irqsoff latency trace v1.1.5 on 6.12.0-rt
# latency: 47 us, #4/4, CPU#2 | (M:RT VP:0, KV:0, RP:0, #P:4)
# ...
# <idle>-0 2d..3 0us : _raw_spin_lock <-native_queued_spin_lock_slowpath
# <idle>-0 2d..3 47us : <stack trace>
# => native_queued_spin_lock_slowpath
# => _raw_spin_lock
# => e1000_intr <-- 找到罪魁祸首:e1000 驱动持有自旋锁 47μspreemptoff tracer(追踪抢占禁用延迟):
echo preemptoff > /sys/kernel/debug/tracing/current_tracer
# 注意:在 PREEMPT_RT 下,preemptoff 追踪的是 raw_spinlock
# 因为 spinlock_rt 不再禁止抢占hwlat tracer(检测 SMI 等硬件延迟):
echo hwlat > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# hwlat tracer 在一个 CPU 上忙等,通过测量"时间空洞"检测 SMI
cat /sys/kernel/debug/tracing/trace
# 若发现 SMI(通常 > 10μs),需在 BIOS 中禁用:
# - USB Legacy Support
# - Thermal Management Interrupt
# - ACPI Power Managementpreemptirqsoff(综合 tracer):
echo preemptirqsoff > /sys/kernel/debug/tracing/current_tracer
# 追踪 IRQ 或抢占被禁用的最长时间段(两者都计入)内核代码中通过以下宏集成 ftrace 追踪:
// 追踪 IRQ 禁用时间(kernel/trace/trace_irqsoff.c)
trace_hardirqs_off() // IRQ 被禁用时调用(由 local_irq_disable 触发)
trace_hardirqs_on() // IRQ 被重新使能时调用
// 追踪抢占禁用时间
trace_preempt_off() // 抢占被禁用时
trace_preempt_on() // 抢占重新使能时
// 延迟超过阈值时打印 WARN(tracer 的触发条件)
// 可通过以下接口调整:
cat /sys/kernel/debug/tracing/tracing_max_latency // 最大记录延迟(ns)+------------------+
| cyclictest 测量 | --> 发现最大延迟异常(> 100μs)
+------------------+
|
v
+------------------+
| hwlat tracer | --> 第一步:排查 SMI/BIOS 固件中断
| rtla hwnoise | 如果发现 SMI,在 BIOS 中禁用相关功能
+------------------+
|
v(无 SMI)
+------------------+
| irqsoff tracer | --> 第二步:确定中断禁用的代码路径
+------------------+ 找到哪个驱动/子系统长时间关中断
|
v
+------------------+
| preemptoff tracer| --> 第三步:确定抢占禁用的代码路径
+------------------+ 在 RT 下主要是 raw_spinlock 持有
|
v
+------------------+
| osnoise tracer | --> 第四步:量化各类噪声的贡献
+------------------+ IRQ/softIRQ/NMI/kthread 各占多少
|
v
+------------------+
| 针对性优化 | --> 1. 消除驱动中的 raw_spinlock 长时持有
| | 2. 禁用 SMI 触发源(BIOS 配置)
| | 3. 隔离 CPU(isolcpus + nohz_full)
| | 4. 调整中断亲和性
+------------------+
平台 配置 最大延迟(cyclictest,60分钟)
-----------------------------------------------------------------------
x86 桌面 PREEMPT_RT,无优化 < 100μs(通常 < 50μs)
x86 服务器 PREEMPT_RT + isolcpus < 50μs(通常 < 20μs)
x86 优化后 RT + isolcpus + bios优化 < 20μs(通常 < 10μs)
ARM Cortex-A53 PREEMPT_RT < 100μs(通常 < 60μs)
ARM Cortex-A72 PREEMPT_RT + isolcpus < 50μs(通常 < 30μs)
RISC-V PREEMPT_RT(实验性) < 200μs(硬件相关)
注:上述数字为典型值,实际结果因硬件、负载、内核配置而差异显著
SMI 干扰可将延迟提升 10~100 倍
Linux 2.6.16 (2006)
-- hrtimer 合入,PREEMPT_RT 第一块重要基础设施
Linux 2.6.25 (2008)
-- Lockdep 锁依赖检查完善,为 RT 调试提供基础
Linux 3.0 (2011)
-- NO_HZ(tickless)机制完善
Linux 4.1 (2015)
-- CPU 隔离(isolcpus)改进
Linux 5.15 (2021, LTS)
-- RT mutex 核心代码合入主线
-- spinlock_rt.c 合入
-- 中断线程化基础设施完整合入
Linux 6.1 (2022, LTS)
-- PREEMPT_RT Kconfig 选项正式出现在 menuconfig
-- softirq RT 路径合入
-- preempt.h 中 RT 分支完整合入
Linux 6.6 (2023, LTS)
-- 大量 RT 锁原语完善
-- migrate_disable/enable 完善
-- RT 测试套件(rtla)集成
Linux 6.12 (2024, LTS) [重大里程碑]
-- PREEMPT_RT 完全合入主线
-- 不再需要维护独立的 linux-rt 分支补丁
-- Thomas Gleixner 宣告 20 年开发工作完成
对用户的影响:
- 主流发行版(Ubuntu 25.04+、Fedora 41+、Debian 13+)的标准内核可直接启用 RT
- 不再需要下载独立 RT 补丁、打补丁、单独编译
- RT 特性与内核其他功能保持同步更新,减少兼容性问题
linux-rt外部补丁维护压力消失,社区力量集中于主线优化
仍需关注的事项:
- 硬件驱动:部分驱动仍有
raw_spinlock的长时持有,需要厂商适配 - 固件延迟:BIOS/UEFI 的 SMI 无法被 RT 内核消除,需 BIOS 配置配合
- 内存带宽:CPU 缓存预热、NUMA 拓扑等因素仍会影响延迟
- 中断控制器:某些 APIC 配置可能引入额外延迟
# 方法一:查看抢占模型
cat /sys/kernel/debug/sched/preempt
# 输出: PREEMPT_RT
# 方法二:通过内核配置
zcat /proc/config.gz | grep CONFIG_PREEMPT
# CONFIG_PREEMPT_RT=y
# 方法三:uname
uname -v | grep -i "preempt"
# ... SMP PREEMPT_RT ...
# 方法四:检查调用 preempt_model_rt()
cat /sys/kernel/preempt| 文件路径 | 主要内容 |
|---|---|
include/linux/preempt.h |
抢占计数位图定义、RT vs 非 RT 宏差异、migrate_disable 注释 |
include/linux/spinlock_rt.h |
RT spinlock API 兼容层(spin_lock→rt_spin_lock 映射) |
include/linux/rtmutex.h |
rt_mutex_base/rt_mutex 结构体定义、公开 API |
include/linux/rcupdate.h |
rcu_read_lock/unlock、PREEMPT_RCU 声明 |
include/linux/hrtimer.h |
hrtimer_mode 枚举(含 HARD/SOFT 区分)、hrtimer API |
| 文件路径 | 主要内容 |
|---|---|
kernel/locking/rtmutex.c |
RT mutex 完整实现:加锁、PI 链传播、优先级调整 |
kernel/locking/rtmutex_common.h |
rt_mutex_waiter、rt_waiter_node 内部结构体 |
kernel/locking/spinlock_rt.c |
RT spinlock/rwlock 实现(基于 rtmutex) |
kernel/locking/rwbase_rt.c |
RT rwlock 公共基础(读写锁状态机) |
kernel/irq/manage.c |
中断管理:irq_thread()、setup_irq_thread()、强制线程化 |
kernel/softirq.c |
softirq 处理,RT/非RT 双路径,softirq_ctrl 结构 |
kernel/sched/rt.c |
SCHED_FIFO/RR 调度类,RT 带宽控制定时器 |
kernel/sched/deadline.c |
SCHED_DEADLINE EDF 调度实现,admission control |
kernel/rcu/tree.c |
RCU tree 实现,use_softirq RT 分支 |
| 文件路径 | 说明 |
|---|---|
Documentation/locking/rt-mutex-design.rst |
RT mutex 设计文档 |
Documentation/admin-guide/rtla/ |
rtla 工具套件文档 |
tools/tracing/rtla/ |
rtla 工具源码(hwnoise、timerlat、osnoise) |
Documentation/timers/hrtimers.rst |
hrtimer 子系统设计文档 |
include/linux/preempt.h
(PREEMPT_LOCK_OFFSET=0)
|
+------------+------------------+
| |
include/linux/ include/linux/
spinlock_rt.h rtmutex.h
(API 兼容层) (rt_mutex_base)
| |
| spin_lock → rt_spin_lock |
v v
kernel/locking/ kernel/locking/
spinlock_rt.c rtmutex.c
| |
| 内含 rtlock_lock() | PI 链传播
v v
rt_mutex_base.owner task->pi_blocked_on
(cmpxchg 快路径) (等待者红黑树)
| |
+------------------+----------------+
|
kernel/sched/rt.c
(SCHED_FIFO/RR/DEADLINE)
|
v
kernel/irq/manage.c
(irq_thread: SCHED_FIFO 50)
|
v
kernel/softirq.c
(softirq_ctrl: local_lock)
(use_softirq=false on RT)
|
v
kernel/rcu/tree.c
(rcuc/rcuog 线程替代 softirq)
由 Claude Code 分析生成