多路复用 epoll -- epoll 的工作原理,epoll 的优缺点,epoll 实现 TCP 回显服务器

目录

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;
}

实现基于 `epoll` 的高性能多路复用服务器,涉及多个关键步骤和系统调用。以下是一个完整的实现思路和代码示例。 --- ### ### 1. 创建 epoll 实例 使用 `epoll_create()` 或 `epoll_create1()` 创建一个 epoll 实例,用于管理监听的文件描述符集合。通常传入一个大于零的值作为最大文件描述符数(现代 Linux 系统忽略该参数)[^3]。 ```c int epfd = epoll_create1(0); if (epfd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); } ``` --- ### ### 2. 监听 socket 并添加到 epoll 中 创建 TCP 套接字并绑定、监听。然后将监听套接字加入 epoll 实例中,设置为边缘触发(Edge Triggered, ET)模式以提高性能。 ```c struct sockaddr_in addr; int listen_sock = socket(AF_INET, SOCK_STREAM, 0); // 设置地址和端口 memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(8080); bind(listen_sock, (struct sockaddr *)&addr, sizeof(addr)); listen(listen_sock, SOMAXCONN); struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; // 边缘触发 ev.data.fd = listen_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE); } ``` --- ### ### 3. 循环等待事件并处理 使用 `epoll_wait()` 等待事件发生,并根据返的事件类型进行处理,如接受新连接或读写数据。 ```c #define MAX_EVENTS 1024 struct epoll_event events[MAX_EVENTS]; while (1) { int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_wait"); exit(EXIT_FAILURE); } for (int i = 0; i < nfds; ++i) { if (events[i].data.fd == listen_sock) { // 处理新连接 struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_fd = accept(listen_sock, (struct sockaddr *)&client_addr, &client_len); if (client_fd == -1) { perror("accept"); continue; } // 设置非阻塞 int flags = fcntl(client_fd, F_GETFL, 0); fcntl(client_fd, F_SETFL, flags | O_NONBLOCK); struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; ev.data.fd = client_fd; if (epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev) == -1) { perror("epoll_ctl: client_fd"); close(client_fd); } } else { // 处理客户端数据 int client_fd = events[i].data.fd; char buf[512]; ssize_t count = read(client_fd, buf, sizeof(buf)); if (count == -1) { perror("read"); close(client_fd); epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL); } else if (count == 0) { // 客户端关闭连接 close(client_fd); epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL); } else { // 数据 write(client_fd, buf, count); } } } } ``` --- ### ### 4. 支持多线程处理 为了提升并发能力,可以采用多线程模型。每个线程独立运行 `epoll_wait()`,或者将任务分发给工作线程池进行处理。例如: - **主线程负责监听新连接**,并将新连接分配给子线程。 - **每个子线程维护自己的 epoll 实例**,处理已连接客户端的数据读写。 也可以使用 **线程池 + 共享队列** 模式,将事件放入队列后由多个线程消费[^1]。 --- ### ### 5. 避免常见问题 在实际开发中,需要注意以下几个问题: - **共享缓冲区导致的数据覆盖问题**:每个连接应拥有独立的读写缓冲区,避免多个连接同时访问同一块内存区域[^2]。 - **缺乏分包机制**:TCP 是流式协议,需自行实现分包逻辑(如基于长度、结束符等)[^2]。 - **ET/LT 模式选择**:ET 模式效率更高但需要配合非阻塞 I/O 使用;LT 模式更简单但可能多次触发相同事件。 --- ### ### 6. 性能优化建议 - 使用非阻塞 I/O 配合边缘触发(ET),减少系统调用次数。 - 合理设置 `epoll_wait()` 的超时时间,避免 CPU 空转。 - 对于 HTTP 服务,解析请求报文时使用词法分析器(如 flex/bison)而非正则表达式,提高健壮性[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值