Epoll的学习,在select和poll的基础上

1:回顾select和poll的核心特点

epoll是为了解决select/poll的性能缺陷设计的,先明确两者的短板,才能理解epoll的优势

1:select的核心

API

cpp 复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • 数据结构 :用3 个位图分别监听读、写、异常事件。
  • 致命缺陷
    1. fd 有上限:默认 1024,修改需重新编译内核;
    2. 输入输出合一:位图需要每次循环重置,使用繁琐;
    3. 全量拷贝:每次调用都把所有 fd 从用户态拷贝到内核态;
    4. O (n) 遍历:返回后必须遍历所有 fd 找就绪,并发高时效率暴跌。

2:poll核心

API

cpp 复制代码
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • 数据结构pollfd结构体数组(fd+监听事件events+返回事件revents)。
  • 改进点
    1. 无 fd 数量上限(仅受内存限制);
    2. 输入输出分离,不用重置参数。
  • 未解决的缺陷
    1. 每次调用仍全量拷贝结构体数组到内核;
    2. 返回后仍O (n) 遍历所有 fd 找就绪;
    3. 高并发、少就绪场景下,效率依然极低。

2:epoll的核心理论

epoll是Linux 2.5.44内核引入的高性能I/O多路转接方案,专为大批量fd,高并发场景设计,是Nginx/Redis/网关的核心依赖

1:epoll三大系统调用

epoll把注册事件和等待事件拆分独立调用,从根源解决性能问题。

1:epoll_create:创建epoll句柄
cpp 复制代码
int epoll_create(int size);
  • 作用:在内核创建eventpoll结构体,返回 epoll 文件描述符;
  • 注意:Linux2.6.8 后size参数被忽略,用完必须close关闭。
2:epoll_ctl:事件注册/修改/删除(只调用一次)
cpp 复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 核心作用:提前把要监听的 fd 告诉内核,不用每次循环重复传递;
  • op参数(3 种操作):
    • EPOLL_CTL_ADD:注册新 fd 到 epoll;
    • EPOLL_CTL_MOD:修改已注册 fd 的监听事件;
    • EPOLL_CTL_DEL:从 epoll 中删除 fd;

epoll_event结构体:

cpp 复制代码
typedef union epoll_data { void *ptr; int fd; } epoll_data_t;
struct epoll_event { uint32_t events; epoll_data_t data; };

常用事件宏:

  • EPOLLIN:可读;EPOLLOUT:可写;EPOLLET:边缘触发;EPOLLONESHOT:只监听一次。
3:epoll_wait:等待就绪事件(核心调用)
cpp 复制代码
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • 作用:只返回已就绪的 fd,不用遍历全量;
  • 参数:
    • events:用户态数组,内核把就绪事件拷贝到这里;
    • maxevents:数组最大长度;
    • timeout:-1 = 永久阻塞,0 = 立即返回,>0 = 超时毫秒数;
  • 返回值:就绪 fd 数量,0 = 超时,<0 = 出错。

2:epoll内核工作原理

内核为每个 epoll 句柄创建 **eventpoll结构体 **,包含两个关键成员:

  1. 红黑树rbr:存储所有被监听的 fd,高效增删改查 + 去重(O (logn));
  2. 就绪链表rdlist :仅存储已就绪的 fd,epoll_wait 直接读取(O (1))。

完整工作流程

  • epoll_ctl添加 fd 时,fd 挂载到红黑树 ,并与网卡驱动绑定回调函数ep_poll_callback
  • fd 就绪(有数据 / 可写)时,驱动触发回调,将 fd 加入就绪链表
  • epoll_wait直接拷贝就绪链表的 fd 到用户态,仅返就绪数量,无需遍历

3:epoll两种触发模式

1:水平触发LT
  • 原理:只要 fd 缓冲区有数据 / 可写epoll_wait持续返回就绪;
  • 例子:缓冲区 2KB 数据,read 读 1KB,下次epoll_wait仍通知读就绪,直到读完;
  • 特点:支持阻塞 / 非阻塞 IO,编程简单,不易丢数据。
