Appearance
I/O 管理:设备驱动、缓冲、SPOOLing
概念
I/O 管理要解决的问题是:CPU 与各种外设的"速度差"和"接口差",怎么用一层软件抹平。
一句话说清它是什么:I/O 管理是操作系统里负责"跟硬件打交道"的那部分——它向上提供统一的读写接口(read/write),向下把抽象命令翻译成"往哪个寄存器写什么值",中间还要用缓冲把"快 CPU"和"慢设备"解耦。
三种"差"对应三种手段:
| 差在哪 | 表现 | 手段 |
|---|---|---|
| 速度差 | CPU 纳秒级、磁盘毫秒级、打印机秒级 | 缓冲(buffering) |
| 接口差 | 每类设备的寄存器、命令字完全不同 | 设备驱动 + 设备独立性软件 |
| 独占 vs 共享 | 打印机一次只能一个进程用 | SPOOLing(虚拟设备) |
⚠️ 与
arch/41-io.md的分工:那一章从硬件看(接口寄存器、总线、DMAC 怎么挪用周期);本章从操作系统看(四层软件结构、设备分配表、缓冲怎么组织)。两章的"三种 I/O 控制方式"共用同一组口径(1 GHz、块 4 KiB、设备 40 MB/s、中断 90 周期),本章多补一种"通道方式"——这是 408 操作系统大纲里要求、而计组大纲里没有的一项。
原理
一、I/O 系统的四层软件结构
text
┌──────────────────────────────────────────────┐
│ 用户层 I/O 软件 printf / 系统调用库 / │ ← 面向用户
│ SPOOLing、虚拟设备 │
├──────────────────────────────────────────────┤
│ 设备独立性软件 统一接口 + 设备命名 + │ ← ★ 本章主体
│ (设备无关软件) 缓冲管理 + 设备分配 + │
│ 差错处理 + 逻辑设备表 │
├──────────────────────────────────────────────┤
│ 设备驱动程序 把"读一块"翻译成 │ ← 与硬件相关
│ 具体设备寄存器的操作序列 │
├──────────────────────────────────────────────┤
│ 中断处理程序 被设备中断唤醒, 唤醒驱动 │ ← 最靠近硬件
└──────────────────────────────────────────────┘
──────────────── 硬件 ────────────────⚠️ 谁"与设备相关"是本节唯一的考点:
- 设备驱动程序:与设备相关——每种设备一套("驱动程序要配套装"就是这个意思)。
- 设备独立性软件:与设备无关——所有设备共用一套,只认"逻辑设备",不认"具体型号"。"设备独立性"的含义:用户用逻辑设备名(如
/dev/printer)编程,系统通过逻辑设备表(LUT)映射到物理设备名——换个型号的打印机不用改程序,这就是"独立性"的全部含义。
二、I/O 设备的分类
| 分类角度 | 类型 | 特征 | 例子 |
|---|---|---|---|
| 传输单位 | 块设备 | 可寻址、以块为单位、速度较高 | 磁盘、SSD |
| 字符设备 | 不可寻址、以字符为单位、常靠中断驱动 | 键盘、鼠标、串口 | |
| 共享属性 | 独占设备 | 一次只能一个进程用 | 打印机、磁带机 |
| 共享设备 | 可同时被多个进程"宏观同时"访问 | 磁盘 | |
| 虚拟设备 | 用 SPOOLing 把独占设备"变"成共享 | 经 SPOOLing 的打印机 | |
| 传输速率 | 高速 / 中速 / 低速 | 相差可达 | 磁盘 / 光驱 / 键盘 |
⚠️ 块设备 vs 字符设备的分界线是"能不能寻址":磁盘能直接跳到第 1000 块(可寻址);键盘只能按顺序收字符(不可寻址)。这个区别决定了"能不能用 DMA"——不可寻址的设备没法让 DMAC 自己算地址。
三、四种 I/O 控制方式:谁在盯着搬运
这是本章与 arch/41-io.md 的交叉点,核心判据只有一条:"谁在盯着数据搬运"。
text
① 程序查询 CPU ──[不停读状态]──> 设备
CPU 死等, 数据单位 = 1 个字, CPU 全程占用
② 中断驱动 CPU ──[发起]──> 设备 ──[每字 1 次中断]──> CPU
CPU 每字看一眼, 中断频率 = 每字 1 次
③ DMA CPU ──[初始化]──> DMAC ──自己搬整块──> 内存
整块搬完才中断 1 次, 数据不经过 CPU
④ 通道 CPU ──[发通道程序]──> 通道 ──控制多台设备---> 内存
一组数据块传完才中断 1 次, 通道自己能执行"通道程序"量化对比(沿用 arch/41-io.md 的口径:1 GHz、块 4 KiB、设备 40 MB/s、中断开销 90 周期、DMA 初始化 50 周期):
| 方式 | 谁盯着 | 中断频率 | CPU 开销(周期) | CPU 占用率 |
|---|---|---|---|---|
| 程序查询 | CPU 死等 | 无中断 | 全程空转 | 100.000% |
| 中断驱动 | CPU 每字一眼 | 每字 1 次 | 90.000% | |
| DMA | DMAC 全程 | 整块 1 次 | 1.137% | |
| 通道 | 通道程序 | 一组块 1 次 | 0.137% |
⚠️ 三句话记住这条演进线:
- 中断方式的开销正比于"数据量",而它摊薄的时间越来越短——设备越快,CPU 占用率越高(40 MB/s 时已占 90%,再快就物理上做不到了)。
- DMA 把"每字一次"降成"每块一次",一步降了 79.2 倍(
)。关键动作是"数据不经过 CPU"。 - 通道把"每块一次"降成"每组一次",又降了一个数量级(
倍)。关键动作是"通道自己能执行通道程序",CPU 只发一次命令。 注意:"通道"不是"更快的 DMA",而是"能自己跑程序的 DMA"——通道里有一套自己的指令系统(通道命令字 CCW),能一条命令控制多台设备、连续传多块。
四、缓冲区管理:单缓冲与双缓冲
为什么要缓冲(四条,全背):
text
① 缓和 CPU 与 I/O 设备的速度差
② 减少对 CPU 的中断频率, 放宽对中断响应时间的限制
③ 解决数据粒度不匹配(设备按块, 进程按字节)
④ 提高 CPU 与 I/O 设备之间的并行性设三个时间量(考试固定记号):
| 记号 | 含义 |
|---|---|
| 把一块数据从设备读入缓冲区的时间 | |
| 把缓冲区数据传送到用户区的时间 | |
| CPU 处理这一块数据的时间 |
单缓冲:一块只能"边读边算",读和算不能重叠:
text
单缓冲(每块用的时间 = max(C, T) + M)
设备 ──[T: 读入缓冲]──┐
│ 这两个可以重叠 -> 取 max
CPU ──[C: 处理上一块]┘
CPU ──[M: 把缓冲搬到用户区]──> 这一步必须独占, 不可重叠双缓冲:读下一块和算当前块可以同时进行:
text
双缓冲(每块用的时间 = max(C + M, T))
设备 ──[T: 读入"另一个"缓冲]──────────────┐
│ 取 max
CPU ──[C: 处理当前块]─[M: 搬到用户区]────┘⚠️ 两个公式的差别不是"快一倍",而是"谁被藏起来了":
- 单缓冲:
和 重叠,但 藏不掉(必须等数据搬到用户区,缓冲区才能腾出来)——所以 是"硬加项"。 - 双缓冲:设备读下一块时 CPU 在处理当前块,两者完全并行——只有当"CPU 一趟活(
)"比"设备读一块( )"还慢时,CPU 才会成为瓶颈。 - 所以双缓冲有没有用,完全取决于
与 谁大——见下节例 1 的扫描表。
循环缓冲与缓冲池(两级进阶):
| 结构 | 组成 | 适用 |
|---|---|---|
| 循环缓冲 | 单向/双向连续数据流(如音频采集) | |
| 缓冲池 | 三个队列(空缓冲 / 输入 / 输出)+ 四种缓冲区 | 通用,系统级共享 |
缓冲池的"四个动作"(缓冲池的核心,必背):
text
收容输入: 从"空缓冲队列"取一块 -> 设备把数据读进来 -> 挂入"输入队列"
提取输入: 从"输入队列"取一块 -> 进程把数据算走 -> 该块回"空缓冲队列"
收容输出: 从"空缓冲队列"取一块 -> 进程把结果写进来 -> 挂入"输出队列"
提取输出: 从"输出队列"取一块 -> 设备把数据输出走 -> 该块回"空缓冲队列"⚠️ "四个动作"名称容易记混,判据是"数据往哪边走":"收容" = 数据进缓冲区,"提取" = 数据出缓冲区;"输入" = 设备→内存方向,"输出" = 内存→设备方向。 "收容输入"就是"设备把数据读进来放进缓冲"。
五、设备分配:三张表与分配顺序
数据结构(四张表,逐级向下):
text
SDT (系统设备表) 全系统一张: 记录所有设备及其 DCT 指针
│
├─> DCT (设备控制表) 每台设备一张: 设备状态 + 指向 COCT 的指针
│ │
│ └─> COCT (控制器控制表) 每个控制器一张: 状态 + 指向 CHCT
│ │
│ └─> CHCT (通道控制表) 每个通道一张: 状态
└─> (逻辑设备表 LUT: 逻辑设备名 -> 物理设备名)分配顺序绝对不能反:
释放顺序:
⚠️ 为什么"必须先分配设备、最后分配通道":因为"能不能用"是从上往下确认的——只有设备空闲,才值得去看它的控制器;只有控制器空闲,才值得去看它挂的通道。反过来说,如果先占了通道、结果发现控制器被别人占了,通道就白占着不能用——这叫"死锁式的资源占用",也是分配顺序必须"自顶向下"的原因。释放顺序相反的道理一样:把"最上游的通道"先放开,让等在通道上的人能继续往下走。
SPOOLing(假脱机技术)——把独占设备变成共享设备:
text
磁盘上开辟两个区域:
输入井 (存放从输入设备预读进来的数据)
输出井 (存放进程要输出的数据)
内存里放两张缓冲区:
输入缓冲区 / 输出缓冲区
再加上: 输入进程 / 输出进程 / 井管理程序
★ 进程"输出"到打印机时, 其实只是把数据写进了"输出井"的一块
进程立即返回继续干活 -> 打印机由"输出进程"慢慢按顺序打
-> 进程"以为"自己独占打印机, 实际上大家的数据都在井里排队SPOOLing 的三层收益:
| 层次 | 收益 |
|---|---|
| 对进程 | 把"独占设备"变成了"共享设备"(虚拟设备技术)——不用等打印完 |
| 对 CPU | 输出操作(写磁盘)比输出到打印机快得多,CPU 等待时间大幅缩短 |
| 对设备 | 打印机不再空转等数据,利用率提高 |
⚠️ SPOOLing 的代价:需要磁盘空间(井),而且"假脱机"依赖磁盘——磁盘坏了,井里的数据全丢。 它是一种"用空间和可靠性换并发度"的折中。
示例
例 1:C 实现——单/双缓冲时间与四种 I/O 方式的 CPU 占用
缓冲参数:
(读入缓冲区)、 (送到用户区)、 (CPU 处理)。 I/O 方式参数:CPU 1 GHz、块 4 KiB、设备 40 MB/s、中断开销 90 周期、DMA 初始化 50 周期。
#include <stdio.h>
#define T_READ 100.0 /* 磁盘读入缓冲区(us) */
#define T_MOVE 20.0 /* 缓冲区 -> 用户区(us) */
#define T_CPU 50.0 /* CPU 处理一块(us) */
#define FREQ_MHZ 1000.0 /* CPU 主频 */
#define BLK_KB 4 /* 一次传输的数据块 */
#define DEV_MBPS 40.0 /* 设备传输率 */
#define IRQ_CYC 90.0 /* 一次中断的开销(周期) */
#define DMA_INIT 50.0 /* DMA 初始化(周期) */
static double dmax(double a, double b) { return a > b ? a : b; }
int main(void) {
double single = dmax(T_CPU, T_READ) + T_MOVE;
double dbl = dmax(T_CPU + T_MOVE, T_READ);
double blk_ns, words, irq, dma, ch;
int ns[3] = {1, 10, 100};
int i;
printf("T(读入缓冲) = %.0f us, M(送到用户区) = %.0f us, C(CPU处理) = %.0f us\n",
T_READ, T_MOVE, T_CPU);
printf("单缓冲 每块 = max(C,T)+M = max(%.0f,%.0f)+%.0f = %.1f us\n",
T_CPU, T_READ, T_MOVE, single);
printf("双缓冲 每块 = max(C+M,T) = max(%.0f,%.0f) = %.1f us\n",
T_CPU + T_MOVE, T_READ, dbl);
printf("\n");
for (i = 0; i < 3; i++) {
printf("处理 %3d 块: 单缓冲 %8.1f us, 双缓冲 %8.1f us, 加速比 %.4f\n",
ns[i], single * ns[i], dbl * ns[i], single / dbl);
}
/* 设备侧时间: 4 KB / 40 MB/s = 102.4 us = 102400 ns
1 GHz 下 1 ns = 1 个时钟周期, 所以周期数与 ns 数值相同 */
blk_ns = BLK_KB * 1024.0 / (DEV_MBPS * 1000000.0) * 1000000000.0;
words = BLK_KB * 1024.0 / 4.0; /* 按 4 B 一个字算 */
irq = words * IRQ_CYC; /* 每字一次中断 */
dma = words * 1.0 + DMA_INIT + IRQ_CYC; /* 每字挪用 1 周期 + 初始化 + 收尾中断 */
ch = DMA_INIT + IRQ_CYC; /* 整块传完才中断一次 */
printf("\n4 KB @ %.0f MB/s -> 设备侧 %.1f us = %.0f 个时钟周期 (1 GHz)\n",
DEV_MBPS, blk_ns / 1000.0, blk_ns);
printf(" 程序查询: CPU 全程空转 -> 占用 %.3f%% (CPU 不能做别的事)\n", 100.0);
printf(" 中断方式: %4.0f 字 x 90 周期 = %7.0f 周期 -> 占用 %.3f%%\n",
words, irq, irq / blk_ns * 100.0);
printf(" DMA : %4.0f x 1 + 50 + 90 = %7.0f 周期 -> 占用 %.3f%%\n",
words, dma, dma / blk_ns * 100.0);
printf(" 通道 : 50 + 90 = %4.0f 周期 (整块传完才中断) -> 占用 %.3f%%\n",
ch, ch / blk_ns * 100.0);
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
T(读入缓冲) = 100 us, M(送到用户区) = 20 us, C(CPU处理) = 50 us
单缓冲 每块 = max(C,T)+M = max(50,100)+20 = 120.0 us
双缓冲 每块 = max(C+M,T) = max(70,100) = 100.0 us
处理 1 块: 单缓冲 120.0 us, 双缓冲 100.0 us, 加速比 1.2000
处理 10 块: 单缓冲 1200.0 us, 双缓冲 1000.0 us, 加速比 1.2000
处理 100 块: 单缓冲 12000.0 us, 双缓冲 10000.0 us, 加速比 1.2000
4 KB @ 40 MB/s -> 设备侧 102.4 us = 102400 个时钟周期 (1 GHz)
程序查询: CPU 全程空转 -> 占用 100.000% (CPU 不能做别的事)
中断方式: 1024 字 x 90 周期 = 92160 周期 -> 占用 90.000%
DMA : 1024 x 1 + 50 + 90 = 1164 周期 -> 占用 1.137%
通道 : 50 + 90 = 140 周期 (整块传完才中断) -> 占用 0.137%⚠️ 三点说明:
dmax手写而不是用<math.h>的fmax——引<math.h>在很多平台上要加-lm,考试手写时更省事。- 注释里那句"1 GHz 下 1 ns = 1 个时钟周期"是关键:
,在 1 GHz 上正好 102400 个周期——数值相等是巧合(主频恰好 1 GHz),换个主频就要单独乘。 - 本机无 C 编译器:此段代码逐行人工审查,并用等价的 Python 实现实跑核对,输出 12 行逐字一致。
例 2:Python——双缓冲的适用边界、缓冲池三队列、SPOOLing
def pad(s, w):
"""按"显示宽度"补空格: 中文算 2 列, 否则终端里对不齐"""
return s + ' ' * max(0, w - sum(2 if ord(c) > 0x2000 else 1 for c in s))
print('=== ① 双缓冲什么时候才有用 ===')
T, M = 100.0, 20.0
print(' 固定 T=%.0f us(读入缓冲), M=%.0f us(送到用户区), 扫描 C(CPU 处理时间)' % (T, M))
print(' C(us) 单缓冲 双缓冲 加速比 瓶颈在哪')
for C in [10, 50, 60, 70, 80, 90, 100, 120, 200, 300]:
s = max(C, T) + M
d = max(C + M, T)
who = 'I/O(设备慢)' if C + M <= T else ('过渡区' if C <= T else 'CPU(算得慢)')
print(' %5.0f %7.1f %7.1f %7.4f %s' % (C, s, d, s / d, who))
print(' ★ 结论: C+M <= T 时双缓冲把 I/O 全藏起来(加速 1.2 倍);')
print(' C > T 时 CPU 才是瓶颈, 双缓冲一点不加(加速比 1.0)')
print()
print('=== ② 四种 I/O 控制方式的 CPU 占用 ===')
FREQ, BLK_KB, DEV = 1000.0, 4, 40.0
blk_ns = BLK_KB * 1024.0 / (DEV * 1e6) * 1e9
words = BLK_KB * 1024.0 / 4
print(' CPU %.0f MHz, 块 %d KB, 设备 %.0f MB/s -> 设备侧 %.1f us = %.0f 周期'
% (FREQ, BLK_KB, DEV, blk_ns / 1000, blk_ns))
rows = [('程序查询', 'CPU 死等', '无中断', blk_ns, 100.0),
('中断驱动', 'CPU 每字一眼', '每字 1 次', words * 90, words * 90 / blk_ns * 100),
('DMA', 'DMAC 全程', '整块 1 次', words * 1 + 50 + 90,
(words * 1 + 50 + 90) / blk_ns * 100),
('通道', '通道程序', '一组块 1 次', 50 + 90, (50 + 90) / blk_ns * 100)]
print(' ' + pad('方式', 10) + pad('谁在盯着搬运', 16) + pad('中断频率', 12) + 'CPU 开销')
for nm, who, freq, cyc, pct in rows:
print(' ' + pad(nm, 10) + pad(who, 16) + pad(freq, 12)
+ '%8.0f 周期 -> %8.3f%%' % (cyc, pct))
print(' ★ 中断 -> DMA 降 %.1f 倍; DMA -> 通道 再降 %.1f 倍'
% ((words * 90) / (words * 1 + 50 + 90), (words * 1 + 50 + 90) / (50 + 90)))
print()
print('=== ③ 缓冲池: 三个队列的流转 ===')
free_q, in_q, out_q = ['B1', 'B2', 'B3'], [], []
def show(tag):
print(' ' + pad(tag, 24) + pad('空缓冲=' + str(free_q), 26)
+ pad('输入=' + str(in_q), 14) + '输出=' + str(out_q))
show('初始')
b = free_q.pop(0); in_q.append(b)
show('收容输入(设备写 ' + b + ')')
b = in_q.pop(0); free_q.append(b)
show('提取输入(进程算完 ' + b + ')')
b = free_q.pop(0); out_q.append(b)
show('收容输出(进程写 ' + b + ')')
b = out_q.pop(0); free_q.append(b)
show('提取输出(设备送走 ' + b + ')')
print(' ★ 四个动作: 收容输入/提取输入/收容输出/提取输出; 谁用谁归还')
print()
print('=== ④ SPOOLing: 3 个进程共享 1 台打印机 ===')
jobs = [('P1', 3), ('P2', 1), ('P3', 2)]
spool = []
for name, n in jobs:
for i in range(n):
spool.append('%s-%d' % (name, i + 1))
print(' %s 提交 %d 份 -> 输出井里有 %d 份 (进程立刻返回, 不必等打印完)'
% (name, n, len(spool)))
order = []
while spool:
order.append(spool.pop(0))
print(' 打印机按井中顺序输出: %s' % ' '.join(order))
print(' ★ 独占设备 -> 虚拟设备; 每个进程都"以为"自己独占打印机')
print()
print('=== ⑤ 设备分配与释放的顺序 ===')
print(' 分配: 设备 -> 控制器 -> 通道 (自顶向下确认)')
print(' 释放: 通道 -> 控制器 -> 设备 (与分配相反)')
print(' 表链: SDT(全系统 1 张) -> DCT(每设备 1 张) -> COCT(每控制器 1 张) -> CHCT(每通道 1 张)')
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照(真实运行结果):
=== ① 双缓冲什么时候才有用 ===
固定 T=100 us(读入缓冲), M=20 us(送到用户区), 扫描 C(CPU 处理时间)
C(us) 单缓冲 双缓冲 加速比 瓶颈在哪
10 120.0 100.0 1.2000 I/O(设备慢)
50 120.0 100.0 1.2000 I/O(设备慢)
60 120.0 100.0 1.2000 I/O(设备慢)
70 120.0 100.0 1.2000 I/O(设备慢)
80 120.0 100.0 1.2000 I/O(设备慢)
90 120.0 110.0 1.0909 过渡区
100 120.0 120.0 1.0000 过渡区
120 140.0 140.0 1.0000 CPU(算得慢)
200 220.0 220.0 1.0000 CPU(算得慢)
300 320.0 320.0 1.0000 CPU(算得慢)
★ 结论: C+M <= T 时双缓冲把 I/O 全藏起来(加速 1.2 倍);
C > T 时 CPU 才是瓶颈, 双缓冲一点不加(加速比 1.0)
=== ② 四种 I/O 控制方式的 CPU 占用 ===
CPU 1000 MHz, 块 4 KB, 设备 40 MB/s -> 设备侧 102.4 us = 102400 周期
方式 谁在盯着搬运 中断频率 CPU 开销
程序查询 CPU 死等 无中断 102400 周期 -> 100.000%
中断驱动 CPU 每字一眼 每字 1 次 92160 周期 -> 90.000%
DMA DMAC 全程 整块 1 次 1164 周期 -> 1.137%
通道 通道程序 一组块 1 次 140 周期 -> 0.137%
★ 中断 -> DMA 降 79.2 倍; DMA -> 通道 再降 8.3 倍
=== ③ 缓冲池: 三个队列的流转 ===
初始 空缓冲=['B1', 'B2', 'B3'] 输入=[] 输出=[]
收容输入(设备写 B1) 空缓冲=['B2', 'B3'] 输入=['B1'] 输出=[]
提取输入(进程算完 B1) 空缓冲=['B2', 'B3', 'B1'] 输入=[] 输出=[]
收容输出(进程写 B2) 空缓冲=['B3', 'B1'] 输入=[] 输出=['B2']
提取输出(设备送走 B2) 空缓冲=['B3', 'B1', 'B2'] 输入=[] 输出=[]
★ 四个动作: 收容输入/提取输入/收容输出/提取输出; 谁用谁归还
=== ④ SPOOLing: 3 个进程共享 1 台打印机 ===
P1 提交 3 份 -> 输出井里有 3 份 (进程立刻返回, 不必等打印完)
P2 提交 1 份 -> 输出井里有 4 份 (进程立刻返回, 不必等打印完)
P3 提交 2 份 -> 输出井里有 6 份 (进程立刻返回, 不必等打印完)
打印机按井中顺序输出: P1-1 P1-2 P1-3 P2-1 P3-1 P3-2
★ 独占设备 -> 虚拟设备; 每个进程都"以为"自己独占打印机
=== ⑤ 设备分配与释放的顺序 ===
分配: 设备 -> 控制器 -> 通道 (自顶向下确认)
释放: 通道 -> 控制器 -> 设备 (与分配相反)
表链: SDT(全系统 1 张) -> DCT(每设备 1 张) -> COCT(每控制器 1 张) -> CHCT(每通道 1 张)五条结论:
- 双缓冲只在
时才有效:本题 、 ,所以 时加速比恒为 1.2; 时降到 1.0909; 时加速比正好 1.0000(一点不加)——"双缓冲不是万能的,它只在'I/O 是瓶颈'时有用"这句话有了完整的量化边界。 - 四种 I/O 方式的占用率呈"数量级台阶":100% → 90.000% → 1.137% → 0.137%;中断→DMA 降 79.2 倍,DMA→通道再降 8.3 倍——每一次跃迁都对应一句原理(中断是"每字一次"、DMA 是"数据不走 CPU"、通道是"CPU 只发一次命令")。
- 缓冲池的流转只有一个规则:谁用谁归还。无论"收容"还是"提取",缓冲区用完都回到"空缓冲队列"(脚本里
free_q每次都回到 3 个)——这就是缓冲池"共享"的实现方式。 - SPOOLing 让三个进程同时"提交"输出:输出井累积到 6 份,而打印机按 FIFO 顺序输出
P1-1 P1-2 P1-3 P2-1 P3-1 P3-2——进程 A 的 3 份没有被进程 B 插队打乱,这就是"虚拟独占"的效果。 - 设备分配必须"设备 → 控制器 → 通道",释放反过来——"自顶向下确认,自底向上释放",两条顺序写反了就是整道题的分。
考点
考点
1. 必背结论
- I/O 系统四层:用户层 I/O 软件 / 设备独立性软件 / 设备驱动程序 / 中断处理程序。
- 与设备相关的是"设备驱动"(每类设备一套);设备独立性软件与设备无关(所有设备共用)。
- "设备独立性"= 用逻辑设备名编程 + LUT 映射到物理设备——换设备不用改程序。
- 块设备可寻址(磁盘);字符设备不可寻址(键盘)——这一条决定了"能不能用 DMA"。
- 按共享属性分:独占设备 / 共享设备 / 虚拟设备;SPOOLing 把独占设备变成虚拟设备。
- 四种 I/O 控制方式的分界:"谁在盯着搬运"——CPU 死等(查询)/ CPU 每字一眼(中断)/ DMAC 全程(DMA)/ 通道程序(通道)。
- 中断频率:查询无中断、中断每字一次、DMA 每块一次、通道每组一次。
- 缓冲的四个作用:缓和速度差 / 减少中断频率 / 解决粒度不匹配 / 提高并行性。
- 单缓冲
;双缓冲 。 - 双缓冲的适用边界:
时有效(把 I/O 藏起来); 时 CPU 是瓶颈,双缓冲无效。 - 缓冲池的三个队列 / 四个动作:空缓冲队列 / 输入队列 / 输出队列;收容输入 / 提取输入 / 收容输出 / 提取输出——用完的缓冲区一律回"空缓冲队列"。
- 设备分配三张表链:SDT(1 张)→ DCT(每设备)→ COCT(每控制器)→ CHCT(每通道)。
- 分配顺序:设备 → 控制器 → 通道;释放顺序相反。
- SPOOLing 五部件:输入井 / 输出井(磁盘上)+ 输入缓冲区 / 输出缓冲区(内存里)+ 输入进程 / 输出进程 + 井管理程序。
- 锚点(本章定死,与
arch/41-io.md同口径):CPU 1 GHz、块 4 KiB、设备 40 MB/s(设备侧 102.4 μs = 102400 周期)、中断 90 周期、DMA 初始化 50 周期 → 程序查询 100% / 中断 90.000%(92160 周期)/ DMA 1.137%(1164 周期)/ 通道 0.137%(140 周期); 中断→DMA 降 79.2 倍、DMA→通道 再降 8.3 倍。 - 锚点(缓冲):
、 、 → 单缓冲 120.0 μs、双缓冲 100.0 μs、加速比 1.2000; 时加速比恒 1.2, 时恒 1.0。
2. 高频陷阱
- 把"设备驱动程序"当成与设备无关的:错。驱动是"每种设备一套",与设备相关;"设备独立性软件"才是共用的那一层。
- 说"设备独立性 = 不要驱动程序":错。独立性解决的是"用户程序不用关心具体型号",驱动照样要为每类设备写。
- 把"通道"当成"更快的 DMA":不准确。通道有自己的指令系统(通道命令字),能执行"通道程序"控制多台设备连续传输——DMA 只能搬一块,通道能按程序搬一串。
- 认为"程序查询方式没有浪费 CPU":错。CPU 全程在"读状态→判断→跳回"空转——本题 100% 占用,效率最低。
- 算中断方式占用率时忘了"设备越快、占用越高":错。中断开销固定 90 周期,而摊薄它的时间随设备提速而缩短——40 MB/s 已占 90%,再快就超过 100%(物理上不可能),只能改用 DMA。
- 单缓冲公式写成
时漏掉 :错。 是"硬加项"(数据必须搬进用户区,缓冲区才能腾出来),它在单缓冲里是藏不掉的那一段。 - 双缓冲公式写成
:错。双缓冲是 ——两个公式混用是最常见的失分点。 - 认为"双缓冲一定比单缓冲快一倍":错。本题只快 1.2 倍;当 CPU 才是瓶颈(
)时,双缓冲完全无效(加速比 1.0)。 - 认为"缓冲区越多越好":不准确。缓冲区多会增加内存开销与复制次数,且"缓冲"本身不能提高设备速度——它只是把"等待"重新分配了。
- 把"收容"与"提取"记反:"收容"= 数据进缓冲区;"提取"= 数据出缓冲区。"收容输入"是设备把数据读进来(不是进程读走)。
- 设备分配顺序写成"控制器 → 设备 → 通道":错。必须是"设备 → 控制器 → 通道"(自顶向下确认可用性),释放时反过来。
- 认为 SPOOLing 用的是内存而不是磁盘:错。输入井/输出井开在磁盘上(内存里只有"输入缓冲区/输出缓冲区"两个小缓冲)。
- 说"SPOOLing 提高了设备速度":错。设备的物理速度一点没变;变的是"进程不必等它"——SPOOLing 提高的是并发度和 CPU 利用率,不是设备速率。
3. 解题模板("I/O 缓冲与设备题")
① 算缓冲时间: 认准 T(设备->缓冲) / M(缓冲->用户区) / C(CPU 处理) 三个量
单缓冲 = max(C, T) + M
双缓冲 = max(C + M, T)
要判"双缓冲有没有用": 比较 C+M 与 T
② 算 I/O 方式占用率:
设备侧时间 t = 块大小 / 传输率
查询: 100%
中断: 每字一次中断 -> 周期数 = 字数 x 单次中断开销 ; 占用 = 周期数 / t(周期)
DMA : 每字挪用 1 周期 + 初始化 + 收尾中断
通道: 初始化 + 收尾中断(整块/一组只一次)
注意单位统一: 1 GHz 下 1 ns = 1 周期
③ 缓冲池流转: 只记一句"谁用谁归还", 用完回"空缓冲队列"
④ 设备分配: 设备 -> 控制器 -> 通道 (释放反向); 表链 SDT -> DCT -> COCT -> CHCT
⑤ SPOOLing: 五部件 + 三层收益(进程: 独占变共享 / CPU: 等待变短 / 设备: 不空转)4. 与相邻章节的接口
arch/41-io.md(I/O 系统:查询/中断/DMA):硬件视角的那一半——那章讲接口寄存器、DMAC 组成、周期挪用;本章讲软件层次、设备分配、缓冲。两章共用同一组锚点数字。os/33-disksched.md(磁盘调度):"磁盘 I/O 队列"就是本章"设备分配"要排队的那条队列——那章决定"先服务哪个请求",本章决定"谁能用这台设备"。os/31-disk.md(磁盘组织):"提前读 / 延迟写 / 磁盘缓存"是本章"缓冲"思想在磁盘上的两处具体应用。os/30-filesystem.md(文件系统):read/write从文件系统下来,正好落在本章的"设备独立性软件"这一层——"上层只给逻辑块号,下层负责变成设备命令"就是分层。os/10-process.md(进程与线程):进程"阻塞在 I/O 上"就是被本章的驱动程序挂起;SPOOLing 的"输出进程"本身就是一个常驻系统进程。os/14-classic.md(经典同步问题):缓冲池的三个队列是标准的"生产者-消费者"模型——empty/full两个信号量在这里就是"空缓冲队列长度"和"输入/输出队列长度"。os/23-virtual.md(虚拟内存):缺页时"从磁盘调入页面"走的就是本章的 I/O 通路(驱动 + 缓冲 + 中断)。
小结
- I/O 管理抹平三种"差":速度差 → 缓冲;接口差 → 驱动 + 设备独立性;独占/共享 → SPOOLing。
- 四层结构:用户层软件 / 设备独立性软件 / 设备驱动 / 中断处理;只有"设备驱动"与设备相关。
- 四种控制方式看"谁盯着搬运":查询 100% → 中断 90.000% → DMA 1.137% → 通道 0.137%(中断→DMA 降 79.2 倍、DMA→通道 再降 8.3 倍)。
- 缓冲两个公式:单缓冲
、双缓冲 ;锚点( =100、 =20、 =50)→ 120.0 / 100.0 μs,加速比 1.2000。 - 双缓冲的边界:
才有增益; 时加速比正好 1.0。 - 缓冲池:三队列(空缓冲/输入/输出)+ 四动作(收容输入/提取输入/收容输出/提取输出);用完一律回空缓冲队列。
- 设备分配:SDT → DCT → COCT → CHCT;分配"设备 → 控制器 → 通道",释放反过来。
- SPOOLing = 输入井 + 输出井 + 两个缓冲区 + 两个进程 + 井管理程序,把独占设备变成虚拟设备;它不提高设备速率,只提高并发度。
下一篇:磁盘调度算法
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。