Linux select 多路转接详解

Linux select 多路转接详解:从 fd_set 到 TCP 服务端事件循环

写网络服务端时,一个很现实的问题是:一个进程如何同时关注多个文件描述符?

如果只用阻塞 IO,程序一旦卡在某个 recvread 上,就没法及时处理其他连接。如果把所有 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 就绪条件和典型服务端写法展开。读完后,再去看 pollepoll,会更容易理解它们为什么要改进 select

一、select 是什么

select 是一种 IO 多路转接机制。所谓"多路",指的是多个文件描述符;所谓"转接",可以理解为程序不再阻塞在某一个具体的 readrecv 上,而是把多个 fd 统一交给 select 等待。

它主要完成两件事:

  • 监视多个文件描述符的状态变化;
  • 当一个或多个 fd 可读、可写或出现异常时返回。

直观地说,select 像一个值班员。应用程序把一批 fd 交给它,它负责等待这些 fd 中谁先"有事"。等 select 返回后,应用程序再根据结果去执行 acceptreadrecvsend 等操作。

二、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:本质是位图

selectfd_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,可用下面的过程理解:

  1. 先调用 FD_ZERO,把集合全部清空;
  2. 调用 FD_SET,把 fd 1、fd 2、fd 5 加入集合;
  3. 调用 select(6, &readfds, NULL, NULL, NULL),因为最大 fd 是 5,所以 nfds 传 6;
  4. 程序阻塞在 select
  5. 如果 fd 1 和 fd 2 可读,select 返回;
  6. 返回后的 readfds 只保留 fd 1、fd 2,fd 5 被清掉;
  7. 程序遍历集合,用 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 时,不要继续依赖这次传入的 readfdswritefdsexceptfdstimeout。稳妥做法是丢弃本轮临时状态,下一轮重新构造。

六、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_ZEROFD_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
mapunordered_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 可读后,调用 recvread。如果返回 0,表示对端关闭连接;如果返回失败,也要根据错误情况处理。常见做法是把该 fd 从集合中删除,并关闭 socket。

5. 只监视读事件够不够

简单请求响应模型里,先只监视读事件通常够用。需要处理大量待发送数据、连接建立过程或复杂错误场景时,才会更系统地使用写事件和异常事件。

十二、总结

select 的核心可以概括为一句话:把多个 fd 放进集合里,让内核帮我们等待它们的状态变化,返回后再由应用程序逐个处理就绪 fd

掌握 select 需要抓住几个关键点:

  • nfds 是最大 fd 加 1;
  • fd_set 本质是位图,用 FD_ZEROFD_SETFD_CLRFD_ISSET 操作;
  • select 会修改传入的 fd 集合,所以每轮调用前要重新准备;
  • 监听 socket 可读表示可以 accept 新连接;
  • 普通连接 socket 可读表示可以 readrecv
  • select 简单直观,但存在 fd 数量限制、集合拷贝、内核遍历和应用层再遍历等问题。

把这些机制理解清楚后,再学习更高效的 IO 事件通知机制,就能看出它们到底是在优化哪些痛点,而不是只停留在"接口不同"的层面。

相关推荐
spider_xcxc1 小时前
Redis 深度实践:安全管控、性能压测与持久化分析(二)
运维·前端·redis·云计算·bootstrap·运维开发
浩瀚之水_csdn2 小时前
Python 3 网络编程详解:从原理到实战
开发语言·网络·python
用户029266661332 小时前
Linux 用户管理知识与应用实践(三:用户组及修改用户密码)
linux
犹豫的哑铃2 小时前
WebSocket 协议
网络·websocket·网络协议
apcipot_rain2 小时前
计科八股2026705——正态分布、独立与不相关、范数、列表元组集合、链表队列、sort函数、URL执行
python·计算机网络·概率论
云絮.2 小时前
计算机相关工作原理
服务器·前端·数据库
Mortalbreeze2 小时前
深入 Linux 线程机制(三):线程互斥——竞争条件与互斥锁的本质
linux·运维·服务器·c++·算法
鱼听禅2 小时前
CentOS 源码模式配置SVNAdmin2管理SVN仓库
linux·svn·centos
Tian_Hang2 小时前
Eclipse Mosquitto 安装及介绍
java·运维·服务器·ide·sql·mysql·eclipse
Amonter2 小时前
RK3399ProD移植主线Linux内核
linux·rockchip·rk3399pro