1全景:一次 I/O 涉及哪些"角色"
任何一次设备通信,本质都是数据在"设备 ↔ 总线 ↔ CPU/内存 ↔ 操作系统"之间的一次旅行。先认识这条链路上的常驻角色,后面每一节都是放大其中某一环。
👆 点一下上面任意模块,看它与 CPU/内存之间的通信路径被高亮(其它变暗)。这是理解"它们怎么对话"的几何直觉。
① CPU
总指挥。通过读写寄存器和设备"对话",并通过中断线被动接收设备信号。
② 总线 Bus
所有设备共享的"公路"。PCIe 是今天的高速主干;USB/ SATA 等挂在它下面的"支线"上。
③ 设备控制器 Controller
每个设备自带的小芯片(网卡有网卡芯片、磁盘有磁盘控制器)。它替设备理解总线和寄存器。
④ 操作系统 / 驱动
住在 RAM 里的软件层。它封装硬件差异,让上层应用只看到统一的"文件"或"套接字"。
2硬件底座:端口、总线与控制器
设备从不直接怼到 CPU 上。它们通过控制器连到总线,总线再连到 CPU。CPU 只跟"地址 + 数据 + 控制"三条线打交道。
🔢 地址总线
CPU 用来"指名"要访问谁:是内存的某个格子,还是某设备的某个寄存器。位数决定可寻址空间(如 64 位)。
📦 数据总线
真正搬运数据的线。PCIe 用高速差分串行信号(lane),带宽 = lane 数 × 单 lane 速率。
🚦 控制总线
读写方向、时钟、中断请求(IRQ)等"红绿灯"信号。比如 INTR 线是设备喊 CPU 的喇叭。
关键概念:控制器内部有"寄存器"和"缓冲区"
控制器的寄存器就是 CPU 与设备之间的"信箱"。CPU 往控制寄存器写命令("开始读"),从状态寄存器读结果("忙/就绪"),从数据寄存器搬数据。数据多了则先进控制器的 FIFO/缓冲区,避免 CPU 频繁来取。这就是后面所有机制的物理基础。
3CPU 如何"看见"设备:I/O 编址
CPU 只会做一件事:按地址读/写。设备寄存器也得有个"地址",CPU 才能访问。历史上有两套编址方案。
内存映射 I/O(MMIO)
把设备寄存器映射到一段物理内存地址。CPU 用普通的 MOV 指令读写这些地址,就像访问内存一样。
- 寄存器地址落在地址空间里,与 RAM 共享
- 指令无差别:
mov rax, [0xFE000000] - 现代主流:PCIe 设备、绝大多数外设都用 MMIO
- 优点:编译器/CPU 能用所有访存优化;缺点:占用地址空间
端口映射 I/O(PMIO / x86)
独立一套I/O 地址空间(64K 端口),用专用指令 IN/OUT 访问,与内存地址互不干扰。
- 经典 x86 遗留方案,老式并口/串口常用
in al, 0x60读键盘端口;out 0x20, al- 优点:地址空间独立、不占内存;缺点:指令专用、数量有限
- 今天多用于早期启动、老设备,新设备几乎都 MMIO
; 例子:用 MMIO 读网卡状态寄存器(假设映射到 0xFE000000)
mov rax, [0xFE000000] ; 像读内存一样读"网卡状态"
test rax, 0x1 ; 第 0 位 = 就绪位?
jnz ready ; 就绪则跳转
; 同样的事,若用老式 PMIO 端口 0x300:
in al, 0x300 ; 专用 IN 指令
test al, 0x1
jnz ready
4寄存器与"握手协议"
有了地址,CPU 还不能乱写。设备很慢、CPU 很快,两者靠寄存器做状态握手。这三类寄存器是 I/O 的"语法"。
👆 点上方三类寄存器之一,下面会解释它在握手里的角色。
点击上图查看 控制 / 状态 / 数据 寄存器各自在"握手"中的职责。
// 典型握手伪代码(CPU 视角,读一个字节)
while (status_reg & BUSY) ; 轮询:等到设备不忙
control_reg = READ_CMD ; 写控制:我要读
while (!(status_reg & READY)); 等"数据就绪"
data = data_reg ; 取走数据
5三种数据传输方式(核心原理)
CPU 与设备之间搬数据,有三条路:轮询(PIO)、中断、DMA。它们解决的是同一个问题——"CPU 怎么高效地把数据从设备搬到内存"。下面用对比 + 动画讲清。
① 轮询 PIO
CPU 反复读状态寄存器,设备没就绪就空转。实现最简单,但CPU 被绑死,只适合极慢/极简设备(早期串口)。
② 中断驱动
CPU 先去干别的;设备就绪后发 IRQ 打断 CPU,CPU 再来取数据。解放了 CPU,但每个字节都惊动 CPU,高频数据仍费 CPU。
③ DMA
CPU 把"从设备搬 N 字节到内存地址 X"的任务委托给 DMA 控制器。设备自己把数据写进内存,搬完才中断一次。大块数据几乎零 CPU 占用。
6中断机制:设备如何"喊"CPU
中断是异步 I/O 的灵魂。设备不能打断 CPU 的"思考",但能拉一根 IRQ 线请求注意。CPU 收到后会暂停当前任务、保存现场、跳转处理、恢复现场。
中断做了什么(微观)
- 设备拉 IRQ 线,中断控制器把"向量号"发给 CPU
- CPU 执行完当前指令,关中断、保存寄存器到内核栈
- 查 IDT(中断描述符表)找到对应 ISR 入口
- 执行 ISR(如网卡驱动取包),开中断
iret恢复现场,回到原进程
上半部 / 下半部
网卡中断不能做太久(否则卡住其它中断)。所以驱动把活分成两半:
- 上半部(top half):极快,只收包、入队、清中断
- 下半部(softirq / tasklet / NAPI):稍后慢慢处理协议栈、交给应用
7操作系统与设备驱动:软件这一层
硬件差异巨大,但应用只想"open / read / write"。这层翻译由操作系统 + 设备驱动完成。驱动是内核里"懂某款硬件"的专属代码。
字符设备 / 块设备 / 网络设备
键盘是字符设备(按字节流);磁盘是块设备(按扇区块);网卡是网络设备(按包,走 socket 而非"文件")。三类有不同抽象接口。
驱动到底干了啥
1) 探测/初始化硬件;2) 把 MMIO 地址映射进内核;3) 注册中断处理;4) 实现 read/write 等回调;5) 用 DMA 描述符让硬件搬数据。
用户态 vs 内核态
驱动跑在内核态(特权级高,能碰硬件)。应用通过系统调用跨界请求,避免任意程序乱改硬件。
为什么"一切皆文件"
Unix 把设备也暴露成 /dev/... 节点。应用用同一套 open/read/write 就能操作串口、磁盘,不必关心硬件细节。
8网卡专题:一个数据包是怎么进来的(动画)
把前面所有原理串起来:下面用分步动画演示接收(RX)一个网络包的完整旅程。点"下一步"或"自动播放"。
write(socket) → 协议栈封帧 → 驱动写 TX 描述符 → 网卡 DMA 取数据 → 编码成电信号发出 → 发完中断通知 CPU。注意数据同样不经 CPU 中转,全靠 DMA。9对比总结:一张表看懂全局
| 环节 | 主角 | 机制 | 解决的问题 |
|---|---|---|---|
| 设备挂接 | 控制器 + 总线 | PCIe/USB 等把设备连入系统 | 让五花八门的硬件有统一入口 |
| CPU 寻址 | MMIO / PMIO | 寄存器映射到地址,CPU 按地址读写 | CPU 只认地址,设备"伪装"成内存/端口 |
| 速度匹配 | 状态寄存器 | 忙/就绪位握手 | 弥合 CPU 与设备百万倍速度差 |
| 搬数据 | PIO / 中断 / DMA | 轮询 → 中断通知 → DMA 自搬 | 高效、低 CPU 占用地传输 |
| 异步通知 | IRQ + 中断控制器 | 设备拉线打断 CPU,走 IDT→ISR | 设备就绪时再叫 CPU,不空等 |
| 软件封装 | OS + 驱动 | 统一抽象(文件/socket)+ 特权隔离 | 屏蔽硬件差异,安全暴露给应用 |
| 网卡实战 | NIC + DMA + NAPI | DMA 灌内存环 + 中断/轮询混合 | 百万级 PPS 而 CPU 不被淹没 |
一句话记 DMA
CPU 当包工头,DMA 是搬运工——交代"从哪搬、搬多少、放哪",工人自己干,干完喊一声。
一句话记中断
门铃。设备在门口按铃(IRQ),CPU 暂停手头事去开门(ISR),处理完继续。
一句话记驱动
翻译官。把内核的"读网卡"翻译成该硬件寄存器的一串特定操作。