第 63 章 备选 I/O 模型 (Alternative I/O Models)

      +

      核心结论

      • 单 fd 阻塞 I/O 模型不足以应对多客户端监控:(1) 非阻塞 polling 浪费 CPU;(2) 多线程/进程开销过大;(3) 阻塞单 fd 无法同时等 stdin 和 socket。备选三方案解决「同时监控多 fd、何时 ready」。

      • I/O 多路复用 (select / poll)select(int nfds, fd_set *readfds/writefds/exceptfds, timeval *timeout)poll(struct pollfd[], nfds, int timeout);水平触发;可移植;FD_SETSIZE=1024 是 select 的硬上限;fd 集合每次调用前需重新初始化。

      • 信号驱动 I/O (signal-driven / SIGIO):fcntl F_SETOWN 设 owner、O_ASYNC 启用、edge-triggered;fd 可读即向进程发 SIGIO(Linux 也支持 F_SETSIG 改 realtime signal + siginfo 提供 fd 与事件);handler 只设 flag,主循环 poll 自旋 + 把数据读空到 EAGAIN。

      • Linux 专用 epoll APIepoll_create 创建 fd、epoll_ctl 注册 fd 与事件、epoll_wait 等事件;既支持水平触发(默认)也支持 edge-triggered;事件就绪由内核直接 push 给 userspace;fd 数量 O(1) 不增长 → C10K 主流方案。

      • Level-triggered vs Edge-triggered:LT = 状态通知(fd 没读完下次还会通知);ET = 状态变化通知(fd 首次就绪时一次通知,消耗到 EAGAIN);ET 必须配 O_NONBLOCK + 读到 EAGAIN。

      • pselect 与 self-pipe trickpselect 处理「等待信号 + 等 fd」原子化;self-pipe trick 把信号 handler 写 pipe 字节,让主循环也用 select 监控信号——是处理「信号 + 多 fd」的常用模式。

      本章主旨

      本章是单 fd 阻塞模型之外的三大 I/O 模型——select/poll、signal-driven I/O、epoll。读者需建立:(1) 三个模型解决的问题与性能 trade-off;(2) select vs poll 的 API 异同;(3) LT vs ET 的语义差;(4) epoll 在 Linux 高并发服务中的核心地位(NGINX / Envoy / Redis 等);(5) 处理「信号 + 多 fd」时 pselect 或 self-pipe trick。

      一、核心概念

      本章围绕 6 个核心概念:阻塞 I/O vs 备选模型、select/poll 的 fd_set/pollfd、signal-driven I/O (O_ASYNC + SIGIO)、epoll 三大调用、LT vs ET、pselect 与 self-pipe trick。

      概念 定义 + 重要性 实现提示

      三种备选 I/O 模型对比

      (1) select/poll:水平触发 + 系统调用传全部 fd 集;(2) signal-driven:edge-triggered SIGIO,O_ASYNC 启用;(3) epoll:用户态 ↔ 内核态共享兴趣列表,LT/ET 皆可。Linux C10K 默认 epoll,BSD/Solaris 用 kqueue / /dev/poll。

      §63.1;select/poll 优势:可移植;缺点:随 fd 数线性恶化。epoll 优势:O(1) 监控大量 fd;缺点:仅 Linux。

      select 系统调用

      select(nfds, readfds, writefds, exceptfds, timeout);fd_set 三类(read/write/except);FD_SETSIZE=1024 默认上限;timeout 为 NULL = 永久阻塞;tv_sec=0/tv_usec=0 = poll;返回正值是就绪 fd 个数(多重集合重复计数)。

      §63.2.1;FD_ZERO/FD_SET/FD_CLR/FD_ISSET 4 个宏;值-结果,每次调用前重新初始化;nfds = max_fd + 1;exceptfds = OOB + pty packet mode 状态变化。

      poll 系统调用

      poll(struct pollfd fds[], nfds_t nfds, int timeout);pollfd.fd 指定 fd、events 注册事件位掩码、revents 返回位掩码;timeout 单位 ms;POLLIN/POLLOUT/POLLPRI/POLLHUP/POLLERR/POLLNVAL/POLLRDHUP 为常用 bit;revents = POLLNVAL 表 fd 已关。

      §63.2.2;POLLIN/POLLRDNORM 等价;POLLRDBAND Linux 未用;POLLRDHUP 2.6.17+ 需 _GNU_SOURCE;跨实现行为有差异。

      Signal-driven I/O / O_ASYNC

      fcntl(F_SETOWN, pid) 设 owner;fcntl(F_SETFL, flags | O_ASYNC | O_NONBLOCK) 启用;事件来临时发 SIGIO;Linux 扩展:F_SETSIG 改 realtime signal + SA_SIGINFO 让 siginfo 给 si_fd/si_band;handler 通常只 set flag,主循环读到 EAGAIN。

      §63.3;默认 SIGIO 是普通信号,多事件会丢失 → 改 realtime signal 排队;F_SETOWN 可指定进程组;demo_sigio.c 是经典示例。

      epoll API (LT/ET)

      epoll_create(size) 建 epoll fd;epoll_ctl(epfd, op, fd, &event) 注册 / 修改 / 删除(EPOLL_CTL_ADD/MOD/DEL);event.events = EPOLLIN/OUT/ERR/ET/LT/HUP/ONESHOT/RDHUP/PRI;epoll_wait(epfd, events, maxevents, timeout) 拉 ready 列表;内核维护兴趣列表,O(1) 注册。

      §63.4;ET 模式必须 O_NONBLOCK;events 结构内核 ↔ 用户拷贝,避免每次重传全部 fd;EPOLLEXCLUSIVE 用于多进程 listen fd 防 thundering herd;EPOLLONESHOT EPOLLET 防多线程共享 fd 问题。

      self-pipe trick 与 pselect

      在 async-signal-safe handler 中 write(pipefd[1], &ch, 1) 写一字节;主循环 select 监控 pipefd[0],可读则处理信号;pselect 是 select 的原子版,可临时换 mask 同时响应 signal;

      §63.5;alarm/timeout 配合它实现「无限等待 + 定时唤醒」;Common pattern 见 libev / libuv。

      二、详细笔记

      63.1 三种备选 I/O 模型概述

      What:除「进程单 fd 阻塞 I/O」外的三种方式——I/O 多路复用(select/poll)、signal-driven I/O、Linux epoll。

      Why:高并发服务器或同时等输入+网络的应用不能用单 fd blocking。fork-per-client 资源消耗大;polling 浪费 CPU。

      How

      维度 select/poll signal-driven I/O epoll

      触发

      水平(state)

      边沿(event)

      LT / ET 均可(默认 LT)

      性能

      随 fd 数线性恶化

      与 event 数相关

      O(1)(事件)

      可移植性

      SUSv3,跨 UNIX

      历史接口 + Linux 扩展

      Linux-only

      内核记住 fd 列表

      ❌ 每次重传

      适合负载

      fd < 1000

      fd 上千 + sparse

      C10K / C10M

      When:(1) 移植要求高、fd 少 → select/poll;(2) 高并发 Linux → epoll;(3) 信号驱动 I/O 较罕见,需配合 realtime signal + siginfo 才有完整 fd 信息。

      Example:现代 NGINX 用「preforked 多 worker + 单进程 epoll + worker 间共享 listen fd(EPOLLEXCLUSIVE)」。

      63.1.1 Level-Triggered vs Edge-Triggered

      What:LT(level)— fd 仍处 ready 状态,每次调用 select/poll/epoll_wait 都会再次通知;可不读空。ET(edge)— 仅在 ready 状态变化时通知;通知一次后必须读到 EAGAIN,否则可能漏数据。

      Why:ET 比 LT 更省系统调用(OS 只在状态变化时唤醒),但编程要求严——必须 O_NONBLOCK + 循环 read/write 到 EAGAIN。

      How

      模型 LT / ET?

      select

      LT only

      poll

      LT only

      signal-driven I/O

      ET only

      epoll

      LT(默认)/ ET(EPOLLET)

      When:(1) 调试、fuzzing → LT 简单;(2) 高并发减唤醒次数 → ET(但配合 measure 防饿死)。

      Example:epoll ET 下 read 必须循环到 read() == -1 && errno == EAGAIN;不然下次不再通知。

      63.2.1 select 系统调用

      Whatint select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

      Why:可移植、无须新 API;缺点 FD_SETSIZE=1024,密集 fd 集仍有 O(N) 内核扫描。

      How

      #include <sys/time.h>
      #include <sys/select.h>
      int select(int nfds, fd_set *readfds, fd_set *writefds,
                 fd_set *exceptfds, struct timeval *timeout);
      /* FD_ZERO/FD_SET/FD_CLR/FD_ISSET 4 个宏 */
      struct timeval { time_t tv_sec; suseconds_t tv_usec; };

      返回值:-1 = 错误(EBADF / EINTR);0 = 超时;正数 = ready fd 数(同 fd 在多集合中重数)。

      // 摘自《The Linux Programming Interface》 第 63 章 Listing 63-1 (片段)
      fd_set readfds, writefds;
      FD_ZERO(&readfds);
      FD_ZERO(&writefds);
      int nfds = 0;
      for (j = 2; j < argc; j++) {
          int fd; char rw[10];
          sscanf(argv[j], "%d%2[rw]", &fd, rw);
          if (fd >= FD_SETSIZE) errExit("limit");
          if (fd >= nfds) nfds = fd + 1;
          if (strchr(rw, 'r')) FD_SET(fd, &readfds);
          if (strchr(rw, 'w')) FD_SET(fd, &writefds);
      }
      int ready = select(nfds, &readfds, &writefds, NULL, NULL);
      for (int fd = 0; fd < nfds; fd++)
          printf("%d: %s%s\n", fd, FD_ISSET(fd, &readfds) ? "r" : "",
                                FD_ISSET(fd, &writefds) ? "w" : "");

      When:(1) 写 portable C 在 UNIX;(2) fd 少(< 100);(3) 教学。

      Example:TLPI t_select 演示:监控 fd 0 上输入有超时 read = 1, 0:r,监控 fd 1 上输出立即 ready 1:w

      63.2.2 poll 系统调用

      Whatint poll(struct pollfd fds[], nfds_t nfds, int timeout);;数组元素 {fd, events, revents};events 为注册 bit、revents 为返回值。

      Why:select 之后的「更现代」API;pollfd 数组长度理论上无上限,灵活;fd 重复无需重新初始化(events 与 revents 分开)。

      How

      #include <poll.h>
      struct pollfd {
          int   fd;
          short events;       /* POLLIN / POLLOUT / POLLPRI / POLLRDHUP */
          short revents;      /* 与 events 同样的 bit + POLLERR/POLLHUP/POLLNVAL */
      };
      int poll(struct pollfd fds[], nfds_t nfds, int timeout);
      /* timeout: -1 永远;0 poll;>0 ms */

      关键 bit:

      Bit 含义

      POLLIN / POLLRDNORM

      普通数据可读

      POLLPRI

      高优先级数据可读(TCP OOB)

      POLLRDHUP

      对端关闭写半边(需 _GNU_SOURCE,2.6.17+)

      POLLOUT / POLLWRNORM

      数据可写

      POLLERR

      错误(仅返在 revents)

      POLLHUP

      hangup(仅返在 revents)

      POLLNVAL

      fd 无效(关闭、revents 专用)

      When:(1) 想明确区分多 fd 状态(不重数)、(2) fd > FD_SETSIZE、(3) 跨 UNIX 但仍是现代 API。

      Example:TLPI poll_pipes.c 创建 10 个 pipe,写 3 个随机 pipe,poll 等所有读端 ready。

      63.2.3 fd 何时算 ready

      What:SUSv3 说「readfds 中 fd 在 read() 时不会阻塞」即 ready;select/poll 只是「能否不阻塞」不保证「一定读到数据」。

      Why:理解「何时算 ready」避免误读;要区分 fd 类型(regular file / terminal / pipe / socket)行为差异。

      How(关键摘要):

      fd 类型 select r poll revents

      regular file

      始终 ready(read 立即返)

      始终 POLLIN/POLLOUT

      terminal input ready

      r

      POLLIN

      pipe read end, data + write end open

      r

      POLLIN

      pipe read end, write end closed

      r

      POLLIN

      POLLHUP

      pipe write end, space + read end open

      w

      POLLOUT

      socket listen, 新连接

      r

      POLLIN

      socket data ready

      r

      POLLIN

      socket peer close (FIN)

      rw

      POLLIN

      POLLOUT

      POLLRDHUP

      socket OOB data

      x

      POLLPRI

      When:(1) pipe read 端 select 检测 close → POLLHUP;(2) 写一个 fd 试图 always write——select 可能 short write,因 socket send buffer 满。

      Example:TLPI 表 63-3 ~ 63-6 给出全文对照。

      63.2.4 select vs poll 对比

      What:三个层次——实现、API、可移植性、性能。

      维度 select poll

      上限

      FD_SETSIZE=1024(glibc)

      理论无上限

      fd 集合 vs 数组

      fd_set 位图(重初始化)

      pollfd 数组(events/revents 分开)

      timeout 精度

      μs

      ms

      关闭 fd 错误返回

      -1, EBADF

      revents = POLLNVAL 标识哪个

      性能

      稀疏 fd 集差(且 2.4 中较明显)

      密集 fd 集也 O(N);2.6 优化收敛差距

      可移植性

      SUSv3

      SUSv3

      实现

      都用内核内部同一套 poll routine

      When:(1) FD_SETSIZE 够用 + 需要 μs 超时 → select;(2) fd 多、需要明确哪个 fd 关闭 → poll;(3) Linux fd 大量 → epoll。

      63.3 Signal-Driven I/O / SIGIO

      What:让内核在 fd 可读/可写时主动发信号给进程,进程不用 poll/wait。Linux 用 O_ASYNC;BSD 用 FASYNC(POSIX.1g 弃)。

      Why:(1) 完全异步;(2) 多 fd 时性能优于 select/poll;(3) 缺点:信号编程易错(handler 限制、非 async-signal-safe)。

      How:(4 步开启)

      /* 1. 装 SIGIO handler(必须先于 O_ASYNC) */
      sigaction(SIGIO, &sa, NULL);
      
      /* 2. F_SETOWN 设 owner(pid 或 -pgid) */
      fcntl(fd, F_SETOWN, getpid());
      
      /* 3. 加 O_ASYNC + O_NONBLOCK */
      int flags = fcntl(fd, F_GETFL);
      fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
      
      /* 4. handler 仅设 flag,主循环读空到 EAGAIN */
      static volatile sig_atomic_t gotSigio = 0;
      static void sigioHandler(int sig) { gotSigio = 1; }
      
      for (;;) {
          if (gotSigio) {
              while (read(fd, &ch, 1) > 0)
                  /* process ch */ ;
              gotSigio = 0;
          }
          /* 干别的活 */
      }

      Linux 增强:用 realtime signal (SIGRTMIN+n) 替代 SIGIO——多事件可排队:

      fcntl(fd, F_SETSIG, SIGRTMIN+1);
      sa.sa_flags = SA_SIGINFO;
      sa.sa_sigaction = sigioHandler;   /* (int sig, siginfo_t *si, void *ucontext) */
      /* si->si_fd = 事件 fd;si->si_band = 类似 revents bit */

      When:(1) 高并发 + 单进程 + 不希望 select loop——但 epoll 几乎已取代;(2) 教学价值大于实用价值。

      Example:TLPI demo_sigio.c 在终端上启用 O_ASYNC,进入 cbreak 模式,主循环打 cnt 计数,按 x / # 立即响应。

      63.4 epoll API

      What:Linux 2.6 引入的高性能 I/O 事件通知 API;用户态 ↔ 内核态共享一个「兴趣列表」,内核主动通知 ready 事件。

      Why:(1) 解决 select/poll O(N) 性能;(2) fd 集合不每次重传;(3) 支持 ET;(4) 是 NGINX / Envoy / Redis 等高并发服务的事实标准。

      How

      调用 语义

      epoll_create

      <sys/epoll.h>

      建 epoll fd(旧版带 size 提示,新版可填任意正数)

      epoll_create1

      同上

      建 + flags(EPOLL_CLOEXEC)

      epoll_ctl

      同上

      epfd 上 op(ADD/MOD/DEL) fd + event 兴趣

      epoll_wait

      同上

      等 ready 事件,events[] 数组填 revents;maxevents 单次最大数

      epoll_pwait

      同上

      同上 + 临时 sigmask(类似 pselect)

      epoll_event 结构:

      struct epoll_event {
          uint32_t events;    /* EPOLLIN/OUT/PRI/ERR/HUP/ET/LT/ONESHOT/RDHUP/EXCLUSIVE/WAKEUP/... */
          epoll_data_t data;  /* union { int fd; uint32_t u32; uint64_t u64; void *ptr; } */
      };

      EPOLLIN/OUT/HUP/ERR 是「事件」;EPOLLET = edge-triggered;EPOLLONESHOT = 一次性;EPOLLEXCLUSIVE = 多 worker listen 防 thundering herd。

      典型 server(ET + nonblocking):

      int epfd = epoll_create1(0);
      struct epoll_event ev;
      ev.events = EPOLLIN | EPOLLET;
      ev.data.fd = listen_fd;
      epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
      for (;;) {
          int n = epoll_wait(epfd, events, MAX, -1);
          for (int i = 0; i < n; i++) {
              if (events[i].data.fd == listen_fd) {
                  /* accept 所有待连接(ET 模式) */
                  for (;;) {
                      int cfd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK);
                      if (cfd < 0) break;
                      epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, ...);
                  }
              } else if (events[i].events & EPOLLIN) {
                  int fd = events[i].data.fd;
                  for (;;) {
                      ssize_t nr = read(fd, buf, BUF);
                      if (nr < 0 && errno == EAGAIN) break;
                      if (nr <= 0) { close(fd); break; }
                      /* process buf */
                  }
              }
          }
      }

      When:(1) C10K+ 服务首选;(2) 多线程可结合 EPOLLEXCLUSIVE / EPOLLONESHOT 防 race;(3) signal 配合 epoll_pwait。

      Exampleaccept4(fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC) 是 Linux 3.9+ 一步完成 accept + 设 nonblocking + close-on-exec。

      63.4.5 epoll vs select/poll 性能

      What:select/poll 在内核中需要遍历所有 fd 列表;epoll 在 fd 集大 + active 数小时显著优于 select/poll。

      Why:理解「为什么 epoll 适合 C10K」——O(1) 关注列表 + O(K) ready 数(K = 本轮 ready)。

      How:(benchmark 数字 TLPI 给出):

      监控 fd 数 select/poll epoll

      10-100

      快(或近)

      略增 setup 开销

      100-1000

      慢(线性)

      O(1)

      1000-10000

      不可用(实际也接近 limit)

      高效

      10000+

      不可用

      高效

      When:(1) fd 上千、活跃 fd 比例小 → epoll;(2) 仅有几十 fd → select/poll 足够;(3) 监控数千 inactive fd 上偶发事件 → epoll。

      Example:QT/GTK GUI 程序需要监控 stdio + X server socket + filesystem fd —— 数量小,select 完全够;Nginx 处理 10k 长连接 → epoll 必备。

      63.4.6 Edge-triggered 模式的注意事项

      What:ET 要求 (1) O_NONBLOCK 必须开启;(2) 接事件后循环 read/write 到 EAGAIN;(3) 防止一个 fd 饿死其他 fd。

      Why:(1) ET 的语义就是「fd 就绪时只通知一次」,漏读则数据可能驻留内核直到下次 close;(2) 长 read 阻塞会让其他 fd 等不到机会。

      How(epoll ET 服务器关键写法):

      1. accept 后立即 accept4(SOCK_NONBLOCK)

      2. 注册 EPOLLIN | EPOLLET;

      3. 事件到达后 while (read() > 0) 直到 EAGAIN。

      When:(1) 严格避免一 fd 饿死其他 fd 时 → ET;(2) 写 fd 时担心写阻塞 → 必须 O_NONBLOCK;(3) 单线程 ET server + 公平 schedule。

      Example:写 epoll ET echo server:每 receive event 必须一次读到 EAGAIN,否则 echo 漏数据。

      63.5 pselect 与 self-pipe trick

      What:(1) pselectselect 之前原子地设置 sigmask;(2) self-pipe trick:signal handler 写 1 字节到 pipe,主循环用 select 监控 read 端——将「信号 + 多 fd」统一到 select。

      Why:经典问题——「如何在主循环既能 select 又能处理信号」;原子地换 mask 可避免 EINTR 时 mask 没改导致的竞争。

      How

      #include <signal.h>
      #include <sys/select.h>
      int pselect(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, const struct timespec *timeout,
                  const sigset_t *sigmask);
      /* sigmask 临时代替本进程 mask;select 返回前恢复 */

      self-pipe trick 5 步: . 创建 pipe(fd[2]); . 设 handler sigio_handler,handler 内 write(fd[1], "x", 1)(async-signal-safe); . 把 fd[0] 加到 select 监控集合; . 主循环 select 阻塞到 fd[0] ready → 读 1 字节 → 处理信号; . 用单独的 sigprocmask 阻塞信号避免 race(通常在 select 前临时阻塞、handler 写完 pipe 后原子化)。

      When:(1) 想阻塞等 fd ready + 同时响应信号 → pselect 或 self-pipe;(2) alarm/timeout 信号要唤醒 select → self-pipe。

      Example:libev / libuv 内部实现即 self-pipe + epoll 或 kqueue。

      三、关键图表

      关键系统调用一览
      函数 用途

      select

      <sys/select.h>

      多 fd 可移植多路复用

      poll

      <poll.h>

      多 fd 多路复用(数组 + revents)

      epoll_create / epoll_create1

      <sys/epoll.h>

      建 epoll 实例

      epoll_ctl

      <sys/epoll.h>

      注册 / 修改 / 删除兴趣

      epoll_wait / epoll_pwait

      <sys/epoll.h>

      等 ready 事件

      fcntl F_SETOWN / F_SETSIG

      <fcntl.h>

      O_ASYNC 信号 owner 与 signal 选择

      F_GETOWN / F_GETSIG

      同上

      反向读取

      accept4

      <sys/socket.h>

      accept + 设 nonblocking + CLOEXEC

      pselect

      <sys/select.h>

      select + 临时 sigmask

      poll / epoll 事件 bit 速查
      事件 bit poll epoll

      可读

      POLLIN

      EPOLLIN

      可写

      POLLOUT

      EPOLLOUT

      高优先级

      POLLPRI

      EPOLLPRI

      hangup

      POLLHUP

      EPOLLHUP

      错误

      POLLERR

      EPOLLERR

      peer close 写半

      POLLRDHUP(_GNU_SOURCE)

      EPOLLRDHUP

      一次性

      (无)

      EPOLLONESHOT

      边沿触发

      (无)

      EPOLLET

      独占(多进程 listen)

      (无)

      EPOLLEXCLUSIVE

      唤醒 epoll_wait 中

      (无)

      EPOLLWAKEUP

      四、思维导图

      mindmap
        root((第 63 章 备选 I/O))
          三种模型
            select poll 多路复用
            signal driven SIGIO
            epoll LT ET
          阻塞模型局限
            单 fd 阻塞
            polling 费 CPU
            多进程开销
          select
            fd_set 三类
            FD_SETSIZE 1024
            timeout NULL 无限
            重初始化
          poll
            pollfd 数组
            events revents 分开
            POLLIN POLLOUT HUP
            POLLRDHUP
          fd 何时 ready
            regular file
            pipe socket
            OOB 数据
            peer close
          signal driven
            O_ASYNC F_SETOWN
            SIGIO handler
            F_SETSIG realtime
            siginfo si_fd si_band
          epoll
            epoll_create
            epoll_ctl add mod del
            epoll_wait
            EPOLLIN OUT HUP ET
            EPOLLONESHOT EXCLUSIVE
          LT vs ET
            状态 vs 状态变化
            O_NONBLOCK 必开
            循环到 EAGAIN
            防一个 fd 饿死
          pselect self pipe
            原子 sigmask
            pipe trick 写 1 byte
            libev libuv 实例

      五、重点与易错点

      1. 三种模型的核心区别是「内核是否记住 fd 列表」——select/poll 每次重传;signal-driven 与 epoll 内核持久化,性能随 fd 数线性 vs 与事件数相关。

      2. select 的 FD_SETSIZE=1024 是硬上限——不是由 nfds 控制,是 fd_set 类型大小限定;要突破必须改头文件 FD_SETSIZE 并重编译,几乎没人这么干。

      3. select 三类 fd_set 都需在每次调用前重新初始化(value-result);可用 nfds = max_fd + 1 减少内核工作量。

      4. select 不应被信号自动重启——handler 内务必不让 SA_RESTART + select 组合;多数实现 errno = EINTR。

      5. select 与 poll 在 2.6 性能差距已收敛——但大量 fd 仍 prefer poll(fd 集合可变、POLLNVAL 标识能力)。

      6. O_ASYNC 必须先装 SIGIO handler 再启用——否则 default kill 进程;handler 内仅设置 sig_atomic_t flag,不要做复杂事。

      7. signal-driven I/O 默认 SIGIO 是普通信号——多事件会丢失;用 F_SETSIG 改 realtime signal + SA_SIGINFO + siginfo.si_fd 才有完整事件信息。

      8. epoll 是 Linux 2.6 起几乎替代 select/poll——C10K / C10M 推荐;学习曲线稍陡(LT/ET 语义、event.data 选取)。

      9. epoll ET 模式必须 O_NONBLOCK——否则 read/write 阻塞会等到的下一次 ready 永远不会到,被该 fd 阻塞。

      10. epoll ET + accept 必须 accept 到 EAGAIN——不要 accept 一次就 break。

      11. epoll ET 容易饿死其他 fd——accept 后立即处理所有 ready fd,处理其他 fd 时不要在同一轮花太久。

      12. EPOLLONESHOT 用于多线程共享同一 fd——一个 worker 处理完后用 EPOLL_CTL_MOD 重新 arm。

      13. EPOLLEXCLUSIVE 防多 worker thundering herd(listen fd);多个 worker 同时 accept 时只唤醒一个。

      14. pselect 是 select 的原子版本——临时换 sigmask;常配合 self-pipe trick;解决「select + 信号」race。

      15. self-pipe trick handler 中必须 async-signal-safe——只能 write()/_exit() 等;不要 printf,不要 malloc。

      16. select timeout 在 Linux 上是 value-result(被减到剩余);portable code 每次调用前重新初始化,勿依赖。

      17. 跨章衔接:第 56-62 章铺垫 socket/term;本章 epoll 是高并发 server 的核心;第 64 章 pty 数据交换常用 epoll 监控;第 44 章管道+ poll demo(poll_pipes);第 33 章线程 + epollone-shot 是高性能网络编程模板。