Linux专题九:I/O复用(水平以及边缘触发放到libevent库那一专题细讲)

一、I/O 复用基础概念

1. 核心定义与意义

  • I/O 复用:允许单进程 / 线程同时监控多个文件描述符(FD)的 I/O 状态(可读 / 可写 / 异常),当某个 FD 就绪时(如套接字有数据到达、管道可写入),通知进程 / 线程处理,避免因单个 FD 阻塞导致其他 FD 无法响应。
  • 核心价值 :**我们知道,在单线程的tcp网络编程中,如果客户端建立连接后不发送数据,那么其他客户端相连接上服务器是不可能的,因此多线程诞生了,但是这相当于一个餐厅招了很多个服务员给客户提供一对一服务,这个体系大了之后,会有很多的开销,因此,有什么方法可以让一个服务员在一个客户无需求的情况下去服务其他有需求的客户呢?这样即解决了服务客户的问题,有解决了开销的问题,一举两得,这就是i/o复用技术,**所以i/o非要用的核心意义就是,解决传统 "一 FD 一线程" 模型在高并发场景下的资源浪费问题(线程创建 / 切换开销大),平衡 CPU 利用率和系统资源消耗,是高性能网络编程(如服务器同时处理多客户端)的核心技术。
  • 适用场景
    1. 服务器需同时处理多个客户端连接(如 TCP 服务器、UDP 服务器);
    2. 需监控多个 I/O 设备(如同时处理标准输入、套接字、管道);
    3. 需在有限进程 / 线程资源下实现高并发 I/O 处理。

2. 关键术语

  • 文件描述符(FD):非负整数,标识操作系统中的打开文件 / 设备 / 套接字(如标准输入 0、标准输出 1、套接字 FD);

  • 就绪事件:FD 满足 I/O 操作的条件(如 "可读" 指 FD 有数据可读取,"可写" 指 FD 可写入数据);

  • 轮询:主动遍历所有监控的 FD,检查是否就绪(select/poll 采用此方式);

  • 事件驱动:注册 FD 和事件,内核主动通知就绪的 FD(epoll 采用此方式,性能更高)

  • 水平触发(LT,Level Trigger):
    select/poll/epoll默认均为LT模式。当fd就绪后,若用户未处理完数据,内核会持续通知该fd就绪(直到数据被处理完)。
    优点:编程简单,不易遗漏事件;缺点:可能重复触发事件,增加开销。

  • 边缘触发(ET,Edge Trigger):
    仅epoll支持ET模式。当fd状态**从未就绪变为就绪**时,内核仅通知一次,即使数据未处理完,后续也不再通知。
    优点:减少事件触发次数,性能更高;缺点:编程复杂,需一次性读取完所有数据(通常结合非阻塞I/O使用)。

二、select 函数(基础 I/O 复用)

1. 核心函数与参数

(1)函数原型与头文件
cpp 复制代码
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, 
           fd_set *readfds, 
           fd_set *writefds, 
           fd_set *exceptfds, 
           struct timeval *timeout);
(2)参数详解
参数名 数据类型 功能说明
nfds int 需监控的最大 FD 值 + 1(select 会遍历 0~nfds-1 的所有 FD,超出范围的 FD 不监控);示例:监控 FD=3、5、7 时,nfds=8
readfds fd_set* 可读事件 FD 集合(输入输出参数):- 输入:告诉 select 需监控哪些 FD 的可读事件;- 输出:select 返回后,仅保留已就绪的可读 FD
writefds fd_set* 可写事件 FD 集合(用法同 readfds);无需监控则传NULL
exceptfds fd_set* 异常事件 FD 集合(用法同 readfds);无需监控则传NULL
timeout struct timeval* 超时时间,控制 select 的阻塞行为(结构体定义见下文);传NULL表示永久阻塞
(3)fd_set集合操作宏

fd_set是 FD 的位掩码(1 位对应 1 个 FD),需通过以下宏操作,不可直接赋值:

