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 统计 ftrace 追踪 示波器/逻辑分析仪 中断线程化 IRQ affinity 中断负载均衡 降低中断延迟,提升系统确定性 说明: 从延迟来源出发,选择合适的分析工具定位问题,再针对性采用优化手段,最终实现低延迟、高确定性的中断系统。

这张图把整个优化流程串起来了。你从左上角的“硬件延迟”开始,顺着箭头往下走,每一步都有对应的工具和手段。我个人建议,拿到一个新板子,先跑一遍 perf 看看中断分布,心里有个底,再决定要不要动 affinity 或线程化。

最后说一句:中断优化没有银弹。每个系统都有自己的瓶颈,可能是关中断时间太长,可能是中断分配不均,也可能是 ISR 里干了太多不该干的事。关键是学会用工具去“看见”问题,而不是靠猜。perf 和 ftrace 就是你的眼睛,用好它们,中断延迟就不再是玄学。