【Linux】多路转接poll、epoll

I/O 多路转接之 poll

一、poll 函数接口

cpp 复制代码
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

// pollfd结构(描述单个监控对象)
struct pollfd {
    int fd;         /* 要监控的文件描述符(-1表示忽略此结构体) */
    short events;   /* 期望监控的事件(输入参数) */
    short revents;  /* 实际发生的事件(输出参数,由内核填充) */
};

二、参数说明

  1. fdsstruct pollfd类型数组,每个元素对应一个待监控的文件描述符(fd),包含 "要监控的 fd、期望事件、实际事件" 三部分,实现输入输出参数分离。
  2. nfdsfds数组中有效元素的数量(即实际要监控的 fd 总数),无需计算 "最大 fd+1"。
  3. timeout :超时时间,单位为毫秒(ms) ,取值分三种:
    • -1:永久阻塞,直到有 fd 就绪或被信号中断;
    • 0:非阻塞模式,立即返回(仅检查当前 fd 状态,不等待);
    • 正数:最多阻塞timeout毫秒,超时无事件则返回 0。

三、events 与 revents 的取值

events(输入)用于指定要监控的事件,revents(输出)用于返回实际发生的事件,常用取值如下:

事件宏 含义说明
POLLIN 可读事件(如 socket 接收缓冲区有数据、对端正常关闭连接)
POLLOUT 可写事件(如 socket 发送缓冲区有空闲空间,可无阻塞写入数据)
POLLERR 错误事件(无需在events中设置,内核自动检测并填充到revents
POLLHUP 挂起事件(如 socket 对端关闭连接,或 fd 关联的设备断开)
POLLNVAL 无效 fd 事件(如 fd 未打开或已关闭,revents中返回,提示参数错误)

四、返回结果

  • 返回值 < 0 :调用失败,错误原因存于errno(如EINTR被信号中断、EINVAL参数无效);
  • 返回值 = 0:超时,指定时间内无任何 fd 就绪;
  • 返回值 > 0 :就绪的 fd 数量(即revents非 0 的pollfd结构体个数)。

五、socket 就绪条件

与 select 完全一致,核心判断标准如下:

读就绪

  • socket 接收缓冲区字节数 ≥ 低水位标记(SO_RCVLOWAT),读操作不阻塞且返回值 > 0;
  • 对端关闭连接(读操作返回 0);
  • 监听 socket 有新连接请求;
  • socket 发生错误(revents中会同时包含POLLINPOLLERR)。

写就绪

  • socket 发送缓冲区空闲字节数 ≥ 低水位标记(SO_SNDLOWAT),写操作不阻塞且返回值 > 0;
  • socket 写操作被关闭(如调用shutdown(fd, SHUT_WR),写操作会触发SIGPIPE);
  • 非阻塞connect成功或失败后。

异常就绪

  • socket 收到带外数据(TCP 紧急数据),revents中会包含POLLPRI

六、poll 的优点

相比 select,poll 主要解决了以下痛点:

  1. 无 fd 数量限制 :select 受FD_SETSIZE(默认 1024)限制,而 poll 通过数组长度nfds控制,理论上仅受系统资源(内存)限制;
  2. 接口更友好
    • 无需计算 "最大 fd+1",直接传入数组长度nfds
    • events(输入)与revents(输出)分离,无需每次调用前重新初始化监控集合(数组可复用,仅需重置revents);
  3. 错误事件处理更清晰POLLERRPOLLHUP等错误事件无需手动监控,内核会自动填充到revents,减少用户代码冗余。

七、poll 的缺点

poll 本质仍属于 "遍历式" I/O 多路复用,与 select 存在共通缺陷:

  1. 遍历开销高 :poll 返回后,需遍历整个fds数组检查revents,才能确定哪些 fd 就绪;当 fd 数量庞大(如万级)时,遍历耗时显著;
  2. 用户态 - 内核态拷贝开销 :每次调用 poll,需将整个fds数组从用户态拷贝到内核态;fd 数量越多,拷贝数据量越大,开销越高;
  3. 效率线性下降:若大量 fd 处于未就绪状态,内核仍需逐一检查每个 fd 的状态,导致效率随 fd 数量增加而线性降低。

八、poll 使用示例:监控标准输入

cpp 复制代码
#include <poll.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    struct pollfd poll_fd;  // 单个pollfd结构体(监控标准输入)
    poll_fd.fd = 0;         // 标准输入的fd为0
    poll_fd.events = POLLIN; // 期望监控"可读"事件
    poll_fd.revents = 0;    // 初始化输出事件为0

    for (;;) {  // 循环监控
        // 调用poll:监控1个fd,超时1000ms(1秒)
        int ret = poll(&poll_fd, 1, 1000);
        
        if (ret < 0) {  // 调用失败
            perror("poll");
            continue;
        } else if (ret == 0) {  // 超时无事件
            printf("poll timeout\n");
            continue;
        }

        // 检查是否为期望的可读事件
        if (poll_fd.revents == POLLIN) {
            char buf[1024] = {0};
            read(0, buf, sizeof(buf) - 1);  // 读取标准输入
            printf("stdin:%s", buf);
            poll_fd.revents = 0;  // 重置输出事件,避免下次误判
        }
    }
    return 0;
}

