3、中断与异常处理优化:从根源到实战
中断,是嵌入式系统里最核心的异步事件处理机制。说白了,CPU 正在跑你的主循环,突然外设喊了一嗓子:“我有数据了!”CPU 就得停下手里的活,去响应这个请求。这个“停下手里的活”到“开始干活”之间的时间差,就是中断延迟。
我见过不少项目,系统跑着跑着就丢数据、卡顿,查到最后都是中断延迟惹的祸。这一章,咱们就把中断延迟的根儿挖出来,再聊聊怎么用工具把它压到最低。
核心观点:中断延迟不是玄学,它是可测量、可分析、可优化的。你只要掌握了延迟的来源和对应的调优手段,大部分性能问题都能迎刃而解。
3.1 中断延迟的来源分析
中断延迟,从硬件触发到软件开始执行 ISR,中间经历了好几个环节。我习惯把它拆成三部分:硬件延迟、内核延迟、以及最容易被忽略的——中断嵌套与共享带来的排队延迟。
| 延迟阶段 | 典型耗时 | 主要影响因素 |
|---|---|---|
| 硬件延迟 | 几十 ns ~ 几 μs | 总线仲裁、中断控制器响应时间、CPU 流水线排空 |
| 内核延迟 | 几 μs ~ 几十 μs | 关中断时间、中断向量查找、寄存器保存 |
| 排队与嵌套 | 不确定,可能上百 μs | 高优先级中断打断、共享中断线、ISR 执行时间过长 |
我在一个 ARM Cortex-A7 的项目上遇到过,网卡中断偶尔会丢包。用示波器一量,从硬件拉高 IRQ 引脚到 CPU 进入中断服务函数,竟然差了 80 μs。后来发现是内核里某个驱动在关中断的临界区里待了太久。嗯,这里要注意:关中断时间过长,是所有中断延迟问题的头号元凶。
避坑指南:我曾经在调试一个多媒体播放器时,发现音频中断偶尔会“吞掉”视频同步信号。查了两天才发现,音频驱动在 ISR 里做了太多计算,导致视频中断被延迟了将近 200 μs。记住:ISR 里只做最必要的事,把耗时操作扔给 tasklet 或 workqueue。
3.2 中断线程化与 IRQ affinity
传统的中断上下文是原子性的,你不能 sleep,不能拿信号量。这给驱动开发带来了很多限制。Linux 从 2.6.30 开始引入了中断线程化,说白了就是把 ISR 的一部分放到内核线程里跑,这样你就可以在里面舒舒服服地 sleep 了。
中断线程化的好处很明显:
- 降低关中断时间:硬中断部分只做最紧急的寄存器操作,剩下的交给线程
- 可睡眠:线程上下文里可以调用
wait_event、mutex_lock等阻塞函数 - 可被调度:系统负载高时,中断线程可以被其他任务抢占,避免饿死用户态进程
怎么用?很简单,在 request_threaded_irq() 里注册两个 handler:
// 硬中断 handler:只做最少的硬件操作
static irqreturn_t my_irq_handler(int irq, void *dev_id) {
// 读状态寄存器,清中断标志
writel(0x1, reg_base + INT_CLR);
// 返回 IRQ_WAKE_THREAD,告诉内核去唤醒线程
return IRQ_WAKE_THREAD;
}
// 线程化 handler:可以 sleep,做复杂处理
static irqreturn_t my_thread_fn(int irq, void *dev_id) {
struct my_dev *dev = dev_id;
// 这里可以拿 mutex,可以 schedule_work
mutex_lock(&dev->lock);
// ... 处理数据
mutex_unlock(&dev->lock);
return IRQ_HANDLED;
}
// 注册时传入两个函数
ret = request_threaded_irq(irq_num, my_irq_handler, my_thread_fn,
IRQF_TRIGGER_RISING, "my_device", dev);
再说 IRQ affinity。这个机制允许你把某个中断绑定到指定的 CPU 核心上。为什么需要这个?你想想看,如果所有中断都往 CPU0 上涌,CPU0 忙死,其他核心闲死,缓存还不停失效。把中断分散到不同核心,能显著提升吞吐量。
设置方法有两种:
- 运行时设置:
echo 1 > /proc/irq/<irq_num>/smp_affinity(1 表示 CPU0,2 表示 CPU1,4 表示 CPU2,以此类推) - 代码里设置:在驱动 probe 时调用
irq_set_affinity_hint()
个人经验:我习惯把网卡中断绑到 CPU1,把存储设备中断绑到 CPU2,CPU0 留给系统调度和用户态应用。这样隔离之后,网络吞吐量提升了 15%,而且延迟抖动明显减小。
3.3 中断负载均衡
手动设置 affinity 在小系统上还行,但到了 8 核、16 核的 SoC 上,手动配就太累了。这时候需要自动化的中断负载均衡。
Linux 提供了 irqbalance 守护进程。它会周期性地检查每个 CPU 的中断负载,然后动态调整中断的 affinity,让各个核心的负载尽量均匀。
启动方式很简单:
# 安装并启动 irqbalance
apt-get install irqbalance
systemctl enable irqbalance
systemctl start irqbalance
# 查看当前均衡策略
irqbalance --debug
不过要注意,irqbalance 不是万能的。我在一个实时性要求很高的项目上,发现 irqbalance 频繁迁移中断导致延迟抖动变大。后来我干脆关掉它,手动绑核,配合 CPU 隔离(isolcpus),效果反而更好。
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 手动 affinity | 核心数少、负载固定 | 延迟可控、无抖动 | 配置繁琐、不灵活 |
| irqbalance | 多核、负载动态变化 | 自动均衡、省心 | 可能引入迁移抖动 |
| CPU 隔离 + 手动绑核 | 实时系统、确定性要求高 | 极低延迟、完全隔离 | 浪费核心资源 |
3.4 使用 perf 和 ftrace 分析中断
光说不练假把式。咱们得用工具把中断延迟“抓”出来看。
perf:看中断频率和分布
perf 可以统计每个中断触发的次数、耗时、以及分布在哪个 CPU 上。
# 统计所有中断事件
perf stat -e irq:irq_handler_entry,irq:irq_handler_exit -a sleep 5
# 用 perf top 看哪个中断最频繁
perf top -e irq:irq_handler_entry
# 记录中断事件并生成报告
perf record -e irq:irq_handler_entry,irq:irq_handler_exit -ag sleep 10
perf report
输出里你会看到类似这样的信息:
# 每个中断的触发次数和平均耗时
irq_handler_entry:irq_handler_exit
129 eth0 平均耗时 3.2 μs
45 mmc0 平均耗时 8.7 μs
12 timer 平均耗时 1.1 μs
如果某个中断的平均耗时异常高,比如超过 50 μs,那基本可以断定这个 ISR 有问题。
ftrace:追踪中断延迟的细节
ftrace 能让你看到中断发生的完整上下文,包括关中断的时间、中断嵌套的情况。
# 挂载 tracefs
mount -t tracefs nodev /sys/kernel/tracing
# 只追踪中断相关事件
echo "irq:*" > /sys/kernel/tracing/set_event
# 开启追踪
echo 1 > /sys/kernel/tracing/tracing_on
# 跑你的测试程序,然后关掉
echo 0 > /sys/kernel/tracing/tracing_on
# 查看结果
cat /sys/kernel/tracing/trace
输出示例:
# tracer: nop
#
# entries-in-buffer/entries-written: 142/142 #P:4
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
<idle>-0 [001] d.h. 123.456789: irq_handler_entry: irq=29 name=eth0
<idle>-0 [001] d.h. 123.456792: irq_handler_exit: irq=29 ret=handled
注意看那个 d.h. 字段:d 表示中断被禁用(irqs-off),h 表示在硬中断上下文。如果 irqs-off 标记持续很长时间,说明关中断时间太长,这就是延迟的根源。
我的调试习惯:先用 perf 找到“嫌疑中断”(频率高或耗时长的),再用 ftrace 深挖这个中断的上下文。配合 trace-cmd 和 Kernelshark 图形化工具,能直观看到中断之间的时间关系。
3.5 中断优化知识体系
下面这张图总结了中断优化的核心路径,从延迟来源到分析工具,再到优化手段,一目了然。
这张图把整个优化流程串起来了。你从左上角的“硬件延迟”开始,顺着箭头往下走,每一步都有对应的工具和手段。我个人建议,拿到一个新板子,先跑一遍 perf 看看中断分布,心里有个底,再决定要不要动 affinity 或线程化。
最后说一句:中断优化没有银弹。每个系统都有自己的瓶颈,可能是关中断时间太长,可能是中断分配不均,也可能是 ISR 里干了太多不该干的事。关键是学会用工具去“看见”问题,而不是靠猜。perf 和 ftrace 就是你的眼睛,用好它们,中断延迟就不再是玄学。