3. Linux内核实时机制

中断,是实时系统的命门。你想想看,一个高优先级的中断来了,CPU 得立刻放下手头的事去伺候它。如果这个中断服务程序(ISR)里干了太多活,比如处理网络协议栈、操作磁盘 I/O,那系统响应其他紧急任务的能力就大打折扣。说白了,传统的中断机制在实时性面前,是个潜在的“定时炸弹”。

我个人习惯,在做实时性优化时,第一件事就是审视系统中所有中断的“体重”。太重的中断,必须减肥。怎么减?中断线程化就是最核心的手段之一。

3.1 中断上下文的拆分:上半部与下半部

在讲线程化之前,得先理解 Linux 中断处理的基本模型——上半部(Top Half)和下半部(Bottom Half)。

这个模型其实很朴素:

  • 上半部(Hard IRQ):在关闭本地中断或屏蔽当前中断线的上下文中执行。要求快、快、快!只做最必要的事,比如清除硬件中断标志、拷贝硬件寄存器数据到内存。绝对不能睡眠,也不能持有锁去等待。
  • 下半部(SoftIRQ / Tasklet / Workqueue):在开中断的上下文中执行,处理上半部留下的“重活”。比如网络驱动中,上半部只把收到的数据包从硬件 FIFO 搬到内存 sk_buff 里,然后触发下半部;下半部再去调用协议栈处理这个包。

我在项目中遇到过一个问题:一个 FPGA 采集卡的中断处理程序,上半部里居然做了 200 微秒的数据校验。这在普通桌面系统上可能感觉不到,但在我们的工业控制场景里,直接导致另一个高优先级任务错过了 500 微秒的 deadline。嗯,这就是典型的“上半部超重”。

核心原则: 上半部只做“原子级”的硬件操作,其余全部推给下半部。这是实时性优化的第一道防线。

3.2 中断线程化的原理与实现

传统的下半部(SoftIRQ/Tasklet)虽然开了中断,但它们仍然运行在中断上下文中,不能被其他进程抢占,也不能被高优先级实时任务打断。说白了,它们还是“特权阶级”。

中断线程化,就是把整个中断处理过程(除了最最紧急的硬件操作)变成一个内核线程。这个线程可以被调度器管理,有优先级,可以被抢占,可以睡眠。这样一来,实时任务就可以通过设置更高的优先级,在中断线程运行之前抢到 CPU。

为什么会这样?因为线程化的中断处理程序运行在进程上下文,它不再是“不可侵犯”的了。

3.2.1 实现方式:request_threaded_irq

在 Linux 内核中,注册一个线程化中断的接口是 request_threaded_irq()。它的原型如下:

int request_threaded_irq(unsigned int irq,
                         irq_handler_t handler,
                         irq_handler_t thread_fn,
                         unsigned long irqflags,
                         const char *devname,
                         void *dev_id);

参数说明:

参数含义
irq中断号
handler上半部处理函数(可以传 NULL,表示全部线程化)
thread_fn线程化处理函数(下半部,运行在进程上下文)
irqflags中断标志,如 IRQF_TRIGGER_RISING
devname设备名称
dev_id设备 ID,用于共享中断

这里有个关键点:handler 和 thread_fn 可以同时存在。我一般这样用:

  • handler:只做硬件 ACK(应答),清除中断挂起位。必须快,几微秒内返回。
  • thread_fn:做所有需要等待、需要锁、需要调度的操作。比如 SPI 读写、数据解析、唤醒用户态进程。
我的习惯: 如果硬件设计允许,我倾向于把 handler 设为 NULL,让内核自动处理硬件 ACK,然后全部逻辑交给 thread_fn。这样最干净,也最容易调试。

3.2.2 一个简单的驱动示例

假设我们有一个 GPIO 按键中断,按下时触发。传统写法可能直接在 ISR 里调用 input_report_key()。但线程化后,我们可以这样写:

static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
    /* 上半部:只做硬件 ACK,返回 IRQ_WAKE_THREAD */
    disable_irq_nosync(irq);
    return IRQ_WAKE_THREAD;
}

static irqreturn_t gpio_irq_thread_fn(int irq, void *dev_id)
{
    struct gpio_dev *dev = dev_id;
    int val;

    /* 下半部:在进程上下文中读取 GPIO 状态 */
    val = gpio_get_value(dev->gpio_pin);
    input_report_key(dev->input_dev, KEY_ENTER, val);
    input_sync(dev->input_dev);

    enable_irq(irq);
    return IRQ_HANDLED;
}