I/O 多路转接之 epoll

一、epoll 初识

按照 man 手册定义,epoll 是 "为处理大批量文件描述符而设计的改进版 poll",于 Linux 2.5.44 内核中引入。它解决了 select/poll 的效率瓶颈,是 Linux 2.6 + 下性能最优的 I/O 多路复用机制,被广泛用于高并发服务器(如 Nginx、Redis)。

核心优势:基于 "回调 + 红黑树 + 就绪队列",实现高效的 fd 监控,避免遍历和频繁拷贝

二、epoll 的相关系统调用

epoll 通过 3 个核心系统调用完成功能,分别对应 "创建句柄、注册事件、等待就绪" 三步。

1. epoll_create:创建 epoll 句柄

cpp 复制代码
#include <sys/epoll.h>
int epoll_create(int size);
  • 功能 :创建一个 epoll 实例(句柄),内核会为其分配一个eventpoll结构体(存储监控信息)。
  • 参数size:Linux 2.6.8 之后被忽略,仅需传入一个大于 0 的整数(历史用途是指定预分配的 fd 监控数量)。
  • 返回值 :成功返回 epoll 句柄(非负整数),失败返回 - 1(错误存于errno)。
  • 注意 :epoll 句柄使用后需调用close()关闭,避免资源泄漏。

2. epoll_ctl:注册 / 修改 / 删除事件

