目录

1. epoll 介绍

2. epoll 的工作原理

2.1 epoll 的三大核心组件(数据结构)

2.2 epoll 模型的相关函数

2.3 关键结构体 epoll_event

2.4 epoll 工作流程

3. epoll 的优缺点

4. 使用 epoll 实现TCP 回显服务器

4.1 前置代码

4.2 代码实现


1. epoll 介绍

        epoll 是 Linux 内核特有的高性能 I/O 多路复用机制,设计目标是解决 select/poll 在高并发场景下的性能瓶颈(如 FD 数量限制、线性扫描效率低、拷贝开销大)。它是 Linux 高性能服务器(如 Nginx、Redis)的核心依赖,支持万级以上海量连接的高效监控,核心优势是 事件驱动、零拷贝、O (1) 就绪事件查询

2. epoll 的工作原理

2.1 epoll 的三大核心组件(数据结构)

        1. 红黑树:epoll 模型在内核中是使用红黑树来存储 “正在监控的 FD + 对应的事件”(如 FD=5 监控 EPOLLIN),红黑树支持 O (log n) 复杂度的增删改查,高效管理海量 FD。

        2. 就绪链表:当FD 状态变化(如客户端发送数据),内核会将该 FD 的事件从红黑树中取出(这里的取出不是将红黑树中的节点取出移动到就绪链表中,而是将红黑树的节点链入到就绪链表中,也就是该节点同时属于红黑树和就绪链表两个数据结构),加入就绪链表。

        3. 用户态缓冲区:该缓冲区是用户自定义的缓冲区,epoll_wait 直接将就绪链表中的事件拷贝到用户态缓冲区,无需二次遍历扫描就绪事件。

2.2 epoll 模型的相关函数

原型:
    int epoll_create(int size);
 
头文件:
    #include <sys/epoll.h>
 
参数:
    size:size 可以忽略,但是必须是一个大于 0 的整数
 
返回值:
    函数成功时,返回一个 fd,该 fd 为这个 epoll 实例的 fd
    函数错误时,返回 -1
功能:
    创建一个 epoll 实例,返回一个 epoll FD,内核为该实例分配红黑树存储监控的 FD 与事件以及就绪链表
存储触发的就绪事件。
原型:
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
 
头文件:
    #include <sys/epoll.h>
 
参数:
    epfd:待操作的 epoll 实例的 FD。
    op:对 fd 的具体操作。EPOLL_CTL_ADD,添加 FD 及监控事件;EPOLL_CTL_DEL,移除 FD 及监控事件;
    EPOLL_CTL_MOD,修改 FD 及监控事件。
    fd:待操作的 fd。
    event:具体的监控事件。

 
返回值:
    成功时返回 0,失败返回 -1.
功能:
    给 epfd 指向的 epoll 实例中添加/删除/改变 fd 及其监控事件。
原型:
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
 
头文件:
    #include <sys/epoll.h>
 
参数:
    epfd:待操作的 epoll 实例的 FD。
    events:用户定义的 epoll_event 数组。
    maxevents:最大可接收的就绪事件数,必须大于 0,且不能超过 events 数组的长度(避免数组越界)。
    timeout:超时时间。timeout > 0:阻塞 timeout 毫秒,若期间有事件就绪则立即返回;超时无就绪事件则返回 0;timeout = 0:非阻塞模式,立即返回(无论是否有就绪事件);timeout = -1:永久阻塞,直到有事件就绪或被信号中断。

 
返回值:
    成功时返回 0,失败返回 -1.
功能:
    给 epfd 指向的 epoll 实例中添加/删除/改变 fd 及其监控事件。

2.3 关键结构体 epoll_event

struct epoll_event {
    uint32_t     events;  // 事件类型(如 EPOLLIN、EPOLLOUT)
    epoll_data_t data;    // 用户数据(通常存储 FD 或自定义指针)
};

// 用户数据联合体(可选择存储 FD 或指针)
typedef union epoll_data {
    void        *ptr;  // 自定义指针(如指向 FD 对应的连接上下文)
    int          fd;   // 待监控的文件描述符(最常用)
    uint32_t     u32;  // 32 位整数(少用)
    uint64_t     u64;  // 64 位整数(少用)
} epoll_data_t;

        常见事件类型:

2.4 epoll 工作流程

​​​​​​​​​​​​​​        1. 初始化 epoll 实例(epoll_create):用户调用 int epfd = epoll_create(size),内核为该 epfd 分配独立的内存空间,创建红黑树和就绪链表,并返回 epfd

        2. 添加 / 修改 / 删除监控 FD(epoll_ctl):用户通过 epoll_ctl 操作红黑树,管理监控的 FD 及事件。

        3. 等待就绪事件(epoll_wait):用户创建 struct epoll_event events[64] 数组(用户态缓冲区),调用 int ready_num = epoll_wait(epfd, events, 64, -1);timeout=-1 表示永久阻塞,直到有事件就绪)。内核检查就绪链表是否为空:若非空:直接将就绪链表中的事件(最多 maxevents 个)批量拷贝到用户态 events 数组,返回拷贝的事件总数(ready_num)。若为空:根据 timeout 处理。

        4. 处理就绪事件epoll_wait 返回后,用户态遍历 events 数组,处理每个就绪 FD 的事件。

        5. 停止监控并销毁 epoll 实例(close (epfd)):不再需要监控时,用户调用 close(epfd);内核销毁该 epfd 对应的红黑树和就绪链表,释放所有关联资源,同时移除所有 FD 的监控。

