Skip to content

Latest commit

 

History

History
2026 lines (1541 loc) · 70.1 KB

File metadata and controls

2026 lines (1541 loc) · 70.1 KB

Linux PREEMPT_RT 实时内核深度解析

基于 Linux Kernel 源码深度分析(Linux 6.12+) 路径:kernel/locking/ | kernel/irq/ | include/linux/preempt.h | include/linux/spinlock_rt.h


目录

  1. 实时内核背景
  2. 内核抢占模型对比
  3. RT spinlock:rtmutex 替换
  4. rtmutex 与优先级继承
  5. 中断线程化
  6. Softirq 线程化与 local_bh_disable 语义变化
  7. RCU 在 RT 下
  8. 内存分配与实时性
  9. 高精度定时器与 tickless
  10. CPU 隔离
  11. 实时调度类
  12. 实时内核测试与延迟追踪
  13. PREEMPT_RT 合入主线进展
  14. 关键文件目录索引

1. 实时内核背景

1.1 工业场景的延迟需求

实时系统的核心指标不是吞吐量,而是有界延迟(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)在负载较大时调度延迟可达数毫秒甚至数十毫秒,完全无法满足上述需求。

1.2 延迟来源深度分析

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

1.3 PREEMPT_RT 补丁的历史

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 在内核邮件列表宣告完成

2. 内核抢占模型对比

Linux 内核支持多种抢占模型,通过 Kconfig 在编译时选择,抢占性越强,实时性越好,但代码复杂度也越高。

2.1 五种抢占模型

+-------------------+------------------------------------------+------------------+
| 配置选项           | 说明                                     | 典型最大延迟      |
+-------------------+------------------------------------------+------------------+
| 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 (优化后) |
+-------------------+------------------------------------------+------------------+

2.2 preempt_count 位图结构

内核用一个 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

2.3 RT 内核中 softirq_count 的关键差异

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)))
#endif

2.4 PREEMPT_LOCK_OFFSET:spinlock 不再禁止抢占

include/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 的任务完全可以被抢占。这一行改变了整个内核锁的语义。

2.5 preempt_disable_nested:RT 专用的局部禁抢占

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 操作。

2.6 migrate_disable:替代 preempt_disable 的迁移语义

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() → 仅禁止迁移,不禁止抢占

3. RT spinlock:rtmutex 替换

3.1 spinlock_t 在 RT 下的变化

在 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,而非传统的原子计数器。

3.2 API 兼容层

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 状态始终有效,无需保存/恢复。

3.3 rt_spin_lock 实现

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);   // 竞争时进入慢路径,睡眠等待
}

3.4 rt_spin_unlock 实现

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 保护的正确语义。

3.5 状态保存语义

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 等),获得锁后恢复原状态,不会丢失其他来源的唤醒信号。

3.6 RT rwlock

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

3.7 RT spinlock 与普通 spinlock 对比总结

+-------------------------+------------------------+------------------------+
| 特性                    | 普通 spinlock          | RT spinlock            |
+-------------------------+------------------------+------------------------+
| 底层实现                | 原子变量 + 忙等        | rtmutex(睡眠锁)      |
| 竞争时行为              | 自旋等待(占 CPU)     | 睡眠阻塞(让出 CPU)   |
| 是否禁止抢占            | 是(PREEMPT_LOCK)     | 否(PREEMPT_LOCK=0)   |
| 是否禁止中断            | spin_lock_irq 时禁止   | 永不禁止中断           |
| 优先级继承              | 无                     | 有(通过 rtmutex PI)  |
| API 兼容性              | 标准                   | 完全兼容(宏替换)     |
| 可在 RT 任务中使用      | 有延迟风险             | 安全,有界延迟         |
| lockdep 支持            | 完整                   | 完整                   |
+-------------------------+------------------------+------------------------+

4. rtmutex 与优先级继承

4.1 rt_mutex 数据结构

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)

4.2 rt_mutex_waiter 等待者结构

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 计算)

