Linux Kernel PREEMPT_RT 实时内核深度实战:从调度延迟确定性到 AI 推理低时延保障

引言

硬实时系统要求在限定时间内对外部事件作出确定性响应。Linux 作为通用操作系统,其默认调度策略以吞吐量最优为目标,而非延迟确定性。PREEMPT_RT 补丁集历经二十余年开发,终于在 Linux 6.12 中正式主线化,标志着 Linux 从此具备生产级硬实时能力。

本文将深入 PREEMPT_RT 的核心机制:实时调度类、线程化中断、优先级继承协议、自旋锁迁移策略,并结合 AI 推理服务的低延迟保障工作负载,给出可落地的工程实战方案。

一、内核抢占模型演进

1.1 抢占粒度的演进

Linux 内核抢占模型经历了四个阶段:

┌─────────────────────────────────────────────────────────────────┐
│ Model              │ Preempt Point    │ Latency    │ Era       │
├─────────────────────────────────────────────────────────────────┤
│ NONE               │ 用户态返回       │ 100ms+     │ 2.4 之前  │
│ Voluntary          │ 显式抢占点       │ 10ms+      │ 2.6早期  │
│ Server             │ 大部分内核代码   │ 1-10ms     │ 2.6.23+   │
│ Desktop/FULL       │ 几乎全部         │ 100μs级    │ 5.15+     │
│ PREEMPT_RT         │ 几乎全部+中断线程 │ 10-50μs   │ 6.12 主线 │
└─────────────────────────────────────────────────────────────────┘

在传统 CONFIG_PREEMPT_VOLUNTARY 模式下,内核代码执行期间不会被强制抢占,必须等到显式调用 cond_resched() 或返回用户态。这在 GUI 和吞吐量场景下是合理的,但对实时任务而言是不可接受的。

1.2 CONFIG_PREEMPT_RT 的三大支柱

原则:将内核中非确定性延迟源最小化,通过中断线程化和锁机制替换实现抢占点全覆盖。

/* 内核配置关键选项 */
CONFIG_PREEMPT_RT=y
CONFIG_HZ_1000=y              // 1000Hz tick 频率
CONFIG_HIGH_RES_TIMERS=y       // 高精度定时器
CONFIG_NO_HZ_FULL=y            // 全动态 tick,减少时钟中断干扰
CONFIG_RCU_NOCB_CPU=y          // 将 RCU 回调卸载到专用线程

二、线程化中断:消除中断不可抢占窗口

2.1 传统中断模型的硬伤

传统中断处理中,ISR(Interrupt Service Routine)运行在关中断或中断上下文中,优先级高于任何内核线程。当一个高优先级实时任务就绪时,如果当前 CPU 正在执行 ISR,该任务必须等待 ISR 完成——这个过程完全没有上限。

/* 传统中断注册 —— ISR 不可被抢占 */
request_irq(irq, my_isr, IRQF_SHARED, "mydev", dev);

/* 线程化中断 —— ISR 在独立线程中执行 */
request_threaded_irq(irq, my_irq_check, my_thread_fn,
                     IRQF_ONESHOT, "mydev", dev);

2.2 线程化中断的实现原理

request_threaded_irq 将中断处理拆分为两级:

  1. Primary Handler(顶半部):在中断上下文中快速检查中断是否来自本设备,返回 IRQ_WAKE_THREAD 唤醒线程。
  2. Threaded Handler(底半部):运行在内核线程 irq/%d-%s 中,默认 SCHED_FIFO 优先级 50,可被更高优先级任务抢占。
# 查看线程化中断的实时优先级
ps -e -T -o pid,tid,class,rtprio,comm | grep irq
   PID    TID CLS RTPRIO COMMAND
    72     72 FF     50 irq/40-gpio
    85     85 FF     50 irq/42-nic
    91     91 FF     50 irq/45-nvme

2.3 关键工程实践

/*
 * 实战要点:网络中断与实时任务分离部署
 *
 * 将网卡中断绑定到 CPU1-2,实时任务部署到 CPU3
 * 这样硬件中断不会抢占正在执行实时任务的 CPU
 */
echo "1-2" > /proc/irq/42/smp_affinity_list   # 网卡中断 → CPU1-2
taskset -c 3 chrt -f 99 ./rt_task              # 实时任务 → CPU3, SCHED_FIFO prio 99