宏定义 功能说明 示例
FD_ZERO(fd_set* set) 清空 FD 集合,初始化为空 FD_ZERO(&readfds);
FD_SET(int fd, fd_set* set) 将指定 FD 添加到集合中 FD_SET(0, &readfds);(监控标准输入)
FD_CLR(int fd, fd_set* set) 将指定 FD 从集合中移除 FD_CLR(3, &readfds);
FD_ISSET(int fd, fd_set* set) 检查 FD 是否在集合中(select 返回后用于判断 FD 是否就绪),返回非 0 表示就绪 if (FD_ISSET(3, &readfds)) { ... }
(4)struct timeval超时结构体
cpp 复制代码
struct timeval {
    long tv_sec;  // 秒数
    long tv_usec; // 微秒数(1秒=10^6微秒)
};
  • 超时行为:
    1. timeout=NULL:永久阻塞,直到至少 1 个 FD 就绪;
    2. tv_sec=0且tv_usec=0:不阻塞,立即检查所有 FD 并返回(轮询);
    3. tv_sec>0或tv_usec>0:阻塞指定时间,超时无 FD 就绪则返回 0。

2. 返回值说明

返回值 含义
>0 成功,返回就绪的 FD 总数(可读 + 可写 + 异常事件的 FD 数量之和)
0 超时,无任何 FD 就绪
-1 失败,设置errno(如EBADF= 无效 FD、EINTR= 被信号中断、EINVAL= 参数错误)

3. 关键特性与限制

  • 特性
    1. 跨平台支持(Linux/Windows/BSD 均支持);
    2. 监控 FD 后会修改fd_set集合,仅保留就绪 FD,需每次监控前重新初始化或备份集合;
    3. timeout参数会被修改(超时后存储剩余时间),需每次调用 select 前重新设置。
  • 限制
    1. FD 数量限制:fd_set大小由FD_SETSIZE(通常为 1024)决定,无法监控超过 1024 的 FD;
    2. 性能瓶颈:需遍历 0~nfds-1 的所有 FD,即使仅少数 FD 就绪,高并发场景下效率低;
    3. 集合备份开销:每次调用需备份fd_set,增加内存和 CPU 开销。

4. 示例代码(监控标准输入与套接字)

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000
#define BUF_SIZE 128

int main() {
    // 1. 创建TCP监听套接字
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket创建失败");
        exit(1);
    }

    // 2. 绑定端口
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind失败");
        close(listen_fd);
        exit(1);
    }
    listen(listen_fd, 5);
    printf("服务器启动,监听端口%d,监控标准输入(FD=0)和监听套接字(FD=%d)\n", PORT, listen_fd);

    fd_set readfds;
    int max_fd = listen_fd; // 初始最大FD为监听套接字

    while (1) {
        // 3. 初始化并设置监控的FD集合(每次需重新设置,因select会修改集合)
        FD_ZERO(&readfds);
        FD_SET(0, &readfds);          // 监控标准输入(FD=0)
        FD_SET(listen_fd, &readfds);  // 监控监听套接字

        // 4. 设置超时时间(5秒)
        struct timeval timeout = {5, 0};

        // 5. 调用select监控FD
        int ready_count = select(max_fd + 1, &readfds, NULL, NULL, &timeout);
        if (ready_count == -1) {
            perror("select失败");
            continue;
        } else if (ready_count == 0) {
            printf("select超时(5秒无事件)\n");
            continue;
        }

        // 6. 检查哪些FD就绪
        // 6.1 标准输入就绪(FD=0)
        if (FD_ISSET(0, &readfds)) {
            char buf[BUF_SIZE] = {0};
            ssize_t len = read(0, buf, BUF_SIZE - 1);
            if (len > 0) {
                buf[len - 1] = '\0'; // 去除换行符
                printf("标准输入:%s\n", buf);
                if (strcmp(buf, "quit") == 0) {
                    printf("退出服务器\n");
                    break;
                }
            }
        }

        // 6.2 监听套接字就绪(有新客户端连接)
        if (FD_ISSET(listen_fd, &readfds)) {
            struct sockaddr_in client_addr;
            socklen_t client_len = sizeof(client_addr);
            int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
            if (conn_fd == -1) {
                perror("accept失败");
                continue;
            }
            printf("新客户端连接,conn_fd=%d\n", conn_fd);
            // 更新最大FD(若新FD更大)
            if (conn_fd > max_fd) {
                max_fd = conn_fd;
            }
            // 此处可将conn_fd加入监控集合,后续处理客户端数据(简化示例未实现)
        }
    }

    close(listen_fd);
    return 0;
}

