Linux I/O复用之epoll

如果说 select 是遍历式查表,poll 是遍历式查数组,那么 epoll 就是回调式通知------它彻底解决了轮询带来的 O(n) 性能瓶颈

  1. 核心原理:红黑树 + 就绪链表

epoll 在内核中维护了两个核心数据结构:红黑树(rbtree):存储所有被监视的文件描述符。当调用 epoll_ctl 添加或删除 fd 时,操作的是这棵树,增删改查效率为 O(log n)。就绪链表(rdlist):存储已经发生事件的 fd。当网卡数据到达时,内核通过回调机制(中断处理)直接把就绪的 fd 挂到这个链表上。

工作流程:

当调用 epoll_wait 时,内核不再遍历全部 fd,而是直接把就绪链表(rdlist)中的数据返回给用户。这意味着返回的就绪 fd 数量 = 活跃连接数,时间复杂度仅为 O(1)。

  1. 三大核心 API

函数定义在 <sys/epoll.h> 中:① epoll_create1(创建 epoll 实例)

cpp 复制代码
int epoll_create1(int flags);

flags 通常设为 0(等同于旧的 epoll_create(size),size 已忽略),或 EPOLL_CLOEXEC(子进程不继承)。

返回值是一个文件描述符(epoll 句柄),用完需 close。

② epoll_ctl(增删改监视的 fd)

cpp 复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

op 操作类型:

EPOLL_CTL_ADD:添加 fd

EPOLL_CTL_MOD:修改监听事件

EPOLL_CTL_DEL:删除 fd(此时 event 可传 NULL)

③ epoll_wait(等待事件)

cpp 复制代码
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

events:输出型参数,指向一块预分配内存,内核将就绪的事件拷贝到这里。

maxevents:最多返回几个事件(必须小于等于数组长度)。

timeout:超时毫秒数(-1 阻塞,0 立即返回)。

注意:epoll_wait 每次只返回就绪的 fd,不会修改 events 结构,也不需要像 select 那样重置

  1. 数据结构:struct epoll_event
cpp 复制代码
struct epoll_event {
    uint32_t events;     // 事件掩码(输入输出共用)
    epoll_data_t data;   // 用户数据(联合体)
};