cpp 复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 功能 :向 epoll 句柄(epfd)中注册、修改或删f5t47u除指定 fd 的监控事件。

  • 参数说明

    参数 含义
    epfd epoll_create () 返回的 epoll 句柄
    op 操作类型,用 3 个宏表示:- EPOLL_CTL_ADD:新增 fd 及事件- EPOLL_CTL_MOD:修改已有 fd 的事件- EPOLL_CTL_DEL:删除 fd(event传 NULL)
    fd 要监控的文件描述符
    event struct epoll_event结构体,描述要监控的事件(opEPOLL_CTL_DEL时传 NULL)
  • 核心结构体:struct epoll_event

    cpp 复制代码
    struct epoll_event {
        uint32_t events;  // 要监控的事件(宏集合)
        epoll_data_t data;// 关联数据(如fd、指针等,供用户态识别fd)
    };
    
    // epoll_data_t是联合体,常用fd字段
    typedef union epoll_data {
        void    *ptr;    // 指向用户自定义数据
        int      fd;     // 关联的文件描述符(最常用)
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
  • events常用宏

    事件宏 含义说明
    EPOLLIN 可读事件(同 select/poll,如接收缓冲区有数据、对端关闭)
    EPOLLOUT 可写事件(同 select/poll,如发送缓冲区有空闲空间)
    EPOLLPRI 紧急数据可读(如 TCP 带外数据)
    EPOLLERR fd 错误事件(内核自动检测,无需手动注册)
    EPOLLHUP fd 挂起事件(如对端关闭,内核自动检测)
    EPOLLET 边缘触发(Edge Triggered,ET)模式(默认是水平触发 LT)
    EPOLLONESHOT 单次监控模式(事件触发后,该 fd 自动从 epoll 中移除,需重新注册才能再次监控)
  • 返回值 :成功返回 0,失败返回 - 1(如epfd无效、fd未打开、op操作非法)。

3. epoll_wait:等待事件就绪

cpp 复制代码
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • 功能:等待 epoll 句柄中监控的 fd 就绪,收集就绪事件并返回给用户态。

  • 参数说明

    参数 含义
    epfd epoll_create () 返回的 epoll 句柄
    events 用户态预分配的struct epoll_event数组,用于存储内核返回的就绪事件(输出参数)
    maxevents events数组的最大长度(必须≤epoll_create () 的size,且大于 0)
    timeout 超时时间(毫秒):- -1:永久阻塞- 0:非阻塞,立即返回- 正数:阻塞指定毫秒
  • 返回值

    • 成功:返回就绪的 fd 数量(即events数组中有效元素的个数);
    • 0:超时,无 fd 就绪;
    • -1:失败(错误存于errno,如EINTR被信号中断)。
  • 注意events数组需用户提前分配内存(如struct epoll_event events[1024];),内核仅负责将就绪事件拷贝到数组中,不负责内存分配。

三、epoll 工作原理

epoll 的高效依赖内核中的eventpoll结构体及 "红黑树 + 双链表 + 回调" 的设计,核心流程如下:

1. 内核核心结构体:eventpoll

每个 epoll 句柄对应一个eventpoll结构体,关键成员如下:

cpp 复制代码
struct eventpoll {
    struct rb_root rbr;        // 红黑树根节点:存储所有注册的fd及事件(高效增删查)
    struct list_head rdlist;   // 双链表:存储就绪的fd对应的事件(供epoll_wait读取)
    // 其他成员(如等待队列、锁等)
};

同时,每个注册的 fd 会对应一个epitem结构体(挂载到红黑树和双链表):

cpp 复制代码
struct epitem {
    struct rb_node rbn;        // 红黑树节点:用于挂载到eventpoll->rbr
    struct list_head rdllink;  // 双链表节点:就绪时挂载到eventpoll->rdlist
    struct epoll_filefd ffd;   // 关联的fd信息
    struct eventpoll *ep;      // 指向所属的eventpoll结构体
    struct epoll_event event;  // 注册的事件类型
};

2. 核心工作流程

  1. 创建 epoll 句柄 :调用epoll_create(),内核创建eventpoll结构体(初始化红黑树和双链表)。
  2. 注册事件 :调用epoll_ctl(EPOLL_CTL_ADD)
    • 内核创建epitem结构体,关联 fd 和事件;
    • epitem插入eventpoll的红黑树(红黑树保证增删查的时间复杂度为 O (log n));
    • 为 fd 注册内核回调函数(ep_poll_callback):当 fd 就绪时,回调函数会将epitem插入eventpoll的双链表(rdlist)。
  3. 等待就绪 :调用epoll_wait()
    • 若双链表rdlist非空,直接将链表中的就绪事件拷贝到用户态的events数组,返回就绪数量;
    • 若双链表为空,进程阻塞在 epoll 的等待队列中,直到有 fd 就绪(回调函数触发,将epitem加入双链表),或超时 / 被信号唤醒。
  4. 处理就绪事件 :用户态遍历events数组,根据data.fdevents处理对应的 IO 操作(如读、写)。

问题 1:你怎么看待就绪队列?答案:就绪队列,epoll 的本质是一个基于事件就绪的生产者消费者模型。epoll 接口是线程安全的。

问题 2:获取就绪事件,如果缓冲区大小不够了怎么办?答案:不影响,没拿完,默认给你保留着,下次拿。细节:内核会严格按照 0 下标开始,依次拷贝保存就绪事件和 fd。应用层处理就绪事件的时候,处理的全都是就绪的,基本不需要非法检测。

四、epoll 的优点(对比 select/poll)

  1. 接口更灵活 :拆分为epoll_create/epoll_ctl/epoll_wait,无需每次调用都重新注册所有 fd(仅首次注册,后续修改 / 删除即可);
  2. 数据拷贝轻量 :仅在epoll_ctl注册 / 修改时拷贝 fd 信息到内核,epoll_wait仅拷贝就绪事件(而非所有注册的 fd),拷贝开销极低;
  3. 无 fd 数量限制:红黑树存储注册的 fd,理论上仅受系统内存限制(支持十万级甚至百万级 fd 监控);
  4. 效率无衰减:无需遍历所有注册的 fd,仅需处理双链表中的就绪事件(时间复杂度为 O (1),与 fd 总数无关);
  5. 支持两种触发模式 :可通过EPOLLET选择边缘触发(ET),减少epoll_wait的返回次数,进一步提升效率。

五、epoll 工作方式:水平触发(LT)与边缘触发(ET)

epoll 支持两种事件触发模式,核心差异在于 "fd 就绪后,内核通知用户态的次数",用 "妈妈喊吃饭" 的例子可直观理解:

1. 水平触发(Level Triggered,LT):默认模式

  • 触发规则 :只要 fd 处于就绪状态(如读缓冲区有数据),每次调用epoll_wait都会返回该 fd 的就绪事件,直到 fd 的就绪状态消失(如数据被读完)。
  • 类比:妈妈喊你吃饭,你没动,妈妈会一直喊(直到你去吃)------"亲妈模式"。
  • 示例场景
    1. socket 接收缓冲区收到 2KB 数据,epoll_wait返回EPOLLIN
    2. 用户仅读取 1KB 数据,缓冲区剩余 1KB;
    3. 再次调用epoll_wait,仍会返回EPOLLIN(直到 2KB 数据被读完)。
  • 优点:编程简单,支持阻塞 / 非阻塞 IO;
  • 缺点 :若用户未及时处理就绪事件,epoll_wait会反复返回,可能增加系统调用次数。

2. 边缘触发(Edge Triggered,ET):需显式指定EPOLLET

  • 触发规则 :fd 的就绪状态从 "未就绪" 变为 "就绪" 时,epoll_wait仅返回一次该 fd 的就绪事件;后续即使 fd 仍处于就绪状态(如缓冲区有残留数据),epoll_wait也不会再返回,直到 fd 的就绪状态再次变化(如再次收到数据)。
  • 类比:妈妈喊你吃饭,你没动,妈妈就不喊了 ------"后妈模式"。
  • 示例场景
    1. socket 接收缓冲区收到 2KB 数据(状态从 "空"→"有数据"),epoll_wait返回EPOLLIN
    2. 用户仅读取 1KB 数据,缓冲区剩余 1KB(状态仍为 "有数据",无变化);
    3. 再次调用epoll_wait,不会返回该 fd 的EPOLLIN(需等待对端再次发送数据,状态变化后才触发)。
  • 关键要求 :ET 模式下,fd 必须设为非阻塞(fd必须设置为非阻塞是为了避免阻塞操作导致的进程挂起),且需一次性读完 / 写完缓冲区中的所有数据(避免残留数据无法处理);
  • 优点 :减少epoll_wait的返回次数,降低系统调用开销,适合高并发场景(如 Nginx 默认使用 ET 模式);
  • 缺点 :编程复杂度高,需处理非阻塞 IO 的 "重试" 逻辑(如读数据时循环调用read,直到返回EAGAIN/EWOULDBLOCK)。

3. LT 与 ET 对比总结

维度 水平触发(LT) 边缘触发(ET)
触发次数 就绪状态持续期间,每次epoll_wait都触发 仅状态从 "未就绪→就绪" 时触发一次
fd 状态要求 支持阻塞 / 非阻塞 必须为非阻塞
数据处理要求 可分多次处理缓冲区数据 必须一次性处理完缓冲区所有数据
编程复杂度 低(无需重试) 高(需循环重试,处理EAGAIN
效率 较低(可能多此系统调用) 高(减少系统调用次数)
适用场景 简单业务、低并发 高并发、高性能场景(如服务器)

LT vs ET:谁更高效?

  1. ET通知效率更高,有效通知的数量最大
  2. ET尽快读完所有数据,可以给对方更新一个更大的win窗口,提高对方滑动窗口大小,提高网络发送的报文并发度(同一时间段内,网络设备(如服务器、客户端)能同时发送的报文数量)。

六、epoll 的使用场景

epoll 的高效并非普适,需结合业务场景选择:

  • 适合场景:多连接、且仅少数连接活跃的高并发场景(如互联网 APP 的入口服务器、直播服务器)------ 此时 epoll 无需遍历大量 fd,仅处理活跃连接,效率优势明显。
  • 不适合场景:连接数少且全部活跃的场景(如系统内部服务器间通信)------ 此时 epoll 的红黑树、回调等机制反而增加开销,效率可能不如 select/poll。

七、epoll 使用示例

示例 1:epoll 服务器(LT 模式)

1. 封装 Epoll 类(tcp_epoll_server.hpp)
cpp 复制代码
#pragma once
#include <vector>
#include <functional>
#include <sys/epoll.h>
#include "tcp_socket.hpp"  // 假设已实现TcpSocket类(封装socket/bind/listen/accept等)

// 业务处理回调函数类型:输入请求,输出响应
typedef std::function<void (const std::string& req, std::string* resp)> Handler;

class Epoll {
public:
    Epoll() {
        // 创建epoll句柄(size传10,2.6.8后忽略)
        epoll_fd_ = epoll_create(10);
    }

    ~Epoll() {
        close(epoll_fd_);  // 关闭epoll句柄,释放资源
    }

    // 向epoll中添加fd及事件(LT模式,默认监控EPOLLIN)
    bool Add(const TcpSocket& sock) const {
        int fd = sock.GetFd();
        printf("[Epoll Add] fd = %d\n", fd);

        struct epoll_event ev;
        ev.data.fd = fd;          // 关联fd
        ev.events = EPOLLIN;      // 监控可读事件(LT模式)

        // 注册事件
        int ret = epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev);
        if (ret < 0) {
            perror("epoll_ctl ADD");
            return false;
        }
        return true;
    }

    // 从epoll中删除fd
    bool Del(const TcpSocket& sock) const {
        int fd = sock.GetFd();
        printf("[Epoll Del] fd = %d\n", fd);

        // 删除时event传NULL
        int ret = epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, fd, NULL);
        if (ret < 0) {
            perror("epoll_ctl DEL");
            return false;
        }
        return true;
    }

    // 等待fd就绪,输出就绪的TcpSocket
    bool Wait(std::vector<TcpSocket>* output) const {
        output->clear();
        struct epoll_event events[1000];  // 预分配1000个就绪事件的空间

        // 调用epoll_wait:永久阻塞(timeout=-1)
        int nfds = epoll_wait(epoll_fd_, events, sizeof(events)/sizeof(events[0]), -1);
        if (nfds < 0) {
            perror("epoll_wait");
            return false;
        }

        // 遍历就绪事件,收集对应的TcpSocket
        for (int i = 0; i < nfds; ++i) {
            TcpSocket sock(events[i].data.fd);
            output->push_back(sock);
        }
        return true;
    }

private:
    int epoll_fd_;  // epoll句柄
};