三、poll 函数(改进版 I/O 复用)

1. 核心函数与参数

(1)函数原型与头文件
cpp 复制代码
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
(2)关键结构体struct pollfd
cpp 复制代码
struct pollfd {
    int fd;         // 需监控的文件描述符;若为-1,该结构体被忽略
    short events;   // 待监控的事件(输入参数,位掩码)
    short revents;  // 实际发生的事件(输出参数,由内核填充)
};
(3)参数详解
参数名 数据类型 功能说明
fds struct pollfd* 指向pollfd结构体数组的指针,每个元素对应一个监控的 FD 及事件
nfds nfds_t fds数组中有效元素的个数(内核仅遍历前nfds个元素)
timeout int 超时时间(单位:毫秒):- -1:永久阻塞;- 0:立即返回(轮询);- >0:阻塞指定毫秒数
(4)核心事件宏(eventsrevents取值)
事件宏 含义说明 适用场景(events设置)
POLLIN 可读事件(FD 有数据可读取) 监控套接字数据到达、管道可读、标准输入有输入
POLLOUT 可写事件(FD 可写入数据) 监控套接字发送缓冲区空闲、管道可写入
POLLERR 错误事件(仅revents返回) FD 发生错误(如套接字连接异常)
POLLHUP 挂起事件(仅revents返回) FD 对应的连接断开(如客户端主动关闭连接)
POLLNVAL 无效 FD 事件(仅revents返回) FD 未打开或已关闭

2. 核心函数与参数

epoll 通过 3 个函数实现完整功能:epoll_create(创建实例)、epoll_ctl(管理事件)、epoll_wait(等待就绪)。

(1)epoll 实例创建:epoll_create/epoll_create1

