4、中断处理优化:中断上下文的开销、中断线程化、使用request_threaded_irq、中断合并与负载均衡

中断处理,可以说是嵌入式Linux驱动里最敏感的话题之一。我见过太多系统因为中断处理不当,导致整个系统响应变慢,甚至出现丢数据的情况。说白了,中断处理就像医院的急诊室——处理得快,病人(数据)就没事;处理得慢,可能就出大问题了。

4.1 中断上下文的开销

先聊聊中断上下文。很多新手容易忽略一个问题:中断处理函数是在特殊环境下运行的。它不能睡眠,不能调用可能引起阻塞的函数,连printk都要慎用。

中断上下文的开销主要体现在三个方面:

  • 保存和恢复现场:每次进入中断,CPU都要保存当前寄存器状态,退出时再恢复。这个操作本身就有几十个CPU周期。
  • 中断屏蔽:在中断处理期间,同级别或更低级别的中断会被屏蔽。这意味着其他设备的中断请求得不到及时响应。
  • Cache污染:中断处理会打乱CPU的Cache,导致中断返回后,原来的进程执行效率下降。

关键数据:在ARM Cortex-A7平台上,一次空的中断处理(只做中断确认和清除)大约需要1-2微秒。如果你的中断处理函数里做了复杂操作,这个时间会成倍增长。

我在项目中遇到过这样一个案例:一个网卡驱动在中断处理函数里直接做了协议栈的部分处理,结果导致系统在高负载下频繁丢包。后来我们把处理逻辑移到了下半部,问题就解决了。

4.2 中断线程化

中断线程化,说白了就是把中断处理的一部分工作放到内核线程里去执行。这样做的最大好处是:线程可以睡眠,可以使用信号量,可以调用各种可能阻塞的函数。

为什么要这么做?你想想看,有些中断处理确实需要做很多工作,比如USB热插拔检测、SD卡插拔检测。这些操作往往需要等待硬件稳定,甚至需要与用户空间交互。如果全部在中断上下文里做,系统迟早会崩溃。

中断线程化的实现方式有两种:

  • 使用request_threaded_irq:这是最直接的方式,我们后面会详细讲。
  • 使用工作队列(workqueue):在传统的中断处理函数里,把耗时操作丢给工作队列。

我的建议:如果你的中断处理时间超过100微秒,就应该考虑线程化。100微秒是个经验值,你可以用ftrace来测量实际的中断处理时间。

4.3 使用request_threaded_irq

request_threaded_irq是Linux内核提供的一个API,它允许你注册两个处理函数:一个在中断上下文执行(称为硬中断处理函数),另一个在内核线程上下文执行(称为软中断处理函数或线程化处理函数)。

函数原型如下:

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 设备名称,用于/proc/interrupts显示
dev_id 设备ID,用于共享中断

一个典型的使用示例:

static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    /* 快速处理:只做必要的硬件操作 */
    /* 比如清除中断标志,读取状态寄存器 */
    return IRQ_WAKE_THREAD;
}

static irqreturn_t my_irq_thread_fn(int irq, void *dev_id)
{
    /* 耗时操作:可以睡眠,可以加锁 */
    /* 比如数据处理、协议解析、通知用户空间 */
    return IRQ_HANDLED;
}

/* 注册中断 */
ret = request_threaded_irq(irq_num,
                           my_irq_handler,
                           my_irq_thread_fn,
                           IRQF_TRIGGER_RISING,
                           "my_device",
                           dev_id);

注意:硬中断处理函数必须快速返回。如果它执行时间过长,会阻塞其他中断。我见过有人把整个数据处理都放在硬中断里,结果系统直接卡死。

这里有个小技巧:如果硬中断处理函数里什么都不做,可以直接把handler设为NULL。内核会自动创建一个线程来处理所有中断工作。但我不建议这么做,因为有些硬件操作必须在中断上下文完成,比如确认中断、清除标志。

4.4 中断合并与负载均衡

中断合并,英文叫Interrupt Coalescing。它的思路很简单:与其每个数据包都触发一次中断,不如攒一批再通知CPU。这样可以减少中断次数,降低CPU开销。

中断合并通常在网卡驱动里用得最多。我记得有一次调试一个千兆网卡驱动,发现CPU占用率高达80%,后来启用了中断合并,CPU占用率直接降到了30%。

中断合并的配置参数一般包括:

  • 合并阈值:攒多少个数据包才触发一次中断
  • 合并超时:最多等多久,即使没攒够也要触发中断

负载均衡则是把中断分配到不同的CPU核心上处理。在SMP(对称多处理)系统中,这可以显著提升性能。

Linux提供了irqbalance服务来自动分配中断。你也可以手动设置:

# 查看当前中断的CPU亲和性
cat /proc/irq/31/smp_affinity

# 设置中断31只在CPU0上处理
echo 1 > /proc/irq/31/smp_affinity

# 设置中断31在CPU0和CPU1上处理
echo 3 > /proc/irq/31/smp_affinity

经验之谈:中断负载均衡不是万能的。如果多个中断共享同一个硬件资源(比如同一个DMA控制器),把它们分配到不同CPU上反而可能引起缓存一致性问题。我曾经在一个项目里把两个相关的中断分到了不同CPU,结果性能反而下降了20%。

最后总结一下中断优化的几个要点:

  • 能短则短:中断处理函数越短越好,只做必要操作
  • 能线程则线程:耗时操作一定要线程化
  • 能合并则合并:高频中断考虑合并
  • 能均衡则均衡:多核系统合理分配中断

嗯,中断优化这块内容比较多,但核心思想就一个:让CPU少被中断打扰,把宝贵的时间留给真正的数据处理。下次遇到系统响应慢的问题,不妨先看看/proc/interrupts,说不定问题就出在这里。