typedef union epoll_data {
    void    *ptr;        // 自定义指针(常用,指向自定义结构)
    int      fd;         // 文件描述符(常用)
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

关键点:data.fd 或 data.ptr 会原样返回,方便你定位是哪个 fd 就绪。

常用事件标志:

EPOLLIN 可读

EPOLLOUT 可写

EPOLLRDHUP 对端关闭连接

EPOLLET 边缘触发模式(默认是水平触发)

EPOLLONESHOT 事件触发一次后自动移除,防止多线程竞争

  1. LT(水平触发) vs ET(边缘触发)

LT(Level Triggered,默认) 只要 fd 有数据未读完,epoll_wait 就会反复通知

ET(Edge Triggered,高效) 只在 状态变化 的瞬间通知一次(无数据->有数据)

假设 socket 收到了 2KB 数据,但你在 ET 模式下只读了 1KB。LT 模式:下次 epoll_wait 会再次通知你还有数据。ET 模式:不会再通知你了!除非有新数据到达,触发新的状态变化。

ET 的正确写法:

将 socket 设置为 非阻塞(O_NONBLOCK)。

触发事件后,必须用 循环 read / recv 把数据读干净,直到返回 EAGAIN 或 EWOULDBLOCK 错误。

否则剩余数据会一直滞留在缓冲区,且不会再次触发事件,导致"假死"。

  1. C++ 完整实战:高并发 Echo 服务器(ET 边缘触发 + 非阻塞)

以下代码展示了标准的 ET 模式写法,包含设置非阻塞、循环读数据、EPOLLRDHUP 处理断开

cpp 复制代码
#include <iostream>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>

const int PORT = 8888;
const int BUFFER_SIZE = 1024;
const int MAX_EVENTS = 1024;

// 工具函数:设置文件描述符为非阻塞
int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    // 1. 创建监听 socket
    int listenFd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenFd == -1) {
        perror("socket");
        return -1;
    }

    int opt = 1;
    setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(PORT);
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    if (bind(listenFd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
        perror("bind");
        close(listenFd);
        return -1;
    }

    if (listen(listenFd, 128) == -1) {
        perror("listen");
        close(listenFd);
        return -1;
    }
    // 监听 socket 也必须设为非阻塞,这样 accept 才能配合 ET 模式
    set_nonblocking(listenFd);

    // 2. 创建 epoll 实例
    int epollFd = epoll_create1(0);
    if (epollFd == -1) {
        perror("epoll_create1");
        close(listenFd);
        return -1;
    }

    // 3. 将监听 socket 注册到 epoll(使用 ET 边缘触发)
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;  // 边缘触发
    ev.data.fd = listenFd;
    if (epoll_ctl(epollFd, EPOLL_CTL_ADD, listenFd, &ev) == -1) {
        perror("epoll_ctl: listenFd");
        close(listenFd);
        close(epollFd);
        return -1;
    }

    std::cout << "Epoll (ET mode) Server listening on port " << PORT << std::endl;

    // 预分配事件数组
    std::vector<struct epoll_event> events(MAX_EVENTS);

    while (true) {
        // 4. 等待事件(阻塞,-1 表示无限等待)
        int readyCount = epoll_wait(epollFd, events.data(), MAX_EVENTS, -1);

        if (readyCount == -1) {
            if (errno == EINTR) continue; // 被信号中断
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < readyCount; ++i) {
            int curFd = events[i].data.fd;
            uint32_t evt = events[i].events;

            // 处理错误 / 挂起
            if (evt & (EPOLLERR | EPOLLHUP)) {
                std::cout << "Error/Hangup on FD " << curFd << std::endl;
                close(curFd);
                epoll_ctl(epollFd, EPOLL_CTL_DEL, curFd, nullptr);
                continue;
            }

            // 5. 处理新连接(监听 socket 可读)
            if (curFd == listenFd) {
                while (true) {  // ET 模式必须循环 accept,直到 EAGAIN
                    struct sockaddr_in clientAddr;
                    socklen_t addrLen = sizeof(clientAddr);
                    int clientFd = accept(listenFd, (struct sockaddr*)&clientAddr, &addrLen);
                    if (clientFd == -1) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            // 所有连接已处理完毕,正常退出循环
                            break;
                        } else {
                            perror("accept");
                            break;
                        }
                    }
                    std::cout << "New client connected. FD: " << clientFd << std::endl;

                    // 设置客户端为非阻塞(ET 强制要求)
                    set_nonblocking(clientFd);

                    // 注册到 epoll,监听可读和对端关闭
                    struct epoll_event clientEv;
                    clientEv.events = EPOLLIN | EPOLLRDHUP | EPOLLET; // 边缘触发
                    clientEv.data.fd = clientFd;
                    if (epoll_ctl(epollFd, EPOLL_CTL_ADD, clientFd, &clientEv) == -1) {
                        perror("epoll_ctl: client");
                        close(clientFd);
                    }
                }
                continue;
            }

            // 6. 处理客户端数据(普通 fd 可读,且不是监听 fd)
            // ET 模式核心:必须循环读,直到返回 EAGAIN
            if (evt & EPOLLIN) {
                char buffer[BUFFER_SIZE];
                bool connectionClosed = false;

                while (true) {
                    ssize_t bytesRead = read(curFd, buffer, sizeof(buffer) - 1);
                    if (bytesRead > 0) {
                        buffer[bytesRead] = '\0';
                        std::cout << "FD " << curFd << " says: " << buffer;
                        // Echo 回写(注意:生产环境可写可能阻塞,需注册 EPOLLOUT)
                        write(curFd, buffer, bytesRead);
                    } 
                    else if (bytesRead == 0) {
                        // 对端优雅关闭
                        std::cout << "Client FD " << curFd << " disconnected." << std::endl;
                        connectionClosed = true;
                        break;
                    } 
                    else { // bytesRead == -1
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            // 缓冲区数据全部读完,符合 ET 预期,正常退出循环
                            break;
                        } else {
                            perror("read");
                            connectionClosed = true;
                            break;
                        }
                    }
                }

                // 如果连接关闭或出错,清理资源
                if (connectionClosed) {
                    close(curFd);
                    epoll_ctl(epollFd, EPOLL_CTL_DEL, curFd, nullptr);
                }
            }

            // 可选:处理对端关闭(EPOLLRDHUP),这里读事件已经覆盖了 0 字节情况
            if (evt & EPOLLRDHUP) {
                std::cout << "FD " << curFd << " sent RDHUP (half-close)." << std::endl;
                // 通常此时立即关闭,无需再读
                close(curFd);
                epoll_ctl(epollFd, EPOLL_CTL_DEL, curFd, nullptr);
            }
        }
    }

    // 清理
    close(listenFd);
    close(epollFd);
    return 0;
}

代码关键点解析(ET 模式)

非阻塞是 ET 的命脉:set_nonblocking 必须设置。如果不设置,当 while(read()) 读到最后一次时,线程会永久阻塞在 read 上。

accept 也要循环:高并发下可能瞬间涌入多个连接,ET 模式下 listenFd 只触发一次 EPOLLIN,必须循环 accept 直到 EAGAIN,否则会有连接"饿死"。

EAGAIN 是"读完"的信号:在 ET 下,read 返回 -1 且 errno == EAGAIN,是成功的标志,表示本次数据已清空,可以安全退出循环。

EPOLLRDHUP:强烈建议加上,可以在对端关闭写端时(FIN)提前感知,避免多余的 read 调用

相关推荐
dogstarhuang2 小时前
OpenAI GPT-5.6 降价后如何重算 API 账单?多模型路由与成本治理实战
服务器·网络·人工智能·大模型·api·ai应用开发·接口管理
苏宸啊2 小时前
linux网络编程udp服务器和客户端代码编写(echo版本和字典版本)
linux·网络
~|Bernard|2 小时前
Linux 定时任务(Cron)完整教程
linux·运维·服务器
小周学学学3 小时前
vmware-horizon第一章:horizon服务器安装
运维·服务器·vmware
小雪崩3 小时前
嵌入式学习 day25:哈希表及排序与查找
linux·c语言·数据结构·学习·排序算法
奇树谦4 小时前
HDD 为什么适合顺序大文件读写:从 RAID 5 到 RAID 50 的原理与性能分析
linux·运维·网络
hi_ro_a4 小时前
Linux基础开发工具详解
linux·运维·服务器
嵌入式阿蔡5 小时前
Linux_01:交叉编译与开发环境实战——从单片机跨到 Linux 的第一道坎
linux·运维·网络·单片机·嵌入式硬件·嵌入式实时数据库
风静如云5 小时前
Openssl:Win11(Chrome)安装客户端证书
服务器