2. revents的位掩码特性

  • 本质revents是 16 位 short 类型,用每一位表示一个事件是否发生(位掩码),支持同时存储多个事件;

  • 示例 :若 FD 同时触发POLLIN(可读)和POLLHUP(连接断开),revents = POLLIN | POLLHUP(二进制对应位同时为 1);

  • 事件判断 :需用按位与(&) 提取特定事件(不可直接用==,避免忽略同时发生的其他事件),示例:

    cpp 复制代码
    // 正确:判断POLLIN事件是否发生(不受其他事件影响)
    if (fds[i].revents & POLLIN) { ... }
    
    // 错误:若revents同时包含POLLIN和POLLHUP,会判定为false
    if (fds[i].revents == POLLIN) { ... }

    3. 返回值说明

    返回值 含义
    >0 成功,返回就绪的 FD 总数(revents非 0 的 FD 数量)
    0 超时,无任何 FD 就绪
    -1 失败,设置errno(如EBADF= 无效 FD、EINTR= 被信号中断)

    4. 与 select 的对比优势

    对比维度 select poll
    FD 数量限制 FD_SETSIZE(1024)限制,无法监控超过 1024 的 FD 无 FD 数量限制,仅受系统资源限制
    集合处理 需备份fd_set(每次调用会修改),开销大 pollfd数组仅revents被修改,fdevents不变,无需备份
    事件标识 需通过 3 个fd_set区分可读 / 可写 / 异常,事件类型固定 eventsrevents的位掩码支持更多事件类型(如POLLPRI紧急数据)
    遍历效率 需遍历 0~nfds-1 的所有 FD,即使 FD 无效 仅遍历fds数组中的有效元素(fd!=-1),效率更高

    5. 示例代码(监控 TCP 客户端连接与数据)

    cpp 复制代码
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <poll.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    #define PORT 6000
    #define BUF_SIZE 128
    #define MAX_CLIENTS 10  // 最大客户端数量
    
    int main() {
        // 1. 创建监听套接字
        int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
        if (listen_fd == -1) {
            perror("socket创建失败");
            exit(1);
        }
    
        // 2. 绑定端口
        struct sockaddr_in server_addr;
        memset(&server_addr, 0, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(PORT);
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
            perror("bind失败");
            close(listen_fd);
            exit(1);
        }
        listen(listen_fd, 5);
        printf("服务器启动,监听端口%d\n", PORT);
    
        // 3. 初始化pollfd数组(监听套接字+客户端FD)
        struct pollfd fds[MAX_CLIENTS + 1]; // +1用于监听套接字
        memset(fds, 0, sizeof(fds));
        // 第0个元素:监控监听套接字(可读事件)
        fds[0].fd = listen_fd;
        fds[0].events = POLLIN;
        int client_count = 0; // 当前客户端数量
    
        while (1) {
            // 4. 调用poll监控FD(超时3秒)
            int ready_count = poll(fds, client_count + 1, 3000);
            if (ready_count == -1) {
                perror("poll失败");
                continue;
            } else if (ready_count == 0) {
                printf("poll超时(3秒无事件)\n");
                continue;
            }
    
            // 5. 遍历pollfd数组,处理就绪事件
            for (int i = 0; i <= client_count; i++) {
                if (fds[i].revents == 0) {
                    continue; // 无事件,跳过
                }
    
                // 5.1 监听套接字就绪(新客户端连接)
                if (fds[i].fd == listen_fd && (fds[i].revents & POLLIN)) {
                    if (client_count >= MAX_CLIENTS) {
                        printf("客户端数量已满,拒绝新连接\n");
                        continue;
                    }
    
                    struct sockaddr_in client_addr;
                    socklen_t client_len = sizeof(client_addr);
                    int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
                    if (conn_fd == -1) {
                        perror("accept失败");
                        continue;
                    }
    
                    // 添加新客户端到pollfd数组
                    client_count++;
                    fds[client_count].fd = conn_fd;
                    fds[client_count].events = POLLIN; // 监控客户端数据可读
                    printf("新客户端连接,conn_fd=%d,当前客户端数=%d\n", conn_fd, client_count);
                }
    
                // 5.2 客户端FD就绪(有数据或连接断开)
                else if (fds[i].revents & (POLLIN | POLLHUP | POLLERR)) {
                    char buf[BUF_SIZE] = {0};
                    ssize_t len = read(fds[i].fd, buf, BUF_SIZE - 1);
    
                    // 连接断开(len=0)或错误(len=-1)
                    if (len <= 0) {
                        printf("客户端断开连接,conn_fd=%d\n", fds[i].fd);
                        close(fds[i].fd);
                        // 删除该客户端FD(将最后一个客户端FD移到当前位置)
                        fds[i] = fds[client_count];
                        client_count--;
                        i--; // 重新检查当前位置(已替换为最后一个客户端)
                        continue;
                    }
    
                    // 正常读取数据
                    printf("收到conn_fd=%d的数据:%s\n", fds[i].fd, buf);
                    // 回复客户端
                    const char* reply = "已收到数据";
                    write(fds[i].fd, reply, strlen(reply));
                }
            }
        }
    
        close(listen_fd);
        return 0;
    }

    四、epoll 函数(Linux 高性能 I/O 复用)

    1. 核心原理与优势

  • 内核数据结构 :epoll 实例包含两个核心结构:

    1. 红黑树:存储待监控的 FD 及事件(支持高效添加 / 删除 / 修改 FD);
    2. 就绪链表:存储已就绪的 FD(无需遍历所有 FD,直接从链表取就绪 FD,性能极高)。
  • 事件驱动机制:采用 "注册 - 回调" 模式,FD 就绪时内核主动将其加入就绪链表,避免 select/poll 的轮询开销;

  • 优势

    1. 无 FD 数量限制(仅受系统内存限制);
    2. 高性能(O (1) 获取就绪 FD,无需遍历);
    3. 支持边缘触发(ET)和水平触发(LT)两种事件触发模式(LT 默认,ET 更高效)。

