【Linux网络】多路转接epoll(二):epoll的两种工作模式

🎬 个人主页艾莉丝努力练剑
专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录
Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬 艾莉丝的简介:


文章目录

  • 前言
    • [一、 开头部分(框架引入)](#一、 开头部分(框架引入))
      • [1. 导入语](#1. 导入语)
      • [2. 核心知识点概览](#2. 核心知识点概览)
  • [1 ~> epoll 两种工作触发模式](#1 ~> epoll 两种工作触发模式)
    • [1.1 LT 水平触发(默认模式)](#1.1 LT 水平触发(默认模式))
      • [1.1.1 工作规则](#1.1.1 工作规则)
      • [1.1.2 特性与场景](#1.1.2 特性与场景)
    • [1.2 ET 边缘触发](#1.2 ET 边缘触发)
      • [1.2.1 开启方式](#1.2.1 开启方式)
      • [1.2.2 工作规则](#1.2.2 工作规则)
      • [1.2.3 强制要求](#1.2.3 强制要求)
      • [1.2.4 特性与场景](#1.2.4 特性与场景)
    • [1.3 模式对比实测现象](#1.3 模式对比实测现象)
    • [1.4 EPOLLONESHOT 单次触发模式](#1.4 EPOLLONESHOT 单次触发模式)
  • [2 ~> epoll 工作模式的核心概念与直观模型](#2 ~> epoll 工作模式的核心概念与直观模型)
    • [2.1 两种模式的定义与核心差异](#2.1 两种模式的定义与核心差异)
    • [2.2 具象化类比:快递派发模型](#2.2 具象化类比:快递派发模型)
      • [2.2.1 LT 模式:持续通知的快递员](#2.2.1 LT 模式:持续通知的快递员)
      • [2.2.2 ET 模式:仅通知一次的快递员](#2.2.2 ET 模式:仅通知一次的快递员)
    • [2.3 硬件溯源:示波器电平原理](#2.3 硬件溯源:示波器电平原理)
  • [3 ~> 两种模式的底层工作机制与风险分析](#3 ~> 两种模式的底层工作机制与风险分析)
    • [3.1 LT 模式的工作逻辑](#3.1 LT 模式的工作逻辑)
    • [3.2 ET 模式的工作逻辑](#3.2 ET 模式的工作逻辑)
      • [3.2.1 ET 模式的核心风险:数据残留与逻辑死锁](#3.2.1 ET 模式的核心风险:数据残留与逻辑死锁)
  • [4 ~> ET 模式与非阻塞文件描述符的绑定逻辑](#4 ~> ET 模式与非阻塞文件描述符的绑定逻辑)
    • [4.1 核心结论](#4.1 核心结论)
    • [4.2 完整推导逻辑](#4.2 完整推导逻辑)
      • [4.2.1 为什么必须循环读取](#4.2.1 为什么必须循环读取)
      • [4.2.2 为什么必须设置非阻塞](#4.2.2 为什么必须设置非阻塞)
      • [4.2.3 ET 模式标准读取范式](#4.2.3 ET 模式标准读取范式)
    • [4.3 面试核心考点完整答题逻辑](#4.3 面试核心考点完整答题逻辑)
  • [5 ~> ET 模式高效性的原理](#5 ~> ET 模式高效性的原理)
    • [5.1 第一层:通知效率提升](#5.1 第一层:通知效率提升)
    • [5.2 第二层:TCP 滑动窗口优化](#5.2 第二层:TCP 滑动窗口优化)
    • [5.3 补充:LT 与 ET 的工程约束差异](#5.3 补充:LT 与 ET 的工程约束差异)
  • [6 ~> 两种模式的工程实践与代码框架](#6 ~> 两种模式的工程实践与代码框架)
    • [6.1 核心维度对比](#6.1 核心维度对比)
    • [6.2 事件注册代码实现](#6.2 事件注册代码实现)
    • [6.3 核心事件循环框架](#6.3 核心事件循环框架)
  • [7 ~> 通过EpollServer代码演示epoll的两种工作模式](#7 ~> 通过EpollServer代码演示epoll的两种工作模式)
    • [7.1 代码(EpollServer.hpp)](#7.1 代码(EpollServer.hpp))
    • [7.2 两种工作模式的演示](#7.2 两种工作模式的演示)
      • [7.2.1 LT模式(默认模式)](#7.2.1 LT模式(默认模式))
      • [7.2.2 ET模式(需要手动设置)](#7.2.2 ET模式(需要手动设置))
  • [8 ~> epoll的两种工作模式:知识图谱](#8 ~> epoll的两种工作模式:知识图谱)
  • [9 ~> 深度总结与核心考点复盘](#9 ~> 深度总结与核心考点复盘)
  • 结尾


前言

一、 开头部分(框架引入)

1. 导入语

在 Linux 高性能网络编程体系中,epoll 是 IO 多路转接技术的工业级标准实现,而水平触发(LT)与边缘触发(ET)是 epoll 最核心的两种工作模式,二者直接决定了内核事件通知的行为逻辑、服务器的并发吞吐上限,以及上层应用的编码范式。本文从直观类比、硬件溯源、底层机制、工程约束、性能原理五个维度完整拆解两种模式的全部细节,覆盖面试核心考点与工程实践陷阱,适用于体系化复习与漏洞排查。

2. 核心知识点概览

bash 复制代码
epoll 两种工作模式核心体系
┣━━ 基础概念层
┃   ┣━━ 🔹 水平触发(Level Triggered, LT)
┃   ┗━━ 🔹 边缘触发(Edge Triggered, ET)
┣━━ 直观模型与硬件溯源
┃   ┣━━ 快递派发类比模型
┃   ┃   ┣━━ LT:缓冲区有数据就持续通知
┃   ┃   ┗━━ ET:仅数据状态变化时通知一次
┃   ┗━━ 示波器电平原理
┃       ┣━━ LT:高电平持续触发
┃       ┗━━ ET:电平跳变沿触发
┣━━ 底层工作机制
┃   ┣━━ LT:数据非空则持续返回就绪事件
┃   ┣━━ ET:仅数据增量变化时触发一次
┃   ┗━━ 风险:ET未读完数据会残留且不再通知
┣━━ ET 模式工程约束
┃   ┣━━ 强制要求:文件描述符必须设为非阻塞
┃   ┣━━ 编码范式:循环读取直至返回 EAGAIN
┃   ┗━━ 风险根源:阻塞读无数据时会挂起进程
┣━━ 性能差异深层原理
┃   ┣━━ 通知效率:ET消除冗余通知,降低系统调用开销
┃   ┗━━ TCP优化:快速清空缓冲区,通告更大滑动窗口
┗━━ 工程实践对比
    ┣━━ LT:编码简单、兼容阻塞IO、稳定性高
    ┗━━ ET:编码要求高、性能上限高、强行为约束

1 ~> epoll 两种工作触发模式

epoll 支持**水平触发(LT)边缘触发(ET)**两种模式,select、poll 仅等价于 LT 模式。

1.1 LT 水平触发(默认模式)

1.1.1 工作规则

只要 fd 对应的缓冲区存在未处理的就绪数据epoll_wait 就会持续返回该事件,直到数据被完全读取/状态恢复。

1.1.2 特性与场景

  1. 优点:逻辑简单、代码容错率高,阻塞/非阻塞 IO 均可配合使用。
  2. 缺点:若业务代码处理不及时,会导致 epoll_wait 频繁触发事件,占用 CPU。
  3. 默认状态:epoll 不设置 EPOLLET 标志时,即为 LT 模式。

1.2 ET 边缘触发

1.2.1 开启方式

在注册事件时,通过位或添加 EPOLLET 标志,示例:

c 复制代码
ev.events = EPOLLIN | EPOLLET;

1.2.2 工作规则

仅在 fd 状态发生突变的瞬间触发一次事件:例如空缓冲区收到数据、满缓冲区变为可写。若本次未读完数据,剩余数据不会再次触发事件。

1.2.3 强制要求

ET 模式必须配合非阻塞 IO使用。单次事件触发后,需要循环读写直到缓冲区无数据/无空间,否则会造成数据滞留。

1.2.4 特性与场景

  1. 优点:事件触发次数最少,CPU 占用低,适合超高并发场景。
  2. 缺点:代码复杂度高,对 IO 读写逻辑要求严格。

1.3 模式对比实测现象

  1. LT 模式:客户端发送一次数据,服务端若只读取部分内容,epoll_wait 会反复通知读事件,日志持续刷屏。
  2. ET 模式:客户端发送一次数据,仅在数据到达瞬间触发一次事件,剩余数据不会再次通知。

1.4 EPOLLONESHOT 单次触发模式

该标志可与 LT/ET 组合使用。事件触发后,该 fd 会被 epoll 自动屏蔽,不再接收任何事件。若需要继续监听,必须调用 epoll_ctl(EPOLL_CTL_MOD) 重新配置事件。常用于多线程模型,保证一个 fd 同一时间仅被一个线程处理。


2 ~> epoll 工作模式的核心概念与直观模型

2.1 两种模式的定义与核心差异

epoll 作为 Linux 内核提供的 IO 多路转接接口,核心能力是批量监控大量文件描述符的 IO 就绪状态,并将就绪事件返回给应用层。根据内核通知就绪事件的触发条件不同,划分为两种工作模式:

  • 水平触发(Level Triggered, LT):只要文件描述符对应的内核缓冲区中存在未处理的数据,内核就会持续向应用层通知该文件描述符的就绪状态,直至数据被全部读取。
  • 边缘触发(Edge Triggered, ET):仅当文件描述符的状态发生跳变(即数据从无到有、数据量从少到多)的瞬间,内核才会向应用层发送一次就绪通知;若本次通知后应用层未将数据全部读取,内核不会再重复通知,直至下一次新数据到达。

2.2 具象化类比:快递派发模型

以快递员派发包裹的场景做映射理解:快递对应内核缓冲区的数据,快递员的通知电话对应内核的就绪事件,收件人对应应用层程序。

2.2.1 LT 模式:持续通知的快递员

该模式下的快递员责任心极强,只要三轮车上还留有收件人的未取包裹,就会反复拨打电话通知收件人取件;即便收件人每次只取走部分包裹,快递员也会持续致电,直至所有包裹全部被取走。 对应技术逻辑:只要接收缓冲区中存在未读数据,epoll_wait就会持续返回该文件描述符的就绪事件,应用层可以分多次读取数据,不存在数据遗漏的风险。

2.2.2 ET 模式:仅通知一次的快递员

该模式下的快递员仅在新包裹到达、包裹数量发生变化时拨打一次电话,无论收件人是否取件、是否取完,都不会再次致电;若收件人本次未取完包裹,只能等待下一批新包裹到达时,才会再次收到通知。 对应技术逻辑:仅当内核缓冲区的数据量发生增量变化时,才会触发一次就绪通知;若应用层本次未将缓冲区数据全部读取,剩余数据会一直驻留在内核缓冲区,且不会再触发新的就绪事件,存在数据残留甚至丢失的风险。

2.3 硬件溯源:示波器电平原理

两种模式的命名与设计逻辑,源自硬件电路与示波器中的电平触发机制,是软件设计对硬件思想的直接借鉴:

  • 水平触发:对应示波器的高电平触发规则,只要信号维持在高电平区间,就会持续触发采样。对应到软件中,只要缓冲区数据非空(处于 "有数据" 的高电平状态),就持续触发就绪通知。
  • 边缘触发:对应示波器的边沿触发规则,仅在信号从低电平跳转到高电平(上升沿)、或从高电平跳转到低电平(下降沿)的瞬间触发一次采样。对应到软件中,仅在数据状态发生跳变(无数据→有数据、少数据→多数据)的瞬间触发一次就绪通知。

3 ~> 两种模式的底层工作机制与风险分析

3.1 LT 模式的工作逻辑

LT 是 epoll 的默认工作模式,行为逻辑与 select、poll 保持一致,具备最好的兼容性:

  1. 当内核接收缓冲区收到新数据,缓冲区从空变为非空,epoll_wait返回该文件描述符的读就绪事件。
  2. 若应用层本次只读取了部分数据,缓冲区仍有剩余数据,下一次调用epoll_wait时,会立刻再次返回该文件描述符的就绪事件。
  3. 只要缓冲区数据未被清空,就绪通知就会持续触发,不存在数据被遗漏的可能。

LT 模式的编码门槛极低,兼容阻塞式 IO,应用层无需一次性读完所有数据,即便单次读取不完整,后续仍有机会继续读取,因此不易出现逻辑漏洞。

3.2 ET 模式的工作逻辑

ET 是 epoll 的高性能增强模式,触发条件严格限定为 "状态变化":

  1. 仅当缓冲区的数据量发生增量变化(新数据到达)时,才会产生一次就绪通知;数据被读取导致数据量减少,不会触发通知。
  2. 单次通知后,无论应用层是否读取数据、读取多少数据,内核都不会再重复发送该批次数据的就绪通知。
  3. 只有当下一批新数据到达,缓冲区数据量再次增加时,才会触发新的一次通知。

3.2.1 ET 模式的核心风险:数据残留与逻辑死锁

ET 模式存在明确的工程风险,若配合阻塞式 IO 使用,会直接导致数据残留,甚至引发业务逻辑死锁。 以典型的请求 - 响应模型为例:服务器需接收客户端 10KB 请求并处理完成后才返回响应,客户端只有收到响应后才会发送下一个请求:

  1. 客户端发送 10KB 数据到达服务器内核缓冲区,ET 模式触发一次就绪通知。
  2. 服务器使用阻塞read,单次仅读取 1KB 数据,剩余 9KB 驻留在内核缓冲区。
  3. 由于缓冲区数据量没有发生新的增量变化,ET 模式不会再次触发就绪通知,epoll_wait不会再返回该文件描述符。
  4. 服务器等待新的就绪通知来读取剩余 9KB 数据,而客户端等待服务器返回响应才会发送新数据,双方进入互相等待的死锁状态,9KB 数据永久残留。

该风险的本质是:ET 模式不会为 "已通知但未读完" 的数据重复发送事件,若应用层无法保证单次读完所有数据,就必须通过特定编码范式规避风险。


4 ~> ET 模式与非阻塞文件描述符的绑定逻辑

4.1 核心结论

在工程实践中,使用 ET 模式的 epoll,必须将对应的文件描述符设置为非阻塞模式。这并非接口语法的强制约束,而是避免进程挂起、保证数据完整读取的工程必要条件。

4.2 完整推导逻辑

4.2.1 为什么必须循环读取

ET 模式仅通知一次的特性,要求应用层在收到就绪通知后,必须将本次到达的所有数据全部从内核缓冲区读取到用户空间,否则剩余数据将无法再被触发读取。 由于应用层无法预先知道内核缓冲区中总共有多少数据,因此必须采用循环读取的方式,反复调用read接口,直至确认缓冲区已空。

4.2.2 为什么必须设置非阻塞

若文件描述符为阻塞模式,在循环读取的最后一轮,当内核缓冲区的数据已被全部读完时,read调用会进入阻塞状态,导致当前进程 / 线程被挂起,无法继续处理其他文件描述符的事件,整个事件循环直接卡死。 而将文件描述符设置为非阻塞后,当缓冲区无数据时,read会立即返回,并设置errnoEAGAINEWOULDBLOCK,以此作为数据读取完毕的标志,循环终止,进程可继续执行后续逻辑。

4.2.3 ET 模式标准读取范式

ET 模式下的读事件处理,必须遵循 "非阻塞 fd + 循环读取 + 错误码判断" 的固定范式,核心实现如下:

c 复制代码
// ET模式读事件处理标准流程
void handle_read(int fd) {
    char buf[BUFFER_SIZE];
    while (1) {
        ssize_t n = read(fd, buf, sizeof(buf));
        if (n > 0) {
            // 正常读取到数据,执行业务处理
            process_data(buf, n);
        } else if (n == 0) {
            // 对端关闭连接,执行资源清理
            close_connection(fd);
            break;
        } else {
            // 非阻塞模式下缓冲区已空,本次读取结束
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                break;
            }
            // 发生真实读取错误,执行错误处理
            handle_read_error(fd);
            break;
        }
    }
}

4.3 面试核心考点完整答题逻辑

为什么 ET 模式下必须将文件描述符设置为非阻塞模式,完整逻辑链如下:

  1. ET 模式仅在数据状态发生增量变化时触发一次通知,因此要求应用层必须在本次通知中读完所有缓冲区数据,否则剩余数据无法再被触发读取。
  2. 由于应用层无法预知缓冲区数据总量,必须采用循环读取的方式反复调用read
  3. 若文件描述符为阻塞模式,最后一次无数据的read调用会导致进程阻塞挂起,整个事件循环失效。
  4. 因此必须将 fd 设置为非阻塞,通过read返回EAGAIN来判定数据读取完毕,既保证数据全部读出,又避免进程阻塞。

5 ~> ET 模式高效性的原理

ET 模式被公认为高性能服务器的首选方案,其高效性并非单一因素决定,而是由两层机制共同作用的结果。

5.1 第一层:通知效率提升

LT 模式下,只要缓冲区有数据就会持续发送就绪通知,若应用层读取不及时,会产生大量重复的冗余通知,对应频繁的系统调用与内核态 / 用户态切换开销。 ET 模式仅在数据增量变化时发送一次通知,有效通知占比极高,彻底消除了重复通知带来的冗余系统调用开销,降低了内核的事件分发成本。

5.2 第二层:TCP 滑动窗口优化

ET 模式倒逼应用层尽快将数据从内核缓冲区读取到用户空间,使得接收缓冲区可以更快地腾出可用空间。 从 TCP 协议层面看,接收方会根据接收缓冲区的剩余空间向发送方通告窗口大小(Window Size)。接收缓冲区清空越快,可通告的窗口就越大,发送方就可以一次性发送更多的数据,从而提升单位时间内的网络传输吞吐量,降低 ACK 交互的频次开销。

5.3 补充:LT 与 ET 的工程约束差异

LT 模式同样可以采用非阻塞 + 循环读的编码方式,也能实现快速清空缓冲区的效果,但 LT 模式本身不会强制开发者这么做;而 ET 模式从机制上倒逼上层必须采用最优的读取范式。 在大型团队协作中,ET 模式的行为强约束更易保证代码的性能下限,这也是其工程层面的不可替代性。


6 ~> 两种模式的工程实践与代码框架

6.1 核心维度对比

对比维度 水平触发(LT) 边缘触发(ET)
触发条件 缓冲区非空则持续触发 仅数据量增量变化时触发一次
默认属性 epoll 默认模式 需显式设置EPOLLET标志
IO 兼容性 兼容阻塞 IO 与非阻塞 IO 必须配合非阻塞 IO 使用
编码复杂度 低,逻辑简单不易出错 高,必须遵循循环读范式
数据安全性 无数据残留风险 编码不当易导致数据丢失
系统调用开销 存在冗余通知,开销较高 无冗余通知,开销更低
性能上限 较低 更高,适用于高并发场景
适用场景 业务复杂、追求稳定性 高性能框架、极致并发优化

6.2 事件注册代码实现

默认情况下,向 epoll 实例中添加文件描述符为 LT 模式;若需启用 ET 模式,需在事件字段中按位或上EPOLLET标志:

cpp 复制代码
// 添加监听套接字到epoll(默认LT模式)
struct epoll_event ev;
ev.events = EPOLLIN;  // 监听读事件,默认水平触发
ev.data.fd = listen_sock;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);

// 启用边缘触发模式
struct epoll_event et_ev;
et_ev.events = EPOLLIN | EPOLLET;  // 读事件 + 边缘触发
et_ev.data.fd = conn_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock, &et_ev);

6.3 核心事件循环框架

epoll 的核心事件循环逻辑如下,两种模式的差异仅体现在事件处理函数的内部实现:

cpp 复制代码
const int MAX_EVENTS = 1024;
struct epoll_event revs[MAX_EVENTS];
int timeout = -1;  // 阻塞等待,直到有事件就绪

while (true) {
    int n = epoll_wait(epfd, revs, MAX_EVENTS, timeout);
    if (n == 0) {
        // 超时,无就绪事件
        continue;
    } else if (n < 0) {
        // 调用出错,处理错误逻辑
        if (errno == EINTR) continue;  // 被信号中断,重试
        break;
    }
    
    // 遍历所有就绪事件,逐个处理
    for (int i = 0; i < n; ++i) {
        int sockfd = revs[i].data.fd;
        if (revs[i].events & EPOLLIN) {
            // 读事件就绪,根据模式调用对应处理函数
            handle_io_event(sockfd);
        }
        // 其他事件处理(EPOLLOUT、EPOLLERR等)
    }
}

7 ~> 通过EpollServer代码演示epoll的两种工作模式

7.1 代码(EpollServer.hpp)

cpp 复制代码
#pragma once

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

static const uint16_t gport = 8080;
static const int gsize = 128;
static const int gnum = 64;

class EpollServer
{
public:
    EpollServer(uint16_t port = gport)
    : _port(port),
      _listensockfd(std::make_unique<TcpSocket>())
    {
        // 监听套接字
        _listensockfd->BuildSocketMethod(_port);
        // 创建epoll模型
        _epfd = epoll_create(gsize);
        if(_epfd < 0)
        {
            // 创建epoll失败
            LOG(LogLevel::ERROR) << "epoll_creare error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "listen sockfd: " << _listensockfd->Socketfd() << "epfd: " << _epfd; // 走到这步就创建成功了
    }

    void Accepter()
    {
        InetAddr clientaddr;
        int sockfd = _listensockfd->Accepter(&clientaddr);
        LOG(LogLevel::INFO) << "sockfd is : " << sockfd << "client addr: " << clientaddr.StringAddress();
        // 能不能recv(sockfd)? 不能! 因为还没有把这个sockfd添加到epoll中! 只能等到下次事件就绪了才能recv了!
        // sockfd,读取数据? 不能! 因为还没有把这个sockfd添加到epoll中! 只能等到下次事件就绪了才能recv了!
        // IO = 等 + 拷贝,把sockfd托管给epoll!等事件就绪了,才去recv数据! 这样就不会阻塞了! 这就是IO多路复用的核心思想!
        struct epoll_event ev;
        ev.events = EPOLLIN; // 监听读事件
        ev.data.fd = sockfd; // 用户数据
        int m = epoll_ctl(_epfd,EPOLL_CTL_ADD,sockfd,&ev);
        // 至此红黑树就有两个节点了!一个是listen sockfd,一个是刚刚accept到的sockfd!
        // 当这两个sockfd有事件就绪了,都会通知我们!我们就可以去处理了!
        // 因为全是回调,没有循环,节点越来越多,但是我们不需要担心,因为红黑树的查找效率是O(logN),
        // 所以即使有成千上万个节点,我们也能在很短的时间内找到我们需要的节点!这就是epoll的优势!所以我们不需要担心节点越来越多了!
        // 我们只需要关注事件就绪了就去处理就好了!
        // 即钓鱼的鱼竿数量多了,单位时间上鱼的概率高了!
        (void)m;
        LOG(LogLevel::DEBUG) << "epoll_ctl add event: " << sockfd;
    }

    void IOHandler(int fd)
    {
        // 怎么读呢?直接读取就可以了!因为这个fd已经被epoll托管了!
        // 当这个fd有数据就绪了,epoll就会通知我们!我们就可以去读取数据了!这就是IO多路复用的核心思想!
        char buffer[1024];  // BUG!
        ssize_t n = recv(fd,buffer,sizeof(buffer) - 1,0);
        if(n > 0)
        {
            buffer[n] = 0;
            LOG(LogLevel::INFO) << "client say# " << buffer;
            std::string echo_string = "echo# ";
            echo_string += buffer;

            // 直接发送回去!因为这个fd已经被epoll托管了!当这个fd有数据就绪了,epoll就会通知我们!
            // 我们就可以去发送数据了!这就是IO多路复用!
            send(fd,echo_string.c_str(),echo_string.size(),0);
        }
        else if(n == 0)
        {
            // 客户端关闭了连接
            int n = epoll_ctl(_epfd,EPOLL_CTL_DEL,fd,nullptr);  // 要删除fd,必须保证fd本身是合法的!
            // 如果fd已经被关闭了,那么这个fd就不合法了!所以要先关闭fd,再删除fd!这样就不会出问题了!
            LOG(LogLevel::INFO) << "client quit,epoll_ctl del event: " << fd;
            close(fd);
        }
        else
        {
            // 读取数据失败了
            int n = epoll_ctl(_epfd,EPOLL_CTL_DEL,fd,nullptr);  // 要删除fd,必须保证fd本身是合法的!
            // 如果fd已经被关闭了,那么这个fd就不合法了!所以要先关闭fd,再删除fd!这样就不会出问题了!
            LOG(LogLevel::INFO) << "recv error,epoll_ctl del event: " << fd;
            close(fd);
        }
    }

    void EventHandler(struct epoll_event revs[],int ready_num)
    {
        for(int i = 0;i < ready_num;i++)
        {
            uint32_t events = revs[i].events;   // 哪些事件就绪了
            int fd = revs[i].data.fd;  // 哪一个fd
            if(events & EPOLLIN)    
            {
                // 处理读事件
                // listen sockfd | normal sockfd
                if(fd == _listensockfd->Socketfd())
                {
                    // new link ready!
                    Accepter(); // 这部分代码封装到Accepter函数中了
                }
                else
                {
                    // normal data ready!
                    // 读写我依旧觉得碍事,封装一下
                    IOHandler(fd); // 这部分代码封装到IOHandler函数中了
                }
            }
            else if(events & EPOLLOUT)
            {
                // TODO
                // 处理写事件,这里先实现的前面的读事件,后续再实现写事件
            }
        }
    }

    void Dispatcher()
    {
        // 将listen sockfd添加到epoll中!
        struct epoll_event ev;
        ev.events = EPOLLIN; // 监听读事件
        ev.data.fd = _listensockfd->Socketfd(); // 用户数据

        int n = epoll_ctl(_epfd,EPOLL_CTL_ADD,_listensockfd->Socketfd(),&ev);
        if(n >= 0)
        {
            LOG(LogLevel::DEBUG) << "epoll_ctl add : " << _listensockfd->Socketfd() << " success";
        }
        // timeout: -1表示永远等待,0表示不等待,>0表示等待的时间,单位是毫秒
        // int timeout = 2000;
        // timeout为0就是非阻塞一直轮询,但是也可以获取新连接
        // int timeout = 0;
        int timeout = -1; // 永远等待,直到有事件就绪了才返回
        struct epoll_event revs[gnum];
        while(true)
        {
            int n = epoll_wait(_epfd,revs,gnum,timeout);
            if(n == 0)
            {
                LOG(LogLevel::INFO) << "time out...";
            }
            else if(n < 0)
            {
                LOG(LogLevel::ERROR) << "epoll_wait...";
                break;
            }
            else
            {
                // EventHandler(revs,n);    // 事件处理函数调用
                LOG(LogLevel::DEBUG) << "有事件就绪了......";   // LT模式
            }
        }
    }

    ~EpollServer()
    {}
private:
    uint16_t _port;
    // 监听套接字
    std::unique_ptr<Socket> _listensockfd;
    // 需要一个句柄
    int _epfd;
};

有事件就绪,我故意不处理。处理的哪个函数我注释掉了:

不处理怎么办?试一下看看。

7.2 两种工作模式的演示

7.2.1 LT模式(默认模式)

cpp 复制代码
ev.events = EPOLLIN;

一旦连接上就会快速的刷屏,服务器一直告诉我"有事件就绪了",这种就是LT(水平触发)。

select、poll的工作模式类似于一种LT模式。

7.2.2 ET模式(需要手动设置)

cpp 复制代码
ev.events = EPOLLIN | EPOLLET; 
// 监听读事件,边缘触发模式(LT模式是默认的,ET模式需要我们自己设置),
// 像这里这样我就是设置了ET模式了!ET模式需要我们自己设置,LT模式是默认的!
// 所以如果不设置的话就是LT模式了!

我发现:

服务器只是说了一次 ------这种工作模式就是ET


8 ~> epoll的两种工作模式:知识图谱


9 ~> 深度总结与核心考点复盘

本文完整拆解了 epoll 两种工作模式的全部核心逻辑,其本质是 "易用性" 与 "高性能" 之间的权衡,核心考点与重难点可归纳为以下四点:

  1. 触发机制本质差异 LT 的核心是 "状态"------ 只要处于 "有数据" 的状态就持续通知;ET 的核心是 "变化"------ 只有发生 "数据新增" 的状态跳变才通知一次。这是所有衍生特性的根源,也是理解两种模式的根本出发点。
  2. ET 非阻塞的强制逻辑 这是网络编程面试的高频必考题。完整逻辑链必须覆盖四个节点:ET 仅通知一次 → 必须一次性读完所有数据 → 只能通过循环读实现 → 阻塞读在无数据时会挂起进程 → 必须用非阻塞 + EAGAIN 判断结束。缺少任意一环都属于逻辑不完整。
  3. ET 高效性的两层原理 切勿仅回答 "通知次数少" 这一表层原因。完整答案必须包含两个层面:一是减少冗余系统调用,降低内核事件分发成本;二是倒逼快速清空接收缓冲区,扩大 TCP 通告窗口,提升网络传输吞吐量。第二层是多数学习者容易遗漏的深层原理。
  4. 工程选型原则 LT 胜在稳定、编码简单、不易出错,适合业务逻辑复杂、迭代速度快的场景;ET 胜在性能上限高、行为强约束,适合高并发、高性能的底层网络框架,如 Nginx、Redis 均默认采用 ET 模式。实际选型中,若无极致性能需求,LT 模式的工程稳定性更优。

结尾

uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!" "技术之路难免有困惑,但同行的人会让前进更有方向。" |

结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!

往期回顾

【Linux网络】多路转接epoll(一):epoll理论 + 编写v1版本代码

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა