跳转至

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」)

  1. 出处:「Chapter 17: Alternative Methods of I/O」第 251–271 段(text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:259,搜「edge-triggered」)。 ↩↩

  2. 出处:「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」)。 ↩

  3. 出处:「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」)。 ↩↩

  4. 出处:「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」)。 ↩↩

  5. 出处:「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」)。 ↩

  6. 出处:「Chapter 17: Alternative Methods of I/O」第 386–471 段(text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:469,搜「newline character」)。 ↩

  7. 出处:「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」)。 ↩

  8. 出处:「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」)。 ↩

  9. 出处:「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」)。 ↩

  10. 出处:「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」)。 ↩

  11. 出处:「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」)。 ↩↩↩

  12. 出处:「Chapter 17: Alternative Methods of I/O」第 1233–1246 段(text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:1235,搜「same filesystem」)。 ↩

  13. 出处:「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」)。 ↩↩↩↩

  14. 出处:「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」)。 ↩

  15. 出处:「Chapter 17: Alternative Methods of I/O」第 1470–1486 段(text/23-ch17-chapter-17-alternative-methods-of-i-o.txt:1480,搜「without a timeout」)。 ↩