2:边缘触发ET
  • 原理:仅当 fd从未就绪→就绪 时,仅通知一次
  • 例子:缓冲区 2KB 数据,read 读 1KB,下次epoll_wait不再通知,直到新数据到来;
  • 强制要求:必须搭配非阻塞 fd,必须一次性读完 / 写完所有数据;
  • 优势:减少epoll_wait触发次数,性能更高(Nginx 默认用 ET);
  • 缺点:编程复杂度高,容易丢数据。

4:epoll核心优点

  • 无 fd 上限:仅受系统内存限制;
  • 极少拷贝 :仅epoll_ctl时拷贝 fd 到内核,epoll_wait不重复拷贝;
  • O (1) 就绪检测:直接读就绪链表,不用遍历全量 fd;
  • 输入输出分离:事件注册与返回分离,无需重置参数;
  • 支持 ET 高效模式:极致适配高并发。

epoll没有使用内存映射 (mmap) ,就绪事件仍需内核→用户态拷贝,但仅拷贝就绪的少量 fd,开销极低。

3:select/poll/epoll的全方位对比

对比维度 select poll epoll
底层数据结构 3 个位图 pollfd 结构体数组 内核红黑树 + 就绪链表
fd 数量上限 有(默认 1024) 无(内存限制) 无(内存限制)
用户态→内核态拷贝 每次全量拷贝 每次全量拷贝 仅注册时拷贝 1 次
就绪 fd 查找效率 遍历全量 fd(O (n)) 遍历全量 fd(O (n)) 直接读就绪链表(O (1))
触发模式 仅水平触发 LT 仅水平触发 LT 支持 LT+ET 两种
编程复杂度 极高(重置位图 + 算最大 fd) 中等(无需重置) 低(三部曲,事件分离)
性能 低(并发 > 1000 暴跌) 中(无上限但仍遍历) 高(高并发优势极大)
跨平台性 全 Unix/Linux 支持 全 Unix/Linux 支持 仅 Linux 支持
适用场景 少量 fd、跨平台 中等 fd、跨平台 Linux 高并发、大批量连接

4:epoll实战代码

1:EpollLoop.hpp

cpp 复制代码
#pragma once
#include "EventLoop.hpp"
#include "../connection/Connection.hpp"

#include <vector>
#include <sys/epoll.h>
#include <mutex>
#include <unistd.h>

class EpollLoop : public EventLoop{
public:
    bool init() override;
    bool addConnection(Connection* conn) override;
    void run() override;

    ~EpollLoop();
private:
    int m_epfd;//epoll实力文件描述符
    std::vector<Connection*> m_connections;
    std::mutex mtx;
    //epoll最大监听数
    static const int MAX_EVENTS = 1024;
};

2:EpollLoop.cpp

cpp 复制代码
#include "EpollLoop.hpp"
#include "../../config/Config.hpp"
#include "../log/Logger.hpp"
#include "../utils/Utils.hpp"
#include "../connection/Connection.hpp"

#include <iostream>
#include <string.h>
#include <algorithm>

EpollLoop::~EpollLoop()
{
    Utils::closefd(m_epfd);
}

//初始化Epoll
bool EpollLoop::init()
{
    //创建epoll实例(EPOLL_CLOEXEC进程自动关闭释放)
    m_epfd=epoll_create1(EPOLL_CLOEXEC);
    if(Utils::checkError(m_epfd,"Epoll create"))
    {
        LOG_INFO("Epoll created successfully");
        m_connections.clear();
        return true;
    }
    return false;
}