三、实时调度类与优先级继承

3.1 Linux 调度类层次

Linux 调度器采用模块化调度类(Sched Class)架构,优先级从高到低:

SCHED_DEADLINE  ─── 基于 EDF(最早截止时间优先)和 CBS(恒定带宽服务器)
    ↓
SCHED_FIFO      ─── 先进先出,同优先级不轮转,持续运行直到主动让出
    ↓
SCHED_RR        ─── 轮转,同优先级按时间片轮转
    ↓
SCHED_OTHER(CFS) ─── 完全公平调度,普通分时任务
    ↓
SCHED_IDLE      ─── 最低优先级,系统空闲时运行

3.2 SCHED_DEADLINE:理论最优的实时调度

SCHED_DEADLINE 基于实时理论中的 EDF 算法,每个任务声明三个参数:

struct sched_attr {
    .size           = sizeof(struct sched_attr),
    .sched_policy   = SCHED_DEADLINE,
    .sched_runtime  =  5000000,   // 5ms   - 每个周期内可运行时间
    .sched_deadline = 10000000,   // 10ms  - 必须在此时限前完成
    .sched_period   = 10000000,   // 10ms  - 周期
};

sched_setattr(0, &attr, 0);

该调度类的可利用率边界为 runtime ≤ deadline ≤ period,且系统总利用率不超过 100% 时,所有任务均可满足截止时间。

实战:AI 推理请求的截止时间保障

/*
 * 对推理服务最关键的两类延迟:
 * - TTFT (Time To First Token): prefill 阶段,典型预算 50-200ms
 * - TPOT (Time Per Output Token): decode 阶段,典型预算 10-30ms
 */
int setup_deadline_sched(int token_budget_us, int deadline_us) {
    struct sched_attr attr = {
        .size         = sizeof(attr),
        .sched_policy = SCHED_DEADLINE,
        .sched_runtime  = token_budget_us * 1000ULL,
        .sched_deadline = deadline_us * 1000ULL,
        .sched_period   = deadline_us * 1000ULL,
    };
    return sched_setattr(0, &attr, 0);
}

3.3 优先级反转问题

当低优先级任务持有高优先级任务所需资源(如互斥锁)时,若中等优先级任务抢占低优先级任务,高优先级任务会被无限期阻塞——这就是经典的优先级反转问题。

时间线 (Priority Inversion):
─────────────────────────────────────────────────────────
Low(Prio=10):     [acquire lock]=====[???? blocked by Mid]=====[release]
Mid (Prio=50):         [=======================]  (preempt Low)
High(Prio=99):  [.. waiting for lock ..∞ waiting ..]  (被优先级反转!)

如果没有优先级继承:High 被阻塞时间 = Mid 的执行时间(无界)

3.4 Priority Inheritance Mutex 实现

PREEMPT_RT 强制所有 mutex 实现优先级继承协议:

/*
 * 优先级继承过程:
 * 1. High(Prio=99) 请求锁,锁被 Low(Prio=10) 持有
 * 2. Low 的优先级被临时提升到 99
 * 3. Low 无法被 Mid(Prio=50) 抢占(因为 Low 现在在 Prio=99)
 * 4. Low 快速释放锁
 * 5. Low 恢复原始优先级 10
 * 6. High 获得锁,执行完毕
 * 7. Mid 得以运行
 */
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);

pthread_mutex_t lock;
pthread_mutex_init(&lock, &attr);

// 此时锁具有 RT-mutex 语义,PTHREAD_PRIO_INHERIT 生效

内部实现基于 rt_mutex 结构体,其等待队列按优先级排序:

struct rt_mutex {
    raw_spinlock_t      wait_lock;
    struct rb_root_cached   waiters;  // 红黑树,按 prio 排序
    struct task_struct  *owner;
};

struct rt_mutex_waiter {
    struct rb_node      tree_entry;
    struct task_struct  *task;
    struct rt_mutex     *lock;
};

四、自旋锁迁移:从关抢占到 RT-Mutex

4.1 传统自旋锁的问题

在不可抢占内核中,spin_lock() 通过禁止 CPU 间并行来保护临界区。但在可抢占内核中,如果一个低优先级任务持有自旋锁,高优先级任务在同一 CPU 上自旋等待是浪费时间(而传统模式中高优先级任务又被禁止抢占低优先级)。

