3. 中断与轮询的对比:为什么中断能替代轮询,以及轮询的致命弱点

讲完了轮询和中断的基础概念,这一章我们直接切入核心问题:为什么中断能替代轮询? 或者说,轮询到底有什么致命弱点,让我们不得不“抛弃”它。

我个人习惯从实际效果出发。光看理论定义,你很难感受到两者的差距。必须先搞清楚轮询“死”在哪,才能明白中断“活”在哪。

3.1 轮询的致命弱点:CPU 被“白白占用”

说白了,轮询最大的问题就是让 CPU 一直做“无效劳动”。

我在一个嵌入式项目中遇到过类似的情况:一个 IO 密集型系统,CPU 几乎 100% 在跑一个 while 循环,不停地读取外部设备的状态寄存器。你猜怎么着?CPU 大部分时间读到的都是“无事件”。但 CPU 就是不能停下来,必须一遍遍地问:“有数据吗?有数据吗?”

这就是轮询的致命代价:CPU 的时间片被无差别地消耗掉了。

  • CPU 利用率假象:你用 top 命令看,CPU 使用率可能很高。但仔细分析,这些“高”都是轮询线程消耗的,而不是在处理实际业务。
  • 延迟不稳定:轮询的轮询间隔是固定的。如果事件在轮询刚检查完的那一瞬间发生,那它必须要等下一个轮询周期才能被处理。这个最坏延迟就是你设置的轮询周期。
  • 系统扩展性差:如果你有 10 个设备需要轮询,CPU 就得来回跑。设备多了,轮询周期就得拉长,或者 CPU 被彻底占满。系统性能很难线性扩展。
核心矛盾:轮询的本质是“主动询问”。CPU 作为核心资源,必须主动去关注外部事件。这导致了 CPU 的“忙等”,即 CPU 在等待事件的过程中,无法去执行其他有意义的任务。

你想想看,如果在一个高并发服务器上,使用轮询模式去处理所有网络连接,CPU 需要频繁地检查每个 socket 是否有数据。这简直是一场灾难。我记得有一次,团队里新来的同事尝试用 while(1) 循环去读非阻塞的网络 socket,结果服务器直接卡死了。因为 CPU 被这个死循环占满,其他所有任务都无法调度。

3.2 中断如何“拯救”CPU?

那中断是怎么做到的呢?

中断的核心思想是“被动通知”。CPU 不再主动去问,而是宣告:“我先把活干完。你有事了,主动来找我。”

我的理解是,这相当于从“拉模式”变成了“推模式”。硬件设备在数据准备好后,会通过总线发送一个中断请求信号(IRQ)。CPU 收到信号后,会暂停当前正在执行的任务,保存当前上下文,然后跳转到对应的中断处理程序(ISR)去处理事件。处理完后,再恢复现场,继续执行刚才被打断的任务。

这样做的好处非常明显:

  • CPU 零忙等:在中断到来之前,CPU 可以完全去做其他事情,比如运行用户程序、处理其他中断。
  • 响应速度快:事件发生与 CPU 响应处理之间的延迟,只取决于中断响应硬件延迟和软件的中断响应时间,极低。
  • 系统扩展性极佳:无论有多少设备,只要它们都能产生中断,CPU 就能在事件发生时进行响应。不会因为设备数量增加而导致 CPU 被无意义的轮询消耗殆尽。
避坑提醒:我曾经在一款 ARM 芯片上调试 USB 中断时,忽略了中断优先级配置。结果高优先级的中断服务函数里执行了耗时的操作,导致低优先级的中断一直被阻塞,系统响应变得极其迟钝。注意,中断服务函数要“快进快出”,不能在里面做复杂计算或调用阻塞性函数。

3.3 直观对比:Polling vs. Interrupt

为了让你更直接地感受两者的差异,我们来看一个代码级的对比。

假设我们要等待一个 UART 接收 1 字节数据。

Polling(轮询)

// while 循环,直到收到数据
while (!(UART_STATUS & RX_READY_BIT)) {
    // CPU 不断读寄存器,做无效循环
}
char byte = UART_DATA;
// 处理接收到的数据
process_byte(byte);

Interrupt(中断)

// UART 初始化时:
void uart_init(void) {
    // 配置 UART,开启接收中断
    UART_CTRL |= RX_INTERRUPT_ENABLE;
}

// 中断服务函数:
void UART_IRQHandler(void) {
    char byte = UART_DATA;
    // 处理接收到的数据
    process_byte(byte);
}

你看,轮询模式里,CPU 就在那死死等着。而中断模式里,CPU 在 uart_init 之后就解放了,可以去做其他事情。当数据到达时,硬件自动调用 UART_IRQHandler。这差距就出来了。

下面这个表格,总结了两者的核心差异。

对比维度 轮询(Polling) 中断(Interrupt)
触发方式 CPU 主动查询、等待 硬件被动通知、打断
CPU 利用率 低,存在大量无效忙等 高,只在需要时才占用 CPU
响应延迟 依赖轮询周期,最高延迟与周期相关 极低,取决于硬件延迟和 ISR 执行时间
系统可扩展性 差,设备增多时会线性增加 CPU 负载 好,设备增多时负载增加较小
实现复杂度 简单,直接循环 较高,需配置中断控制器、ISR 编写、上下文保存/恢复
典型应用场景 简单、低速外设,或对延迟不敏感的场合 大多数实时系统、高性能网络、IO 密集型应用

3.4 两者比较的流程图

为了更清晰地展示这个“拉”与“推”的差异,我画了一张流程图。

轮询与中断工作流程对比图 轮询 vs 中断工作流程对比 轮询 (Polling) CPU 开始执行轮询循环 检查状态寄存器 有事件? 无(继续轮询) 处理 中断 (Interrupt) CPU 执行其他任务 硬件事件发生 CPU 被中断打断 保存现场,执行 ISR

我个人的一个小建议:在设计系统时,你首先要评估外设的事件频率。如果事件频率极高(比如高速 ADC 采样),那中断反而会成为负担,因为 CPU 会被频繁打断,导致上下文切换开销过高,此时可能降级到轮询或使用 DMA。但就常规的 IO 事件、键盘、鼠标、网卡而言,中断是当之无愧的首选。

3.5 中断处理:一个隐藏的“坑”

说了这么多中断的好话,但中断也不是完美的。它有个全局性的“死区”,叫中断屏蔽。

当一个中断正在被处理时,CPU 通常会禁止其他同级或更低优先级的中断。这意味着,如果你在处理一个中断时,花了太长时间,其他重要的中断就会被延迟。这就是卡顿的一个常见来源。

我曾经在一个 I2C 总线的中断处理函数里,不小心加入了 100ms 的延时。结果导致系统其他高优先级中断(比如定时器)没有被及时响应,整个系统的实时性就崩了。

所以,处理中断要遵循一个原则:快干快走。ISR 里只做必要的、最小化的处理,比如读取数据、设置标志位。复杂的数据处理、业务逻辑,丢到任务或下半部(Bottom Half)去执行。

嗯,这就是问题的关键。轮询让你白白消耗 CPU,中断让你高效利用 CPU。这也是为什么在现代操作系统中,中断机制是处理硬件事件的标准做法。