//添加客户端到epoll
bool EpollLoop::addConnection(Connection* conn)
{
    if(conn==nullptr||m_epfd<0)
    {
        LOG_FATAL("Epoll addConnection failed");
        return false;
    }

    std::lock_guard<std::mutex> lock(mtx);
    int clientfd=conn->getFd();

    struct epoll_event ev{};
    ev.events = EPOLLIN;//监听读事件
    ev.data.ptr = conn;//直接绑定连接对象,高效查找

    //EPOLL_CTL_ADD.添加fd到epoll
    if(epoll_ctl(m_epfd,EPOLL_CTL_ADD,clientfd,&ev)<0)
    {
        LOG_FATAL("epoll_ctl add failed,fd=%d",clientfd);
        return false;
    }

    m_connections.push_back(conn);
    LOG_INFO("EpollLoop added connection fd=%d", clientfd);
    return true;
}

//epoll事件循环
// 作用:持续监听所有客户端的IO事件(读事件),有消息就处理,没有就阻塞等待
// 运行在独立的子线程中,不阻塞主线程接收新连接
void EpollLoop::run()
{
    LOG_INFO("EpollLoop event loop startd:%p",pthread_self());
    // 定义事件数组:接收内核返回的【就绪客户端事件】,最大存储1024个
    struct epoll_event events[MAX_EVENTS];
    bool has_valid_event=true;
    while(true)
    {
        //线程安全拷贝
        std::vector<Connection*> tmpConns;
        {
            std::lock_guard<std::mutex> lock(mtx);
            tmpConns = m_connections;
        }
        
        // 参数说明:
        // m_epfd:epoll实例描述符
        // events:存储就绪事件的数组
        // MAX_EVENTS:最大监听事件数
        // 5000:超时时间5000ms=5秒,超时自动返回,不会永久卡死
        int eventnum = epoll_wait(m_epfd,events,MAX_EVENTS,5000);

        // eventnum <= 0:代表超时 或 系统调用出错,不处理任何逻辑
        if(eventnum<=0)
        {
            continue;
        }
        else
        {
            if(has_valid_event)
            {
                LOG_INFO("Epoll Success");
                has_valid_event=false;
            }
        }

        //处理事件
        for(int i=0;i<eventnum;i++)
        {
            //从epoll事件中,取出我们绑定的Connection连接对象(裸指针)
            Connection* conn =(Connection*)events[i].data.ptr;

            // ===================== 安全过滤:跳过已断开的客户端 =====================
            // 客户端断开后,状态为CLOSED,直接跳过,不处理,避免野指针/无限打印
            if(conn == nullptr || conn->getStatus() == ConnStatus::CLOSED)
            {
                continue;
            }

            //处理正确事件
            // EPOLLIN:内核通知,客户端有数据可以读取
            if(events[i].events & EPOLLIN)
            {
                conn->readData();
            }
        }
    }
}

3:项目文件

https://github.com/silin-code/study-code/tree/c47bb5ef582a35c62c6f3fe80f214f76659e52f2/Project_TCPServer_Plus

相关推荐
脑子进水养啥鱼?2 小时前
PostgreSql CAST
数据库·postgresql
zhangchaoxies2 小时前
c++怎么在Linux下获取文件被最后一次访问的精确纳秒时间【进阶】
jvm·数据库·python
m0_747854522 小时前
c++怎么在Linux下获取文件被最后一次访问的精确纳秒时间【进阶】
jvm·数据库·python
2301_816660212 小时前
如何用HTML函数工具检测当前设备性能_内置诊断操作【操作】
jvm·数据库·python
zhangchaoxies2 小时前
CSS如何实现移动端视口适配_利用rem与vw单位构建响应式布局
jvm·数据库·python
m0_747854522 小时前
如何创建CDB公共用户_C##前缀强制规则与CONTAINER=ALL
jvm·数据库·python
Absurd5872 小时前
SQL如何高效提取每组首条记录 ROW_NUMBER优化策略
jvm·数据库·python
2501_914245932 小时前
CSS如何更改鼠标悬停时的指针样式_设置cursor属性为pointer或not-allowed
jvm·数据库·python
zjeweler2 小时前
宝藏网站推荐:云服务器特惠与网安学习资源的一站式聚合平台
运维·服务器·学习