epoll 通过 3 个函数实现完整功能:epoll_create(创建实例)、epoll_ctl(管理事件)、epoll_wait(等待就绪)。

(1)epoll 实例创建:epoll_create/epoll_create1
cpp 复制代码
#include <sys/epoll.h>

// 旧版本(size已无实际意义,传>0的整数即可)
int epoll_create(int size);

// 新版本(支持flags参数,如EPOLL_CLOEXEC)
int epoll_create1(int flags);
  • 功能:创建 epoll 实例,内核分配红黑树和就绪链表;
  • 返回值 :成功返回 epoll 实例句柄(epfd),失败返回 - 1;
  • 参数size(Linux 2.6.8 后无效)、flags(如EPOLL_CLOEXEC:进程退出时自动关闭 epfd)。
(2)事件管理:epoll_ctl
cpp 复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 功能:向 epoll 实例的红黑树中添加 / 删除 / 修改 FD 及监控事件;

  • 参数详解

    参数名 功能说明
    epfd epoll_create返回的 epoll 实例句柄
    op 操作类型:- EPOLL_CTL_ADD:添加 FD 及事件;- EPOLL_CTL_DEL:删除 FD;- EPOLL_CTL_MOD:修改 FD 的监控事件
    fd 需操作的文件描述符(如套接字、管道)
    event 指向struct epoll_event的指针,定义监控事件和附加数据
    (3)struct epoll_event结构体
    cpp 复制代码
    typedef union epoll_data {
        void    *ptr;    // 指向自定义数据的指针(如客户端信息结构体)
        int      fd;     // 待监控的FD(最常用)
        uint32_t u32;    // 32位无符号整数
        uint64_t u64;    // 64位无符号整数
    } epoll_data_t;
    
    struct epoll_event {
        uint32_t     events;    // 待监控的事件(位掩码)
        epoll_data_t data;      // 附加数据(联合体,存储FD或自定义数据)
    };
    (4)等待就绪事件:epoll_wait
    cpp 复制代码
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    (5)核心事件宏
    事件宏 含义说明
    EPOLLIN 可读事件(同 poll 的POLLIN
    EPOLLOUT 可写事件(同 poll 的POLLOUT
    EPOLLPRI 紧急数据可读(如 TCP 带外数据)
    EPOLLERR 错误事件(同 poll 的POLLERR
    EPOLLHUP 挂起事件(同 poll 的POLLHUP
    EPOLLET 边缘触发模式(ET,仅在 FD 状态从 "未就绪" 变为 "就绪" 时通知一次)
    EPOLLLT 水平触发模式(LT,默认,FD 就绪时持续通知,直到数据被处理)
    EPOLLONESHOT 一次性事件(事件触发后,该 FD 自动从 epoll 实例中删除,需重新添加才能再次监控)

3. 示例代码(epoll 实现高并发 TCP 服务器)

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000
#define BUF_SIZE 128
#define MAX_EVENTS 100  // 一次最多处理的就绪事件数

int main() {
    // 1. 创建监听套接字
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket创建失败");
        exit(1);
    }

    // 2. 设置FD复用(避免服务器重启时端口占用)
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 3. 绑定端口
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind失败");
        close(listen_fd);
        exit(1);
    }
    listen(listen_fd, 5);
    printf("epoll服务器启动,监听端口%d\n", PORT);

    // 4. 创建epoll实例
    int epfd = epoll_create1(EPOLL_CLOEXEC);
    if (epfd == -1) {
        perror("epoll_create1失败");
        close(listen_fd);
        exit(1);
    }

    // 5. 将监听套接字添加到epoll实例(监控可读事件,水平触发)
    struct epoll_event ev_listen;
    ev_listen.events = EPOLLIN;
    ev_listen.data.fd = listen_fd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev_listen) == -1) {
        perror("epoll_ctl添加监听FD失败");
        close(listen_fd);
        close(epfd);
        exit(1);
    }

    // 6. 初始化就绪事件数组
    struct epoll_event ready_events[MAX_EVENTS];

    while (1) {
        // 7. 等待就绪事件(永久阻塞)
        int ready_count = epoll_wait(epfd, ready_events, MAX_EVENTS, -1);
        if (ready_count == -1) {
            perror("epoll_wait失败");
            continue;
        }

        // 8. 处理就绪事件
        for (int i = 0; i < ready_count; i++) {
            int curr_fd = ready_events[i].data.fd;

            // 8.1 监听套接字就绪(新客户端连接)
            if (curr_fd == listen_fd) {
                struct sockaddr_in client_addr;
                socklen_t client_len = sizeof(client_addr);
                int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
                if (conn_fd == -1) {
                    perror("accept失败");
                    continue;
                }
                printf("新客户端连接,conn_fd=%d\n", conn_fd);

                // 将客户端FD添加到epoll实例(监控可读事件,水平触发)
                struct epoll_event ev_client;
                ev_client.events = EPOLLIN;
                ev_client.data.fd = conn_fd;
                if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev_client) == -1) {
                    perror("epoll_ctl添加客户端FD失败");
                    close(conn_fd);
                    continue;
                }
            }

            // 8.2 客户端FD就绪(有数据或连接断开)
            else if (ready_events[i].events & (EPOLLIN | EPOLLHUP | EPOLLERR)) {
                char buf[BUF_SIZE] = {0};
                ssize_t len = read(curr_fd, buf, BUF_SIZE - 1);

                // 连接断开或错误
                if (len <= 0) {
                    printf("客户端断开连接,conn_fd=%d\n", curr_fd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curr_fd, NULL); // 从epoll中删除
                    close(curr_fd);
                    continue;
                }

                // 读取并回复数据
                printf("收到conn_fd=%d的数据:%s\n", curr_fd, buf);
                const char* reply = "epoll服务器已收到:";
                write(curr_fd, reply, strlen(reply));
                write(curr_fd, buf, strlen(buf));
            }
        }
    }

    // 9. 释放资源(实际不会执行,需手动终止)
    close(listen_fd);
    close(epfd);
    return 0;
}