// Epoll服务器类
class TcpEpollServer {
public:
    TcpEpollServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {}

    // 启动服务器,传入业务处理回调
    bool Start(Handler handler) {
        // 1. 创建监听socket
        TcpSocket listen_sock;
        if (!listen_sock.Socket()) return false;

        // 2. 绑定IP和端口
        if (!listen_sock.Bind(ip_, port_)) return false;

        // 3. 开始监听
        if (!listen_sock.Listen(5)) return false;

        // 4. 创建Epoll对象,添加监听socket
        Epoll epoll;
        if (!epoll.Add(listen_sock)) return false;

        // 5. 事件循环
        for (;;) {
            // 6. 等待fd就绪
            std::vector<TcpSocket> ready_socks;
            if (!epoll.Wait(&ready_socks)) continue;

            // 7. 处理就绪的fd
            for (size_t i = 0; i < ready_socks.size(); ++i) {
                if (ready_socks[i].GetFd() == listen_sock.GetFd()) {
                    // 7.1 监听socket就绪:处理新连接
                    TcpSocket new_sock;
                    listen_sock.Accept(&new_sock, NULL, NULL);  // 接收新连接
                    epoll.Add(new_sock);  // 将新连接的socket加入epoll监控
                } else {
                    // 7.2 客户端socket就绪:处理请求
                    std::string req, resp;
                    bool ret = ready_socks[i].Recv(&req);  // 读取请求(假设Recv是阻塞读)
                    if (!ret) {
                        // 客户端关闭连接:从epoll删除并关闭socket
                        epoll.Del(ready_socks[i]);
                        ready_socks[i].Close();
                        continue;
                    }

                    handler(req, &resp);  // 调用业务回调生成响应
                    ready_socks[i].Send(resp);  // 发送响应
                }
            }
        }
        return true;
    }

private:
    std::string ip_;    // 服务器IP
    uint16_t port_;     // 服务器端口
};
2. 业务代码(dict_server.cc)

