Loading [MathJax]/jax/output/CommonHTML/jax.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Linux网络#18】:深入理解select多路转接:传统I/O复用的基石

【Linux网络#18】:深入理解select多路转接:传统I/O复用的基石

作者头像
IsLand1314
发布于 2025-04-03 00:34:58
发布于 2025-04-03 00:34:58
9500
代码可运行
举报
文章被收录于专栏:学习之路学习之路
运行总次数:0
代码可运行

一、前言:🔥 I/O 多路转接

💻

(或称为多任务I/O服务器)是一种高效管理多个I/O操作的技术,允许单线程或单进程同时监控和处理多个I/O事件(如网络套接字、文件描述符等)

  • 核心思想:利用操作系统提供的多路I/O转接机制(如 selectpollepoll 等),由内核帮助应用程序高效地监视多个文件描述符(包括网络连接、管道、文件等)的状态变化,而不是让应用程序自己轮询每个连接的状态
  • 核心目标:用最小资源开销实现高并发I/O处理,尤其适用于需要同时处理大量连接的场景(如Web服务器、实时通信系统等)
  • 这种方式能够显著提高服务器的性能和可扩展性,尤其是在处理大量并发连接时
为什么需要I/O多路转接?

传统阻塞I/O模型中,每个I/O操作会阻塞线程直至完成。若需处理多个连接,通常需为每个连接分配独立线程/进程,导致资源消耗大、上下文切换频繁。 而I/O多路转接通过单线程监控多个I/O流,仅在I/O就绪时触发操作,避免了阻塞和资源浪费。

二、I/O 多路转接之 select

1. 初识 select

💻 系统提供

函数来实现多路复用 输入 / 输出 模型.

  • select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在 select 这里等待, 直到被监视的文件描述符有一个或多个发生了状态改变;

核心原理

  • select 是一种 同步I/O多路复用 机制,允许程序在一个线程中监听多个文件描述符(如套接字、文件等)的可读、可写或异常事件
  • 其核心是通过 **轮询(polling)**检查文件描述符状态,并阻塞等待直到至少一个描述符就绪或超时。
2. select 函数原型

💤 select 的函数原型如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <sys/select.h>

int select(
    int nfds,               // 监控的最大文件描述符值 +1
    fd_set *readfds,         // 监听可读事件的描述符集合
    fd_set *writefds,        // 监听可写事件的描述符集合
    fd_set *exceptfds,       // 监听异常事件的描述符集合
    struct timeval *timeout  // 超时时间(NULL为无限等待)
);

// 操作fd_set的宏:
FD_ZERO(fd_set *set);        // 清空集合
FD_SET(int fd, fd_set *set); // 添加描述符到集合
FD_ISSET(int fd, fd_set *set); // 检查描述符是否在集合中
FD_CLR(int fd, fd_set *set); // 从集合移除描述符

📚 参数解释:

  • nfds 是需要监视的最大的文件描述符值 +1
  • rdset, wrset, exset 分别对应于需要检测的可读文件描述符的集合 , 可写文件描述符的集合 及 异常文件描述符的集合
  • timeout 为 结构体 timeval, 用来设置 select() 的等待时间
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/* A time value that is accurate to the nearest
   microsecond but also has a range of years.  */
struct timeval
{
  __time_t tv_sec;        /* Seconds.  */
  __suseconds_t tv_usec;    /* Microseconds.  */
};