五、select/poll/epoll 对比与选型

一、核心原理与数据结构

  1. select
  • **核心原理**:通过**位图**(fd_set)存储待监控的文件描述符,内核轮询所有被监控的fd,检查其是否就绪(读/写/异常)。
  • **数据结构**:使用固定大小的位图(默认FD_SETSIZE=1024),每个位代表一个文件描述符,位图的大小限制了可监控的fd数量。
  • **用户态与内核态交互**:每次调用select时,需要将**整个位图从用户态拷贝到内核态**;内核轮询完成后,再将位图拷贝回用户态,用户需遍历整个位图判断哪些fd就绪。
    //痛点就是位图大小没法自定义,可能会不够,第二个就是我们还要自己去循环一变去找寻哪个是就绪的事件。并且增删查都需要用户自己去实现。
  1. poll
  • **核心原理**:解决了select的fd数量限制问题,通过**结构体数组(pollfd)** 存储待监控的fd及其事件,内核同样采用**轮询**方式检查fd就绪状态。
  • **数据结构**:`struct pollfd` 数组,每个元素包含fd、事件(POLLIN/POLLOUT等)和返回事件,数组大小由用户自定义,无固定上限(受系统资源限制)。
  • **用户态与内核态交互**:每次调用poll时,需将**整个pollfd数组拷贝到内核态**;内核轮询后,修改数组中的返回事件并拷贝回用户态,用户需遍历数组判断就绪fd。
    //解决了可能select位图大小可能不够的痛点,但是用户还是需要去遍历一便自定义的事件监控数组去找寻发生事件的文件描述符,并且增删操作还得是用户自己去实现。
  1. epoll
  • **核心原理**:Linux 2.6后引入的**事件驱动**模型,通过内核态的**红黑树**存储待监控的fd,**就绪链表**存储已就绪的fd,避免了轮询所有fd。
  • **数据结构**:
  • **红黑树**:存储用户注册的所有fd和事件,支持高效的增删改查(O(logn))。
  • **就绪链表**:内核检测到fd就绪后,将其加入链表,用户只需遍历该链表即可获取就绪fd。
  • **epoll_event**:用户态与内核态交互的事件结构体,包含fd和事件类型。
  • **用户态与内核态交互**:
  • **注册阶段**:通过`epoll_ctl`将fd注册到红黑树,仅需一次拷贝(后续无需重复拷贝)。
  • **等待阶段**:通过`epoll_wait`获取就绪fd,内核仅将**就绪的fd信息**拷贝到用户态,无需拷贝全部监控fd。
    解决了上面select以及poll监控函数的弊端,是现在常用的io复用函数。
