从敲下一个键到屏幕上出现字母,中间发生了什么?这件事我到学完 I/O 系统才算真正想明白:键被按下时,键盘控制器产生一个中断,CPU 手头正在跑的东西被打断,跳去处理键值,数据进了内核缓冲区,再经图形栈变成屏幕上的像素,然后 CPU 回到被打断的地方,像什么都没发生过。整个过程几十微秒,涉及本章的全部主角:三种 I/O 方式(轮询、中断、DMA)、设备寄存器、总线、中断向量表。这一章是硬件课里最贴近「活的世界」的一章,前面所有零件(ALU、流水线、Cache、MMU)在这里第一次和真实设备握手。
CPU 一拍一纳秒,机械硬盘寻道几毫秒,差一百万倍以上;键盘更是想等多久等多久。让百亿级晶体管的 CPU 空转等一个键盘,是任何设计者都不能接受的浪费。所以 I/O 系统的全部问题可以浓缩成一句:谁来盯着设备,以及盯着的时候 CPU 在干嘛。三个答案对应三种活法:
轮询:CPU 主动,反复读设备的状态寄存器,「好了吗」「好了吗」。设备数据一就绪立刻能拿到,延迟最低,但等待全程 CPU 被占死。只在两种场景合理:设备极快(等的时间短到不值得切换),或者系统简单到只有这一件事可做(单片机点个灯)。
中断:设备主动。设备就绪时向 CPU 发一个电信号,CPU 把手头程序按个暂停键,去处理设备,处理完回来继续。CPU 平时零开销,代价是每次「暂停-恢复」的现场保存与恢复(下面细说)。绝大多数设备走这条道,它是「事件驱动」这个词语的硬件本体。
DMA(Direct Memory Access):针对大批量数据(磁盘读一块、网卡收一个包)的升级版。数据搬运这件事本身也外包给专门的 DMA 控制器:CPU 只负责下指令(从哪搬到哪、搬多少),然后去干别的;DMA 控制器在设备和内存之间直通搬运,搬完发一个中断告诉 CPU「活干完了」。没有 DMA,每传 4KB 就要 CPU 亲手搬一千次,CPU 会变成高级搬运工。
三种方式的分工最后落成一条铁律:小数据、要求快响应用中断;大数据用 DMA + 完成中断;只有极端低延迟或极简系统才轮到轮询。第 8 章缺页处理从磁盘读页,走的就是「系统调用 → DMA 搬运 → 完成中断 → 唤醒进程」整条链。
CPU 和设备之间没有魔法通道,接口就是几个寄存器:状态寄存器(设备忙不忙、数据好了没)、数据寄存器(收发数据本身)、控制寄存器(发命令)。这和函数接口的思想一模一样:约定几个「变量」,往里写命令、从里读状态。
这些寄存器摆在哪个地址空间,有两种流派:
in/out 指令访问。MMIO 胜在统一:普通访存指令就能驱动设备,指针、结构体、编译器优化全套通用,现代 RISC 基本只留这一种。但它和 Cache 相性极差:设备状态随时会自己变(网卡来包了),把状态寄存器缓存进 L1,读到的永远是昨天的新闻。所以 MMIO 区域一律禁用 Cache(第 7 章提过 MMIO 强制写直达,病根在此):这类地址要么标记为不可缓存,要么用特殊指令绕过缓存直访。这是「软件的局部性假设在硬件边界上失效」的经典案例。
CPU、内存、各种设备,物理上都挂在总线上。经典三组信号线:地址线(CPU 要访问谁)、数据线(传输内容)、控制线(读还是写、时序)。地址线宽度决定寻址空间,数据线宽度乘频率决定带宽,这两条公式考试常考,工程上对应「64 位机」「DDR5-6400」这些商品名里的数字。
共享总线有个天生的毛病:一条路大家走,谁占着别人都得等,慢设备能把整条总线拖下水。所以「总线」这个词在现代机器里已经是半历史概念:PCIe 改成了交换式点对点,每个设备有专属链路,交换机负责路由。但地址/数据/控制这套抽象还在,「总线仲裁」(多个主设备抢通道时定优先级)的思想也还在,只是从「抢一条线」变成了「抢一个交换端口」。
中断到达后,CPU 的动作有一套固定流程,值得背下来,因为操作系统的进程切换、缺页处理(第 8 章)、系统调用,全是同一套骨架:
① 设备拉起中断请求线(每个设备有编号,IRQ)
② CPU 每条指令执行完检查:有中断且未被屏蔽?
③ 保存现场:PC 和关键寄存器压入内核栈
(被中断的程序对此一无所知,恢复时必须严丝合缝)
④ 查中断向量表:中断号 → 服务程序入口,跳转
⑤ 中断服务程序干活:读设备状态、取走数据、给设备回执
⑥ 恢复现场,回到被打断的那条指令之后
第 ③ 步和第 10 章函数调用的「保存寄存器」是同构的,只是层次更低、由硬件与内核协作完成;第 ④ 步的中断向量表是「用查表代替逐个判断」的又一次胜利(RISC-V 叫 mtvec,x86 叫 IDT,名字不同思想相同)。中断处理有个工程铁律:越短越好。耗时的活(拷贝数据、解析协议)从中断里挪出去交给「下半部」或内核线程,把中断线尽快放掉,否则高频设备(万兆网卡每秒百万级中断)能把 CPU 全部时间花在进出中断上,这叫中断风暴,是内核性能调优的真实战场。
三者共用「打断当前程序 → 保存现场 → 查向量表 → 跳转处理」这一整套机制,区别只在于谁按的门铃:
ecall、x86 的 syscall)。理解这扇统一的门,很多概念会自动归位:用户态和内核态的切换发生在过门时;信号机制建立在中断之上;C 指针课里讲的段错误,审判流程就是 8.5 节那条链。到操作系统课再学进程切换时,你会发现它无非是「中断 + 换页表 + 换栈」的组合技。
cat /proc/interrupts,找到你正在用的键盘(或鼠标)那行,按几下键再 cat 一次,观察计数器涨了多少;顺便找找 NVME、RES(函数间中断)这些行,猜猜各自谁在触发。