📚 参数 timeout 取值:

  • NULL: 则表示 select() 没有 timeoutselect 将一直被阻塞, 直到某个文件描述符上发生了事件
  • 0: 仅检测描述符集合的状态, 然后立即返回, 并不等待外部事件的发生(非阻塞
  • 特定的时间值struct timeval timeout = {10, 0} : 如果在指定的时间段里没有事件发生,select 将超时返回
2.1 关于 fd_set 结构
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typedef long int __fd_mask;


/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
#define __FDELT(d) ((d) / __NFDBITS)
#define __FDMASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))

/* fd_set for select and pselect. */
typedef struct
  {
    /* XPG4.2 requires this member name. Otherwise avoid the name
       from the global namespace. */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
  } fd_set;

/* Maximum number of file descriptors in `fd_set'. */
#define FD_SETSIZE __FD_SETSIZE   //__FD_SETSIZE等于1024

/* Access macros for `fd_set'.  */
#define FD_SET(fd, fdsetp)      __FD_SET (fd, fdsetp)
#define FD_CLR(fd, fdsetp)      __FD_CLR (fd, fdsetp)
#define FD_ISSET(fd, fdsetp)    __FD_ISSET (fd, fdsetp)
#define FD_ZERO(fdsetp)         __FD_ZERO (fdsetp)
  • 其实这个结构就是一个 整数数组,更严格的说, 是一个 “位图” . 使用位图中对应的位来表示要监视的文件描述符.
    • 一个long int类型的数组。因为每一位可以代表一个文件描述符。所以fd_set最多表示1024个文件描述符!
  • 提供了一组操作 fd_set 的接口, 来比较方便的操作位图
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void FD_CLR(int fd, fd_set *set);     // 用来清除描述词组 set 中相关 fd 的位
int  FD_ISSET(int fd, fd_set *set); // 用来测试描述词组 set 中相关 fd 的位是否为真
void FD_SET(int fd, fd_set *set);     // 用来设置描述词组 set 中相关 fd 的位
void FD_ZERO(fd_set *set);             // 用来清除描述词组 set 的全部位
2.2 函数返回值
  • 执行成功则返回 文件描述符状态已改变的个数
  • 如果返回 0 代表在描述符状态改变前已超过 timeout 时间
  • 当有错误发生时则返回-1, 错误原因存于 errno, 此时参数 readfds, writefds, exceptfds 和 timeout 的值变成不可预测

🙅 错误值可能为:

  • EBADF文件描述词为无效的或该文件已关闭
  • EINTR此调用被信号所中断
  • EINVAL参数 n 为负值
  • ENOMEM核心内存不足
3. 理解 select 执行过程

🦈 理解 select 模型的关键在于理解 fd_set, 为说明方便, 取 fd_set 长度为 1 字节, fd_set 中的每一 bit 可以对应一个文件描述符 fd_set。 则 1 字节长的 fd_set 最大可以对应 8 个 fd.

  • 执行 fd_set ; FD_ZERO(&set);set 用位表示是 0000,0000
  • 若 fd= 5,执行 FD_SET(fd,&set); 后 set 变为 0001,0000(第 5 位置为 1)
  • 若再加入 fd= 2, fd=1,则 set 变为 0001,0011
  • 执行 select(6,&set,0,0,0) 阻塞等待
  • select 返回, 此时 set 变为 0000,0011。 注意: 没有事件发生的 fd=5 被清空
3.1 socket 就绪条件
读就绪
  • socket 内核中, 接收缓冲区中的字节数, 大于等于低水位标记 SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于 0;
  • socket TCP 通信中, 对端关闭连接, 此时对该 socket 读, 则返回 0;
  • 监听的 socket 上有新的连接请求;
  • socket 上有未处理的错误;
写就绪
  • socket 内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记 SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于 0;
  • socket 的写操作被关闭(close 或者 shutdown). 对一个写操作被关闭的 socket 进行写操作, 会触发 SIGPIPE 信号;
  • socket 使用非阻塞 connect 连接成功或失败之后;
  • socket 上有未读取的错误;
异常就绪(选学)
  • socket 上收到带外数据. 关于带外数据, 和 TCP 紧急模式相关(回忆 TCP 协议头中, 有一个紧急指针的字段), 自己收集相关资料
3.2 select 的特点
  • 可监控的文件描述符个数取决于 sizeof(fd_set) 的值. 我这边服务器上 sizeof(fd_set)= 512, 每 bit 表示一个文件描述符, 则我服务器上支持的最大文件描述符是 512*8=4096.
  • I将 fd 加入 select 监控集的同时, 还要再使用一个数据结构 array 保存放到 select 监控集中的 fd
    1. 用于再 select 返回后, array 作为源数据和 fd_set 进行 FD_ISSET 判断**
    2. select 返回后会把以前加入的但并无事件发生的 fd 清空, 则每次开始 select 前都要重新从 array 取得 fd 逐一加入(FD_ZERO 最先), 扫描 array 的同时 取得 fd 最大值 maxfd, 用于 select 的第一个参数

备注: fd_set 的大小可以调整, 可能涉及到重新编译内核.

3.3 select 优缺点

优点

缺点

跨平台支持(所有UNIX/Linux系统)

文件描述符数量受限(默认1024,由FD_SETSIZE定义)

简单易用,适合少量并发场景

线性扫描,时间复杂度O(n)(效率随描述符数量下降)

超时机制灵活

每次调用需重置fd_set(额外内存拷贝开销)

  • 每次调用 select:都需要手动设置 fd 集合(从用户态拷贝到内核态), 从接口使用角度来说也非常不便,而且 这个开销在 fd 很多时会很大
  • 同时每次调用 select 都需要在内核遍历传递进来的所有 fd, 这个开销在 fd 很多时也很大
3.4 注意事项
  1. 描述符上限:通过 FD_SETSIZE 宏定义(通常1024),需重新编译内核修改。
  2. 性能问题:当监控数千描述符时,select 的轮询效率远低于 epollkqueue
  3. 水平触发select 是水平触发模式,若未处理就绪事件,会持续通知。
  4. 非阻塞I/O:结合非阻塞socket可避免单次read/write阻塞整个程序。
4. 代码示例

示例一:检测标准输入输出

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>

int main()
{
    fd_set read_fds;
    FD_ZERO(&read_fds); // 清空
    FD_SET(0, &read_fds);

    while(true)
    {
        printf("> ");
        fflush(stdout);
        int ret = select(1, &read_fds, NULL, NULL, NULL);
        if(ret < 0){
            perror("Select");
            continue;
        }
        if(FD_ISSET(0, &read_fds)){
            char buf[1024] = {0};
            read(0, buf, sizeof(buf) - 1);
            printf("Input: %s", buf);
        }
        else{
            printf("Error! Invalid fd\n");
            continue;
        }
        FD_ZERO(&read_fds);
        FD_SET(0, &read_fds);
    }

    return 0;
}
  • 当只检测文件描述符 0(标准输入)时,因为输入条件只有在你有输入信息的时候才成立,所以如果一直不输入,就会产生超时信息

示例二:TCP 服务器使用 select 处理多客户端

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/select.h>
#include <cstring>

#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};

    // 创建TCP socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置socket选项(允许地址重用)
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    // 绑定socket到端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 开始监听
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    fd_set readfds;  // 描述符集合
    int client_sockets[MAX_CLIENTS] = {0}; // 客户端socket数组
    int max_sd;

    while (true) {
        FD_ZERO(&readfds);           // 清空集合
        FD_SET(server_fd, &readfds); // 添加服务器socket到监听集合
        max_sd = server_fd;

        // 添加所有客户端socket到集合
        for (int i = 0; i < MAX_CLIENTS; i++) {
            int sd = client_sockets[i];
            if (sd > 0) {
                FD_SET(sd, &readfds);
                if (sd > max_sd) max_sd = sd;
            }
        }

        // 调用select,阻塞等待事件
        int activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
        if ((activity < 0) && (errno != EINTR)) {
            perror("select error");
        }

        // 检查服务器socket是否有新连接
        if (FD_ISSET(server_fd, &readfds)) {
            if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
                perror("accept");
                exit(EXIT_FAILURE);
            }

            // 将新客户端socket加入数组
            for (int i = 0; i < MAX_CLIENTS; i++) {
                if (client_sockets[i] == 0) {
                    client_sockets[i] = new_socket;
                    std::cout << "New client connected, socket fd: " << new_socket << std::endl;
                    break;
                }
            }
        }

        // 处理客户端数据
        for (int i = 0; i < MAX_CLIENTS; i++) {
            int sd = client_sockets[i];
            if (FD_ISSET(sd, &readfds)) {
                int valread = read(sd, buffer, BUFFER_SIZE);
                if (valread == 0) {  // 客户端断开连接
                    getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
                    std::cout << "Client disconnected" << std::endl;
                    close(sd);
                    client_sockets[i] = 0;  // 清除socket
                } else {  // 处理数据
                    buffer[valread] = '\0';
                    std::cout << "Received: " << buffer << std::endl;
                    send(sd, buffer, strlen(buffer), 0); // 回显数据
                }
            }
        }
    }

    return 0;
}
  1. 初始化服务器
    • 创建TCP socket,绑定端口并开始监听。
    • 设置 SO_REUSEADDR 允许地址重用(避免端口占用)。
  2. select 监听流程
    • 使用 fd_set 管理需要监听的描述符集合。
    • 每次循环重新初始化集合,添加服务器socket和所有客户端socket。
    • 调用 select 阻塞等待事件,返回就绪的描述符数量。
  3. 处理新连接
    • 当服务器socket就绪(FD_ISSET),调用 accept 接受新连接。
    • 将新客户端socket存入数组。
  4. 处理客户端数据
    • 遍历所有客户端socket,检查是否有数据可读。
    • read 返回0,表示客户端断开连接,关闭socket并清理数组。
    • 否则回显接收到的数据。