二:总结:
对比维度 select poll epoll(Linux)
FD 数量限制 FD_SETSIZE(1024)限制 无限制(仅受系统资源) 无限制(仅受系统资源)
性能(高并发) 低(O (n) 遍历所有 FD) 低(O (n) 遍历所有监控 FD) 高(O (1) 从就绪链表取 FD)
事件通知方式 轮询 轮询 事件驱动(注册 - 回调)
集合处理 需备份fd_set(每次调用修改) pollfd数组仅revents修改,无需备份 红黑树管理,添加 / 删除 / 修改高效
跨平台支持 支持(Linux/Windows/BSD) 支持(Linux/BSD,Windows 不原生支持) 仅 Linux 支持
触发模式 仅水平触发(LT) 仅水平触发(LT) 支持水平触发(LT,默认)和边缘触发(ET)
适用场景 小规模 FD 监控(<1024)、跨平台需求 中等规模 FD 监控、跨平台需求(非 Windows) Linux 高并发场景(如服务器同时处理数千 / 数万客户端)

选型建议

  1. 小规模 FD(<1024)+ 跨平台:选 select;
  2. 中等规模 FD + 跨平台(非 Windows):选 poll;
  3. Linux 系统 + 高并发(>1024 FD):选 epoll(性能最优);
  4. 边缘触发(ET)需求:仅 epoll 支持,适用于需减少事件通知次数的场景(如高吞吐服务器)。
相关推荐
渣渣盟2 小时前
NFS服务器配置全攻略:从入门到精通
linux·运维·服务器
缘如风2 小时前
Linux上sunrpc 111端口关闭
linux·服务器
I · T · LUCKYBOOM3 小时前
iptables防火墙
linux·运维·服务器·网络·安全
山上三树3 小时前
main()函数带参数的用法
linux·c语言
凌波粒3 小时前
Linux-Ubuntu系统环境搭建
linux·运维·ubuntu
鸠摩智首席音效师3 小时前
如何在 Linux 中使用 uptime 命令 ?
linux·运维·服务器
HalvmånEver3 小时前
Linux:匿名管道(进程间通信二)
linux·运维·服务器
lengjingzju4 小时前
一网打尽Linux IPC(一):进程间通信完全指南——总体介绍
linux·服务器·c语言
阿豪学编程4 小时前
【Linux】进程信号深度解析
linux·运维·服务器