从鼠标移动到 CPU 中断:一次“硬件中断”的真实路径
# 从鼠标移动到 CPU 中断:一次“硬件中断”的真实路径
我们平时说“硬件中断”,很容易脑补成这样:
这个直觉很自然,但在 USB 鼠标这个例子里,它并不准确。
真实情况更像是:
也就是说,鼠标不是直接把 CPU 叫起来的人。真正发起 CPU 硬件中断的,通常是 xHCI Controller。
这篇文章就从一个很普通的问题开始:
鼠标动了一下,CPU 到底是怎么知道的?
# 先拆掉一个误会
如果 CPU 想知道鼠标有没有动,最朴素的办法是一直问:
这叫轮询。
轮询不是不能用,但 CPU 不可能把大量时间都花在这种事情上。系统里不只有鼠标,还有键盘、网卡、磁盘、定时器、摄像头、传感器、DMA 控制器。CPU 如果什么都亲自问,就会很忙,而且忙得不太聪明。
所以系统通常会用中断:
这个模型适合理解中断的动机,但还不够精确。因为“设备”这个词太笼统了。
在 USB 鼠标这个例子里,真正通知 CPU 的并不是鼠标本体。
# USB 鼠标不是直接中断 CPU
USB 有一个很容易让人误会的名字:Interrupt Transfer。
很多人看到 USB 鼠标使用 interrupt transfer,就会以为:
但 USB 里的 interrupt transfer,和 CPU 意义上的 hardware interrupt,不是同一层东西。
USB 是 host 主导的总线。也就是说,USB 设备通常不能随便自己开口。设备什么时候传数据,主要由 host 侧控制器安排。
对于 USB 鼠标来说,大概是这样:
所以 USB 里的 interrupt transfer 更像是在说:
这个 endpoint 需要被周期性、低延迟地检查。
它不是说鼠标可以直接电气上打断 CPU。
这里有两个名字相同、层次不同的东西:
名字都叫 interrupt,但不要把它们混成一件事。
# xHCI 像一个 USB 管家
xHCI Controller 可以粗略理解成 USB 总线的硬件管家。
CPU 不想一直亲自问鼠标,于是把任务交给 xHCI:
于是完整路径变成:
CPU 看到的中断源,往往不是“鼠标”,而是“xHCI Controller”。驱动进入中断处理函数后,会去看 xHCI 的 event ring。它发现有 transfer event,再进一步知道这是某个 USB 鼠标的 HID report 到了。
最后,操作系统的 input 子系统才把它变成我们熟悉的鼠标移动事件。
所以更准确的一句话是:
鼠标数据是 xHCI 通过 USB 总线拿到的;CPU 中断是 xHCI Controller 发出来的。
# 硬件中断到底是不是硬件发的
是的,硬件中断当然是硬件发的。
只是这个“硬件”,不一定是你手里那个最终设备。
在不同场景里,CPU 眼里的中断源可能是:
- xHCI USB 控制器
- UART 控制器
- 网卡控制器
- 定时器
- GPIO 控制器
- DMA 控制器
- 磁盘控制器
- GPU
这些控制器才是经常直接和中断控制器打交道的硬件。
所以“鼠标移动触发了硬件中断”这句话,如果当作日常表达,没有问题。但如果往底层拆,就应该理解成:
这中间隔着好几层。
# 中断控制器站在哪里
xHCI 通常也不是直接把中断送进 CPU 核心。
中间还有一个角色:中断控制器。
在 ARM 系统里,常见的是 GIC,也就是 Generic Interrupt Controller。在 x86 系统里,则常见 APIC、IO-APIC、Local APIC 这一套。
中断控制器要处理的问题包括:
- 这是哪个中断源来的?
- 这个中断优先级是多少?
- 应该送给哪个 CPU?
- CPU 当前能不能接?
- 这个中断现在处于什么状态?
所以更完整的路径是:
如果放回 USB 鼠标:
这才是一次“鼠标移动到 CPU 中断”的真实路径。
# Pending 和 Active 是什么
理解 GIC 时,经常会看到几个状态:
这些词一开始很抽象,但可以用排队系统来理解。
# Inactive
没有中断。
# Pending
中断已经来了,但 CPU 还没开始处理。
比如 xHCI 产生了 USB 事件,并通知了 GIC。GIC 先把这件事记下来。这时中断就是 Pending。
# Active
CPU 已经响应这个中断,并且正在处理。
当 CPU acknowledge 这个中断后,GIC 就知道这个中断已经被 CPU 拿去处理了,于是状态变成 Active。
# Active and Pending
这个状态最容易让人卡住。
它的意思是:
比如:
这时 GIC 不能假装第二次没发生。于是它会记录成:
意思是:
等 CPU 把当前这次处理完,如果 pending 还在,GIC 还会继续让 CPU 处理下一轮。
# 边沿触发和电平触发
讲 GIC 状态时,经常会碰到另外两个词:
这两个词也容易听起来很硬。其实抓住一句话就够:
# 什么是高电平和低电平
一根信号线上的电压,可以被系统解释成 0 或 1。
比如:
电平不是瞬间,它是一种可以持续的状态。
一根线可以一直保持低电平:
也可以一直保持高电平:
边沿才是瞬间。
从低电平变成高电平,叫上升沿:
从高电平变成低电平,叫下降沿:
所以:
# 边沿触发像按门铃
边沿触发关心的是:
它关心变化,不关心变化后的状态保持多久。
可以把它想成按门铃:
哪怕你只按了很短一下,只要系统捕捉到了这个变化,就记一次事件。
所以边沿触发更像:
在 GIC 里,如果一个边沿触发中断来了,GIC 会把这个事件记下来:
即使外设那边的信号很快又回去了,GIC 也已经记账了。
# 电平触发像一直举手
电平触发关心的是:
比如高电平有效:
只要信号保持高电平,系统就认为中断请求还在。
可以把它想成举手:
所以电平触发更像:
这就是为什么很多设备适合电平触发。比如:
- UART 接收 FIFO 里还有数据
- 定时器 pending 位还没清
- 网卡还有包没处理
- GPIO 按键还被按住
- 传感器 INT 引脚还保持有效
这些表达的都不是“刚刚发生了一下”,而是“状态还存在”。
# 为什么电平触发要清状态
电平触发有一个非常重要的习惯:
中断处理函数里通常要清掉设备侧的 pending 状态。
比如定时器到期后,设备内部可能有一个状态位:
于是中断线保持有效。CPU 进入中断处理函数后,必须清掉它:
如果不清,设备会继续表示“我还有事”,中断控制器也会继续看到有效电平。结果就是 CPU 处理完刚返回,马上又进中断。
这不是系统坏了,而是设备一直在举手。
# 再回到鼠标
现在把整条链路串起来:
所以 CPU 不是直接被鼠标打断的。
更准确地说:
# 一个容易记住的分层
可以把这件事分成三层:
很多时候,CPU 意义上的中断源在第二层,而不是第一层。
所以当我们说“鼠标触发了中断”,这句话在应用层理解没有问题。但在底层路径上,更精确的说法是:
这两个“触发”不是同一件事。
# 总结
“硬件中断”确实是硬件发出来的,只是发中断的硬件不一定是最终外设。
在 USB 鼠标这个例子里:
如果再压缩成一句话:
鼠标移动不是鼠标直接打断 CPU,而是 xHCI Controller 通过 USB 轮询拿到鼠标数据后,再向中断控制器发起硬件中断,最终通知 CPU 处理。
理解了这一点,再看 Pending、Active、边沿触发、电平触发,就不会觉得它们是孤立的术语了。它们描述的是同一条路上的不同环节:事件怎么被硬件记住,怎么排队,怎么交给 CPU,以及什么时候算处理完。