5. 使用场景
  • 需要兼容多平台的轻量级应用。
  • 并发连接数较少(如<1000)。
  • 超时机制需要精细控制的场景(如同时等待I/O和定时任务)

三、后言

★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【Linux】的内容,比如:多路转接之 epollpoll 模型,请持续关注我 !!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-04-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Linux 网络编程:从 Socket API 到极简 Redis 发布/订阅 sub/pub 服务的实现
本文旨在系统性地阐述 Linux 环境下的网络编程基础,重点关注 Socket 应用程序接口(API)的原理与应用。通过循序渐进的方式,结合具体的 C 语言代码示例,我们将剖析核心系统调用的机制,并最终构建一个基于传输控制协议(TCP)的简化版发布/订阅(Publish/Subscribe, Pub/Sub)服务器模型。
Piper破壳
2025/05/07
590
I/O多路转接之select
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
海盗船长
2020/08/27
8640
【网络编程】十五、多路转接之 select
​ 系统提供 select 函数来实现多路复用输入/输出模型,这个函数是用来让我们的程序监视多个文件描述符的状态变化的,程序会停在 select 函数中等待,直到被监视的文件描述符有一个或多个发生了状态改变。
利刃大大
2025/05/23
1160
【网络编程】十五、多路转接之 select
【在Linux世界中追寻伟大的One Piece】多路转接select
其实这个结构就是一个整数数组,更严格的说,是一个"位图"。使用位图中对应的位来表示要监视的文件描述符。
枫叶丹
2024/11/07
960
【在Linux世界中追寻伟大的One Piece】多路转接select
详解I/O多路转接之select
对大量的描述符进行I/O事件监控—可以告诉进程现在有哪些描述符就绪了,然后进行就可以只针对就绪了的描述符进行响应操作,避免对没有就绪的I/O操作所导致的效率降低和流程阻塞。
海盗船长
2020/08/27
9910
I/O 多路复用, select, poll, epoll
I/O 是应用程序必然逃不掉的一个话题。大家在计算机基础学习中,学过计组,操作系统和计网,而想要把 I/O 研究深入肯定要将对这三个计算机基础方面有所深入。
ge3m0r
2024/05/16
1180
嵌入式Linux:I/O多路复用
Linux中的I/O多路复用是指一种同时监控多个文件描述符的机制,允许程序在不阻塞的情况下等待多个I/O事件。
不脱发的程序猿
2025/04/19
740
嵌入式Linux:I/O多路复用
【Linux】从零开始使用多路转接IO --- select
上一篇文章我们讲解了五种IO模型的基本概念,并通过系统调用使用了非阻塞IO。 一般的服务器不会使用非阻塞IO,因为非阻塞IO非常耗费CPU资源,导致CPU发热效率下降!非阻塞IO只有在特定情况下才比较好用!
叫我龙翔
2024/11/04
1210
【Linux】从零开始使用多路转接IO --- select
彻底理解 IO多路复用
https://github.com/caijinlin/learning-pratice/tree/master/linux/io
范蠡
2020/08/18
1.5K0
TCP服务器的演变过程:IO多路复用机制select实现TCP服务器
本系列文章旨在带领大家逐步深入TCP服务器的开发世界,从最基础的一对一连接通信开始,逐步探索更复杂、更高效的服务器模型。从零开始,手把手地编写第一个TCP服务器程序,亲身体验“开局一块砖,大厦全靠垒”的成就感。
Lion 莱恩呀
2025/05/18
770
TCP服务器的演变过程:IO多路复用机制select实现TCP服务器
详解I/O多路转接模型:select & poll & epoll
多路转接是IO模型的一种,这种IO模型通过select、poll或者epoll进行IO等待,可以同时等待多个文件描述符,当某个文件描述符的事件就绪,便会通知上层处理对应的事件。
二肥是只大懒蓝猫
2023/10/13
7130
详解I/O多路转接模型:select & poll & epoll
【Linux网络】多路转接:select、poll、epoll
在Linux中,常见的多路转接/复用有 select、poll 和 epoll 。
_小羊_
2025/03/11
3100
【Linux网络】多路转接:select、poll、epoll
高级IO(网络)
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一 般只有特定场景下才使用.
ljw695
2025/01/03
680
高级IO(网络)
I/O多路复用select/poll/epoll
早期操作系统通常将进程中可创建的线程数限制在一个较低的阈值,大约几百个。因此, 操作系统会提供一些高效的方法来实现多路IO,例如Unix的select和poll。现代操作系统中,线程数已经得到了极大的提升,如NPTL线程软件包可支持数十万的线程。
WindSun
2019/09/09
1.3K0
I/O多路复用select/poll/epoll
linux 网络编程 I/O复用 select,poll ,epoll
http://blog.csdn.net/zs634134578/article/details/19929449
bear_fish
2018/09/20
2.7K0
select,poll,epoll区别
select的本质是采用32个整数的32位,即32*32= 1024来标识,fd值为1-1024。当fd的值超过1024限制时,就必须修改FD_SETSIZE的大小。这个时候就可以标识32*max值范围的fd。
阳光岛主
2019/02/19
1.4K0
深入理解select的行为
select的第一个参数为输入参数,其它4个参数既是输入也是输出。3个事件集合:读事件集合、写事件集合、异常事件集合。输出为触发了该事件的集合。最后一个参数为还剩余多少时间,如果timeout了,则其为0。
Stare
2019/03/27
2K0
深入理解select的行为
IO多路转接之select
多路转接是IO模型的一种,这种IO模型通过select函数进行IO等待,并且select函数能够同时等待多个文件描述符的就绪状态,单个文件描述符的等待与阻塞IO类似。
二肥是只大懒蓝猫
2023/10/13
3310
IO多路转接之select
深入底层探析网络编程之多路复用器(select,poll,epoll)
IO模型 只关注IO,不关注IO读写完成后的事情。 同步:程序(APP)自己进行读/写操作 异步:由Kernel完成读/写,程序跑起来感觉像没有访问IO,访问的是buffer 阻塞:BLOCKING,一直等待着方法有效的返回结果 非阻塞:NONBLOCKING,调用方法的时候就返回是否读取到,(java中要么返回null,要么返回具体的对象) 所以IO模型有: 同步阻塞:程序(APP)自己读取,调用了方法后一直等待着有效的返回结果 同步非阻塞:程序(APP)自己读取,调用方法的瞬间就给出是否读取到的返回结
行百里er
2020/12/02
1K0
深入底层探析网络编程之多路复用器(select,poll,epoll)
Linux内核编程--常见IO模型与select/poll/epoll编程
套接字上的数据传输分两步执行:第一步,等待网络中的数据送达,将送达后的数据复制到内核中的缓冲区。第二步,把数据从内核中的缓冲区拷贝到应用进程的缓冲区。整个过程的运行空间是从应用进程空间切换到内核进程空间然后再切换回应用进程空间。
Coder-ZZ
2022/06/23
1.5K0
Linux内核编程--常见IO模型与select/poll/epoll编程
相关推荐
Linux 网络编程:从 Socket API 到极简 Redis 发布/订阅 sub/pub 服务的实现
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档