只需将服务器对象类型改为TcpEpollServer,传入业务回调(如字典查询)即可:

cpp 复制代码
#include "tcp_epoll_server.hpp"
#include <unordered_map>

// 字典业务回调:输入单词,输出释义
void DictHandler(const std::string& req, std::string* resp) {
    std::unordered_map<std::string, std::string> dict = {
        {"apple", "苹果"},
        {"banana", "香蕉"},
        {"orange", "橙子"}
    };
    auto it = dict.find(req);
    if (it != dict.end()) {
        *resp = it->second;
    } else {
        *resp = "未找到该单词";
    }
}

int main() {
    TcpEpollServer server("0.0.0.0", 8080);  // 监听所有IP的8080端口
    server.Start(DictHandler);  // 启动服务器,传入字典业务回调
    return 0;
}

示例 2:epoll 服务器(ET 模式)

ET 模式需修改两部分:1. TcpSocket 类支持非阻塞 IO;2. Epoll 类支持注册 ET 事件。

1. 修改 TcpSocket 类(支持非阻塞读写)

tcp_socket.hppTcpSocket类中添加非阻塞相关接口:

cpp 复制代码
class TcpSocket {
public:
    // 其他已有接口(Socket/Bind/Listen/Accept等)...

    // 设置fd为非阻塞模式
    bool SetNoBlock() {
        int fl = fcntl(fd_, F_GETFL);  // 获取当前状态标记
        if (fl < 0) {
            perror("fcntl F_GETFL");
            return false;
        }
        // 添加非阻塞标志(O_NONBLOCK)
        int ret = fcntl(fd_, F_SETFL, fl | O_NONBLOCK);
        if (ret < 0) {
            perror("fcntl F_SETFL");
            return false;
        }
        return true;
    }