4.2 PREEMPT_RT 的解决方案:自旋锁变睡眠锁

/*
 * PREEMPT_RT 中的 spinlock_t 本质是一个 RT-mutex
 *
 * 行为变化:
 * - 不可睡眠上下文(硬中断、原子上下文):仍是真正的自旋锁
 * - 可睡眠上下文(线程上下文):变为可睡眠的 rt_mutex
 */

#ifdef CONFIG_PREEMPT_RT
typedef struct raw_spinlock {
    struct rt_mutex lock;     // rt_mutex 基于 PI 实现
} raw_spinlock_t;
#else
typedef struct raw_spinlock {
    arch_spinlock_t raw_lock;
} raw_spinlock_t;
#endif

关键影响:

传统 spin_lock PREEMPT_RT 中的 spin_lock
CPU 忙等待 可睡眠等待,持有锁时允许抢占
调用前后禁止抢占 允许被更高优先级任务抢占
不可睡眠上下文唯一选择 可睡眠时使用 mutex 更安全

实战建议:当临界区需要睡眠(如拷贝用户态数据)时,必须使用 mutex,不能用 spin_lock。

五、实时性测量与性能调优

5.1 cyclictest:黄金标准延迟测量工具

# 安装
apt install rt-tests

# 标准测试:测量调度延迟分布
cyclictest -D 10m -a 3 -t 1 -p 99 -i 1000 -h 400

# 参数说明:
# -D 10m       :运行 10 分钟
# -a 3        :绑定到 CPU 3
# -t 1        :单线程
# -p 99       :SCHED_FIFO 优先级 99
# -i 1000     :周期 1000μs
# -h 400      :直方图上限 400μs

典型输出与分析:

# /dev/cpu_dma_latency set to 0us
policy: fifo: loadavg: 0.00 0.01 0.05 1/56 1234

T: 0 (  1234) P:99 I:1000 C: 6283000 Min:      4 Act:    8 Avg:    9 Max:     127
T: 0 (  1235) P:99 I:2000 C: 3141000 Min:      3 Act:    7 Avg:    8 Max:      98

指标解读:

指标 含义 健康值(PREEMPT_RT)
Min 最小延迟 3-8 μs
Avg 平均延迟 5-15 μs
Max 最坏情况延迟 < 50 μs 合格,< 100 μs 良好

5.2 常见延迟源与抑制手段

# 1. 禁用 CPU 频率调节(锁定最高频)
for cpu in /sys/devices/system/cpu/cpu[0-3]; do
    echo "performance" > $cpu/cpufreq/scaling_governor
done

# 2. 禁用 C-State 深度休眠(限制 C-state 到 C1)
# GRUB: processor.max_cstate=1 intel_idle.max_cstate=1
for cpu in /sys/devices/system/cpu/cpu*/cpuidle/state*/disable; do
    # 只保留 C1(state0),禁用 C1E/C3/C6/C7
    echo 1 > $cpu  # 注意:state0=保留, state1+可能需选择性禁用
done

# 3. 隔离 CPU 核心(GRUB 参数)
# GRUB: isolcpus=3 nohz_full=3 rcu_nocbs=3
# 效果:CPU3 不参与常规调度,专用于实时任务

# 4. 内存锁定(防止 page fault 导致延迟)
mlockall(MCL_CURRENT | MCL_FUTURE);

5.3 内核追踪:实时延迟根因分析

# 使用 ftrace 追踪最大延迟来源
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo "preemptirqs" > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/events/preemptirq/irq_disable/enable
echo 1 > /sys/kernel/debug/tracing/events/preemptirq/preempt_disable/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 复现延迟尖峰后查看
cat /sys/kernel/debug/tracing/trace | tail -100

六、实战:构建低延迟 AI 推理服务

6.1 系统架构设计

┌─────────────────────────────────────────────────────────────────────┐
│                 Low-Latency AI Inference Pipeline                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  NIC ──[CPU0-1: io_uring data plane]──→ Ring Buffer ──→           │
│                                                                     │
│  CPU3: SCHED_FIFO 99                                                │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │ Pre-tokenization → KV-Lookup → Prefill → First Token Output │  │
│  │ (mlockall + isolated CPU + POLL immedately from ring buffer) │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                                                                     │
│  CPU3: SCHED_FIFO 98                                                │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │ Decode Loop: paged attention KV cache → Logits → Sample     │  │
│  │ (per-token deadline scheduling for TPOT guarantee)          │  │
│  └──────────────────────────────────────────────────────────────┘  │
│                                                                     │
│  CPU0-1: Background (logging, metrics, monitoring)                  │
└─────────────────────────────────────────────────────────────────────┘