3. epoll 的优缺点

        优点:

        1. 无 FD 数量硬限制,支持海量连接。

        2. 就绪事件查询效率高(O (1) 复杂度)epoll 内核维护 “就绪链表”,仅当 FD 状态变化时才加入链表,epoll_wait 直接从链表中批量拷贝就绪事件到用户态,无需遍历所有监控 FD,查询效率与 FD 总数无关(O (1))。

        3. 内核 / 用户态拷贝开销极低select/poll 每次调用需将完整的监控 FD 集合从用户态拷贝到内核态,返回时再拷贝回用户态,拷贝开销与 FD 总数成正比;epoll 仅在 epoll_ctl 时传递一次 FD 及事件配置,epoll_wait 仅拷贝 “就绪事件”(数量远小于总监控 FD)。

        缺点:

        1. 仅限 Linux 系统,无跨平台兼容性epoll 是 Linux 内核 2.6 版本后引入的特有机制,不支持 Windows、FreeBSD、macOS 等系统。

4. 使用 epoll 实现TCP 回显服务器

4.1 前置代码

        参考多路复用 select 中的前置代码。

4.2 代码实现

// EpollServer.hpp -- Epoll 服务器类

#pragma once

#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/epoll.h>
#include "Socket.hpp"

using namespace SocketModule;
using namespace LogModule;

class EpollServer
{
    const static int size = 64;
    const static int defaultfd = -1;

public:
    EpollServer(int port) : _listensock(std::make_unique<TcpSocket>()), _isrunning(false), _epfd(defaultfd)
    {
        // 1. 创建套接字并绑定端口号并开始进行监听
        _listensock->BuildTcpSocketMethod(port);

        // 2. 创建 epoll 模型
        _epfd = epoll_create(256);
        if (_epfd < 0)
        {
            LOG(LogLevel::FATAL) << "epoll_create error";
            exit(EPOLL_CREATE_ERR);
        }
        LOG(LogLevel::INFO) << "epoll_create success, epfd: " << _epfd;

        // 3. 将 listensock 托管到 epoll 模型中
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = _listensock->Fd(); // ev.data 是用户维护的数据,常见的是 fd
        int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->Fd(), &ev);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "add listensock to epoll failed";
            exit(EPOLL_CTL_ERR);
        }
        LOG(LogLevel::INFO) << "add listensock to epoll success, listensock: " << _listensock->Fd();
    }

    void Start()
    {
        _isrunning = true;
        int timeout = -1;
        while (_isrunning)
        {
            int n = epoll_wait(_epfd, _revs, size, timeout);
            switch (n)
            {
            case -1:
                // 1. epoll 错误
                LOG(LogLevel::ERROR) << "epoll error";
                break;
            case 0:
                // 2. epoll 阻塞等待超时
                LOG(LogLevel::INFO) << "epoll timeout...";
                break;
            default:
                // 3. 有事件就绪
                LOG(LogLevel::DEBUG) << "有事件就绪..., n: " << n;   // 若下列添加连接时为 ev.events |= EPOLLIN;添加这句会导致一直打印,还不知道原因
                Dispatcher(n); // 进行事件派发
                break;
            }
        }
        _isrunning = false;
    }

    void Stop()
    {
        _isrunning = false;
    }

    ~EpollServer()
    {
        _listensock->Close();
        if (_epfd > 0)
            close(_epfd);
    }

    // 事件派发器
    void Dispatcher(int rnum)
    {
        for (int i = 0; i < rnum; ++i)
        {
            // epoll也要循环处理就绪事件--这是应该的,本来就有可能有多个fd就绪!
            int sockfd = _revs[i].data.fd;
            uint32_t revent = _revs[i].events;
            if (revent & EPOLLIN)
            {
                // 读事件就绪
                if (sockfd == _listensock->Fd())
                {
                    // 读事件就绪 && 新连接到来
                    Accepter();
                }
                else
                {
                    // 读事件就绪 && 普通socket可读
                    Recver(sockfd);
                }
            }
        }
    }

    // 连接管理器
    void Accepter()
    {
        InetAddr client;
        int sockfd = _listensock->Accept(&client);
        if (sockfd >= 0)
        {
            LOG(LogLevel::INFO) << "get a new link, sockfd: " << sockfd << ", client is: " << client.StringAddr();
            // 将新连接托管给 epoll
            struct epoll_event ev;
            ev.events = EPOLLIN;
            ev.data.fd = sockfd;
            int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
            if (n < 0)
                LOG(LogLevel::WARNING) << "add sockfd to epoll failed";
            else
            {
                LOG(LogLevel::INFO) << "add sockfd to epoll success";
            }
        }
    }

    // IO处理器
    void Recver(int sockfd)
    {
        char buffer[1024];
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); // 这里读的时候会有 bug,不能保证一次读取全部的数据
        if (n > 0)
        {
            // 1. 读取到客户端传入的数据
            buffer[n] = 0;
            std::cout << "sockfd: " << sockfd << ", client say@ " << buffer << "\r\n";
        }
        else if (n == 0)
        {
            // 2. 客户端退出 -- 先将 fd 对应的内核数据结构 epoll_event 从内核中移除,再关闭连接
            LOG(LogLevel::INFO) << "sockfd: " << sockfd << ", client quit...";
            epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
            close(sockfd);
        }
        else
        {
            // 3. 读取出错 -- 关闭连接并将其 fd 从 pollfd 数组中移除
            LOG(LogLevel::ERROR) << "recv error";
            epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
            close(sockfd);
        }
    }

private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    int _epfd;                      // epoll 模型的 fd
    struct epoll_event _revs[size]; // 用于每次从就绪队列中提取的缓冲数组
};
// main.cc -- 主函数

#include "EpollServer.hpp"

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        exit(USAGE_ERR);
    }

    Enable_Console_Log_Strategy();
    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<EpollServer> svr = std::make_unique<EpollServer>(port);
    svr->Start();
    return 0;
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