/* 注册中断 */
ret = request_threaded_irq(irq_num,
                           gpio_irq_handler,
                           gpio_irq_thread_fn,
                           IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                           "gpio_key",
                           dev);

注意看 IRQ_WAKE_THREAD 这个返回值。它告诉内核:“上半部搞定了,请唤醒我的线程化处理函数。” 这是线程化中断的“握手信号”。

3.3 中断响应延迟优化

中断线程化虽然提升了系统的可调度性,但它也引入了一个代价:中断响应延迟变长了。

为什么?因为从硬件中断触发,到线程化处理函数真正开始执行,中间多了一个调度过程。这个延迟包括:

  1. 硬件中断到上半部执行的时间(通常极短,纳秒级)。
  2. 上半部执行时间(微秒级)。
  3. 内核唤醒中断线程、调度器选择该线程的时间(可能几十到几百微秒,取决于系统负载)。
  4. 中断线程被调度到 CPU 上执行的时间。

我曾经在一个项目中,把所有的中断都线程化了,结果发现一个 1kHz 的传感器中断,响应抖动从原来的 5 微秒变成了 200 微秒。嗯,这就是“矫枉过正”。

避坑指南: 不是所有中断都适合线程化。高频中断(比如 10kHz 以上的数据采集)如果线程化,调度开销会吃掉大量 CPU,得不偿失。我一般只对低频、处理逻辑复杂的中断做线程化。

3.3.1 优化手段:优先级与 CPU 亲和性

既然中断线程变成了一个普通线程,我们就可以用调度器的手段来优化它:

  • 设置实时优先级: 使用 sched_setscheduler() 将中断线程设置为 SCHED_FIFO 或 SCHED_RR,并赋予高优先级。这样它能更快地被调度。
  • 绑定 CPU: 使用 pthread_setaffinity_np() 或内核的 irq_set_affinity_hint(),将中断线程绑定到某个专用 CPU 核心上,避免缓存抖动。
  • 降低线程优先级反转: 确保中断线程不会因为等待某个低优先级任务持有的锁而被阻塞。必要时使用 mutex 而非 spinlock,并考虑优先级继承协议。

我个人习惯,在系统启动脚本里,会专门写一段代码来调整关键中断线程的调度参数:

# 找到中断线程的 PID(例如 gpio_key 对应的线程)
cat /proc/interrupts | grep gpio_key
# 假设 PID 是 1234
chrt -f -p 80 1234
taskset -p 0x01 1234

这样,这个中断线程就拥有了 FIFO 调度策略、优先级 80,并且只跑在 CPU0 上。响应延迟可以从几百微秒降到几十微秒。

3.3.2 延迟测量与调优

优化不能靠猜。我常用的测量方法是:

  1. 在硬件中断触发时,用 GPIO 输出一个脉冲。
  2. 在中断线程处理函数入口处,再输出一个脉冲。
  3. 用示波器或逻辑分析仪测量两个脉冲之间的时间差。

这个时间差就是“线程化引入的调度延迟”。通过调整优先级和 CPU 亲和性,你可以直观地看到延迟曲线的变化。

经验数据: 在一个 4 核 ARM Cortex-A72 平台上,未优化的线程化中断延迟约 150-300 微秒;优化后(FIFO 优先级 90 + 独占 CPU),延迟可降至 20-50 微秒。如果硬件支持快速中断控制器,甚至可以压到 10 微秒以内。

3.4 中断线程化知识结构图

下面这张图梳理了中断线程化的核心逻辑和优化路径:

中断线程化知识结构图 硬件中断触发 上半部 (handler) 线程化处理 (thread_fn) 优化手段 设置实时优先级 (SCHED_FIFO) | 绑定 CPU 核心 降低优先级反转 | 延迟测量与调优 关键权衡 线程化提升可调度性,但引入调度延迟。高频中断不宜线程化,低频复杂中断收益最大。 优化后延迟可从 200μs 降至 20μs 以内(视硬件平台而定)。

中断线程化不是银弹,它是实时性优化工具箱里的一把精密扳手。用对了地方,系统响应如丝般顺滑;用错了,反而会引入不必要的抖动。我的建议是:先测量,再优化,最后验证。别凭感觉拍脑袋。