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,说不定问题就出在这里。