    // 非阻塞读:循环读取直到缓冲区为空(处理ET模式的残留数据)
    bool RecvNoBlock(std::string* buf) const {
        buf->clear();
        char tmp[1024 * 10] = {0};  // 10KB临时缓冲区

        for (;;) {
            ssize_t read_size = recv(fd_, tmp, sizeof(tmp) - 1, 0);
            if (read_size < 0) {
                // EAGAIN/EWOULDBLOCK:非阻塞读时缓冲区为空,正常返回
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    break;
                }
                perror("recv");
                return false;
            } else if (read_size == 0) {
                // 对端关闭连接,返回false
                return false;
            }

            tmp[read_size] = '\0';
            *buf += tmp;

            // 读取到的数据小于缓冲区大小,说明已读完所有数据
            if (read_size < (ssize_t)sizeof(tmp) - 1) {
                break;
            }
        }
        return true;
    }

    // 非阻塞写:循环写入直到数据写完(处理发送缓冲区满的情况)
    bool SendNoBlock(const std::string& buf) const {
        ssize_t cur_pos = 0;        // 当前写入位置
        ssize_t left_size = buf.size();  // 剩余未写入数据大小

        for (;;) {
            ssize_t write_size = send(fd_, buf.data() + cur_pos, left_size, 0);
            if (write_size < 0) {
                // EAGAIN/EWOULDBLOCK:发送缓冲区满,重试
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    continue;
                }
                perror("send");
                return false;
            }

            cur_pos += write_size;
            left_size -= write_size;

            // 所有数据写入完成,退出循环
            if (left_size <= 0) {
                break;
            }
        }
        return true;
    }

private:
    int fd_;  // 类内部存储的socket fd
};
2. 修改 Epoll 类(支持 ET 模式注册)

修改Epoll::Add方法,增加epoll_et参数控制是否启用 ET 模式:

cpp 复制代码
class Epoll {
public:
    // 其他接口不变...

    // 添加fd,支持LT/ET模式(默认LT)
    bool Add(const TcpSocket& sock, bool epoll_et = false) const {
        int fd = sock.GetFd();
        printf("[Epoll Add] fd = %d\n", fd);

        struct epoll_event ev;
        ev.data.fd = fd;
        if (epoll_et) {
            ev.events = EPOLLIN | EPOLLET;  // ET模式:添加EPOLLET标志
        } else {
            ev.events = EPOLLIN;            // LT模式
        }

        int ret = epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev);
        if (ret < 0) {
            perror("epoll_ctl ADD");
            return false;
        }
        return true;
    }

    // 其他接口不变...
};
3. 修改 TcpEpollServer 的 Start 方法(使用非阻塞 IO)

在处理新连接和客户端请求时,使用非阻塞读写接口:

cpp 复制代码
bool TcpEpollServer::Start(Handler handler) {
    // 1-3步(创建监听socket、绑定、监听)与LT模式一致...

    Epoll epoll;
    epoll.Add(listen_sock);  // 监听socket仍用LT模式(ET模式需非阻塞accept,暂不实现)

    for (;;) {
        std::vector<TcpSocket> ready_socks;
        if (!epoll.Wait(&ready_socks)) continue;

        for (size_t i = 0; i < ready_socks.size(); ++i) {
            if (ready_socks[i].GetFd() == listen_sock.GetFd()) {
                // 处理新连接:设置为非阻塞,添加到epoll(ET模式)
                TcpSocket new_sock;
                listen_sock.Accept(&new_sock, NULL, NULL);
                new_sock.SetNoBlock();  // 关键:ET模式必须设为非阻塞
                epoll.Add(new_sock, true);  // 启用ET模式
            } else {
                // 处理客户端请求:使用非阻塞读
                std::string req, resp;
                bool ret = ready_socks[i].RecvNoBlock(&req);  // 非阻塞读
                if (!ret) {
                    epoll.Del(ready_socks[i]);
                    ready_socks[i].Close();
                    continue;
                }

                handler(req, &resp);
                ready_socks[i].SendNoBlock(resp);  // 非阻塞写
                printf("[client %d] req: %s, resp: %s\n", ready_socks[i].GetFd(), req.c_str(), resp.c_str());
            }
        }
    }
    return true;
}

附录:回调机制(内核代码片段)

epoll 的回调机制核心在于 fd 就绪时,内核通过ep_poll_callbackepitem加入就绪链表,关键代码如下:

1. epoll_ctl 中的回调注册

cpp 复制代码
static int ep_insert(struct eventpoll *ep, struct epoll_event *event, struct file *tfile, int fd) {
    // 其他初始化...
    struct ep_pqueue epq;
    epq.epi = epi;
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);  // 注册回调函数ep_ptable_queue_proc
    // 其他逻辑...
}

// 为fd注册内核回调
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt) {
    struct epitem *epi = ep_item_from_epqueue(pt);
    struct eppoll_entry *pwq;

    if ((pwq = kmem_cache_alloc(pwq_cache, SLAB_KERNEL))) {
        init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);  // 回调函数设为ep_poll_callback
        pwq->whead = whead;
        pwq->base = epi;
        add_wait_queue(whead, &pwq->wait);  // 将回调加入fd的等待队列
        // 其他逻辑...
    }
}

2. 就绪回调函数:ep_poll_callback

cpp 复制代码
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key) {
    struct epitem *epi = ep_item_from_wait(wait);
    struct eventpoll *ep = epi->ep;

    write_lock_irqsave(&ep->lock, flags);
    // 将就绪的epitem加入eventpoll的就绪链表(rdlist)
    list_add_tail(&epi->rdllink, &ep->rdlist);
    write_unlock_irqrestore(&ep->lock, flags);

    // 唤醒阻塞在epoll_wait的进程
    wake_up_interruptible(&ep->wq);
    // 其他逻辑...
}

3. socket 就绪时的回调触发

socket 接收数据后,内核会调用sock_def_readable,唤醒等待队列中的回调:

cpp 复制代码
void sock_def_readable(struct sock *sk, int len) {
    read_lock(&sk->sk_callback_lock);
    if (sk->sk_sleep && waitqueue_active(sk->sk_sleep)) {
        wake_up_interruptible(sk->sk_sleep);  // 唤醒等待队列,触发ep_poll_callback
    }
    // 其他逻辑...
}
相关推荐
跃渊Yuey2 小时前
【Linux】Linux进程信号产生和保存
linux·c语言·c++·vscode
CaspianSea2 小时前
清理 Ubuntu里不需要的文件
linux·运维·ubuntu
c++逐梦人2 小时前
命令⾏参数和环境变量
linux·操作系统·进程
天码-行空2 小时前
达梦数据库(DM8)详细安装教程
linux·运维·数据库
白驹过隙不负青春2 小时前
Centos7开启、关闭swap
linux·centos
负二代0.02 小时前
Linux下的软件管理
linux·运维
Web极客码2 小时前
WordPress维护指南
服务器·网络·wordpress
物理与数学2 小时前
Linux内核 mm_struct
linux·linux内核
leiming62 小时前
手写Linux C UDP通信
linux·c语言·udp