5.1 I/O 系统基本概念
I/O 设备按使用特性分类:
- 存储设备:磁盘、磁带、光盘。
- 输入输出设备:键盘、鼠标、显示器、打印机。
- 通信设备:网卡、调制解调器。
I/O 设备按信息传输单位分类:
- 块设备:以数据块为单位传输,如磁盘。可寻址、可随机访问。
- 字符设备:以字符为单位传输,如键盘、打印机。不可寻址、通常顺序访问。
I/O 控制方式
I/O 控制方式的发展目标是减少 CPU 对 I/O 的干预,提高 CPU 与 I/O 设备的并行程度。共有四种控制方式:
| 控制方式 | CPU 干预程度 | 数据传输单位 | 特点 |
|---|---|---|---|
| 程序查询方式 | 高(轮询等待) | 字 | CPU 全程等待,效率最低 |
| 程序中断方式 | 中(中断时干预) | 字 | CPU 与 I/O 并行,但每字需中断 |
| DMA 方式 | 低(开始/结束干预) | 块 | DMA 控制器直接控制内存交换 |
| 通道方式 | 最低 | 一组块 | 通道是专用处理器,CPU 仅需一条 I/O 指令 |
1. 程序查询方式
CPU 不断轮询设备状态寄存器,判断 I/O 是否完成。CPU 与 I/O 串行工作,效率最低,仅适用于简单、低速设备。
2. 程序中断方式
CPU 发出 I/O 命令后继续执行其他任务;设备完成 I/O 后向 CPU 发中断信号;CPU 响应中断,执行中断服务程序完成数据传输。
- 优点:CPU 与设备并行工作,利用率提高。
- 缺点:每传输一个字就要中断一次,中断处理开销大;不适用于高速设备。
3. DMA 方式 (Direct Memory Access)
DMA 的工作过程:
- CPU 向 DMAC 发出 I/O 命令(含读/写、内存地址、传输字数)。
- DMAC 独立控制数据传输,每传输一个字,DMAC 自动修改内存地址和计数器。
- 整块数据传输完毕,DMAC 向 CPU 发中断信号。
- CPU 响应中断,进行后续处理。
DMA 与中断方式的区别:
| 对比项 | 中断方式 | DMA 方式 |
|---|---|---|
| 中断触发 | 每传输一个字 | 整块传输完 |
| 数据传输 | 经 CPU 寄存器中转 | 外设↔内存直接交换 |
| 传输单位 | 字 | 块(连续多个字) |
| CPU 干预 | 每次中断都需 CPU | 仅开始和结束 |
| 适用设备 | 中低速 | 高速块设备 |
交互式动画 · DMA工作流程
DMA控制器独立完成数据传送,只需在开始和结束时中断CPU。步进观察完整流程。
4. 通道方式
通道分为三类:
- 字节多路通道:服务于多台低速字符设备,按字节分时轮转。
- 数组选择通道:服务于高速块设备,一次只服务一台设备,传输完一块再切换。
- 数组多路通道:结合上两者,可分时服务多台高速设备,每次传输一块。
5.2 SPOOLing 技术
SPOOLing 系统的组成:
- 输入井和输出井:磁盘上的两个存储区,模拟脱机输入/输出的磁带。输入井收容低速设备输入的数据;输出井收容 CPU 输出的数据。
- 输入缓冲区和输出缓冲区:内存中的缓冲区,暂存输入井↔设备、输出井↔设备之间的数据。
- 输入进程和输出进程:模拟脱机的外围控制机,控制 I/O 传输。
SPOOLing 的典型应用——共享打印机:
- 打印机是独占设备,多个进程直接使用会产生等待。
- SPOOLing 为每个打印请求在输出井中建立空文件,将打印数据暂存。
- 输出进程依次从输出井取出数据送给打印机。
- 宏观上多进程"同时"使用打印机,独占设备变为共享设备。
5.3 缓冲区管理
引入缓冲的目的是:缓和 CPU 与 I/O 设备速度不匹配的矛盾;减少对 CPU 的中断频率;提高 CPU 与 I/O 设备的并行性。
1. 单缓冲 (Single Buffer)
系统在主存中为每个 I/O 操作分配一个缓冲区。设备先将数据送入缓冲区,CPU 从缓冲区取数据处理。两者交替使用同一个缓冲区。
2. 双缓冲 (Double Buffer)
分配两个缓冲区。设备向一个缓冲区输入数据的同时,CPU 可从另一个缓冲区取数据处理。两者可并行工作。
3. 循环缓冲 (Circular Buffer)
多个缓冲区组成循环队列。生产者指针 (in) 和消费者指针 (out) 分别指向下一个可写入和可读取的位置。适合输入输出速度不匹配的场合。
4. 缓冲池 (Buffer Pool)
多个缓冲区组成公用缓冲池,可供多个进程共享。缓冲池中的缓冲区按状态分为:空缓冲队列、输入队列、输出队列。既可用于输入也可用于输出,利用率高。
| 缓冲策略 | 缓冲区数 | 并行程度 | 适用场景 |
|---|---|---|---|
| 单缓冲 | 1 | 低 | 简单 I/O |
| 双缓冲 | 2 | 中 | CPU 与设备速度接近 |
| 循环缓冲 | 多个 | 较高 | 生产者-消费者 |
| 缓冲池 | 多个(共享) | 高 | 多进程并发 I/O |
缓冲区管理伪代码
// 单缓冲:CPU 与设备串行使用同一个缓冲区
void SingleBuffer_IO(){
while(true){
device_read(buffer); // 设备写入缓冲区
cpu_process(buffer); CPU 从缓冲区读取处理
// 二者串行:max(T,C)+M
}
}
// 双缓冲:CPU 与设备并行
void DoubleBuffer_IO(){
int buf[2];
int in = 0, out = 1;
device_read(buf[in]); // 先装满 buf[0]
while(true){
par { // 并行执行
device_read(buf[in]); // 设备向 buf[in] 写入
cpu_process(buf[out]); CPU 从 buf[out] 读取
}
in = 1-in; out = 1-out; // 交换
}
}
// 循环缓冲:in/out 指针环形使用
const int N = 10; // 缓冲区数量
Buffer ring[N];
int in = 0, out = 0, count = 0;
void Producer(){
while(true){
if(count == N) sleep(); // 缓冲区满
ring[in] = produce();
in = (in+1) % N;
count++;
}
}
void Consumer(){
while(true){
if(count == 0) sleep(); // 缓冲区空
data = ring[out];
out = (out+1) % N;
count--;
consume(data);
}
}
5.4 设备分配与回收
设备分配的数据结构:
- 系统设备表 (SDT):记录系统中所有设备,每个设备占一个表项。
- 设备控制表 (DCT):每个设备一张,记录设备类型、标识符、状态、COCT 指针、等待队列。
- 控制器控制表 (COCT):每个控制器一张。
- 通道控制表 (CHCT):每个通道一张。
设备分配方式:静态分配(作业级,运行期间独占,利用率低)和动态分配(进程级,按需申请释放)。设备独立性:用户使用逻辑设备名申请,系统通过 LUT 映射到物理设备。
5.5 I/O 软件层次结构
I/O 软件采用分层设计,每层利用下层提供的服务实现新功能,向上层屏蔽下层细节。从低到高分四层:
| 层次 | 名称 | 主要功能 | 与硬件相关性 |
|---|---|---|---|
| ① 最低层 | 中断处理程序 | 响应 I/O 中断,唤醒等待的驱动程序,处理中断返回 | 直接相关 |
| ② 次低层 | 设备驱动程序 | 将上层抽象的 I/O 请求转化为具体设备控制指令,驱动设备工作 | 直接相关 |
| ③ 中间层 | 设备无关 I/O 软件 | 提供统一接口(read/write/open)、缓冲管理、设备分配、错误报告、独占设备的分配与释放 | 无关 |
| ④ 最高层 | 用户层 I/O 软件 | 提供系统调用接口(如 C 库的 printf 调用 write 系统调用)、SPOOLing 假脱机技术 | 无关 |
I/O 请求处理流程
- 用户进程调用库函数(如 printf)
- 用户层软件将请求转为
write系统调用,陷入内核 - 设备无关软件检查缓冲区,分配设备,将请求加入请求队列
- 设备驱动程序取出请求,向设备控制器写入命令寄存器,启动 I/O
- 设备完成后产生中断,中断处理程序响应中断,唤醒等待进程
I/O 应用程序接口
操作系统为应用程序提供了一组标准化的 I/O 接口,屏蔽底层设备的差异。按设备类型分类:
1. 字符设备接口
字符设备以字节流的形式访问,不支持随机寻址。典型接口:open、read、write、close。常见字符设备:键盘、鼠标、串口、终端。字符设备通常以流的方式顺序访问,无块结构。
2. 块设备接口
块设备以固定大小数据块为单位访问,支持随机寻址。典型接口:open、read、write、seek、ioctl。常见块设备:磁盘、SSD。块设备支持随机读写任意位置的数据块,OS 通过缓冲区/页缓存优化块设备的访问。
3. 网络设备接口 (Socket)
网络设备通过 Socket(套接字)接口访问,不同于传统的 read/write 模型。典型接口:socket、bind、listen、connect、accept、send、recv。网络通信使用报文而非字节流/块。
阻塞 I/O vs 非阻塞 I/O
| 类型 | 行为 | 特点 |
|---|---|---|
| 阻塞 I/O | 进程发出 I/O 请求后,若数据未就绪则该进程挂起等待,直到数据准备好才返回 | 编程简单,进程被阻塞期间 CPU 可调度其他进程 |
| 非阻塞 I/O | 进程发出 I/O 请求后,若数据未就绪则立即返回一个错误码(如 EWOULDBLOCK),进程可继续执行或轮询重试 | 不阻塞进程,但需反复轮询(忙等),CPU 利用率低 |
同步 I/O vs 异步 I/O
| 类型 | 行为 | 特点 |
|---|---|---|
| 同步 I/O (Synchronous) | 进程发起 I/O 请求后,必须等待 I/O 操作完成(数据已从内核拷贝到用户空间)才能继续执行 | 包括阻塞 I/O、非阻塞 I/O、I/O 多路复用(select/poll/epoll) |
| 异步 I/O (Asynchronous) | 进程发起 I/O 请求后立即返回,内核在后台完成整个 I/O(包括数据拷贝到用户空间),然后通知进程 | 真正不阻塞,进程无需等待任何 I/O 阶段(如 Linux AIO、io_uring) |
5.6 固态硬盘 SSD [22新增]
SSD 结构
SSD 主要由两大部分组成:
- 闪存芯片 (NAND Flash):存储数据的核心介质。以页 (Page)为最小读写单位(通常 4KB~16KB),以块 (Block)为最小擦除单位(通常 128~512 页,即几 MB)。
- SSD 控制器 (Controller):负责管理闪存芯片的读写、垃圾回收、磨损均衡、坏块管理、缓存管理等。控制器内包含嵌入式处理器、RAM 缓存和固件。
- FTL (Flash Translation Layer):控制器中的核心软件层,将逻辑块地址 (LBA) 映射到物理闪存位置。FTL 是实现 SSD 高性能和长寿命的关键。
SSD 读写特性
| 特性 | 说明 |
|---|---|
| 读快写慢 | 读取延迟约 0.05~0.1ms;写入延迟约 0.1~1ms。读取远快于写入,原因是写入可能涉及擦除、编程和垃圾回收。 |
| 写前擦除 (Erase-before-Write) | 闪存的物理特性:不能直接覆盖写入。必须先擦除整个块(设置为全 1),才能将数据编程写入。读/写以页为单位,擦除以块为单位。 |
| 无寻道时间 | 没有磁头移动和盘片旋转,访问任意位置的时间几乎一致,随机访问性能远超 HDD。 |
| 有限擦写次数 (P/E Cycles) | 每个闪存块的可擦写次数有限(SLC ~10 万次,MLC ~1 万次,TLC ~3000 次,QLC ~1000 次),超出后块会失效。 |
磨损均衡 (Wear Leveling)
为延长 SSD 寿命,控制器通过 FTL 实现磨损均衡,使所有块的擦写次数均匀分布:
- 动态磨损均衡:仅对空闲块进行均衡——写新数据时优先选择擦除次数最少的空闲块。简单但不能均衡已存储"冷数据"的块。
- 静态磨损均衡:对所有块(包括存放不变数据的块)进行均衡——定期将"冷数据"从擦除次数少的块迁移到擦除次数多的块,释放出低磨损块。效果更好但开销更大。
垃圾回收 (Garbage Collection, GC)
由于写前擦除的特性,当需要擦除一个块时,该块中可能仍包含有效数据(未被删除/更新的页)。GC 过程:
- 选择一个待回收的块 → 将该块中的有效页迁移到其他空闲位置 → 擦除该块(现在全是无效页)→ 该块变为空闲块可重新使用。
- GC 是 SSD 写入性能波动的主要原因——当空闲块不足时触发 GC,会显著增加写延迟(称为"写放大" Write Amplification)。
SSD vs HDD 对比
| 对比项 | HDD (机械硬盘) | SSD (固态硬盘) |
|---|---|---|
| 结构 | 盘片+磁头+电机(机械) | 闪存芯片+控制器(电子) |
| 随机读延迟 | ~5-10ms(含寻道+旋转) | ~0.05-0.1ms |
| 随机写延迟 | ~5-10ms | ~0.1-1ms(受 GC 影响) |
| 顺序读/写速度 | ~150-250 MB/s | ~500-7000 MB/s |
| 抗震性 | 差(机械部件脆弱) | 好(无机械部件) |
| 寿命 | 无固定写入限制 | 有限擦写次数(P/E Cycles) |
| 价格/容量比 | 低 | 高(但持续下降) |
| 磁盘调度 | 需要(优化寻道) | 不需要——随机访问时间均一 |