输入/输出 — 设备比内存慢得多:轮询、中断、DMA 与两种映射
输入/输出 — 设备比内存慢得多:轮询、中断、DMA 与两种映射
这一章讲什么: 程序怎么和键盘、磁盘这些「外界」打交道。核心矛盾一句话: 内存和 CPU 节奏同步,而 I/O 设备慢得离谱、脾气各异——2 GHz 的 CPU 等你敲一个键 能跑 2 亿条指令。所以 I/O 的全部技术,都是围绕「怎么等、谁来等、数据走哪条路」 这三个问题给出的不同答案。 主线例子: 16550 UART(串口芯片)的轮询读写——三个函数 init/in/out, C 版与汇编版对照,看 I/O 编程到底在写什么。
1. 时序:I/O 问题比内存问题难在哪
内存:时序由硬件兜底
内存的时序「相对统一,不依赖于外部事件」——不用程序员操心,一条 mov 进、一条 mov 出。 书里补了一刀:即便如此,CPU 还是比内存(尤其 DRAM)快,访问内存本身常常就是 程序最大的 slowdown 来源(第 8 章的缓存就是为这个擦屁股)1。
I/O 设备:慢,而且各自为政
两个量级对比把差距钉死2:
- 键盘:快打 120 词/分钟 = 每秒 10 字符 = 100 毫秒一个字符;2 GHz CPU 在这 期间能执行约 2 亿条指令;而且击键间隔忽长忽短,毫无规律;
- 磁盘:典型 SSD 约 500 MB/s,而 DDR4 内存约 20 GB/s——内存快 40 倍; 机械盘上数据可能刚错过磁头,得等盘转一整圈。
「慢」之外还有「杂」:每个设备的时序特性都不同,访问它们必须明确编程—— 这就是 I/O 编程存在的原因2。
总线:按速度分层
既然设备快慢悬殊,总线也跟着分层:内存和显卡走高速总线,磁盘、键盘走慢速总线。 书里用 2005 年的 PC 讲了这个层级:北桥(内存控制器中枢)经前端总线连 CPU 和 PCI-E, 南桥(I/O 控制器中枢)连 SATA、USB 等慢速设备;2008/2011 年起内存控制器先后进了 CPU 芯片,如今整台手机就是一颗 SoC(片上系统)3。层级化的理由,始终是 「让每条总线的协议匹配所连设备的速度」。
2. CPU 怎么够到设备:设备控制器与两种映射
设备控制器:每个设备门口的中介
CPU 不直接碰设备,而是和设备控制器打交道——键盘控制器检测哪个键被按下、 SHIFT 有没有按住,转成位模式交给 CPU。控制器给 CPU 暴露一组寄存器4:
| 寄存器类型 | 作用 |
|---|---|
| 传输 | 写数据给输出设备 |
| 接收 | 从输入设备读数据 |
| 状态 | 设备现在什么状态(有没有新数据/能不能收新数据) |
| 控制 | 给设备下命令(设置参数、触发动作) |
「读写设备」和「读写内存」的机器动作几乎一样,差别全在时序: 内存随时就绪,设备可能没数据(读)或忙(写)——所以每次读写前要看状态寄存器4。
方案一:端口映射 I/O(隔离 I/O)
x86 有一组独立的 I/O 端口(编号 0x0000–0xffff),地址空间和内存完全分开,
专用指令访问:in 从端口读、out 往端口写(宽度由寄存器名定:al 一字节、
ax 两字节、eax 四字节)5。CPU 发出特殊控制信号区分「这次地址总线上走的是端口号
还是内存地址」——好处是整个 64 位内存空间都留给程序5。
方案二:内存映射 I/O
把设备寄存器映射到内存地址空间的一段,普通的访存指令(mov)就能读写设备。 这要先理解两个背景概念6:
- 虚拟内存:程序跑在一个从 0 开始的连续虚拟地址空间里(gdb 里看到的都是它); x86-64 实际用 48 位,最大 256 TiB——比物理内存(4~16 GiB)大得多;
- 页与 MMU:操作系统把程序切成 4 KB 的页,维护一张页表; CPU 里的内存管理单元(MMU)拿虚拟地址查页表换成物理地址, 页不在内存就触发页面故障异常,操作系统把页从磁盘加载进来。
内存映射 I/O 就是在这个机制上再加一层:让某些虚拟地址不映射到物理内存, 而是映射到 I/O 端口。此后 mov 这些地址 = 读写设备寄存器。 最大优点是可以用 C 写 I/O 函数——in/out 只有汇编够得着6。
三种传输方式:轮询、中断、DMA
数据真跑起来,还有三种模式7:
轮询(polling) 中断驱动 DMA
┌──────┐ ┌──────┐ ┌──────┐
│循环读│ │CPU │ │CPU │
│状态位│ │干别的│ │干别的│
│直到好│ │ ↑ │ │ ↑ │
└──────┘ │设备喊│ │设备直│
占着 CPU │"好了"│ │写内存│
└──────┘ │ ↑ │
需要第 21 章 │完事喊│
的中断机制 └──────┘
大块数据专用书里的一句话点破轮询的死穴:等设备的漫长时间里 CPU 被白白占用—— 单片机(微波炉控制)无所谓,多任务电脑上不可接受7。 DMA(直接内存访问)让磁盘这类高速设备绕过 CPU 直写内存,传完再发中断通知 操作系统——CPU 从头到尾只接两个电话:开工一个、完工一个7。
3. 主线例子:16550 UART 的轮询 I/O
UART 是什么
UART(通用异步收发器)做「并行 ⇄ 串行」转换:把 CPU 给的整字节拆成一位一位 的电压变化发出去,接收方再拼回来。发送时先拉低线路发一个起始位,逐位移出数据位, 最后回到高电平发停止位;双方必须约定同一波特率(每秒位数)。它是理解「设备控制器」 最经典的样本8。
寄存器地图与 DLAB 把戏
16550 UART 有 12 个 8 位寄存器,端口偏移 0–7(表 20-1)。一个妙处:同一端口偏移 对应不同寄存器——读端口 0 是接收缓冲(RBR),写端口 0 是发送保持(THR); 线路控制寄存器(LCR)的第 7 位(DLAB)为 1 时,端口 0/1 又变成波特率除数锁存器的 低/高字节。「先设 DLAB 写波特率,再清 DLAB 进通信模式」是初始化序列里的固定仪式9。
我们要用的三个函数(书里明说:代码不完整、不能真跑,只为演示概念)10:
UART_init:写控制寄存器,设好 38400 波特、8 数据位、1 停止位、无校验、关中断关 FIFO;UART_in:轮询线路状态寄存器(LSR)的接收就绪位(RxRDY),等到了就读 RBR;UART_out:轮询发送就绪位(TxRDY),能收了就把字符写进 THR。
C 版与汇编版的对照:三件事值得学
其一,内存映射 I/O 用 C 写就是这么朴素:*(port+LSR) & RxRDY 读状态、
*(port+RBR) 读数据——和访问数组毫无区别,这正是内存映射的价值11。
其二,看编译器的输出能学到它的毛病。 清单 20-4 里 gcc 为 UART_in 生成了
and …; test eax, eax 两行——and 已经设好标志,test 纯属多余;
初始化函数里每个寄存器地址都用 add 现算一遍。作者的方法论值得原样收下:
设备处理程序先用 C 写、生成汇编、挑毛病,再决定是否手写——理由是中断随时会
插入处理程序,里面的代码越短越好12。
其三,汇编版的修复:基址指针 + 偏移量直接寻址(byte ptr LSR[rdi]),
一次访存完成「读状态 + 测位」(and byte ptr LSR[rdi], RxRDY 直接设标志接 jne),
轮询循环瘦到两行13。
端口映射版的插曲:AT&T 语法撞车
改用 in/out 的端口映射版,Linux 的 sys/io.h 提供了 inb/outb 包装。但作者的
-masm=intel 编译直接报错——翻到 -S 输出才发现:io.h 里的宏是写死
AT&T 语法的(inb dx,al),和 Intel 语法(in al, dx)操作数顺序相反14。
一个小事故讲清两件事:内核 C 头文件里嵌的汇编都是 AT&T 传统;以及第 10 章
AT&T 语法的伏笔在这里接住。 用 io.h 就放弃 -masm=intel,想保留 Intel 语法
就自己写内联汇编。
4. 作者的判断与证据
| 说法 | 性质 |
|---|---|
| I/O 比内存慢得多、时序各异,必须编程处理 | 键盘/SSD 的量化对比支撑2 |
| 总线层级化以匹配设备速度;北桥/南桥→SoC | 硬件演进事实,2005 配置图为教学模型3 |
| 轮询占 CPU、中断与 DMA 更优 | 教科书共识;轮询局限书中明写7 |
| 内存映射 I/O 可用高级语言,端口映射必须用汇编 | 机制事实(编译器不生成 in/out)6 |
| 设备处理程序要短、C 写完看汇编挑毛病 | 作者的个人方法论,有实证(and/test 冗余)12 |
| UART 示例代码「不运行在任何已知设备上」 | 作者的明确免责声明10 |
判断(我们的,不是书里的): 本章把「等待」这个隐性成本摆上了台面: 轮询的本质是把 CPU 时间换成编程简单,中断的本质是把编程复杂度换成 CPU 效率。 这个权衡在软件界反复出现(阻塞 vs 非阻塞、同步 vs 回调),I/O 是它的硬件原形。 理解轮询-中断-DMA 的谱系,读现代异步编程模型时会一直看到它的影子。 如果错,会错在: 现代 OS 里轮询并未消失——高速网络(NVMe、内核旁路)恰恰 靠「轮询专用核」复活了,因为中断的开销在微秒级以下时反而更大。谱系的两端 会随硬件成本移动,别把「轮询低级」当定论。
5. 边界与局限
- UART 代码是教学草图,作者明言不能运行;真实驱动要处理错误、超时、并发, 复杂度高几个量级10。
- 中断与异常的机制本身推到第 21 章,本章只用到名词。
- 虚拟内存/页表/MMU 只有概念级介绍——分页机制、TLB、页面置换算法都不在本书范围。
- DMA 只讲工作方式,DMA 控制器与缓存一致性(设备直写内存 vs CPU 缓存)的问题没展开。
- 没有网络 I/O、USB 协议栈等现代设备的任何内容。
6. 可带走的
- 内存时序统一(硬件兜底),I/O 时序各异(必须编程)——这是 I/O 章的第一原理;
- 设备控制器暴露四类寄存器:传输、接收、状态、控制;读写前先看状态;
- 端口映射 I/O:独立地址空间 + in/out,留给汇编;内存映射 I/O:借虚拟内存机制, C 也能写;
- 虚拟内存 = 程序的连续假地址;MMU + 页表翻译成物理地址;缺页触发异常;
- 三种传输:轮询(简单费 CPU)、中断(设备叫人)、DMA(设备自己搬、完事叫人);
- 总线按速度分层(北桥/南桥→SoC 集成)是为了匹配设备时序;
- UART:起止式串行通信;同一端口读写不同寄存器;DLAB 切换波特率锁存器;
- 设备处理程序要短——中断会插进来,作者套路:C 写完 → 看汇编 → 挑毛病;
- and 已设标志就别再接 test;寄存器地址用「基址+偏移」一次算;
- io.h 的 inb/outb 宏是 AT&T 语法,-masm=intel 与之不兼容。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 内存时序统一、CPU 快于内存 | 第20 章 输入/输出 | text/26-ch20.txt:29(搜「时序相对统一」) · text/26-ch20.txt:35(搜「拖慢程序」) |
| 键盘/SSD 的时序量级对比 | 第20 章 输入/输出 | text/26-ch20.txt:41(搜「100 毫秒」) · text/26-ch20.txt:44(搜「500 MB/s」) |
| 总线层级与北桥南桥 | 第20 章 输入/输出 | text/26-ch20.txt:51(搜「总线时序」) · text/26-ch20.txt:65(搜「北桥」) · text/26-ch20.txt:76(搜「SoC」) |
| 设备控制器与四类寄存器 | 第20 章 输入/输出 | text/26-ch20.txt:81(搜「设备控制器」) · text/26-ch20.txt:86(搜「传输」) |
| in / out 指令卡 | 第20 章 输入/输出 | text/26-ch20.txt:112(搜「in —— 读取端口」) · text/26-ch20.txt:118(搜「out —— 写入端口」) |
| 虚拟内存、页、MMU | 第20 章 输入/输出 | text/26-ch20.txt:132(搜「虚拟内存」) · text/26-ch20.txt:145(搜「页面」) · text/26-ch20.txt:156(搜「内存映射单元」) |
| 轮询/中断/DMA | 第20 章 输入/输出 | text/26-ch20.txt:177(搜「轮询」) · text/26-ch20.txt:187(搜「中断驱动」) · text/26-ch20.txt:194(搜「直接内存访问」) |
| UART 原理与起始停止位 | 第20 章 输入/输出 | text/26-ch20.txt:208(搜「UART」) · text/26-ch20.txt:215(搜「起始位」) |
| 16550 寄存器表与 DLAB | 第20 章 输入/输出 | text/26-ch20.txt:227(搜「16550」) · text/26-ch20.txt:251(搜「DLAB」) |
| C 版 UART 三函数 | 第20 章 输入/输出 | text/26-ch20.txt:271(搜「清单 20-1」) · text/26-ch20.txt:337(搜「清单 20-3」) |
| 编译器版的低效与方法论 | 第20 章 输入/输出 | text/26-ch20.txt:392(搜「清单 20-4」) · text/26-ch20.txt:17(搜「20.1 时序考量 」) · text/26-ch20.txt:489(搜「先用 C 语言」) |
| 汇编版 UART 函数 | 第20 章 输入/输出 | text/26-ch20.txt:532(搜「清单 20-6」) · text/26-ch20.txt:577(搜「and byte ptr LSR」) |
| 端口映射版与 AT&T 撞车 | 第20 章 输入/输出 | text/26-ch20.txt:648(搜「端口映射 I/O」) · text/26-ch20.txt:711(搜「operand type mismatch」) · text/26-ch20.txt:783(搜「AT&T 语法」) |
Footnotes
-
出处:「第20 章 输入/输出」第 27–37 段(
text/26-ch20.txt:29,搜「时序相对统一」;text/26-ch20.txt:31,搜「5~10 倍」;text/26-ch20.txt:35,搜「拖慢程序」)。 ↩ -
出处:「第20 章 输入/输出」第 38–49 段(
text/26-ch20.txt:41,搜「100 毫秒」;text/26-ch20.txt:42,搜「2 亿条指令」;text/26-ch20.txt:44,搜「500 MB/s」;text/26-ch20.txt:46,搜「千差万别」)。 ↩ ↩2 ↩3 -
出处:「第20 章 输入/输出」第 51–79 段(
text/26-ch20.txt:59,搜「层级化总线」;text/26-ch20.txt:65,搜「北桥」;text/26-ch20.txt:70,搜「南桥」;text/26-ch20.txt:72,搜「2008 年」;text/26-ch20.txt:76,搜「片上系统」)。 ↩ ↩2 -
出处:「第20 章 输入/输出」第 81–102 段(
text/26-ch20.txt:81,搜「设备控制器」;text/26-ch20.txt:86,搜「传输」;状态寄存器的角色见第 94–98 段,text/26-ch20.txt:94,搜「状态寄存器」)。 ↩ ↩2 -
出处:「第20 章 输入/输出」第 103–126 段(
text/26-ch20.txt:108,搜「0x0000 到 0xffff」;text/26-ch20.txt:111,搜「in 和 out」;text/26-ch20.txt:124,搜「当使用 in 和 out」)。 ↩ ↩2 -
出处:「第20 章 输入/输出」第 128–165 段(虚拟内存与页,
text/26-ch20.txt:132,搜「虚拟内存」;text/26-ch20.txt:145,搜「页面」;text/26-ch20.txt:156,搜「内存映射单元」;内存映射 I/O 见第 161–164 段,text/26-ch20.txt:161,搜「映射到 I/O 端口」)。 ↩ ↩2 ↩3 -
出处:「第20 章 输入/输出」第 172–201 段(三种传输,
text/26-ch20.txt:182,搜「轮询式 I/O」;text/26-ch20.txt:185,搜「不可接受」;text/26-ch20.txt:190,搜「中断控制器」;text/26-ch20.txt:196,搜「直接内存访问」;text/26-ch20.txt:199,搜「发送中断」)。 ↩ ↩2 ↩3 ↩4 -
出处:「第20 章 输入/输出」第 203–226 段(UART 原理,
text/26-ch20.txt:207,搜「通用异步」;text/26-ch20.txt:215,搜「起始位」;text/26-ch20.txt:219,搜「停止位」)。 ↩ -
出处:「第20 章 输入/输出」第 227–257 段(寄存器表与 DLAB,
text/26-ch20.txt:231,搜「RBR」;text/26-ch20.txt:251,搜「DLAB」;读与写同一端口见第 231–232 段,text/26-ch20.txt:230,搜「取决于程序」)。 ↩ -
出处:「第20 章 输入/输出」第 258–265 段(免责声明,
text/26-ch20.txt:264,搜「不完整」)与第 266–386 段(三函数 C 版,text/26-ch20.txt:349,搜「UART_init」;text/26-ch20.txt:367,搜「while」)。 ↩ ↩2 ↩3 -
出处:「第20 章 输入/输出」第 385–387 段(
text/26-ch20.txt:163,搜「高级语言」)。 ↩ -
出处:「第20 章 输入/输出」第 479–492 段(低效分析,
text/26-ch20.txt:17,搜「20.1 时序考量 」;方法论见第 488–490 段,text/26-ch20.txt:488,搜「无法知道外部设备的时序」;text/26-ch20.txt:489,搜「先用 C 语言」)。 ↩ ↩2 -
出处:「第20 章 输入/输出」第 494–604 段(汇编版,
text/26-ch20.txt:553,搜「byte ptr IER[rdi]」;text/26-ch20.txt:577,搜「and byte ptr LSR」;text/26-ch20.txt:600,搜「效率较低」)。 ↩ -
出处:「第20 章 输入/输出」第 648–797 段(端口映射版,
text/26-ch20.txt:670,搜「outb」;报错见第 707–715 段,text/26-ch20.txt:711,搜「operand type mismatch」;AT&T 宏见第 783–797 段,text/26-ch20.txt:783,搜「AT&T 语法」)。 ↩