6.2 关键代码段:实时任务设置

#define _GNU_SOURCE
#include <sched.h>
#include <sys/mman.h>
#include <pthread.h>

int setup_rt_inference_task(int cpu, int prio) {
    /* 1. 锁定所有内存(防 page fault) */
    if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
        perror("mlockall failed");
        return -1;
    }

    /* 2. 禁止内存过度提交 */
    int ret = 0;
    FILE *f = fopen("/proc/sys/vm/overcommit_memory", "w");
    if (f) { fprintf(f, "2"); fclose(f); }

    /* 3. CPU 亲和性绑定 */
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(cpu, &cpuset);
    if (sched_setaffinity(0, sizeof(cpuset), &cpuset) == -1) {
        perror("sched_setaffinity");
        return -1;
    }

    /* 4. 设置实时调度策略 */
    struct sched_param param = { .sched_priority = prio };
    if (sched_setscheduler(0, SCHED_FIFO, &param) == -1) {
        perror("sched_setscheduler");
        return -1;
    }

    /* 5. 禁用动态 tick(需要在内核启动参数 nohz_full 中配置) */
    /* 6. 预热 TLB 和 cache */
    volatile char *warmup = mmap(NULL, 64 * 1024 * 1024,
                                  PROT_READ | PROT_WRITE,
                                  MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE,
                                  -1, 0);
    for (size_t i = 0; i < 64 * 1024 * 1024; i += 4096)
        warmup[i] = 0;
    munmap(warmup, 64 * 1024 * 1024);

    return 0;
}

6.3 使用 chrt 工具快速验证

# 不修改代码,直接以实时优先级运行程序
sudo chrt -f 99 taskset -c 3 ./inference_server

# 验证运行状态
ps -e -o pid,class,rtprio,comm | grep inference_server
  5678 FF     99 inference_server

# 实时测量端到端延迟
#!/bin/bash
# rt_latency_probe.sh:使用 perf + sched_schedstats 追踪
echo 1 > /proc/sys/kernel/sched_schedstats
perf sched record -a -- sleep 10
perf sched latency --sort max | head -20
# 输出各任务的最大调度延迟

七、PREEMPT_RT 生产部署 Checklist

层级 配置项 验证方法
BIOS 关闭 C-State > C2, 关闭 SpeedStep/Turbo, 关闭超线程 cpupower idle-info, lscpu
GRUB isolcpus=3 nohz_full=3 rcu_nocbs=3 processor.max_cstate=1 /proc/cmdline
内核 CONFIG_PREEMPT_RT=y, CONFIG_HZ_1000=y grep PREEMPT /boot/config-$(uname -r)
中断 非实时中断绑定到隔离 CPU 之外 /proc/irq/*/smp_affinity_list
CPU 频率 performance governor cpupower frequency-info
进程 mlockall + SCHED_FIFO + CPU 亲和性 ps -e -o pid,class,rtprio,comm
验证 cyclictest 运行 2 小时以上 Max latency < 50μs

八、总结

PREEMPT_RT 的本质是将操作系统的"响应速度"从统计意义上的"足够快"升级为确定性边界内的"可证明"。其核心机制——线程化中断消除中断不可抢占窗口、优先级继承解决资源争用引起的无界阻塞、自旋锁可睡眠化减小关抢占区域——共同构成了 Linux 实时性的理论闭环。

对于 AI 推理场景,当 p99 延迟差异意味着用户体验的天壤之别时,PREEMPT_RT 提供了从内核层面解决问题的能力。但它绝非银弹:正确配置需要深入理解从 BIOS 到内核参数再到应用代码的完整栈,还需要持续验证——毕竟"不可测量的实时不是实时"。

参考资源

  • Linux Foundation RT Wiki: https://wiki.linuxfoundation.org/realtime/
  • PREEMPT_RT 官方文档: https://wiki.linuxfoundation.org/realtime/documentation
  • Documentation/trace/ftrace.rst —— 内核延迟追踪
  • OSADL Latency Plot: —— 工业级实时性可视化
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部