Linux select 多路转接详解:从 fd_set 到 TCP 服务端事件循环
写网络服务端时,一个很现实的问题是:一个进程如何同时关注多个文件描述符?
如果只用阻塞 IO,程序一旦卡在某个 recv 或 read 上,就没法及时处理其他连接。如果把所有 fd 都设置成非阻塞,再用循环挨个检查,又容易把 CPU 浪费在大量无效轮询上。
select 解决的就是这类问题:让程序把一组文件描述符交给内核监视,然后阻塞等待。只要其中一个或多个 fd 的状态发生变化,select 就返回,程序再去处理真正就绪的 fd。
#mermaid-svg-lWWA2Trd4E3v3cwG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lWWA2Trd4E3v3cwG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lWWA2Trd4E3v3cwG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lWWA2Trd4E3v3cwG .error-icon{fill:#552222;}#mermaid-svg-lWWA2Trd4E3v3cwG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lWWA2Trd4E3v3cwG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lWWA2Trd4E3v3cwG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lWWA2Trd4E3v3cwG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lWWA2Trd4E3v3cwG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lWWA2Trd4E3v3cwG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lWWA2Trd4E3v3cwG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lWWA2Trd4E3v3cwG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lWWA2Trd4E3v3cwG .marker.cross{stroke:#333333;}#mermaid-svg-lWWA2Trd4E3v3cwG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lWWA2Trd4E3v3cwG p{margin:0;}#mermaid-svg-lWWA2Trd4E3v3cwG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lWWA2Trd4E3v3cwG .cluster-label text{fill:#333;}#mermaid-svg-lWWA2Trd4E3v3cwG .cluster-label span{color:#333;}#mermaid-svg-lWWA2Trd4E3v3cwG .cluster-label span p{background-color:transparent;}#mermaid-svg-lWWA2Trd4E3v3cwG .label text,#mermaid-svg-lWWA2Trd4E3v3cwG span{fill:#333;color:#333;}#mermaid-svg-lWWA2Trd4E3v3cwG .node rect,#mermaid-svg-lWWA2Trd4E3v3cwG .node circle,#mermaid-svg-lWWA2Trd4E3v3cwG .node ellipse,#mermaid-svg-lWWA2Trd4E3v3cwG .node polygon,#mermaid-svg-lWWA2Trd4E3v3cwG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lWWA2Trd4E3v3cwG .rough-node .label text,#mermaid-svg-lWWA2Trd4E3v3cwG .node .label text,#mermaid-svg-lWWA2Trd4E3v3cwG .image-shape .label,#mermaid-svg-lWWA2Trd4E3v3cwG .icon-shape .label{text-anchor:middle;}#mermaid-svg-lWWA2Trd4E3v3cwG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lWWA2Trd4E3v3cwG .rough-node .label,#mermaid-svg-lWWA2Trd4E3v3cwG .node .label,#mermaid-svg-lWWA2Trd4E3v3cwG .image-shape .label,#mermaid-svg-lWWA2Trd4E3v3cwG .icon-shape .label{text-align:center;}#mermaid-svg-lWWA2Trd4E3v3cwG .node.clickable{cursor:pointer;}#mermaid-svg-lWWA2Trd4E3v3cwG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lWWA2Trd4E3v3cwG .arrowheadPath{fill:#333333;}#mermaid-svg-lWWA2Trd4E3v3cwG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lWWA2Trd4E3v3cwG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lWWA2Trd4E3v3cwG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lWWA2Trd4E3v3cwG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lWWA2Trd4E3v3cwG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lWWA2Trd4E3v3cwG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lWWA2Trd4E3v3cwG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lWWA2Trd4E3v3cwG .cluster text{fill:#333;}#mermaid-svg-lWWA2Trd4E3v3cwG .cluster span{color:#333;}#mermaid-svg-lWWA2Trd4E3v3cwG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lWWA2Trd4E3v3cwG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lWWA2Trd4E3v3cwG rect.text{fill:none;stroke-width:0;}#mermaid-svg-lWWA2Trd4E3v3cwG .icon-shape,#mermaid-svg-lWWA2Trd4E3v3cwG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lWWA2Trd4E3v3cwG .icon-shape p,#mermaid-svg-lWWA2Trd4E3v3cwG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lWWA2Trd4E3v3cwG .icon-shape .label rect,#mermaid-svg-lWWA2Trd4E3v3cwG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lWWA2Trd4E3v3cwG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lWWA2Trd4E3v3cwG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lWWA2Trd4E3v3cwG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 没有
有
准备一组 fd
调用 select 等待
是否有 fd 就绪
select 返回
遍历 fd 集合,找到就绪 fd
accept/read/write 等后续处理
这篇文章围绕 select 的核心接口、执行过程、socket 就绪条件和典型服务端写法展开。读完后,再去看 poll、epoll,会更容易理解它们为什么要改进 select。
一、select 是什么
select 是一种 IO 多路转接机制。所谓"多路",指的是多个文件描述符;所谓"转接",可以理解为程序不再阻塞在某一个具体的 read 或 recv 上,而是把多个 fd 统一交给 select 等待。
它主要完成两件事:
- 监视多个文件描述符的状态变化;
- 当一个或多个 fd 可读、可写或出现异常时返回。
直观地说,select 像一个值班员。应用程序把一批 fd 交给它,它负责等待这些 fd 中谁先"有事"。等 select 返回后,应用程序再根据结果去执行 accept、read、recv、send 等操作。
二、select 函数原型与参数
select 的函数原型如下:
c
#include <sys/select.h>
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
参数含义可以整理成表格:
| 参数 | 含义 |
|---|---|
nfds |
需要监视的最大文件描述符值加 1 |
readfds |
关心"可读事件"的 fd 集合 |
writefds |
关心"可写事件"的 fd 集合 |
exceptfds |
关心"异常事件"的 fd 集合 |
timeout |
控制 select 的等待时间 |
nfds 很容易写错。假设要监视的最大 fd 是 max_fd,那么第一个参数应该传:
c
select(max_fd + 1, &readfds, &writefds, &exceptfds, &timeout);
为什么要加 1?因为内核遍历时检查的是 [0, nfds) 这个范围。如果最大 fd 是 8,却传了 8,那么 fd 8 不会被检查到。
timeout 的三种用法
timeout 控制 select 等待多久:
| 写法 | 行为 |
|---|---|
NULL |
一直阻塞,直到有 fd 就绪或被信号中断 |
指向 {0, 0} |
立即检测,不等待,常用于轮询 |
指向非零 timeval |
最多等待指定时间,超时返回 0 |
timeval 结构用于描述一段时间:
c
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
};
这里有个细节很容易踩坑:如果你想让 select 周期性超时返回,不能传 NULL,而要传入一个具体的 timeval。另外,在一些系统上,select 返回后可能修改 timeout 的值,所以循环中通常每次都重新设置它。
三、fd_set:本质是位图
select 用 fd_set 表示文件描述符集合。可以把它理解为一个位图:某一位是 1,表示对应的 fd 被加入监视集合;某一位是 0,表示不监视。
例如用 1 个字节做示意,最多可以表示 8 个 fd:
text
bit: 7 6 5 4 3 2 1 0
fd : 7 6 5 4 3 2 1 0
set: 0 0 1 0 0 1 1 0
这个集合表示 fd 1、fd 2、fd 5 被加入监视。实际系统里的 fd_set 比这个示意复杂,但"用位表示 fd 是否在集合中"的思想是一致的。
Linux 提供了一组宏来操作 fd_set:
c
void FD_ZERO(fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
它们的作用如下:
| 宏 | 作用 |
|---|---|
FD_ZERO |
清空整个 fd 集合 |
FD_SET |
把某个 fd 加入集合 |
FD_CLR |
从集合中删除某个 fd |
FD_ISSET |
判断某个 fd 是否在集合中 |
最常见的片段是:
c
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
int ret = select(fd + 1, &readfds, NULL, NULL, NULL);
if (ret > 0 && FD_ISSET(fd, &readfds)) {
// fd 可读,执行 read/recv/accept
}
这个片段背后有一个关键点:传给 select 的 fd 集合既是输入参数,也是输出参数。
调用前,集合表示"我想监视哪些 fd"。调用后,集合会被改写,只保留真正就绪的 fd。也就是说,没有事件发生的 fd 可能被清掉。
这也是 select 编程里最容易出错的地方。
四、select 的执行过程
假设要监视 fd 1、fd 2、fd 5,可用下面的过程理解:
- 先调用
FD_ZERO,把集合全部清空; - 调用
FD_SET,把 fd 1、fd 2、fd 5 加入集合; - 调用
select(6, &readfds, NULL, NULL, NULL),因为最大 fd 是 5,所以nfds传 6; - 程序阻塞在
select; - 如果 fd 1 和 fd 2 可读,
select返回; - 返回后的
readfds只保留 fd 1、fd 2,fd 5 被清掉; - 程序遍历集合,用
FD_ISSET判断哪些 fd 就绪。
用流程图表示:
#mermaid-svg-Trv5lEOtxgllTeUD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Trv5lEOtxgllTeUD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Trv5lEOtxgllTeUD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Trv5lEOtxgllTeUD .error-icon{fill:#552222;}#mermaid-svg-Trv5lEOtxgllTeUD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Trv5lEOtxgllTeUD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Trv5lEOtxgllTeUD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Trv5lEOtxgllTeUD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Trv5lEOtxgllTeUD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Trv5lEOtxgllTeUD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Trv5lEOtxgllTeUD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Trv5lEOtxgllTeUD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Trv5lEOtxgllTeUD .marker.cross{stroke:#333333;}#mermaid-svg-Trv5lEOtxgllTeUD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Trv5lEOtxgllTeUD p{margin:0;}#mermaid-svg-Trv5lEOtxgllTeUD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Trv5lEOtxgllTeUD .cluster-label text{fill:#333;}#mermaid-svg-Trv5lEOtxgllTeUD .cluster-label span{color:#333;}#mermaid-svg-Trv5lEOtxgllTeUD .cluster-label span p{background-color:transparent;}#mermaid-svg-Trv5lEOtxgllTeUD .label text,#mermaid-svg-Trv5lEOtxgllTeUD span{fill:#333;color:#333;}#mermaid-svg-Trv5lEOtxgllTeUD .node rect,#mermaid-svg-Trv5lEOtxgllTeUD .node circle,#mermaid-svg-Trv5lEOtxgllTeUD .node ellipse,#mermaid-svg-Trv5lEOtxgllTeUD .node polygon,#mermaid-svg-Trv5lEOtxgllTeUD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Trv5lEOtxgllTeUD .rough-node .label text,#mermaid-svg-Trv5lEOtxgllTeUD .node .label text,#mermaid-svg-Trv5lEOtxgllTeUD .image-shape .label,#mermaid-svg-Trv5lEOtxgllTeUD .icon-shape .label{text-anchor:middle;}#mermaid-svg-Trv5lEOtxgllTeUD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Trv5lEOtxgllTeUD .rough-node .label,#mermaid-svg-Trv5lEOtxgllTeUD .node .label,#mermaid-svg-Trv5lEOtxgllTeUD .image-shape .label,#mermaid-svg-Trv5lEOtxgllTeUD .icon-shape .label{text-align:center;}#mermaid-svg-Trv5lEOtxgllTeUD .node.clickable{cursor:pointer;}#mermaid-svg-Trv5lEOtxgllTeUD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Trv5lEOtxgllTeUD .arrowheadPath{fill:#333333;}#mermaid-svg-Trv5lEOtxgllTeUD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Trv5lEOtxgllTeUD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Trv5lEOtxgllTeUD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Trv5lEOtxgllTeUD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Trv5lEOtxgllTeUD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Trv5lEOtxgllTeUD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Trv5lEOtxgllTeUD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Trv5lEOtxgllTeUD .cluster text{fill:#333;}#mermaid-svg-Trv5lEOtxgllTeUD .cluster span{color:#333;}#mermaid-svg-Trv5lEOtxgllTeUD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Trv5lEOtxgllTeUD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Trv5lEOtxgllTeUD rect.text{fill:none;stroke-width:0;}#mermaid-svg-Trv5lEOtxgllTeUD .icon-shape,#mermaid-svg-Trv5lEOtxgllTeUD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Trv5lEOtxgllTeUD .icon-shape p,#mermaid-svg-Trv5lEOtxgllTeUD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Trv5lEOtxgllTeUD .icon-shape .label rect,#mermaid-svg-Trv5lEOtxgllTeUD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Trv5lEOtxgllTeUD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Trv5lEOtxgllTeUD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Trv5lEOtxgllTeUD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} -1
0
> 0
FD_ZERO 清空集合
FD_SET 加入要监视的 fd
计算 max_fd + 1
调用 select
返回值 ret
处理错误
超时,没有 fd 就绪
遍历 fd,FD_ISSET 判断就绪项
对就绪 fd 执行 accept/read/write
因此,实际工程中通常会保存一份"原始监视列表",每次调用 select 前重新构造临时的 fd_set。不能指望上一次传进去的集合在返回后还能原样复用。
五、select 返回值与常见错误
select 的返回值很重要:
| 返回值 | 含义 |
|---|---|
> 0 |
有这么多个 fd 状态发生变化 |
0 |
超时返回,没有 fd 就绪 |
-1 |
调用失败,错误原因保存在 errno 中 |
常见错误包括:
| 错误码 | 含义 |
|---|---|
EBADF |
fd 无效,或者文件已经关闭 |
EINTR |
调用过程被信号中断 |
EINVAL |
参数非法,例如 nfds 为负数 |
ENOMEM |
内核内存不足 |
EINTR 在网络程序中比较常见。遇到它时,通常不是连接本身出问题,而是系统调用被信号打断。很多服务端会选择重新进入循环,重新构造集合后再次调用 select。
当 select 返回 -1 时,不要继续依赖这次传入的 readfds、writefds、exceptfds 和 timeout。稳妥做法是丢弃本轮临时状态,下一轮重新构造。
六、socket 的就绪条件
select 返回并不代表已经完成了读写,它只是告诉你"某些 fd 可以继续处理"。那么 socket 到底什么时候算可读、可写或异常?
1. 读就绪
socket 满足下面任一情况时,可能被认为可读:
- 接收缓冲区中的字节数大于等于低水位标记
SO_RCVLOWAT,此时读取不会阻塞,并且通常能读到大于 0 的数据; - TCP 对端关闭连接,此时读操作会返回 0;
- 监听 socket 上有新的连接请求,此时可以执行
accept; - socket 上有未处理的错误。
这里特别要注意监听 socket。服务端把监听 fd 加入 readfds,当它可读时,并不是调用 read,而是调用 accept 接收新连接。
2. 写就绪
socket 满足下面任一情况时,可能被认为可写:
- 发送缓冲区中的可用空间大于等于低水位标记
SO_SNDLOWAT,此时写入不会阻塞,并且通常可以写入大于 0 的数据; - socket 的写方向被关闭,继续写可能触发
SIGPIPE; - 非阻塞
connect连接成功或失败之后; - socket 上有未读取的错误。
在普通回显服务或短请求短响应模型里,初学阶段经常只监视读事件。写事件不是不能监视,而是如果所有连接都长期加入 writefds,可能会让 select 频繁返回,因为很多 socket 在大部分时间都是可写的。
3. 异常就绪
异常集合主要用于检测带外数据等特殊情况。带外数据和 TCP 紧急模式相关,常规业务服务里不一定会用到,但知道 exceptfds 的存在有助于完整理解 select 原型。
七、示例:用 select 检测标准输入
下面是为了帮助理解而补充的完整示例。它监视标准输入 fd 0,如果 5 秒内没有输入,就打印一次超时信息;如果用户输入内容,就读取并输出。
c
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
int main(void) {
while (1) {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
printf("> ");
fflush(stdout);
int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
if (ret < 0) {
perror("select");
continue;
}
if (ret == 0) {
printf("timeout, no input\n");
continue;
}
if (FD_ISSET(STDIN_FILENO, &readfds)) {
char buffer[1024] = {0};
ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
if (n > 0) {
printf("input: %s", buffer);
} else if (n == 0) {
printf("stdin closed\n");
break;
} else {
perror("read");
}
}
}
return 0;
}
编译运行:
bash
gcc select_stdin.c -o select_stdin
./select_stdin
可能的输出:
text
> timeout, no input
> hello
input: hello
>
这段代码有三个关键点:
- 每次循环都重新
FD_ZERO和FD_SET,因为select会修改传入的集合; - 每次循环都重新设置
timeout,避免复用已被修改的时间值; select返回大于 0 后,还要用FD_ISSET判断具体哪个 fd 就绪。
如果把 timeout 参数改成 NULL,程序会一直等到输入出现,不会每 5 秒打印一次超时信息。
八、select 版 TCP 服务端怎么写
把 select 用到 TCP 服务端时,整体流程通常是这样的:
#mermaid-svg-D5vz2QAzUauvKGYT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-D5vz2QAzUauvKGYT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-D5vz2QAzUauvKGYT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-D5vz2QAzUauvKGYT .error-icon{fill:#552222;}#mermaid-svg-D5vz2QAzUauvKGYT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-D5vz2QAzUauvKGYT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-D5vz2QAzUauvKGYT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-D5vz2QAzUauvKGYT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-D5vz2QAzUauvKGYT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-D5vz2QAzUauvKGYT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-D5vz2QAzUauvKGYT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-D5vz2QAzUauvKGYT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-D5vz2QAzUauvKGYT .marker.cross{stroke:#333333;}#mermaid-svg-D5vz2QAzUauvKGYT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-D5vz2QAzUauvKGYT p{margin:0;}#mermaid-svg-D5vz2QAzUauvKGYT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-D5vz2QAzUauvKGYT .cluster-label text{fill:#333;}#mermaid-svg-D5vz2QAzUauvKGYT .cluster-label span{color:#333;}#mermaid-svg-D5vz2QAzUauvKGYT .cluster-label span p{background-color:transparent;}#mermaid-svg-D5vz2QAzUauvKGYT .label text,#mermaid-svg-D5vz2QAzUauvKGYT span{fill:#333;color:#333;}#mermaid-svg-D5vz2QAzUauvKGYT .node rect,#mermaid-svg-D5vz2QAzUauvKGYT .node circle,#mermaid-svg-D5vz2QAzUauvKGYT .node ellipse,#mermaid-svg-D5vz2QAzUauvKGYT .node polygon,#mermaid-svg-D5vz2QAzUauvKGYT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-D5vz2QAzUauvKGYT .rough-node .label text,#mermaid-svg-D5vz2QAzUauvKGYT .node .label text,#mermaid-svg-D5vz2QAzUauvKGYT .image-shape .label,#mermaid-svg-D5vz2QAzUauvKGYT .icon-shape .label{text-anchor:middle;}#mermaid-svg-D5vz2QAzUauvKGYT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-D5vz2QAzUauvKGYT .rough-node .label,#mermaid-svg-D5vz2QAzUauvKGYT .node .label,#mermaid-svg-D5vz2QAzUauvKGYT .image-shape .label,#mermaid-svg-D5vz2QAzUauvKGYT .icon-shape .label{text-align:center;}#mermaid-svg-D5vz2QAzUauvKGYT .node.clickable{cursor:pointer;}#mermaid-svg-D5vz2QAzUauvKGYT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-D5vz2QAzUauvKGYT .arrowheadPath{fill:#333333;}#mermaid-svg-D5vz2QAzUauvKGYT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-D5vz2QAzUauvKGYT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-D5vz2QAzUauvKGYT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D5vz2QAzUauvKGYT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-D5vz2QAzUauvKGYT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D5vz2QAzUauvKGYT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-D5vz2QAzUauvKGYT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-D5vz2QAzUauvKGYT .cluster text{fill:#333;}#mermaid-svg-D5vz2QAzUauvKGYT .cluster span{color:#333;}#mermaid-svg-D5vz2QAzUauvKGYT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-D5vz2QAzUauvKGYT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-D5vz2QAzUauvKGYT rect.text{fill:none;stroke-width:0;}#mermaid-svg-D5vz2QAzUauvKGYT .icon-shape,#mermaid-svg-D5vz2QAzUauvKGYT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D5vz2QAzUauvKGYT .icon-shape p,#mermaid-svg-D5vz2QAzUauvKGYT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-D5vz2QAzUauvKGYT .icon-shape .label rect,#mermaid-svg-D5vz2QAzUauvKGYT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D5vz2QAzUauvKGYT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-D5vz2QAzUauvKGYT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-D5vz2QAzUauvKGYT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
创建监听 socket
bind 绑定地址
listen 进入监听
把 listen_fd 加入监视集合
进入事件循环
select 等待可读事件
listen_fd 就绪?
accept 新连接
把 new_fd 加入监视集合
客户端 fd 就绪
recv/read 请求
读取成功?
处理业务并 send 响应
关闭 fd 并移出集合
为了突出主线,可以把核心逻辑写成下面这种伪代码:
cpp
Selector selector;
selector.Add(listen_sock);
while (true) {
std::vector<TcpSocket> ready_socks;
bool ok = selector.Wait(&ready_socks);
if (!ok) {
continue;
}
for (size_t i = 0; i < ready_socks.size(); ++i) {
if (ready_socks[i].GetFd() == listen_sock.GetFd()) {
TcpSocket new_sock;
listen_sock.Accept(&new_sock, NULL, NULL);
selector.Add(new_sock);
} else {
std::string request;
bool recv_ok = ready_socks[i].Recv(&request);
if (!recv_ok) {
selector.Del(ready_socks[i]);
ready_socks[i].Close();
continue;
}
std::string response;
HandleRequest(request, &response);
ready_socks[i].Send(response);
}
}
}
这个事件循环里有两类 fd:
listen_sock:监听 socket,可读表示有新连接到来,要执行accept;new_sock:已经建立连接的客户端 socket,可读表示客户端发来了请求,要执行recv。
这也是 select 服务端最重要的分支逻辑:同样是读就绪,不同 fd 对应的处理动作不同。
九、封装 Selector 时要注意什么
如果把 select 封装成一个 Selector 类,内部通常需要维护三类状态:
| 成员 | 作用 |
|---|---|
fd_set read_fds_ |
保存当前关注的读事件集合 |
int max_fd_ |
保存当前最大 fd,用于计算 nfds |
map 或 unordered_map |
保存 fd 与 socket 对象之间的映射 |
Add 操作一般会做三件事:
cpp
fd_map_[fd] = sock;
FD_SET(fd, &read_fds_);
max_fd_ = std::max(max_fd_, fd);
Del 操作除了 FD_CLR,还要注意更新最大 fd:
cpp
FD_CLR(fd, &read_fds_);
fd_map_.erase(fd);
for (int i = max_fd_; i >= 0; --i) {
if (FD_ISSET(i, &read_fds_)) {
max_fd_ = i;
break;
}
}
Wait 操作尤其容易写错。因为 select 会修改 fd 集合,所以不能直接把成员变量传进去,应该先复制一份临时集合:
cpp
bool Wait(std::vector<TcpSocket> *output) {
output->clear();
fd_set tmp = read_fds_;
int ret = select(max_fd_ + 1, &tmp, NULL, NULL, NULL);
if (ret < 0) {
perror("select");
return false;
}
for (int fd = 0; fd < max_fd_ + 1; ++fd) {
if (FD_ISSET(fd, &tmp)) {
output->push_back(fd_map_[fd]);
}
}
return true;
}
这里有两个细节:
tmp = read_fds_是必须的,否则原始监视集合会被select改掉;- 遍历条件应该覆盖到
max_fd_,也就是fd < max_fd_ + 1。
十、select 的特点与缺点
select 的优点是接口相对基础,理解成本不高,非常适合用来学习 IO 多路转接的本质。但它也有明显缺点。
1. fd 数量有限
select 能监视多少 fd,取决于 fd_set 的大小以及系统相关限制。常见情况下会受到 FD_SETSIZE 的影响。某些环境中,sizeof(fd_set) 为 512 字节,每个 bit 表示一个 fd,那么理论上能表达 4096 个 fd。
这里不要把某个环境的数值当成永远固定的标准。真正写代码时,应以当前系统和编译环境为准。
2. 每次调用都要重新设置集合
因为 select 会改写传入的 fd_set,所以每次调用前都要重新准备集合。接口使用上不算轻便,尤其在 fd 较多时,维护成本会变高。
3. 用户态到内核态有拷贝成本
每次调用 select,fd 集合都要从用户态拷贝到内核态。fd 数量变多时,这部分开销会更明显。
4. 内核需要遍历 fd 集合
select 并不知道哪个 fd 更可能就绪,它需要检查传入范围内的 fd。nfds 越大,遍历成本越高。
5. 返回后应用层还要遍历
select 返回的是就绪数量,但不会直接给出一个现成的就绪列表。应用程序仍然要从 0 遍历到 max_fd,用 FD_ISSET 找出具体就绪的 fd。
这些缺点也是后续更高级事件通知机制出现的重要原因。
十一、常见问题与易错点
1. 为什么每次循环都要重新 FD_SET
因为 select 返回后,传入的集合只保留就绪 fd,没有就绪的 fd 可能被清除。如果直接复用这个集合,下一轮监视对象就不完整了。
2. nfds 可以随便写一个很大的值吗
不建议。nfds 决定内核检查的范围,应该是当前最大 fd 加 1。写得过大会增加无意义遍历,写得过小会漏掉 fd。
3. 监听 socket 可读时为什么要 accept
监听 socket 的"可读"表示有新的连接请求已经到来。对监听 socket 做普通 read 没有意义,正确动作是 accept,拿到新的连接 socket 后再加入监视集合。
4. 客户端断开连接时怎么处理
连接 socket 可读后,调用 recv 或 read。如果返回 0,表示对端关闭连接;如果返回失败,也要根据错误情况处理。常见做法是把该 fd 从集合中删除,并关闭 socket。
5. 只监视读事件够不够
简单请求响应模型里,先只监视读事件通常够用。需要处理大量待发送数据、连接建立过程或复杂错误场景时,才会更系统地使用写事件和异常事件。
十二、总结
select 的核心可以概括为一句话:把多个 fd 放进集合里,让内核帮我们等待它们的状态变化,返回后再由应用程序逐个处理就绪 fd。
掌握 select 需要抓住几个关键点:
nfds是最大 fd 加 1;fd_set本质是位图,用FD_ZERO、FD_SET、FD_CLR、FD_ISSET操作;select会修改传入的 fd 集合,所以每轮调用前要重新准备;- 监听 socket 可读表示可以
accept新连接; - 普通连接 socket 可读表示可以
read或recv; select简单直观,但存在 fd 数量限制、集合拷贝、内核遍历和应用层再遍历等问题。
把这些机制理解清楚后,再学习更高效的 IO 事件通知机制,就能看出它们到底是在优化哪些痛点,而不是只停留在"接口不同"的层面。