4.3 优先级继承(Priority Inheritance)原理

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 的临界区时间(有界!)

4.4 rt_mutex_adjust_prio_chain:PI 链传播

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_depthinclude/linux/rtmutex.h,第 21 行)控制最大链深度,防止环形死锁导致无限循环:

extern int max_lock_depth;   // 默认值 1024

4.5 rt_mutex_adjust_prio:优先级调整实现

kernel/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 的有效优先级
}

4.6 等待者比较:同时支持 SCHED_FIFO 和 SCHED_DEADLINE

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)均正确工作。

4.7 PI Ceiling vs PI Inheritance 对比

+---------------------------+------------------------------------------+
| 机制                      | 说明                                     |
+---------------------------+------------------------------------------+
| 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。

4.8 PI 链传播的多层嵌套锁示例

三层嵌套锁场景:

任务:  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 的临界区(均有界)

5. 中断线程化

5.1 设计目标与架构

传统硬中断(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 的任务抢占
+------------------+

5.2 irq_thread 主循环

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;
}

5.3 强制线程化:RT 下的差异

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,因为中断处理已在线程中运行,不存在重入问题。

5.4 IRQF_TIMER:保留硬中断

不是所有中断都被线程化。带有 IRQF_TIMER 标志的定时器中断仍然在硬中断上下文运行,这是因为:

  1. 调度器 tick 必须在硬中断中运行,否则 RT 任务的时间片统计会混乱
  2. 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

5.5 中断线程优先级规划

在实际部署中,需要根据中断的实时性要求调整其线程优先级:

# 查看中断线程的默认优先级(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 任务。


6. Softirq 线程化与 local_bh_disable 语义变化

6.1 标准 softirq 的问题

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 任务无法被调度,是延迟的重要来源。

6.2 RT 内核中的 softirq_ctrl 结构

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. */

6.3 local_bh_disable 在 RT 下的语义变化

在非 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)
  --> 若被抢占高优先级任务可以先运行
  --> 返回后继续持有 locksoftirq 不会运行

这一语义变化的关键意义:在 RT 内核中,local_bh_disable() 区段可以被高优先级任务抢占。这解决了原本 BH 禁用段导致的延迟问题。

6.4 softirq 执行路径:ksoftirqd 接管

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 任务抢占

6.5 softirq 线程化与 RCU 的配合

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/Nrcuog/N)完成,而非在 softirq 中进行。


7. RCU 在 RT 下

7.1 传统 RCU 的抢占问题

传统(非 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 任务的延迟将变得不可接受。

7.2 PREEMPT_RCU:允许在读侧临界区内被抢占

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 读侧临界区中
}

7.3 PREEMPT_RCU 的 grace period 影响

允许抢占意味着 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,可配置)

7.4 rcu_note_context_switch:调度器与 RCU 的交互

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

7.5 rcu_read_lock_bh 与 rcu_read_lock_sched 的区别

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 失效

8. 内存分配与实时性

8.1 GFP_ATOMIC vs GFP_KERNEL 在 RT 上下文

内存分配标志与 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 会有以下风险:

  1. 直接回收(direct reclaim):内存不足时触发页面扫描和回收,可能持续数毫秒
  2. OOM killer:极端情况下触发 OOM,杀死其他进程,延迟不可预测
  3. 压缩(compaction):内存碎片整理,可能持续数百毫秒

8.2 页面错误对实时任务的影响

页面错误(page fault)是 RT 系统最常见的"隐藏延迟"来源:

页面错误类型与 RT 影响:

1. 次要缺页(minor fault):
   页面在内存中但未映射(如 CoW)
   延迟:~1~10μs(可接受)

2. 主要缺页(major fault):
   页面不在内存中,需要从磁盘读取
   延迟:~10ms~数百ms(完全不可接受!)

3. 栈增长缺页:
   任务栈自动扩展
   延迟:~1~50μs(需要关注)

