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

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

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


🎬 艾莉丝的简介:


文章目录

  • 前言
  • [1 ~> epoll 整体定位与诞生背景](#1 ~> epoll 整体定位与诞生背景)
    • [1.1 两个问题引入epoll](#1.1 两个问题引入epoll)
    • [1.2 前代方案的核心瓶颈](#1.2 前代方案的核心瓶颈)
    • [1.3 epoll 技术定位](#1.3 epoll 技术定位)
    • [1.4 内核版本说明](#1.4 内核版本说明)
  • [2 ~> epoll 三大核心系统调用](#2 ~> epoll 三大核心系统调用)
    • [2.1 epoll_create:创建 epoll 实例](#2.1 epoll_create:创建 epoll 实例)
      • [2.1.1 函数原型](#2.1.1 函数原型)
      • [2.1.2 参数与返回值](#2.1.2 参数与返回值)
      • [2.1.3 功能本质](#2.1.3 功能本质)
    • [2.2 epoll_ctl:管控监听事件(增/删/改)](#2.2 epoll_ctl:管控监听事件(增/删/改))
      • [2.2.1 函数原型](#2.2.1 函数原型)
      • [2.2.2 参数详解](#2.2.2 参数详解)
      • [2.2.3 返回值](#2.2.3 返回值)
      • [2.2.4 功能本质](#2.2.4 功能本质)
    • [2.3 epoll_wait:等待并获取就绪事件](#2.3 epoll_wait:等待并获取就绪事件)
      • [2.3.1 函数原型](#2.3.1 函数原型)
      • [2.3.2 参数详解](#2.3.2 参数详解)
      • [2.3.3 返回值](#2.3.3 返回值)
      • [2.3.4 功能本质](#2.3.4 功能本质)
    • [2.4 epoll操作函数知识图谱](#2.4 epoll操作函数知识图谱)
  • [3 ~> 核心数据结构与事件宏](#3 ~> 核心数据结构与事件宏)
    • [3.1 struct epoll_event 事件结构体](#3.1 struct epoll_event 事件结构体)
      • [3.1.1 结构体完整定义](#3.1.1 结构体完整定义)
      • [3.1.2 字段说明](#3.1.2 字段说明)
    • [3.2 标准事件宏(比特位标识)](#3.2 标准事件宏(比特位标识))
      • [3.2.1 基础读写事件](#3.2.1 基础读写事件)
      • [3.2.2 异常事件](#3.2.2 异常事件)
      • [3.2.3 模式控制标志](#3.2.3 模式控制标志)
    • [3.3 ~> 内核核心结构体(底层原理)](#3.3 ~> 内核核心结构体(底层原理))
      • [3.3.1 struct eventpoll](#3.3.1 struct eventpoll)
      • [3.3.2 struct epitem](#3.3.2 struct epitem)
    • [3.4 epoll的结构相关知识图谱](#3.4 epoll的结构相关知识图谱)
      • [3.4.1 结构](#3.4.1 结构)
      • [3.4.2 细节](#3.4.2 细节)
  • [4 ~> epoll 内核完整工作原理](#4 ~> epoll 内核完整工作原理)
    • [4.1 核心组件分工](#4.1 核心组件分工)
    • [4.2 完整工作流程](#4.2 完整工作流程)
    • [4.3 与 select/poll 本质差异](#4.3 与 select/poll 本质差异)
    • [4.4 epoll函数的二次理解](#4.4 epoll函数的二次理解)
    • [4.5 epoll工作原理知识图谱](#4.5 epoll工作原理知识图谱)
  • [5 ~> epoll 基础服务端代码实战(Version1版本)](#5 ~> epoll 基础服务端代码实战(Version1版本))
    • [5.1 工程依赖说明](#5.1 工程依赖说明)
    • [5.2 服务端类实现(EpollServer.hpp)](#5.2 服务端类实现(EpollServer.hpp))
    • [5.3 主函数(Main.cc)](#5.3 主函数(Main.cc))
    • [5.4 编译脚本(Makefile)](#5.4 编译脚本(Makefile))
    • [5.5 代码逻辑与现存缺陷](#5.5 代码逻辑与现存缺陷)
      • [5.5.1 执行流程](#5.5.1 执行流程)
      • [5.5.2 已知缺陷](#5.5.2 已知缺陷)
      • [5.5.3 运行测试](#5.5.3 运行测试)
    • [5.6 Version1版本代码知识图谱](#5.6 Version1版本代码知识图谱)
  • [6 ~> 重难点与面试高频考点总结](#6 ~> 重难点与面试高频考点总结)
    • [6.1 基础概念考点](#6.1 基础概念考点)
    • [6.2 模式核心考点](#6.2 模式核心考点)
    • [6.3 代码实操考点](#6.3 代码实操考点)
    • [6.4 性能对比面试题](#6.4 性能对比面试题)
    • [6.5 底层原理延伸考点](#6.5 底层原理延伸考点)
  • 结尾


前言

一、 开头部分

文章导入语

在 Linux IO 多路转接技术体系中,selectpoll 受限于内核轮询遍历、数据拷贝、文件描述符集合维护等性能瓶颈,无法支撑海量并发场景。epoll 作为 Linux 内核专属的高性能多路转接方案,在 2.5.44 版本内核正式推出,重构了事件监听与就绪通知的底层逻辑,依托红黑树、就绪队列与内核回调机制,彻底解决了前代方案的性能缺陷,成为 Linux 平台高并发网络服务的首选技术。本文将从诞生背景、核心 API、内核底层原理、事件模式、完整代码实战、问题分析等维度,系统性梳理 epoll 全套知识,串联理论与工程落地,构建完整的知识闭环。

文章框架思维导图

bash 复制代码
epoll 高性能多路转接 知识总览
├─ 基础认知
│  ├─ 诞生背景:解决 select/poll 遍历、拷贝、性能短板
│  ├─ 技术定位:Linux 专属多路转接、事件通知机制
│  └─ 内核版本:Linux 2.5.44 引入,2.6 版本成熟
├─ 三大核心系统调用
│  ├─ epoll_create:创建 epoll 实例、epoll 句柄
│  ├─ epoll_ctl:对 fd 执行增/删/改监听事件
│  └─ epoll_wait:阻塞等待就绪事件,获取结果
├─ 核心数据结构
│  ├─ struct epoll_event:事件描述、联合体存储fd/自定义数据
│  ├─ 事件宏:EPOLLIN / EPOLLOUT / EPOLLERR / EPOLLHUP 等
│  └─ 内核结构体:eventpoll、epitem(红黑树节点+就绪队列节点)
├─ 内核底层原理
│  ├─ 核心组件:红黑树(托管监听fd)、就绪队列(存储就绪节点)
│  ├─ 回调机制:套接字底层回调触发节点入就绪队列
│  └─ 整体工作流程:创建实例 → 注册监听 → 等待就绪 → 事件处理
├─ epoll 两种工作模式
│  ├─ LT 水平触发:默认模式,数据未读完持续通知
│  └─ ET 边缘触发:仅状态变化时通知,需非阻塞IO配合
├─ 特殊事件标志
│  ├─ EPOLLET:开启边缘触发
│  └─ EPOLLONESHOT:单次事件触发,触发后失效
├─ 代码实战(基础epoll服务端)
│  ├─ 工程结构与基础组件依赖
│  ├─ 完整 C++ 服务端代码实现
│  ├─ 编译脚本与运行测试
│  └─ 代码缺陷与优化方向
├─ 技术对比
│  └─ epoll VS select / poll:性能、数据结构、工作逻辑差异
└─ 重难点 & 面试高频考点

1 ~> epoll 整体定位与诞生背景

1.1 两个问题引入epoll

1.2 前代方案的核心瓶颈

selectpoll 是传统 IO 多路转接实现,二者存在共性性能问题,也是 epoll 诞生的核心动因:

  1. 内核全量遍历:无论文件描述符是否就绪,内核都需要遍历整个监听集合检测事件,文件描述符数量越多,遍历耗时越高。
  2. 用户态与内核态数据拷贝:每次系统调用都需要将完整的监听集合在用户态和内核态之间拷贝,海量 fd 场景下开销剧增。
  3. 参数维护成本select 需要反复重置 fd 集合;poll 虽解耦入参出参,但仍无法规避遍历与拷贝问题。

1.3 epoll 技术定位

epoll 是 Linux 内核专属、专为大批量文件描述符 设计的 IO 多路转接方案,本质依旧是事件等待 + 就绪通知 机制。它并非简单改造 poll,而是重构了底层数据结构与事件触发逻辑,是 Linux 2.6 及以上版本中综合性能最优的多路转接实现。

1.4 内核版本说明

epoll 首次随 Linux 2.5.44 内核发布,在 Linux 2.6 系列内核完成功能迭代与稳定性优化,目前所有主流 Linux 发行版均完整支持该技术。


2 ~> epoll 三大核心系统调用

epoll 拆分了监听注册、事件等待两类行为,提供三组独立系统调用,分别负责创建实例、管控监听 fd、等待就绪事件。所有接口依赖头文件 <sys/epoll.h>

2.1 epoll_create:创建 epoll 实例

2.1.1 函数原型

c 复制代码
#include <sys/epoll.h>
int epoll_create(int size);

2.1.2 参数与返回值

  1. 参数 size :早期内核用于提示内核预期监听的文件描述符总数,用于预分配内存。自 Linux 2.6.8 起,该参数被内核忽略,但传参必须保证大于 0。内核可动态扩容红黑树,不再依赖该数值限制容量。
  2. 返回值 :调用成功返回一个非负整数,即 epoll 句柄(文件描述符) ;调用失败返回 -1,并设置 errno
  3. 补充说明 :Linux 后续推出 epoll_create1(int flags),废弃了 size 参数,推荐在新项目中使用。

2.1.3 功能本质

调用该函数后,内核会创建一个独立的 epoll 实例,内部初始化红黑树 (存储所有被监听的文件描述符)与双向就绪队列(存储已触发事件的节点)。由于 Linux 一切皆文件,epoll 实例也以文件描述符的形式对外暴露。

2.2 epoll_ctl:管控监听事件(增/删/改)

2.2.1 函数原型

c 复制代码
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

2.2.2 参数详解

  1. epfd :由 epoll_create 返回的 epoll 句柄,指定操作目标 epoll 实例。
  2. op :操作类型,支持三种枚举值:
    1. EPOLL_CTL_ADD:向 epoll 实例中新增待监听的文件描述符与对应事件。
    2. EPOLL_CTL_DEL:从 epoll 实例中删除指定文件描述符,停止监听。
    3. EPOLL_CTL_MOD修改已注册文件描述符的监听事件类型。
  3. fd:需要被管控的目标文件描述符(套接字、管道等)。
  4. eventstruct epoll_event 结构体指针,描述需要监听的事件与用户自定义数据。

2.2.3 返回值

调用成功返回 0,失败返回 -1 并设置错误码。

2.2.4 功能本质

该函数是用户态与内核红黑树的交互入口,完成对监听 fd 的增删改操作,同时会修改套接字底层的回调函数指针,为后续事件自动触发做准备。

2.3 epoll_wait:等待并获取就绪事件

2.3.1 函数原型

c 复制代码
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

2.3.2 参数详解

  1. epfd:epoll 实例句柄。
  2. events :用户态数组首地址,输出型参数。内核会将就绪的事件与 fd 信息填充到此数组中。
  3. maxeventsevents 数组的最大元素个数,限制单次获取的事件数量,防止内存越界。
  4. timeout :超时时间,单位为毫秒,行为规则与 poll 完全一致:
    1. timeout < 0(常用 -1):永久阻塞,直到有事件触发才返回。
    2. timeout = 0:非阻塞,立即检测并返回结果。
    3. timeout > 0:限时阻塞,等待指定毫秒数,超时无事件则返回。

2.3.3 返回值

  1. 返回值 > 0:实际就绪的事件数量,取值范围 [1, maxevents]
  2. 返回值 = 0:等待超时,无任何事件就绪。
  3. 返回值 < 0:调用出错。

2.3.4 功能本质

该函数仅检测内核的就绪队列 :若队列非空,内核将队列中的就绪节点拷贝到用户态数组并返回;若队列为空,则根据 timeout 规则阻塞进程。检测队列的时间复杂度为 O(1)。

2.4 epoll操作函数知识图谱


3 ~> 核心数据结构与事件宏

3.1 struct epoll_event 事件结构体

该结构体用于定义监听事件、存储用户数据,是 epoll 三大接口的核心载体。

3.1.1 结构体完整定义

c 复制代码
#include <sys/epoll.h>

// 联合体:四选一存储自定义数据
union epoll_data {
    void    *ptr;    // 通用指针,可挂载自定义对象
    int      fd;     // 常用:存储文件描述符
    uint32_t u32;    // 32位无符号整型
    uint64_t u64;    // 64位无符号整型
};
typedef union epoll_data epoll_data_t;

// epoll 事件主体结构
struct epoll_event {
    uint32_t events;  // 事件位图,使用比特位标识监听/就绪事件
    epoll_data_t data;// 用户数据,内核不会修改此字段
};

3.1.2 字段说明

  1. events:比特位图,组合多个事件宏,作为入参时表示用户需要监听的事件,作为出参时表示内核返回的就绪事件。
  2. data :联合体类型,内核全程不会篡改该字段。工程中最常用 data.fd 存储当前关联的文件描述符,也可通过 ptr 挂载业务对象实现面向对象编程。

3.2 标准事件宏(比特位标识)

所有事件宏均为无符号整型比特位,通过位运算组合使用。分为基础读写事件、异常事件、工作模式标志三大类。

3.2.1 基础读写事件

宏定义 含义 使用场景
EPOLLIN 读事件就绪 fd 缓冲区有数据可读、TCP 对端正常关闭连接
EPOLLOUT 写事件就绪 fd 发送缓冲区有空闲空间,可写入数据
EPOLLPRI 高优先级数据可读 接收 TCP 带外数据,工程极少使用

3.2.2 异常事件

这类事件无需用户主动在 events 中注册,内核会自动监听并在事件触发时返回:

  1. EPOLLERR:文件描述符发生底层错误。
  2. EPOLLHUP:连接挂起,如管道对端关闭、TCP 连接异常断开。

3.2.3 模式控制标志

用于修改 epoll 的工作逻辑,通过位或 | 与基础事件组合使用:

  1. EPOLLET:开启**边缘触发(ET)**模式,epoll 默认是水平触发(LT)。
  2. EPOLLONESHOT :单次触发模式。事件触发后该 fd 自动失效,如需继续监听必须调用 EPOLL_CTL_MOD 重新配置。

3.3 ~> 内核核心结构体(底层原理)

3.3.1 struct eventpoll

epoll 实例的顶层内核结构体,一个 epoll_create 调用对应一个该结构体实例:

c 复制代码
struct eventpoll {
    spinlock_t    lock;        // 自旋锁,保护并发访问
    struct mutex  mtx;         // 互斥锁,保护增删改操作
    wait_queue_head_t wq;      // epoll_wait 等待队列
    struct list_head  rdllist; // 就绪队列(双向链表,存储就绪节点)
    struct rb_root    rbr;     // 红黑树根节点(存储所有监听fd节点)
    struct epitem    *ovflist; // 溢出链表,临时存储迁移中的节点
    struct user      *user;    // 所属用户信息
};

3.3.2 struct epitem

红黑树与就绪队列的共用节点,一个被监听的 fd 对应一个 epitem 对象,同一节点可同时挂载在红黑树与就绪队列

c 复制代码
struct epitem {
    struct rb_node    rbn;     // 红黑树节点
    struct list_head  rdllink; // 就绪队列链表节点
    struct epoll_filefd ffd;   // 关联的文件描述符
    struct epoll_event event;  // 绑定的事件信息
    struct eventpoll *ep;      // 归属的 epoll 实例
    // 其他辅助队列、引用计数等字段
};

3.4 epoll的结构相关知识图谱

epoll的结构分成下面两部分来阐述:

3.4.1 结构

3.4.2 细节


4 ~> epoll 内核完整工作原理

4.1 核心组件分工

  1. 红黑树 :由 eventpoll->rbr 指向,存储所有用户通过 epoll_ctl 注册的文件描述符与监听事件。红黑树作为平衡二叉搜索树,基于 fd 作为键值,保证 fd 增、删、查操作时间复杂度为 O(logN)。
  2. 就绪队列 :由 eventpoll->rdllist 指向,双向链表结构。当 fd 事件触发时,对应的 epitem 节点会被自动移入该队列。epoll_wait 仅遍历该队列,无全局遍历开销。
  3. 回调机制 :Linux 套接字结构体 struct sock 中预置 sk_data_readysk_write_space 等回调函数指针。当网卡收到数据、缓冲区状态变化时,内核硬件中断会触发套接字回调,将对应 epitem 节点加入就绪队列。

4.2 完整工作流程

  1. 创建实例 :调用 epoll_create,内核分配 eventpoll 结构体,初始化空红黑树与空就绪队列,返回 epoll 句柄。
  2. 注册监听 :调用 epoll_ctl(EPOLL_CTL_ADD),内核创建 epitem 节点插入红黑树,同时设置套接字回调函数。
  3. 事件触发 :网卡接收数据/缓冲区状态变化,触发套接字回调,将 epitem 节点移入就绪队列。
  4. 等待事件 :调用 epoll_wait,内核检测就绪队列。队列非空则拷贝事件到用户态数组并返回;队列为空则阻塞进程。
  5. 事件处理:用户遍历返回的事件数组,执行业务读写逻辑。
  6. 销毁监听 :fd 关闭前,调用 epoll_ctl(EPOLL_CTL_DEL) 从红黑树中删除节点,释放内核资源。

4.3 与 select/poll 本质差异

  1. 数据结构:select/poll 使用数组/位图,epoll 使用红黑树+就绪队列。
  2. 事件检测:select/poll 内核全局遍历所有 fd;epoll 仅检测就绪队列,由回调主动推送就绪事件。
  3. 数据拷贝:select/poll 每次调用全量拷贝监听集合;epoll 仅拷贝少量就绪事件,拷贝开销极低。

4.4 epoll函数的二次理解

4.5 epoll工作原理知识图谱


5 ~> epoll 基础服务端代码实战(Version1版本)

本节基于 C++ 实现LT 模式、仅监听读事件 的 epoll TCP 服务端,代码复用通用套接字、日志组件,完全基于前文的 select/poll 代码架构改造,同时修正原始代码中的语法错误与逻辑漏洞。

5.1 工程依赖说明

项目依赖公共基础组件:Socket.hpp(TCP 套接字封装)、Logger.hpp(日志工具)、InetAddr.hpp(网络地址封装)、Mutex.hpp(互斥锁),下文仅展示核心业务代码、主函数与编译脚本。

5.2 服务端类实现(EpollServer.hpp)

cpp 复制代码
#pragma once
#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include <cstring>
#include <unistd.h>
#include "Socket.hpp"
#include "Logger.hpp"
#include "InetAddr.hpp"

// 单次最大接收事件数
static const int gmax_events = 128;
// 默认服务端口
static const uint16_t gdefault_port = 8080;

class EpollServer
{
public:
    // 构造函数:初始化监听套接字、创建epoll实例
    EpollServer(uint16_t port = gdefault_port)
        : _port(port),
          _listensockfd(std::make_unique<TcpSocket>())
    {
        // 1. 创建、绑定、监听套接字
        _listensockfd->BuildSocketMethod(_port);
        // 2. 创建epoll实例,size传任意正数即可
        _epfd = epoll_create(10);
        if (_epfd < 0)
        {
            LOG(LogLevel::FATAL) << "epoll_create failed";
            exit(EXIT_FAILURE);
        }
        LOG(LogLevel::INFO) << "listen fd: " << _listensockfd->Socketfd() 
                            << " , epoll fd: " << _epfd;
    }

    // 析构函数:关闭epoll句柄
    ~EpollServer()
    {
        if (_epfd >= 0)
        {
            close(_epfd);
        }
    }

    // 事件派发主入口
    void Dispatcher()
    {
        // 1. 初始化事件结构体,注册监听套接字到epoll
        struct epoll_event ev;
        memset(&ev, 0, sizeof(ev));
        ev.events = EPOLLIN;          // 监听读事件
        ev.data.fd = _listensockfd->Socketfd();

        // 添加监听套接字
        int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensockfd->Socketfd(), &ev);
        if (ret != 0)
        {
            LOG(LogLevel::FATAL) << "epoll_ctl add listen fd failed";
            exit(EXIT_FAILURE);
        }
        LOG(LogLevel::DEBUG) << "add listen fd to epoll success";

        // 就绪事件存储数组
        struct epoll_event rev_events[gmax_events];
        int timeout = -1; // 永久阻塞,生产环境推荐

        // 事件循环
        while (true)
        {
            // 等待就绪事件
            int ready_num = epoll_wait(_epfd, rev_events, gmax_events, timeout);
            if (ready_num == 0)
            {
                LOG(LogLevel::INFO) << "epoll wait timeout";
                continue;
            }
            else if (ready_num < 0)
            {
                LOG(LogLevel::ERROR) << "epoll_wait error";
                break;
            }

            // 处理所有就绪事件
            EventHandler(rev_events, ready_num);
        }
    }

private:
    // 统一事件处理器
    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;

            // 读事件就绪
            if (events & EPOLLIN)
            {
                // 监听套接字:新连接到来
                if (fd == _listensockfd->Socketfd())
                {
                    Accepter();
                }
                // 普通客户端套接字:数据可读
                else
                {
                    IOHandler(fd);
                }
            }
        }
    }

    // 接收新客户端连接
    void Accepter()
    {
        InetAddr client_addr;
        // 接受连接,epoll已检测就绪,不会阻塞
        int client_fd = _listensockfd->Accepter(&client_addr);
        if (client_fd < 0)
        {
            LOG(LogLevel::ERROR) << "accept new client failed";
            return;
        }
        LOG(LogLevel::INFO) << "new client connected, fd: " << client_fd
                            << " , addr: " << client_addr.StringAddress();

        // 将新客户端fd注册到epoll,监听读事件
        struct epoll_event ev;
        memset(&ev, 0, sizeof(ev));
        ev.events = EPOLLIN;
        ev.data.fd = client_fd;

        int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, client_fd, &ev);
        if (ret != 0)
        {
            LOG(LogLevel::ERROR) << "add client fd to epoll failed";
            close(client_fd);
            return;
        }
    }

    // 客户端IO读写处理
    void IOHandler(int fd)
    {
        char buffer[1024] = {0};
        ssize_t n = recv(fd, buffer, sizeof(buffer) - 1, 0);

        if (n > 0)
        {
            // 读取到有效数据,回显内容
            LOG(LogLevel::INFO) << "client data: " << buffer;
            std::string echo = "echo: " + std::string(buffer);
            send(fd, echo.c_str(), echo.size(), 0);
        }
        else if (n == 0)
        {
            // 客户端正常断开连接
            LOG(LogLevel::INFO) << "client disconnect, fd: " << fd;
            // 先从epoll删除监听,再关闭fd(fd必须合法才能删除)
            epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);
            close(fd);
        }
        else
        {
            // 读写异常
            LOG(LogLevel::WARNING) << "recv error, fd: " << fd;
            epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);
            close(fd);
        }
    }

private:
    uint16_t _port;
    int _epfd;                          // epoll实例句柄
    std::unique_ptr<TcpSocket> _listensockfd; // 监听套接字
};

5.3 主函数(Main.cc

cpp 复制代码
#include "EpollServer.hpp"
#include <memory>

int main()
{
    // 创建并启动epoll服务端
    std::unique_ptr<EpollServer> svr = std::make_unique<EpollServer>();
    svr->Dispatcher();
    return 0;
}

5.4 编译脚本(Makefile)

bash 复制代码
# 编译目标
epoll_server: Main.cc
        g++ -o $@ $^ -std=c++17

# 清理规则
.PHONY: clean
clean:
        rm -f epoll_server

5.5 代码逻辑与现存缺陷

5.5.1 执行流程

  1. 初始化阶段:创建监听套接字、epoll 实例,将监听 fd 注册到 epoll。
  2. 事件循环:循环调用 epoll_wait 等待就绪事件。
  3. 事件分发:区分监听 fd(接收新连接)与客户端 fd(读写数据)。
  4. 连接销毁:客户端断开/异常时,先从 epoll 移除 fd,再关闭文件描述符。

5.5.2 已知缺陷

  1. 仅实现读事件监听,未处理 EPOLLOUT 写事件。
  2. 基于字节流 TCP,未做报文分包处理,存在粘包问题。
  3. 当前为 LT 模式,未实现 ET 边缘触发,不支持极限高并发。
  4. 固定事件数组大小,未实现动态扩容。

5.5.3 运行测试

  1. timeout = -1:永久阻塞,无事件时进程休眠,CPU 占用为 0。
  2. timeout = 0:非阻塞轮询,持续循环检测,CPU 占用极高。
  3. timeout = 2000:2 秒限时阻塞,超时打印日志。

5.6 Version1版本代码知识图谱


6 ~> 重难点与面试高频考点总结

6.1 基础概念考点

  1. epoll 三大函数分工epoll_create 创建实例、epoll_ctl 管控监听 fd、epoll_wait 等待就绪事件。
  2. size 参数演变 :Linux 2.6.8 之后 epoll_create 的 size 参数失效,仅要求传正数。
  3. 内核组件:红黑树存储监听 fd,就绪队列存储就绪节点,回调函数实现事件主动推送。
  4. 事件宏区分EPOLLERREPOLLHUP 无需手动注册,内核自动监听。

6.2 模式核心考点

  1. LT 与 ET 区别:LT 数据未读完持续通知;ET 仅状态变化时通知一次,ET 必须搭配非阻塞 IO。
  2. EPOLLONESHOT 作用:单次触发,常用于多线程模型,避免同一 fd 被多线程同时处理。
  3. select/poll 与 epoll 模式对应:二者仅支持 LT 水平触发。

6.3 代码实操考点

  1. fd 销毁顺序 :调用 EPOLL_CTL_DEL 时要求 fd 必须合法,因此先删监听、后关闭 fd
  2. ET 模式编码要求:循环读写直到缓冲区为空/写满,必须使用非阻塞文件描述符。
  3. epoll_data 联合体用法 :优先使用 data.fd 存储 fd,data.ptr 用于挂载自定义业务对象。

6.4 性能对比面试题

  1. epoll 性能优势根源:无全局遍历、仅拷贝就绪事件、内核回调主动推送,海量 fd 场景下性能碾压 select/poll。
  2. epoll 并非全场景最优:当监听 fd 数量极少、连接活跃度极高时,select/poll 与 epoll 性能差距极小。
  3. 内核数据结构优势 :红黑树保证增删查 O(logN),就绪队列保证事件检测 O(1)

6.5 底层原理延伸考点

  1. 节点复用epitem 节点同时挂载红黑树与就绪队列,无需重复创建对象。
  2. 回调触发链路 :网卡中断 → 套接字回调函数 → 节点加入就绪队列 → epoll_wait 唤醒进程。
  3. epoll 句柄本质 :Linux 下的普通文件描述符,使用完毕必须手动 close 释放资源。

结尾

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

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

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

往期回顾

【Linux网络】多路转接poll

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