蔚淦丞
发布于 2026-09-07 / 20 阅读
0
0

epoll单线程最简模型 + 多线程多Reactor工业模型

一、epoll 核心概念

epoll 是 Linux 专属的高性能 IO 多路复用机制,核心作用是监听海量文件描述符的读写事件,是 C/C++ 高并发 TCP 服务的底层基石。

相较于 select/poll:epoll 采用事件通知机制,仅返回就绪的文件描述符,海量空闲连接不会产生性能损耗,高并发场景性能优势极强。

二、epoll 三大核心函数(最简用法)

epoll 所有功能均依赖三个核心函数,分工明确、解耦清晰。

1. epoll_create

int epoll_create(int size);

创建一个独立的 epoll 实例,返回 epollfd 文件描述符。

  • 新版 Linux 中 size 参数已废弃,无实际限制作用;

  • 支持多线程创建多个独立 epoll 实例,线程间监听资源完全隔离;

  • 使用完毕必须 close 释放,避免文件描述符泄露。

2. epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);

epoll 注册管理核心接口,用于对指定 fd 进行增、删、改监听操作。

操作类型 op:

  • EPOLL_CTL_ADD:新增 fd 到 epoll 监听集合

  • EPOLL_CTL_MOD:修改已注册 fd 的监听事件

  • EPOLL_CTL_DEL:从 epoll 中移除 fd 监听

核心监听事件:

  • EPOLLIN:fd 可读就绪(新连接/客户端数据)

  • EPOLLOUT:fd 可写就绪(内核发送缓冲区空闲)

3. epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

阻塞等待内核返回就绪事件,批量获取就绪的 fd 列表。

  • events:用户态数组,接收内核返回的就绪事件数据;

  • maxevents:单次调用最多返回的就绪事件数量;

  • timeout:-1 永久阻塞、0 非阻塞、正数阻塞指定毫秒;

  • 返回值:就绪事件个数,0 为超时,-1 为执行出错。

三、核心参数误区:MAX_EVENTS

核心结论:MAX_EVENTS 只限制单次 epoll_wait 返回的事件数,不限制 epoll 整体监听的 fd 总数。

epoll 最大可监听 fd 数量,仅由系统参数 ulimit -n 决定,与该参数无关。

场景举例:内核就绪队列存在 1000 个就绪事件,MAX_EVENTS=128

  • 本次 epoll_wait 仅返回 128 个事件;

  • 剩余事件留在内核队列,不会丢失;

  • 下一轮循环调用 epoll_wait 继续分批处理。

工程取舍:参数过小会频繁触发系统调用、性能下降;过大会占用栈内存,常规业务 128/256/512 为最优配置。

四、长连接核心:EPOLLIN / EPOLLOUT 事件区分

单个客户端 fd 可同时监听 EPOLLIN | EPOLLOUT,epoll 返回的 events 是位掩码,可通过位与运算精准判断事件类型,支撑长连接多轮交互。

// 事件判断核心代码
uint32_t rev = events[i].events;
if (rev & EPOLLIN) {
    // 可读事件:客户端发送数据,执行recv读取
}
if (rev & EPOLLOUT) {
    // 可写事件:内核缓冲区空闲,可发送残留数据
}

可写事件使用场景(关键)

TCP 发送缓冲区打满时,send 无法一次性发送全部数据,不能阻塞等待。标准处理流程:

  1. 数据未发送完毕,通过 EPOLL_CTL_MOD 开启 EPOLLOUT 监听;

  2. 内核缓冲区空闲后,触发 EPOLLOUT 事件,续发残留数据;

  3. 数据全部发送完成,立即关闭 EPOLLOUT,避免 CPU 空转 100%。

五、epoll 两种触发模式 LT / ET

  • LT 水平触发(默认):只要 fd 存在未读完数据、缓冲区可写,就会持续触发事件。编程简单、稳定性高,适合绝大多数业务。

  • ET 边沿触发:仅在 fd 状态切换瞬间触发一次事件。必须搭配非阻塞 IO + 循环读写至 EAGAIN,性能更高,开发复杂度更高。

六、代码:单线程最简 epoll 模型

极简实现,仅保留核心逻辑,适合理解 epoll 基础运行流程。

#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>

#define MAX_EVENTS 128
#define PORT 8888

// 设置非阻塞
int set_nonblock(int fd){
    int flag = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flag | O_NONBLOCK);
    return fd;
}

int main(){
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    set_nonblock(listen_fd);

    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);

    bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(listen_fd, 128);

    int epfd = epoll_create(10);
    struct epoll_event ev, events[MAX_EVENTS];

    // 注册监听fd读事件
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

    while(1){
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        for(int i = 0; i < n; i++){
            int fd = events[i].data.fd;
            // 新连接事件
            if(fd == listen_fd){
                int cli_fd = accept(listen_fd, NULL, NULL);
                set_nonblock(cli_fd);
                ev.events = EPOLLIN;
                ev.data.fd = cli_fd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cli_fd, &ev);
            }
            // 客户端数据可读事件
            else{
                char buf[1024] = {0};
                int ret = recv(fd, buf, sizeof(buf)-1, 0);
                if(ret <= 0){
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    close(fd);
                    continue;
                }
                printf("recv: %s\n", buf);
                send(fd, buf, ret, 0);
            }
        }
    }
    return 0;
}

七、核心总结

  1. epoll 三大函数各司其职:epoll_create 创建实例、epoll_ctl 管理监听、epoll_wait 批量获取就绪事件。

  2. MAX_EVENTS 仅控制单次返回事件数,不限制全局监听 fd 总量,内核事件不会丢失。

  3. 通过位与运算区分 EPOLLIN/EPOLLOUT,动态开关可写事件,是长连接多轮收发、半包续发的核心。

  4. 多Reactor模型:主线程负责连接接收,多工作线程独立epoll处理读写,并发性能更强。

  5. C语言无map,工业级采用「fd指针索引数组」维护连接上下文,高效且稳定。

  6. epoll 仅负责事件通知,所有业务状态、数据拼接、残留发送均需用户态手动维护。


评论