1五方角色:谁负责什么(边界)
整体交互之所以复杂,是因为五方各有"责任田",谁也不该越界。先看清边界,后面的协作才不乱。
🧑 应用 App
在用户态。只知道"我想要数据",用 read/open/write 表达,不碰硬件、不知设备型号。
🛡️ 操作系统
在内核态。统一抽象("一切皆文件")、权限检查、进程调度、缓冲管理,是应用与驱动的"中间商 + 保安"。
🔧 设备驱动
住在内核态、紧贴硬件。把 OS 的通用指令翻译成"该设备寄存器的一串特定操作"。一款硬件一份驱动。
⚙️ 硬件控制器
设备自带芯片。寄存器和缓冲区在此;执行驱动命令、负责 DMA 搬数据、就绪后发中断。
🔌 外设
真正产生/消费数据的物理实体:网线信号、磁盘盘片、键帽。它"听不懂"CPU,只跟控制器对话。
🧠 CPU + 内存
CPU 在内核态执行驱动与 OS 代码;内存是数据最终落脚点(DMA 直接写这里)。
2分层架构:请求向下、完成向上
把五方摞成五层,就得到经典的 I/O 软件栈。请求总是一层一层"向下"递交,而数据与完成信号则"向上"回流。点任意一层,看它在交互里的职责。
👆 点击任意一层查看职责。整体规律:请求自上而下传递,数据/中断自下而上回流。两条箭头方向相反,正是"调用"与"返回"的写照。
点击上图查看每一层在"向下递交 / 向上回流"中的具体角色。
3I/O 请求的完整生命周期
把"一次 read"拆开看,会经过 递交 → 排队 → 下发 → 硬件执行 → 中断 → 唤醒 → 返回 七个阶段。下面用分步动画演示,并同步展示发起进程的状态变化。
4控制面 vs 数据面:统一视角
把所有交互按"性质"一分为二,瞬间清晰:控制面是"下命令"(小而频繁,走寄存器/中断),数据面是"搬数据"(量大,靠 DMA 绕开 CPU)。
🎛️ 控制面(Control Plane)
- 内容:启动/停止、设置模式、读状态、中断号
- 通道:CPU ↔ 控制器 的寄存器(MMIO)、IRQ 中断线
- 特点:数据量极小、必须 CPU 亲自参与
- 类比:项目经理发的"开工/验收"指令
📦 数据面(Data Plane)
- 内容:真正的用户数据(一个包、一块扇区)
- 通道:外设 ↔ 控制器 ↔ 内存(DMA),不经 CPU
- 特点:体量巨大、由 DMA 控制器代劳
- 类比:工人把货直接搬进仓库,不惊动经理
5通信频道矩阵:两两之间怎么对话
五方并非两两直连,而是走固定的"频道"。点击右侧任一频道卡片,左侧图中对应的连线会高亮。
6两个真实案例:键盘 vs 磁盘
同样走五层,不同设备差异巨大。切换查看:键盘(极简、每键中断、无 DMA)与磁盘(DMA、I/O 调度、寻道)各自的端到端过程。
键盘为何不用 DMA?
数据量太小(每秒几个字节),DMA 的"建立描述符"开销反而划不来。直接用中断把单个扫描码塞进缓冲区即可。
磁盘为何必须 DMA + 调度?
一次读就是几百 KB~几 MB,且机械盘有寻道延迟。OS 的 I/O 调度器会合并、排序请求以减少寻道,DMA 负责后续大块搬运。
7阻塞 vs 异步:进程何时醒来
同样是"等 I/O",进程可以选择阻塞(睡到数据来)或异步(先去干别的,好了再通知我)。两种模式下五方的交互几乎一样,区别仅在 OS 如何处理发起进程。
🔒 阻塞 I/O(最常见)
调用 read() 后进程立刻进入阻塞态,从运行队列移除,CPU 转去跑别的进程。数据到了由中断唤醒,重新进入就绪→运行。
// 简单、直觉,但线程被"卡住"
char buf[1024];
int n = read(fd, buf, 1024); // 卡这里直到有数据
printf("%.*s", n, buf);
⚡ 异步 / 非阻塞 I/O
进程用 O_NONBLOCK 或 io_uring/epoll:read 立即返回"暂无数据",进程继续干别的;OS 在中断完成后通过回调/事件通知它。
// 一个线程同时盯上千个连接
submit_read(io_uring, fd, buf);
do_other_work(); // 不阻塞
// 稍后 io_uring 告诉你"这块好了"
8一句话模型 & 与上一篇的衔接
应用提需求 → OS 管调度与安全 → 驱动翻译命令 → 控制器执行+DMA 搬数据 → 外设干活;
完成后中断沿原路向上回流,唤醒进程、数据返回。
控制面(CPU↔寄存器/IRQ)小而亲临,数据面(外设→内存 DMA)大而绕开 CPU。
📘 上一篇(机制篇)
深入"怎么做":MMIO/PMIO、DMA、中断、寄存器握手、网卡收包动画。是本文的"零件说明书"。
📗 本篇(架构篇)
讲清"谁和谁、按什么顺序"交互:五方边界、分层、生命周期、控制/数据面、通信矩阵。是上一篇的"装配总图"。