I/O 的替代方法 — 轮询、信号驱动、AIO 与 select¶
这一章讲什么: 到目前所有程序的 I/O 都是阻塞式:read 不到数据就睡到地老天荒。交互程序受不了这个——它们要一边 干活一边随时响应键盘。本章把四条出路排开:轮询、信号驱动、 AIO、select 多路复用,每条都回答两个问题:等待的代价谁付, 数据怎么到你手里。
1. 顶层全景:四条路一张表¶
阻塞 I/O:read 不到就睡 → 简单,但等输入时什么也别想干
│
① 非阻塞 + 轮询:read 没数据立刻返 -1 → 主循环里插空查
│ 代价:轮询越密响应越快,CPU 浪费越大(实测)
▼
② 信号驱动 I/O:O_ASYNC + SIGIO → 数据进内核缓冲就推你一下
│ 边沿触发!没读干净不再响;数据仍要自己 read(半异步)
▼
③ POSIX AIO:aio_read(aiocb) → 填好控制块就返回,
完成时通知你,数据已在你地址空间(真异步)
▼
④ select 多路复用:把一堆 fd 交给内核,谁就绪唤醒谁
→ 单线程守多个输入源的标准答案
图说:①是自己问,②是内核叫你,③是内核替你干,④是 一次问一堆。 贯穿本章的对立概念——边沿触发 vs 电平触发 (借自电路):边沿 = 只在状态变化那一刻通知一次;电平 = 只要处于「有数据」状态,什么时候来问都不阻塞1。信号驱动 是边沿触发,select 是电平触发——这个区别决定了它们的代码 结构。
2. 非阻塞 I/O 与轮询的代价¶
fcntl 三步开 O_NONBLOCK(F_GETFL → 或上标志 → F_SETFL)。 语义:read 无数据返 -1(EAGAIN)、write 不能写返 -1;对普通 文件无意义(数据在缓冲里,读必成功,内核直接让你睡2)。
主线例子 nonblock_demo1:循环里睡 delay 微秒→读 1 字符→
报「读到 X」或「第 N 次扑空」。time 实测:delay=100ms 跑
500 轮 real 50 秒;delay=20ms real 10 秒——轮询频率翻倍
五倍,扑空的 read(每次都是一次系统调用)就翻倍五倍;而
系统时间几乎不变:内核在「空 read」上的花费和「有数据的
read」一样多。结论(原书 NOTE):轮询 I/O 浪费 CPU——
用响应性换 CPU,两头不讨好3。还有终端的默认行为挡路:
不敲回车 read 不返回(第 18 章解决)。
3. 信号驱动 I/O:O_ASYNC + SIGIO¶
不是异步 I/O——名字叫 O_ASYNC 但它只是「通知」:数据到
了内核缓冲时内核给你的进程发 SIGIO,你自己还得 read()
把数据搬进地址空间4。所以它是半异步 / 边沿触发:
信号在「空→有数据」那一刻发一次;你没读干净,之后再来
数据也不再发(状态没变)。五步开通:装 SIGIO handler →
fcntl(fd, F_SETOWN, getpid()) 指定信号归谁 →
F_SETFL | O_ASYNC(顺手加 O_NONBLOCK,免得读穿时阻塞)
→ 主程序该干嘛干嘛 → 收到信号后循环 read 到 -1 为止5。
sigio_counter 的关键观察:终端上 SIGIO 恰好在敲回车那一瞬
发——这是终端的属性(第 18 章改)。安全结构:handler 只置
volatile sig_atomic_t 标志;主循环 pause 醒来、先 BLOCK
SIGIO 再慢慢读(防读一半再来信号打断 printf)6。
高输入量场景改用实时信号(SIGRTMIN+, 可排队、可带数据、
可区分 fd)——标准信号不排队,多 fd 同来全丢7。
4. POSIX AIO:真异步¶
aio_read(&aiocb):填好 aiocb 控制块(fd、缓冲区地址、
字节数、文件内绝对偏移、完成通知方式——信号/sigevent)
→ 调用立即返回 → 内核在后台搬数据 → 完成时通知你,
数据已在你地址空间,read 都不用调8。两个要点:① 每次
aio_read 从 aiocbp->aio_offset 指定的绝对位置开始,不沿
OFD 偏移走;② 可区分 aio_write/aio_error/aio_return 查
状态取结果。比信号驱动可移植(多数 Unix 都有),但注意
glibc 的实现不一定真异步(线程池模拟)——性能场合要实测。
项目:aio_cp——双缓冲版 cp 的顺序铁律¶
AIO 版 spl_cp1(第 4 章的 cp)。思路是双缓冲:两块缓冲区, 内核往 buffer[1] 读的同时,程序把 buffer[0] 写出去,交替 进行——但写是同步的,所以「下一条读」必须先于「本次写」发出10:
read into buffer[0];
read into buffer[1] while writing from buffer[0];
read into buffer[0] while writing from buffer[1];
…
通知与主循环的分工照第 3 节的安全结构:aiocb 的完成通知
设为 SIGIO,handler 只置 input_ready 标志;主循环见到标志:
aio_return() 取回字节数 → 换缓冲、aio_offset += num_bytes_read
(绝对偏移,呼应上一段)→ 先发下一条 aio_read → 再同步
write()11。
如果错,会错在: 把 write 排在 aio_read 前面。write 是同步 调用,会占住主循环——等它返回,下一条读请求才发得出去,读和 写从「交叠」退化成「串行」,异步的优势归零。原书原话:这个顺序 不能反11。 实测也有讲究:源文件和目标在同一文件系统上,读写多半被同一个 驱动串行化,看不出差异——要跨文件系统测,缓冲区大小也值得换着 试12。
5. select:一次问一堆¶
多路复用(multiplexing):select(nfds, &readfds, &writefds,
&exceptfds, &timeout)——把三张 fd 集合交给内核,阻塞到
任一 fd 就绪或超时;返回后拿 FD_ISSET 逐个查谁醒了。
fd_set 是位图,四个宏 FD_ZERO/FD_SET/FD_CLR/FD_ISSET
操作它9。select/pselect/poll/epoll 一族的差别(出现顺序
select→poll→epoll,监控集合的规模和效率递增)点到为止。
电平触发的含义在这最清楚:只要 fd 还有数据,select 立刻
返回,不会像信号驱动那样「通知过一次就不再理你」1。
项目:select_demo——一个 select 程序的全部配料¶
光会四宏不算会用 select;这个程序按「类服务器」的样子凑齐了 真实配料:多个不定时来消息的输入源 + 一个不一定可写的写端 + 一个用户键盘13。
- 六个输入源:六个 sender 子进程,每个握一条自己的私有管道, 以不同速率零星写小消息——模拟「一群 worker 偶尔向管理进程 回报」。文件不能当输入源(永远就绪,select 立刻返回,没意义)。
- 可写端怎么造:再造一条管道,fork 一个 receiver 子进程慢慢 读;select 之前先把这条管道灌满。此后每次 select 报可写, 父进程就写一大块;receiver 按自己的节奏读出一点、腾出空间—— 父子像跳舞一样配合13。
- 承重细节(Linux 口径):select 认为管道写端可写 = 管道里 至少有 PIPE_BUF 字节空位;所以父进程每次可写就写一整块 PIPE_BUF,receiver 读小块、按秒级节奏腾位13。这与第 13 章 管道写语义不矛盾——写 n < PIPE_BUF 只要有空位就能写,只是 select 不报它可写。
- 结构:main 只管造管道、fork 六个 sender 和一个 receiver, 把全部监控逻辑交给 manager();manager 里 select 返回后逐个 FD_ISSET:sender 管道可读就读消息;终端可读就 getchar,q 则 给全部 sender 和 receiver 发 SIGINT 收摊14。
select_tut(2) man 页的「select law」捎带一句:尽量不带超时 (带超时的程序难调试)、集合里只放你打算检查结果的 fd、返回后 每个 fd 都查一遍15。
6. 判断/边界/可带走¶
作者的判断: 轮询两头不讨好有实测;信号驱动「半异步」 的定性精确;四条路的取舍讲得干净34;aio_cp 的「先读 后写」顺序铁律和 select_demo 的 PIPE_BUF 口径,是两条出过 血才会写下的经验1113。边界: epoll 只在检索里出现没展开(边缘/电平双模式是它的卖点);AIO 的 glibc 线程池实现、lio_listio 批量都没讲;非阻塞写、connect 的非阻塞语义未展开。可带走的: ① O_NONBLOCK 三步 fcntl;② 轮询频率 vs CPU 的换算直觉;③ O_ASYNC 五步 + 边沿触发的「读干净」纪律;④ 实时信号可排队;⑤ aiocb 五要素 +绝对偏移;⑥ select 四宏 + 电平触发语义;⑦ 双缓冲交错读写 +「先发起下一条读再写」;⑧ select 判管道可写 = PIPE_BUF 空位(Linux 口径)。
7. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 边沿/电平触发 | Chapter 17: Alternative Methods of I/O | text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:259(搜「edge-triggered」) |
| O_NONBLOCK 对普通文件无效 | 同上 | :68(搜「no effect for regular files」) |
| 轮询代价实测 | 同上 | :198(搜「0m50.072s」) · :216(搜「wasteful」) |
| 信号驱动五步法 | 同上 | :310(搜「SIGIO signal」) · :318(搜「SETOWN」) |
| 半异步定性 | 同上 | :303(搜「asynchronous I/O」) |
| SIGIO 回车时机 | 同上 | :469(搜「newline character」) |
| 实时信号 | 同上 | :477(搜「Real-time signals are queued」) |
| AIO 与 aiocb | 同上 | :751(搜「sets up the operation」) · :888(搜「aio_read」) · :907(搜「absolute file offset」) |
| select 与四宏 | 同上 | :1305(搜「4.2BSD」) · :1351(搜「int select」) |
| 多路复用定义 | 同上 | :1286(搜「multiplexing」) |
| aio_cp 双缓冲与顺序铁律 | 同上 | :1067(搜「read into buffer[0];」) · :1110(搜「An AIO-Based Implementation」) · :1137(搜「handler sets the global input_ready flag」) · :1186(搜「You can’t reverse the order」) |
| aio_cp 实测方法 | 同上 | :1235(搜「same filesystem」) |
| select_demo 配料与 PIPE_BUF | 同上 | :1489(搜「server-like program」) · :1491(搜「multiple sources of infrequent」) · :1515(搜「deems the write end of a pipe」) · :1524(搜「write PIPE_BUF bytes at a time」) |
| select_demo 结构 | 同上 | :1555(搜「NSENDERS」) · :1557(搜「offloads most of its work」) · :1741(搜「fills the pipe to capacity」) |
| select law | 同上 | :1480(搜「without a timeout」) |
-
出处:「Chapter 17: Alternative Methods of I/O」第 251–271 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:259,搜「edge-triggered」)。 ↩↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 59–70 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:68,搜「no effect for regular files」)。 ↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 188–233 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:198,搜「0m50.072s」) · :216(搜「wasteful」)。 ↩↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 300–340 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:303,搜「asynchronous I/O」) · :310(搜「SIGIO signal」)。 ↩↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 318–360 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:318,搜「SETOWN」) · :329(搜「O_ASYNC」)。 ↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 386–471 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:469,搜「newline character」)。 ↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 473–483 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:477,搜「Real-time signals are queued」)。 ↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 749–939 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:751,搜「sets up the operation」) · :888(搜「aio_read」) · :907(搜「absolute file offset」)。 ↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 1286–1320 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:1286,搜「multiplexing」) · :1351(搜「int select」)。 ↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 1057–1108 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:1067,搜「read into buffer[0];」) · :1068(搜「read into buffer[1] while writing」) · :1074(搜「Because writing is synchronous」)。 ↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 1110–1210 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:1110,搜「An AIO-Based Implementation」) · :1137(搜「handler sets the global input_ready flag」) · :1186(搜「You can’t reverse the order」) · :1195(搜「aio_return」) · :1200(搜「Advance offset」) · :1203(搜「start synchronous write」)。 ↩↩↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 1233–1246 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:1235,搜「same filesystem」)。 ↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 1488–1535 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:1489,搜「server-like program」) · :1491(搜「multiple sources of infrequent」) · :1494(搜「enter a q to terminate」) · :1515(搜「deems the write end of a pipe」) · :1516(搜「at least PIPE_BUF bytes」) · :1524(搜「write PIPE_BUF bytes at a time」)。 ↩↩↩↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 1537–1745 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:1555,搜「NSENDERS」) · :1557(搜「offloads most of its work」) · :1730(搜「kill(senders[i], SIGINT)」) · :1741(搜「fills the pipe to capacity」) · :1745(搜「complete program is in the book」)。 ↩ -
出处:「Chapter 17: Alternative Methods of I/O」第 1470–1486 段(
text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:1480,搜「without a timeout」)。 ↩