4. mmap 文件缺页:
   延迟依赖于 I/O,完全不可预测

8.3 mlockall:预锁定内存

解决页面错误的根本方法是使用 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;
}

8.4 Huge Page 对 TLB miss 的优化

普通 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(视工作集和访问模式)

8.5 NUMA 感知内存分配

在多 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 节点分配

9. 高精度定时器与 tickless

9.1 hrtimer 子系统

标准内核的 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.

9.2 调度器 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 任务的带宽限制准确执行。

9.3 hrtimer_cancel_wait_running: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。

9.4 实时任务的精确睡眠路径

用户实时任务 (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 典型值)

9.5 CONFIG_HZ 与实际唤醒延迟

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)

9.6 NO_HZ_FULL 与实时任务

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,延迟最小化

10. CPU 隔离

10.1 隔离机制全景

+----------------------------------------------------------------+
|                    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 不被中断打扰                      |
+----------------------------------------------------------------+

10.2 isolcpus 内核参数

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() 强制将任务绑定到隔离核。

10.3 rcu_nocbs:RCU 回调卸载

作用机制:

无 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 上运行!)

10.4 irqaffinity:中断亲和性

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
done

10.5 cpuset:更精细的 CPU 隔离

cpuset 通过 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_exclusive

10.6 完整的 CPU 隔离配置示例

8 核服务器(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_app

11. 实时调度类

11.1 Linux 调度类层次

Linux 调度器是模块化的,调度类按优先级从高到低排列:

+------------------+   优先级最高
| 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(空闲任务)
+------------------+   优先级最低

11.2 SCHED_FIFO / SCHED_RR

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)

11.3 SCHED_DEADLINE:EDF 实时调度

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)

11.4 SCHED_DEADLINE 与 RT mutex PI 集成

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;
}

11.5 sched_setscheduler 与 CAP_SYS_NICE

设置实时调度策略需要 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, &param) != 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);

11.6 RT 节流(rt_runtime_us / rt_period_us)

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_us

RT 节流的实现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);

11.7 SCHED_DEADLINE 带宽控制

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_us

12. 实时内核测试与延迟追踪

12.1 cyclictest 工作原理

cyclictest 是 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    严重问题,不适合任何实时应用

12.2 rtla 工具套件

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 的持续时间和频率

12.3 ftrace 延迟追踪

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μs

preemptoff 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 Management

preemptirqsoff(综合 tracer):

echo preemptirqsoff > /sys/kernel/debug/tracing/current_tracer
# 追踪 IRQ 或抢占被禁用的最长时间段(两者都计入)

12.4 trace_irqsoff / trace_preemptoff 内核宏

内核代码中通过以下宏集成 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)

12.5 延迟分析流程

+------------------+
| 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. 调整中断亲和性
+------------------+

12.6 典型延迟数字参考

平台            配置                    最大延迟(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 倍

13. PREEMPT_RT 合入主线进展

13.1 主线合入里程碑

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 年开发工作完成

13.2 主线合入的意义

对用户的影响

  • 主流发行版(Ubuntu 25.04+、Fedora 41+、Debian 13+)的标准内核可直接启用 RT
  • 不再需要下载独立 RT 补丁、打补丁、单独编译
  • RT 特性与内核其他功能保持同步更新,减少兼容性问题
  • linux-rt 外部补丁维护压力消失,社区力量集中于主线优化

仍需关注的事项

  1. 硬件驱动:部分驱动仍有 raw_spinlock 的长时持有,需要厂商适配
  2. 固件延迟:BIOS/UEFI 的 SMI 无法被 RT 内核消除,需 BIOS 配置配合
  3. 内存带宽:CPU 缓存预热、NUMA 拓扑等因素仍会影响延迟
  4. 中断控制器:某些 APIC 配置可能引入额外延迟

13.3 验证当前内核的 RT 支持

# 方法一:查看抢占模型
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

14. 关键文件目录索引

核心头文件

文件路径 主要内容
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_waiterrt_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 分析生成