第22讲 · 输入/输出设备原理
2026 南京大学《操作系统原理》第二十二讲 · 课堂实录重构
| 主讲 | 蒋炎岩(Yanyan Jiang) |
| 视频 | https://www.bilibili.com/video/BV1dYLq6gE8W/ |
| 课程主页 | https://jyywiki.cn/OS/2026/ |
| 本讲讲义 | https://jyywiki.cn/OS/2026/lect22.md |
| 讲义 / 幻灯片作者 | 蒋炎岩,https://jyywiki.cn/ |
| 讲义 / 幻灯片许可 | CC BY-NC 4.0 |
| 图片来源 | 书中插图为课堂演示与讲义渲染图;演示类截图为视频画面 |
对应讲次:第22讲(南京大学 操作系统原理)
制作模型:deepseek-flash(coding-agent / pi;据 ~/.pi/agent/settings.json)
编者说明:本书由课堂录像的语音字幕重构而成,术语、章节骨架与参考链接已按课程官方讲义(
lect22.md)校准。讲师的口语风格、类比与案例被尽量保留;编辑性的补充一律以「编者说明」或「编者拓展」标出。
引子:打开操作系统的对象
(参考时间: 00:00)
Virtualization 和 Concurrency 都讲完了:从最底层的 system call 一层层构建出 libc 和整个 Linux 应用世界;再用 spawn/join 打开了「魔鬼的盒子」(并行计算、异步编程、异构计算)。
现在补上最后一块:操作系统中的对象。前面讲系统调用时反复提到,用 open 打开一个对象会得到一个文件描述符——「一个指向操作系统对象的 handle」。因为 Everything is a file,所有对象都用同一套 API 访问:想知道进程树就遍历 /proc,想知道地址空间就读 /proc/<pid>/maps……
「今天我把这个黑盒子打开,展开讲计算机系统里的 I/O 设备是怎么实现的,怎么抽象,最后怎么做成文件系统。」
编者说明:这门课花大量时间讲的 CPU、内存、逻辑门,都是你使用电脑时看不见的东西;你看见的所有东西都是 I/O 设备——键盘、鼠标、屏幕、摄像头、打印机。「我们学的恰恰是底下那个看不见的东西。」
一、I/O 设备
1.1 Canonical Device Model
(参考时间: 00:03)
I/O 设备 = 一个能与 CPU 交换数据的接口/控制器——它使计算机能感知外部状态(眼睛、耳朵)、对外实施动作(手)。「Everything is a state machine:CPU state 本身不能干涉物理世界,而 I/O 设备就是计算机的手和脚。」
最简模型(canonical device model):把设备看成几组约定好功能的寄存器,给寄存器赋予一个内存地址(Address Decoder),CPU 就能直接用指令(in/out/MMIO)和设备交换数据:
- status:设备是否 ready、是否正常工作;
- command:让设备开始/结束某项任务;
- data:实际读写的数据。
「一台打印机内部有很多机械部件,但 CPU 完全不用看到——它看到的只是状态、命令和数据。」
📷 Canonical Device Model:设备 = 几组寄存器(status / command / data),CPU 通过 MMIO 或 in/out 访问
回到原视频 (00:06:00)
编者拓展|中断(interrupt):设备还能向 CPU 发中断。可以用多线程来理解:操作系统创建一个
T_device线程,它P一个信号量等中断;中断到来时处理器强制跳转到一个预设地址执行操作系统的中断处理程序,相当于V一下唤醒T_device。「就和你们在用户态写多线程程序是类似的」——只是实际实现会复杂一些。
1.2 一些简单的设备
(参考时间: 00:09)
LED 灯(GPIO):极简到只需要一个可写的寄存器。在树莓派上,通过 /sys/class/leds/.../brightness 写一个值,就能让灯闪烁:
// 写文件 = 写 GPIO 寄存器
write(fd, "1", 1); // 灯亮
📷 GPIO LED:一个可写寄存器直接控制发光二极管
回到原视频 (00:11:00)
编者拓展|「实现核弹发射器只需要一根线」:讲师说,「你能驱动一个 LED 灯,就能驱动一个继电器,就能驱动任何马达」——因为你可以控制 GPIO 的电压(0 或 3.3V)。密码验证通过就给一个电平,触发另一个设备。「理论上和控制树莓派上的灯没有本质区别。」
串口(UART):/dev/ttyS0 映射到 UART,PC 上叫 COM1(0x3f8):
#define COM1 0x3f8
static void uart_tx(AM_UART_TX_T *send) { outb(COM1, send->data); } // 写寄存器
static void uart_rx(AM_UART_RX_T *recv) {
recv->data = (inb(COM1 + 5) & 0x1) ? inb(COM1) : -1; // 读寄存器
}
📷 UART/COM1:把字符写到 data 寄存器就发出去了,和写 GPIO 电平一样简单
回到原视频 (00:15:00)
编者说明:x86 有独立的 I/O 地址空间(用
in/out指令),而 ARM/RISC-V 把设备寄存器直接映射到内存地址(MMIO)。「你们的数字逻辑 / 计算机系统基础课上做的那个小东西,其实和真正的计算机是非常非常接近的,只要你给它足够的外部设备。」
键盘控制器(8042 PS/2):6 根线(Data、Clock、VCC、GND + 2 预留),映射到 port 0x60(data)、0x64(status/command)。可以发命令配置:0xED(LED 灯控)、0xF3(重复速度与延迟)。
📷 IBM PC/AT 8042 PS/2 键盘控制器:映射到端口 0x60/0x64
回到原视频 (00:18:00)
磁盘控制器(ATA/IDE):primary 0x1f0–0x1f7,secondary 0x170–0x177:
void readsect(void *dst, int sect) {
waitdisk();
out_byte(0x1f2, 1); // sector count
out_byte(0x1f3, sect); // sector
out_byte(0x1f4, sect >> 8); // cylinder (low)
out_byte(0x1f5, sect >> 16); // cylinder (high)
out_byte(0x1f6, (sect >> 24) | 0xe0); // drive
out_byte(0x1f7, 0x20); // command (read)
waitdisk();
for (int i = 0; i < SECTSIZE / 4; i++)
((uint32_t *)dst)[i] = in_long(0x1f0); // data:一次 4 字节
}
📷 ATA 磁盘控制器:往正确的寄存器写扇区号与读命令,再从 data 寄存器搬数据
回到原视频 (00:20:00)
编者说明:「键盘、鼠标、磁盘、终端——这已经构成了一个经典计算机世界的全部。」
1.3 更有趣的设备
(参考时间: 00:21)
打印机:早期的打印机做不了点阵,就是一支笔(绘图仪)——有 X 轴、Y 轴移动 + 提笔/放笔,就能画出任何图形。
编者拓展|打印机 = 程序的解释器:本质上是「用程序控制笔」——先有底层的「提笔/放下/移动」指令,再往上可以有
line x1 y1 x2 y2这样的高级语言,高级语言编译成低级控制序列送给打印机。「一台打印机其实是一个程序的解释器。」随计算机能力增长,就诞生了 PCL、PostScript 这样的页面描述语言(PDL):
- PostScript:一种图灵完备(基于栈的机器)的编程语言,用「图形状态机」描述路径、着色、文本渲染;PDF 是它的「改进版」(去掉了图灵完备部分,更安全,但保留了形状描述能力);
- 历史悠久的
lp/lpr(Line Printer Daemon)命令,以及 IPP(AirPrint)。
<ESC>*t300R // PCL: Set resolution to 300 DPI
<ESC>*r1A // Start raster graphics
<ESC>*b100W // Set width of raster data (100 bytes)
<binary raster data> // Actual image data
<ESC>*rB // End raster graphics
📷 PCL 打印命令:一段段 ESC 序列构成打印机控制程序
回到原视频 (00:26:00)
📷 PostScript:让 DeepSeek 现场生成一个 `.ps`,用 `ps2pdf` 渲染出矢量图形
回到原视频 (00:30:00)
摄像头(WebCam):你买到的所有摄像头都是「免驱」的,要感谢 UVC(USB Video Class):枚举设备 → 协商格式/分辨率/帧率 → 传输开始 → 逐帧读取 MJPEG/H.264。仍然是交换字节流(canonical device model)。它衍生出了行车记录仪、显微镜、内窥镜、平板扫描仪、眼球鼠标……
📷 UVC 摄像头:标准化的协议让「免驱」成为可能
回到原视频 (00:33:00)
1.4 协处理器和总线
(参考时间: 00:34)
打破 I/O 设备与内存之间的边界:如果允许设备访问计算机内的资源呢?
DMA(Direct Memory Access):一个专门执行 memcpy 的协处理器——「早期的多处理器系统」,只能执行 hard-wired 程序(比 CPU 简单很多),但能和 I/O 端口通信,大幅解放 CPU:
void T_i8237() { // Intel 8237 DMA:4 个通道
while (1) {
for (int i = 0; i < 4; i++) {
struct channel_t *ch = &channels[i];
if (ch->count-- > 0) {
if (ch->mode == READ) ch->io = ch->mem[ch->addr++];
else ch->mem[ch->addr++] = ch->io;
break; // Priority
}
}
}
}
📷 DMA:一个只会做 memcpy 的小 CPU,和 CPU 共享内存与 I/O 端口空间
回到原视频 (00:37:00)
异构加速器:GPU 和 NPU:GPU 看起来像「一台完整的计算机」,可以执行 <<< >>> 里的 kernel——但代码和数据都由 CPU 送过去(cudaMalloc、cudaMemcpy,由 DMA 完成),控制指令仍是 canonical device model。
连接万千设备:总线。设备越来越多,「每一个端口写死」不够用了——我们需要一个像地址空间一样可动态分配的东西:
- ISA(IBM PC/AT)、Apple II Bus;
- USB:提供设备的注册与转发(USB PnP 名场面(Windows 98));
- PCIe:今天获得「CPU 直连」待遇的标准设备。PC 上 CPU 有若干 PCIe lanes;总线自带 DMA、能发中断(MSI)、支持 75W 供电(更高功率的显卡才需要 6-pin/8-pin 外接供电)。
lspci # 列出 PCI 设备
lsusb -t # 树状的 USB 设备列表
📷 lspci / lsusb:总线支持设备发现,能读出设备号并加载对应驱动
回到原视频 (00:45:00)
编者拓展|PCIe 6.0 ×16 与 CXL:PCIe 6.0 ×16 带宽可达 128 GB/s(相当于 800 Gbps 网卡)。而基于 PCIe 物理层还诞生了更夸张的 CXL(Compute Express Link),有 CXL.io / CXL.cache / CXL.memory 三个协议:
- CXL 3.0 允许设备直接共享内存——「设备可以是任何东西:GPU、NPU,甚至另一台计算机上的内存!」
- 由此得到 Disaggregated Datacenter(解耦数据中心):「我本地不需要那么大内存,远端有一个 16T 的共享大内存……访问远端内存有一次网络 round trip(几百纳秒)。」「这再次说明:你去了真正的系统里,教科书上学的东西都变了——能留下来的是你分析问题的 first principles。」
📷 CXL 与解耦数据中心:设备可以直接共享(甚至远端)内存
回到原视频 (00:52:00)
二、程序视角的 I/O 设备
2.1 为什么不直接访问设备寄存器
(参考时间: 00:52)
操作系统设计者视角:程序不应该直接访问总线/设备寄存器。
- 访问 → 共享;共享 → 同步;同步 → bug——「两个程序同时想打印,就必然要先后排队;忘记同步就会 data race,打出来就是一堆废墟」;
- 「转念一想:CPU 和内存也是『I/O 设备』啊」——CPU 取指令、写内存;内存 load/store 也是 input/output。既然 CPU/内存可以虚拟化,设备也能!
2.2 Everything is a File
(参考时间: 00:54)
File = 实现了文件操作的「Anything」:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
...
};
📷 Everything is a File:`struct file_operations` 定义了设备虚拟化后的接口
回到原视频 (00:58:00)
- 这是一个巨大的结构体,定义了设备虚拟化以后的接口;
- 设备驱动程序 = 把你对设备的系统调用,翻译成设备能听懂的数据——「就是一段普通的内核代码」;
- 例子:
/dev/null的read直接返回 0、write直接返回count(「一个数据黑洞」)。
二次开发:Linux 在 canonical device model 之上再封装——比如给磁盘构造文件系统,让共享变得更容易。
编者拓展|显卡的「保底模式」:为什么没有装显卡驱动时,屏幕不会全黑?因为显卡会假装成一块老旧的 VGA 显示器——一个 frame buffer(每个像素一个颜色,24-bit 真彩或 RGB256),于是所有操作系统都能认识它。「虽然性能差一点:没有 DMA、没有 3D、不能加载 kernel」——直到加载了真正的驱动。
2.3 自己写一个设备驱动:核弹发射器
(参考时间: 01:00)
理解了驱动的职责,我们就能实现 struct file_operations 里的操作,模拟一个设备。讲师实现了一个「核弹发射器」:
read:总是返回字符串this is dangerous;write:做密码校验——正确才「发射」;
// launcher 的 read:返回 "this is dangerous"
// launcher 的 write:memcmp 校验密码,正确则在内核日志打印「核弹发射」
用 insmod 加载后,/dev/ 里就会多出设备文件(一个字符设备):
$ cat /dev/nuke # open → read → 走到驱动的 launcher_read
this is dangerous
$ echo wrong > /dev/nuke # 内核日志:incorrect secret, cannot launch
$ ./launch # 密码正确 → 日志打印「核弹发射」
📷 核弹发射器驱动:`cat /dev/nuke` 读出 `this is dangerous`——一个实现了 `file_operations` 的内核模块
回到原视频 (01:04:00)
编者拓展|设备驱动 = 「翻译层」:讲师说,「所有的设备驱动都是这样的」——打印机接受命令、显卡接受贴图和 kernel,都可以封装成这样的接口。而且「今天 web coding 就够了:我旧的驱动在新内核上不能编译,直接让 AI 帮我 fix。」
2.4 ioctl:设备不仅仅是数据
(参考时间: 01:06)
file_operations 的基本假设是设备是一个字节流/字节序列。但打印机、显卡除了 data 还有 command 和 status。「打印机你想让它停下来怎么办?难道还写数据吗?」
ioctl()系统调用操纵特殊文件的底层设备参数。 尤其地,许多字符特殊文件(如终端)的操作特性可以用ioctl()请求控制。参数、返回值和语义都随具体设备驱动而不同。
「『非数据』的设备功能,几乎全部依赖 ioctl。」例子:终端的 buffer mode(为什么 libc 能「智能」实现)、打印机的卡纸/清洁/自动装订、键盘跑马灯、磁盘健康状况、网卡、GPU……
📷 KVM Device:一个虚拟化子系统,整个都建立在 `ioctl` 这一个 API 之上
回到原视频 (01:14:00)
KVM Device:/dev/kvm 提供硬件虚拟化机制,让你在用户态通过 ioctl 在虚拟化环境中运行代码直到 VM Exit:
int kvm = open("/dev/kvm", ...); // 打开 KVM 设备
ioctl(kvm, KVM_CREATE_VM, ...); // 在内核里创建虚拟机
ioctl(kvm, KVM_SET_USER_MEMORY_REGION, ...);// 把进程内存交给虚拟机
ioctl(vcpu, KVM_SET_REGS, ...); // 设置 VCPU 寄存器
ioctl(vcpu, KVM_RUN, ...); // 运行,直到 VM Exit
// exit_reason: KVM_EXIT_MMIO / hypercall / shutdown / ...
编者拓展|「堆叠的💩山」与 UNIX 的负担:讲师说,
ioctl是一个「堆叠的💩山」——因为设备的复杂性是无法降低的(「就是有那么多功能」)。NVIDIA 每代新 GPU 都有额外功能,每个功能都需要开关。「这其实是 UNIX 一个很大的负担——ioctl有非常复杂的 hidden specifications。」他以 procfs 为例:你觉得「Linux 系统调用也就 300 条,做一个兼容系统应该不难」——但 Windows Subsystem for Linux 试图把每个 Linux 进程都做成一个 Windows 进程、拦截并模拟每个系统调用,最终「从工程意义上做不到 100% 兼容」,只好改用虚拟机。「如果 system call 可以轻易模拟,他们干嘛要搞个虚拟机?」
2.5 终于理解了操作系统的对象
(参考时间: 01:19)
程序如何访问设备? 以 GPU 为例——「一个有自己内存的协处理器」:
- 通过
mmap把内存交给设备驱动/设备; - 通过
ioctl提交命令(doorbell)。
分层:Linux DRM(Direct Rendering Manager)内核模块 → libdrm(用户态库,封装 ioctl)→ Mesa(用户态 OpenGL/Vulkan 实现,通过 libdrm 提交渲染命令)。
WebCam(UVC / V4L2):Linux 通过 Video for Linux 2(V4L2) 把摄像头抽象成 /dev/videoX,提供标准 ioctl(查询格式、设置帧率、申请缓冲区)和 mmap 获取视频帧。
📷 用 `strace` 拆开摄像头程序:open /dev/video0 → 一系列 V4L2 ioctl 查询与设置 → mmap 缓冲 → ioctl 等帧 → write 出图
回到原视频 (01:21:00)
graph TD
App["应用程序:read/write/ioctl/mmap"] --> VFS["VFS(Everything is a file)"]
VFS --> DRV["设备驱动:struct file_operations 的实现"]
DRV -->|"翻译"| REG["设备寄存器(status/command/data)"]
REG --> BUS["总线(PCIe / USB)"]
BUS --> DEV["实际设备(GPU / 摄像头 / 磁盘)"]
DRV -.->|"ioctl:非数据功能"| DRV
总结
- I/O 设备是「计算」与「物理世界」的桥梁:canonical device model——设备 = 几组寄存器(status/command/data),通过 MMIO 或
in/out访问,还能发中断; - 从简单到复杂:GPIO LED → 串口 UART → 键盘 → 磁盘(ATA)→ 打印机/PostScript → 摄像头(UVC)→ DMA(memcpy 协处理器) → 异构加速器(GPU/NPU) → 总线(ISA/USB/PCIe/CXL);
- 程序不应该直接访问设备:访问 → 共享 → 同步 → bug。Everything is a file:设备驱动程序 = 把系统调用翻译成设备能听懂的数据(一段普通的内核代码);
ioctl:设备的「非数据」功能几乎全部依赖它——「堆叠的💩山」,因为设备的复杂性无法降低;- 程序视角:
read/write(数据)+ioctl(控制)+mmap(共享内存)——GPU、摄像头(V4L2)都是如此; - Takeaway:输入/输出设备五花八门,越来越多的设备上甚至「自带电脑」。但无论如何,操作系统都把它们抽象成一个可以读写、可以控制的、实现了
struct file_operations的文件(操作系统对象)。「我们程序的每一个系统调用,在操作系统里兜了一大圈,最终都要变成总线上某个设备寄存器上的一个值——这就是计算机的本质,没有什么神秘的。」
附:官方参考与延伸阅读
本讲的讲义见课程主页,以下链接按书中出现顺序整理:
| 主题 | 链接 |
|---|---|
| GPIO LED(persistence 演示) | https://jyywiki.cn/OS/demos/persistence/gpio-led |
| PostScript(persistence 演示) | https://jyywiki.cn/OS/demos/persistence/postscript |
| CalComp 绘图仪手册 | https://jyywiki.cn/OS/manuals/CalComp-Software-Reference.pdf |
| 一个设备驱动程序(launcher) | https://jyywiki.cn/OS/demos/persistence/launcher |
| KVM Device(persistence 演示) | https://jyywiki.cn/OS/demos/persistence/kvm |
| WebCam(persistence 演示) | https://jyywiki.cn/OS/demos/persistence/webcam |
| USB PnP 名场面(Windows 98) | https://jyywiki.cn/OS/img/win98-scanner.mp4 |
| Qualcomm Neural Processing SDK | https://docs.qualcomm.com/nav/home/index_SNPE.html |
| IPP: Internet Printing Protocol (RFC 8011) | https://www.rfc-editor.org/rfc/rfc8011 |
| Compute Express Link (CXL) | https://www.computeexpresslink.org/ |
| OSTEP:Operating Systems: Three Easy Pieces(第 36 章 I/O Devices) | https://pages.cs.wisc.edu/~remzi/OSTEP/ |
阅读材料:读 OSTEP 第 36 章 I/O Devices;读一读 CalComp 绘图仪或 PostScript 的手册,感受「canonical device model + 程序解释器」的组合。
版权说明:本讲讲义与幻灯片作者为蒋炎岩,采用 Creative Commons BY-NC 4.0 许可,来源于 https://jyywiki.cn/OS/2026/。视频画面与字幕版权归原作者所有。本书为课堂内容的学习性重构。