Linux epoll 详解与实战

一、什么是 epoll

epoll 是 Linux 内核提供的高性能 I/O 事件通知机制(I/O event notification facility),专门用于监控大量文件描述符上的事件。它是 selectpoll 的改进版本,解决了它们在处理大量并发连接时的性能瓶颈,是现代 Linux 高并发服务器的基石。

I/O 多路复用的演进

在理解 epoll 之前,我们需要了解 I/O 多路复用的背景。传统的阻塞 I/O 模型中,每个连接需要一个线程来处理,当并发连接数达到数万甚至数十万时,线程切换的开销会变得不可接受。I/O 多路复用允许单个线程同时监控多个文件描述符,当任何一个文件描述符就绪时,程序得到通知并进行处理。

┌─────────────────────────────────────────────────────────────────┐
│                    I/O 多路复用演进历史                          │
│                    Evolution of I/O Multiplexing                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   1983            1997              2002                        │
│    │               │                 │                          │
│    ▼               ▼                 ▼                          │
│ ┌──────┐       ┌──────┐         ┌──────┐                        │
│ │select│ ───▶ │ poll │ ──────▶ │epoll │                        │
│ └──────┘       └──────┘         └──────┘                        │
│    │               │                 │                          │
│    │               │                 │                          │
│    ▼               ▼                 ▼                          │
│ • 1024 fd 限制  • 无 fd 限制     • 无 fd 限制                   │
│ • O(n) 扫描     • O(n) 扫描      • O(1) 事件通知                │
│ • 每次拷贝全部  • 每次拷贝全部   • 只拷贝一次                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

为什么需要 epoll

传统的 selectpoll 存在以下问题:

第一个问题是每次调用都需要拷贝。每次调用 selectpoll 时,都需要将所有被监控的文件描述符从用户空间拷贝到内核空间。当监控 10000 个连接时,每次调用都要拷贝这 10000 个文件描述符的信息,即使其中只有几个有事件发生。

第二个问题是内核需要线性扫描。内核收到调用后,需要遍历所有文件描述符来检查哪些就绪。这意味着时间复杂度为 O(n),当 n 很大时,即使大部分连接都是空闲的,每次调用的开销也很大。

第三个问题是 select 有最大文件描述符数量限制。select 使用固定大小的位图来表示文件描述符集合,通常限制为 1024,这个限制在编译时就已确定。

┌─────────────────────────────────────────────────────────────────┐
│              select/poll 的性能问题                              │
│              Performance Issues with select/poll                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  User Space                         Kernel Space                │
│  用户空间                            内核空间                    │
│                                                                 │
│  ┌─────────────────┐                                            │
│  │ fd_set (10000)  │                                            │
│  │ [1,2,3,...,10000]────────────────────┐                       │
│  └─────────────────┘                    │                       │
│         │                               ▼                       │
│         │ Every call               ┌──────────┐                 │
│         │ 每次调用                 │  Copy    │                 │
│         │                          │  拷贝    │                 │
│         │                          └────┬─────┘                 │
│         │                               │                       │
│         │                               ▼                       │
│         │                          ┌──────────┐                 │
│         │                          │  Scan    │                 │
│         │                          │  O(n)    │                 │
│         │                          │  扫描    │                 │
│         │                          └────┬─────┘                 │
│         │                               │                       │
│         │                               ▼                       │
│         │                          Only 3 ready                 │
│         │                          只有 3 个就绪                 │
│         │                               │                       │
│         ▼                               │                       │
│  ┌─────────────────┐                    │                       │
│  │ Return & copy   │◀───────────────────┘                       │
│  │ back all 10000  │                                            │
│  │ 返回并拷贝全部  │                                            │
│  └─────────────────┘                                            │
│                                                                 │
│  Problem: 10000 fds copied, only 3 are ready!                   │
│  问题:拷贝了 10000 个 fd,只有 3 个就绪!                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

epoll 如何解决这些问题

epoll 通过以下机制解决上述问题:

首先,epoll 在内核中维护一个事件表。通过 epoll_ctl 注册文件描述符时,信息被保存在内核中,后续的 epoll_wait 调用不需要再次传递这些信息。这是"一次注册,多次使用"的模式。

其次,epoll 使用回调机制而非轮询。当文件描述符就绪时,内核通过回调函数将其加入就绪队列,epoll_wait 只需要检查这个就绪队列,时间复杂度为 O(1)。

最后,epoll 没有文件描述符数量限制。epoll 使用红黑树存储被监控的文件描述符,理论上只受系统资源限制。

┌─────────────────────────────────────────────────────────────────┐
│                    epoll 的解决方案                              │
│                    How epoll Solves the Problems                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  User Space                         Kernel Space                │
│  用户空间                            内核空间                    │
│                                                                 │
│                                   ┌─────────────────────────┐   │
│                                   │     epoll instance      │   │
│                                   │     epoll 实例          │   │
│  ┌──────────────┐                 │                         │   │
│  │epoll_ctl ADD │────────────────▶│  ┌─────────────────┐    │   │
│  │   (once)     │                 │  │   Red-Black     │    │   │
│  │  (只需一次)  │                 │  │   Tree          │    │   │
│  └──────────────┘                 │  │   红黑树        │    │   │
│                                   │  │   (all fds)     │    │   │
│                                   │  └────────┬────────┘    │   │
│                                   │           │             │   │
│                                   │           │ callback    │   │
│                                   │           │ 回调        │   │
│                                   │           ▼             │   │
│                                   │  ┌─────────────────┐    │   │
│  ┌──────────────┐                 │  │   Ready List    │    │   │
│  │ epoll_wait   │◀────────────────│  │   就绪队列      │    │   │
│  │              │   Only ready    │  │   (only ready)  │    │   │
│  │              │   只返回就绪    │  └─────────────────┘    │   │
│  └──────────────┘                 │                         │   │
│        │                          └─────────────────────────┘   │
│        ▼                                                        │
│  Returns only 3 ready fds                                       │
│  只返回 3 个就绪的 fd                                           │
│                                                                 │
│  Advantage: No matter how many fds, only ready ones returned!   │
│  优势:无论有多少 fd,只返回就绪的!                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

二、epoll 内部数据结构

理解 epoll 的内部数据结构有助于正确使用它并进行性能调优。

┌─────────────────────────────────────────────────────────────────┐
│                  epoll 内部数据结构                              │
│                  epoll Internal Data Structures                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│                    struct eventpoll                             │
│               ┌──────────────────────────┐                      │
│               │                          │                      │
│               │  ┌────────────────────┐  │                      │
│               │  │    Red-Black Tree  │  │                      │
│               │  │    红黑树          │  │                      │
│               │  │                    │  │                      │
│               │  │    Stores all      │  │                      │
│               │  │    registered fds  │  │                      │
│               │  │    存储所有注册的  │  │                      │
│               │  │    文件描述符      │  │                      │
│               │  │                    │  │                      │
│               │  │   ┌───┐            │  │                      │
│               │  │   │ 5 │            │  │                      │
│               │  │   └─┬─┘            │  │                      │
│               │  │    ╱ ╲             │  │                      │
│               │  │ ┌─┴─┐ ┌─┴─┐        │  │                      │
│               │  │ │ 3 │ │ 8 │        │  │                      │
│               │  │ └───┘ └───┘        │  │                      │
│               │  │                    │  │                      │
│               │  │  O(log n) lookup   │  │                      │
│               │  │  O(log n) 查找     │  │                      │
│               │  └────────────────────┘  │                      │
│               │                          │                      │
│               │  ┌────────────────────┐  │                      │
│               │  │    Ready List      │  │                      │
│               │  │    就绪链表        │  │                      │
│               │  │                    │  │                      │
│               │  │  ┌───┐   ┌───┐     │  │                      │
│               │  │  │fd3│──▶│fd8│──▶∅ │  │                      │
│               │  │  └───┘   └───┘     │  │                      │
│               │  │                    │  │                      │
│               │  │  O(1) access       │  │                      │
│               │  │  O(1) 访问         │  │                      │
│               │  └────────────────────┘  │                      │
│               │                          │                      │
│               │  ┌────────────────────┐  │                      │
│               │  │    Wait Queue      │  │                      │
│               │  │    等待队列        │  │                      │
│               │  │                    │  │                      │
│               │  │  Blocked threads   │  │                      │
│               │  │  waiting for events│  │                      │
│               │  │  阻塞等待事件的    │  │                      │
│               │  │  线程队列          │  │                      │
│               │  └────────────────────┘  │                      │
│               │                          │                      │
│               └──────────────────────────┘                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

红黑树(Red-Black Tree)用于存储所有被监控的文件描述符,支持 O(log n) 的插入、删除和查找操作。当调用 epoll_ctl 添加、修改或删除文件描述符时,操作的就是这棵红黑树。

就绪链表(Ready List)存储所有当前有事件发生的文件描述符。当某个文件描述符上有事件发生时,内核通过回调函数将其加入就绪链表。epoll_wait 调用时,只需要检查这个链表并返回其中的元素。

等待队列(Wait Queue)存储所有因调用 epoll_wait 而阻塞的进程或线程。当有事件发生时,内核会唤醒等待队列中的进程。


三、epoll 核心 API 详解

epoll 只有三个核心系统调用,简洁而强大。

epoll_create1 - 创建 epoll 实例

#include <sys/epoll.h>

int epoll_create1(int flags);

此函数创建一个 epoll 实例并返回对应的文件描述符。这个文件描述符本身也是一个资源,使用完毕后需要调用 close 关闭。

flags 参数通常传入 0。也可以使用 EPOLL_CLOEXEC 标志,使文件描述符在调用 exec() 系列函数时自动关闭,这是一个安全的做法,可以防止子进程意外继承 epoll 文件描述符。

返回值为 epoll 文件描述符,失败时返回 -1 并设置 errno

┌─────────────────────────────────────────────────────────────────┐
│                    epoll_create1 流程                            │
│                    epoll_create1 Flow                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   User Code                          Kernel                     │
│   用户代码                            内核                       │
│                                                                 │
│   int epfd = epoll_create1(0);                                  │
│        │                                                        │
│        │                                                        │
│        ▼                                                        │
│   ┌─────────┐                    ┌─────────────────────┐        │
│   │  Call   │ ──────────────────▶│ Allocate eventpoll │        │
│   │  调用   │                    │ struct in kernel   │        │
│   └─────────┘                    │ 在内核中分配       │        │
│                                  │ eventpoll 结构     │        │
│                                  └──────────┬──────────┘        │
│                                             │                   │
│                                             ▼                   │
│                                  ┌─────────────────────┐        │
│                                  │ Initialize:        │        │
│                                  │ • Red-black tree   │        │
│                                  │ • Ready list       │        │
│                                  │ • Wait queue       │        │
│                                  │ 初始化红黑树、     │        │
│                                  │ 就绪链表、等待队列 │        │
│                                  └──────────┬──────────┘        │
│                                             │                   │
│                                             ▼                   │
│   ┌─────────┐                    ┌─────────────────────┐        │
│   │ epfd=3  │◀───────────────────│ Return fd          │        │
│   │         │                    │ 返回文件描述符     │        │
│   └─────────┘                    └─────────────────────┘        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

注意:还有一个旧的 epoll_create(int size) 函数,其中 size 参数在现代内核中已被忽略,但必须大于 0。推荐使用 epoll_create1

epoll_ctl - 管理监控的文件描述符

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

此函数用于添加、修改或删除被监控的文件描述符。

epfdepoll_create1 返回的 epoll 文件描述符。

op 参数指定操作类型,有三个可选值:EPOLL_CTL_ADD 用于注册新的文件描述符到 epoll 实例,EPOLL_CTL_MOD 用于修改已注册的文件描述符的关注事件,EPOLL_CTL_DEL 用于从 epoll 实例中删除文件描述符。

fd 是要操作的目标文件描述符。

event 是指向 epoll_event 结构的指针,描述要监控的事件和关联的数据。

struct epoll_event {
  uint32_t events;    // Epoll events (EPOLLIN, EPOLLOUT, etc.)
                      // 事件类型
  epoll_data_t data;  // User data variable
                      // 用户数据
};

typedef union epoll_data {
  void *ptr;          // Pointer to user-defined data
                      // 指向用户定义数据的指针
  int fd;             // File descriptor
                      // 文件描述符
  uint32_t u32;       // 32-bit integer
  uint64_t u64;       // 64-bit integer
} epoll_data_t;

epoll_data 是一个联合体,允许用户在事件中携带额外信息。最常用的是 fd 字段,用于在事件触发时识别是哪个文件描述符;ptr 字段可以指向用户自定义的连接对象。

┌─────────────────────────────────────────────────────────────────┐
│                    epoll_ctl 操作示意                            │
│                    epoll_ctl Operations                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│                        Red-Black Tree                           │
│                        红黑树                                    │
│                                                                 │
│                           ┌───┐                                 │
│                           │ 5 │                                 │
│                           └─┬─┘                                 │
│                            ╱ ╲                                  │
│                         ┌─┴─┐ ┌─┴─┐                             │
│                         │ 3 │ │ 8 │                             │
│                         └───┘ └───┘                             │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                         │    │
│  │  EPOLL_CTL_ADD fd=6:                                    │    │
│  │  添加 fd=6:                                             │    │
│  │                                                         │    │
│  │                           ┌───┐                         │    │
│  │                           │ 5 │                         │    │
│  │                           └─┬─┘                         │    │
│  │                            ╱ ╲                          │    │
│  │                         ┌─┴─┐ ┌─┴─┐                     │    │
│  │                         │ 3 │ │ 8 │                     │    │
│  │                         └───┘ └─┬─┘                     │    │
│  │                                ╱                        │    │
│  │                            ┌─┴─┐                        │    │
│  │                            │ 6 │  ◀── NEW              │    │
│  │                            └───┘      新增              │    │
│  │                                                         │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                         │    │
│  │  EPOLL_CTL_MOD fd=3:                                    │    │
│  │  修改 fd=3:                                             │    │
│  │                                                         │    │
│  │  Find fd=3 in tree, update its events                   │    │
│  │  在树中找到 fd=3,更新其事件                            │    │
│  │                                                         │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                         │    │
│  │  EPOLL_CTL_DEL fd=8:                                    │    │
│  │  删除 fd=8:                                             │    │
│  │                                                         │    │
│  │                           ┌───┐                         │    │
│  │                           │ 5 │                         │    │
│  │                           └─┬─┘                         │    │
│  │                            ╱ ╲                          │    │
│  │                         ┌─┴─┐ ┌─┴─┐                     │    │
│  │                         │ 3 │ │ 6 │                     │    │
│  │                         └───┘ └───┘                     │    │
│  │                                                         │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

epoll_wait - 等待事件发生

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

此函数阻塞等待事件发生,是 epoll 的核心调用。

epfd 是 epoll 文件描述符。

events 是用于存放就绪事件的数组,由调用者分配。

maxeventsevents 数组的大小,告诉内核最多返回多少个事件。

timeout 参数控制等待行为:-1 表示永久阻塞直到有事件发生或被信号中断,0 表示立即返回,即使没有事件也不阻塞(非阻塞轮询模式),正数表示最多等待指定的毫秒数。

返回值有三种情况:大于 0 表示就绪的文件描述符数量,events 数组的前 n 个元素包含就绪事件;等于 0 表示超时,没有任何事件发生;等于 -1 表示出错,具体错误码在 errno 中。

┌─────────────────────────────────────────────────────────────────┐
│                    epoll_wait 执行流程                           │
│                    epoll_wait Execution Flow                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   User Code                          Kernel                     │
│   用户代码                            内核                       │
│                                                                 │
│   epoll_event events[1024];                                     │
│   int n = epoll_wait(epfd, events, 1024, -1);                   │
│        │                                                        │
│        │                                                        │
│        ▼                                                        │
│   ┌─────────┐                                                   │
│   │  Call   │                                                   │
│   │  调用   │                                                   │
│   └────┬────┘                                                   │
│        │                                                        │
│        │                          ┌─────────────────────┐       │
│        │                          │ Check ready list    │       │
│        └─────────────────────────▶│ 检查就绪链表        │       │
│                                   └──────────┬──────────┘       │
│                                              │                  │
│                                              ▼                  │
│                                   ┌─────────────────────┐       │
│                              ┌────│ Ready list empty?   │       │
│                              │    │ 就绪链表为空?      │       │
│                              │    └─────────────────────┘       │
│                              │                                  │
│                   ┌──────────┴──────────┐                       │
│                   │ Yes                 │ No                    │
│                   │ 是                  │ 否                    │
│                   ▼                     ▼                       │
│        ┌─────────────────────┐  ┌─────────────────────┐         │
│        │ Add to wait queue   │  │ Copy ready events   │         │
│        │ Block thread        │  │ to user space       │         │
│        │ 加入等待队列        │  │ 拷贝就绪事件到      │         │
│        │ 阻塞线程            │  │ 用户空间            │         │
│        └──────────┬──────────┘  └──────────┬──────────┘         │
│                   │                        │                    │
│                   │ Event arrives          │                    │
│                   │ 事件到达               │                    │
│                   │                        │                    │
│                   ▼                        │                    │
│        ┌─────────────────────┐             │                    │
│        │ Wake up thread      │             │                    │
│        │ 唤醒线程            │             │                    │
│        └──────────┬──────────┘             │                    │
│                   │                        │                    │
│                   └────────────┬───────────┘                    │
│                                │                                │
│                                ▼                                │
│   ┌─────────┐       ┌─────────────────────┐                     │
│   │ n = 3   │◀──────│ Return count        │                     │
│   │         │       │ 返回数量            │                     │
│   └─────────┘       └─────────────────────┘                     │
│        │                                                        │
│        ▼                                                        │
│   Process events[0], events[1], events[2]                       │
│   处理 events[0], events[1], events[2]                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

四、epoll 事件类型详解

epoll 支持多种事件类型,理解它们对于正确处理各种 I/O 场景至关重要。

┌─────────────────────────────────────────────────────────────────┐
│                    epoll 事件类型                                │
│                    epoll Event Types                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  事件类型          触发条件                         常见场景    │
│  Event Type        Trigger Condition                Use Case    │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  EPOLLIN           数据可读                         接收数据    │
│                    Data available to read           Receive     │
│                                                                 │
│  EPOLLOUT          可以写入数据                     发送数据    │
│                    Ready for writing                Send        │
│                                                                 │
│  EPOLLERR          发生错误                         错误处理    │
│                    Error condition                  Error       │
│                                                                 │
│  EPOLLHUP          文件描述符被挂起                 连接关闭    │
│                    Hang up                          Closed      │
│                                                                 │
│  EPOLLRDHUP        对端关闭连接或关闭写端           对端关闭    │
│                    Peer closed or shutdown write    Peer close  │
│                                                                 │
│  EPOLLET           边缘触发模式                     高性能场景  │
│                    Edge-triggered mode              High perf   │
│                                                                 │
│  EPOLLONESHOT      单次触发后自动禁用               多线程安全  │
│                    One-shot mode                    Thread safe │
│                                                                 │
│  EPOLLEXCLUSIVE    独占唤醒模式                     避免惊群    │
│                    Exclusive wakeup mode            No stampede │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

EPOLLIN 和 EPOLLOUT

EPOLLIN 表示文件描述符可读,即接收缓冲区中有数据可以读取。对于监听套接字(listening socket),EPOLLIN 表示有新的连接请求到达,可以调用 accept

EPOLLOUT 表示文件描述符可写,即发送缓冲区有空间可以写入。需要注意的是,大多数时候套接字都是可写的,因此不应该一直监听 EPOLLOUT,否则会导致 busy loop。正确的做法是只在需要写入数据且上次写入返回 EAGAIN 时才添加 EPOLLOUT 监听。

EPOLLERR 和 EPOLLHUP

EPOLLERR 表示文件描述符发生了错误。这个事件会被自动监听,不需要显式指定。发生时应该关闭连接。

EPOLLHUP 表示文件描述符被挂起,通常意味着连接的另一端已经关闭。这个事件也会被自动监听。

EPOLLRDHUP

EPOLLRDHUP 是 Linux 2.6.17 引入的事件,表示对端关闭了连接或者关闭了写入端(half-close)。使用这个事件可以更快地检测到连接关闭,而不需要等到 read 返回 0。这是一个非常有用的事件,推荐在实际应用中使用。

EPOLLET

EPOLLET 启用边缘触发模式,这是一个重要的性能优化选项。关于边缘触发和水平触发的详细区别,将在下一节详细讨论。

EPOLLONESHOT

EPOLLONESHOT 使事件只触发一次,触发后文件描述符会被自动禁用,需要使用 EPOLL_CTL_MOD 重新启用。这个选项主要用于多线程环境,确保同一个文件描述符的事件只被一个线程处理。


五、触发模式:LT vs ET 深入分析

epoll 支持两种触发模式,理解它们的区别对于正确使用 epoll 至关重要。

水平触发 Level Triggered (LT)

水平触发是默认模式,也是 selectpoll 使用的模式。其行为特点是:只要文件描述符处于就绪状态,epoll_wait 就会持续返回这个文件描述符。

┌─────────────────────────────────────────────────────────────────┐
│                水平触发模式 (Level Triggered)                    │
│                Level Triggered Mode                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Time ──────────────────────────────────────────────────────▶   │
│  时间                                                           │
│                                                                 │
│  Buffer State (缓冲区状态):                                     │
│                                                                 │
│        1000 bytes arrive                                        │
│        1000 字节到达                                            │
│               │                                                 │
│               ▼                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │████████████│          │          │          │         │   │  │
│  │  1000 B    │  900 B   │  800 B   │  700 B   │  ...    │ 0 │  │
│  └───────────────────────────────────────────────────────────┘  │
│        │           │          │          │                 │    │
│        ▼           ▼          ▼          ▼                 ▼    │
│                                                                 │
│  epoll_wait returns (epoll_wait 返回):                          │
│                                                                 │
│      ┌───┐      ┌───┐     ┌───┐     ┌───┐              ┌───┐   │
│      │ ✓ │      │ ✓ │     │ ✓ │     │ ✓ │    ...       │ ✗ │   │
│      └───┘      └───┘     └───┘     └───┘              └───┘   │
│                                                                 │
│       │           │          │          │                       │
│       ▼           ▼          ▼          ▼                       │
│  read 100B   read 100B  read 100B  read 100B                    │
│                                                                 │
│                                                                 │
│  特点 (Characteristics):                                        │
│  • As long as data in buffer, epoll_wait keeps returning        │
│    只要缓冲区有数据,epoll_wait 持续返回                        │
│  • Safe to read any amount each time                            │
│    每次可以只读取部分数据                                       │
│  • May cause more system calls                                  │
│    可能导致更多系统调用                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

水平触发模式的优点是编程简单,不容易丢失数据。即使一次 read 没有读取完所有数据,下次 epoll_wait 还会通知你。缺点是当缓冲区一直有数据时,会产生很多次 epoll_wait 返回和系统调用。

边缘触发 Edge Triggered (ET)

边缘触发需要显式指定 EPOLLET 标志。其行为特点是:只有当文件描述符的状态发生变化时才会通知,即从不可读变为可读,或从不可写变为可写。

┌─────────────────────────────────────────────────────────────────┐
│                边缘触发模式 (Edge Triggered)                     │
│                Edge Triggered Mode                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Time ──────────────────────────────────────────────────────▶   │
│  时间                                                           │
│                                                                 │
│  Buffer State (缓冲区状态):                                     │
│                                                                 │
│        1000 bytes arrive                                        │
│        1000 字节到达                                            │
│               │                                                 │
│               ▼                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │████████████│          │                              │    │  │
│  │  1000 B    │  900 B   │  ...remaining data...        │ 0  │  │
│  └───────────────────────────────────────────────────────────┘  │
│        │           │                                       │    │
│        ▼           ▼                                       ▼    │
│                                                                 │
│  epoll_wait returns (epoll_wait 返回):                          │
│                                                                 │
│      ┌───┐      ┌───┐                                  ┌───┐   │
│      │ ✓ │      │ ✗ │     NO MORE NOTIFICATIONS!       │ ✗ │   │
│      └───┘      └───┘     不再有通知!                  └───┘   │
│                                                                 │
│       │           │                                             │
│       ▼           │                                             │
│  read 100B        │                                             │
│                   │                                             │
│                   ▼                                             │
│            ┌─────────────────────────────────────────┐          │
│            │ PROBLEM: 900 bytes stuck in buffer!    │          │
│            │ 问题:900 字节数据滞留在缓冲区!       │          │
│            │                                         │          │
│            │ No notification until NEW data arrives │          │
│            │ 直到新数据到达才会有通知               │          │
│            └─────────────────────────────────────────┘          │
│                                                                 │
│  正确做法 (Correct Approach):                                   │
│                                                                 │
│      ┌───┐                                                     │
│      │ ✓ │                                                     │
│      └───┘                                                     │
│       │                                                        │
│       ▼                                                        │
│  ┌─────────────────────────────────────────┐                   │
│  │  Loop read until EAGAIN!                │                   │
│  │  循环读取直到 EAGAIN!                  │                   │
│  │                                          │                   │
│  │  while (true) {                          │                   │
│  │    n = read(fd, buf, size);              │                   │
│  │    if (n == -1 && errno == EAGAIN)       │                   │
│  │      break;  // Done, no more data       │                   │
│  │    // process data...                    │                   │
│  │  }                                       │                   │
│  └─────────────────────────────────────────┘                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

边缘触发模式的优点是效率高,每次状态变化只通知一次,减少了系统调用次数。缺点是编程复杂度高,必须循环读取直到 EAGAIN,必须使用非阻塞 I/O,否则可能永久丢失事件通知。

两种模式的选择

┌─────────────────────────────────────────────────────────────────┐
│                LT vs ET 选择指南                                 │
│                LT vs ET Selection Guide                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│                    ┌─────────────────┐                          │
│                    │ 选择触发模式?  │                          │
│                    │ Choose mode?    │                          │
│                    └────────┬────────┘                          │
│                             │                                   │
│              ┌──────────────┴──────────────┐                    │
│              │                             │                    │
│              ▼                             ▼                    │
│     ┌─────────────────┐           ┌─────────────────┐           │
│     │ 追求简单可靠?  │           │ 追求极致性能?  │           │
│     │ Simple&reliable?│           │ Max performance?│           │
│     └────────┬────────┘           └────────┬────────┘           │
│              │                             │                    │
│              ▼                             ▼                    │
│     ┌─────────────────┐           ┌─────────────────┐           │
│     │      LT         │           │      ET         │           │
│     │   水平触发      │           │   边缘触发      │           │
│     └─────────────────┘           └─────────────────┘           │
│              │                             │                    │
│              ▼                             ▼                    │
│     • 代码简单                    • 减少系统调用               │
│       Simple code                   Fewer syscalls              │
│     • 不易出错                    • 需要非阻塞 I/O              │
│       Less error-prone              Requires non-blocking       │
│     • 适合初学者                  • 必须循环读写                │
│       Good for beginners            Must loop until EAGAIN      │
│     • 性能稍低                    • 适合高并发场景              │
│       Slightly lower perf           Good for high concurrency   │
│                                                                 │
│  实际应用 (Real-world usage):                                   │
│  • Nginx: 默认使用 ET 模式                                      │
│  • Redis: 使用 LT 模式                                          │
│  • libevent: 支持两种模式                                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

六、完整代码实现

下面是一个完整的 echo 服务器实现,展示了 epoll 的实际应用。代码使用现代 C++ 风格,采用 RAII 管理资源,使用边缘触发模式以获得最佳性能。

头文件和常量定义

#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <memory>
#include <cstring>
#include <cerrno>

#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// Server configuration constants
// 服务器配置常量
constexpr int PORT = 8080;
constexpr int MAX_EVENTS = 1024;
constexpr int BUFFER_SIZE = 4096;

工具函数:设置非阻塞模式

在使用边缘触发模式时,文件描述符必须设置为非阻塞模式。这是因为边缘触发只在状态变化时通知一次,如果使用阻塞 I/O,可能会导致程序在没有数据时阻塞,而后续到达的数据不会再触发通知。

// Set file descriptor to non-blocking mode
// This is REQUIRED for edge-triggered epoll
// 设置文件描述符为非阻塞模式
// 边缘触发 epoll 必须使用非阻塞模式
bool set_nonblocking(int fd) {
  // Get current flags
  // 获取当前标志
  int flags = fcntl(fd, F_GETFL, 0);
  if (flags == -1) {
    std::cerr << "fcntl F_GETFL failed: " << strerror(errno) << "\n";
    return false;
  }

  // Add O_NONBLOCK flag
  // 添加 O_NONBLOCK 标志
  if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
    std::cerr << "fcntl F_SETFL failed: " << strerror(errno) << "\n";
    return false;
  }

  return true;
}

FileDescriptor 类:RAII 封装

使用 RAII(Resource Acquisition Is Initialization)模式管理文件描述符,确保资源在对象生命周期结束时自动释放,避免资源泄漏。

// RAII wrapper for file descriptors
// Automatically closes fd when object is destroyed
// 文件描述符的 RAII 封装
// 对象销毁时自动关闭 fd
class FileDescriptor {
public:
  FileDescriptor() : fd_(-1) {}

  explicit FileDescriptor(int fd) : fd_(fd) {}

  // Move constructor - transfer ownership
  // 移动构造函数 - 转移所有权
  FileDescriptor(FileDescriptor&& other) noexcept : fd_(other.fd_) {
    other.fd_ = -1;
  }

  // Move assignment - transfer ownership
  // 移动赋值 - 转移所有权
  FileDescriptor& operator=(FileDescriptor&& other) noexcept {
    if (this != &other) {
      close();
      fd_ = other.fd_;
      other.fd_ = -1;
    }
    return *this;
  }

  // Disable copy to prevent double-close
  // 禁用拷贝以防止重复关闭
  FileDescriptor(const FileDescriptor&) = delete;
  FileDescriptor& operator=(const FileDescriptor&) = delete;

  // Destructor automatically closes fd
  // 析构函数自动关闭 fd
  ~FileDescriptor() {
    close();
  }

  void close() {
    if (fd_ >= 0) {
      ::close(fd_);
      fd_ = -1;
    }
  }

  int get() const { return fd_; }
  bool valid() const { return fd_ >= 0; }

  // Release ownership without closing
  // 释放所有权但不关闭
  int release() {
    int fd = fd_;
    fd_ = -1;
    return fd;
  }

private:
  int fd_;
};

Connection 类:客户端连接抽象

每个客户端连接被封装为一个 Connection 对象,包含文件描述符、连接信息和读写缓冲区。

// Represents a single client connection
// 表示单个客户端连接
class Connection {
public:
  Connection(int fd, const std::string& addr, int port)
    : fd_(fd), address_(addr), port_(port) {}

  int fd() const { return fd_.get(); }
  const std::string& address() const { return address_; }
  int port() const { return port_; }

  // Read data from connection
  // Returns: >0 bytes read, 0 closed, -1 EAGAIN, -2 error
  // 从连接读取数据
  // 返回: >0 读取字节数, 0 关闭, -1 EAGAIN, -2 错误
  ssize_t read(std::string& data) {
    char buffer[BUFFER_SIZE];
    ssize_t n = ::read(fd_.get(), buffer, sizeof(buffer));

    if (n > 0) {
      // Successfully read data
      // 成功读取数据
      data.append(buffer, n);
      return n;
    } else if (n == 0) {
      // Peer closed connection (EOF)
      // 对端关闭连接 (EOF)
      return 0;
    } else {
      // n == -1, check errno
      // n == -1, 检查 errno
      if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // No more data available in non-blocking mode
        // 非阻塞模式下没有更多数据
        return -1;
      }
      // Actual error occurred
      // 发生实际错误
      return -2;
    }
  }

  // Write data to connection
  // 向连接写入数据
  ssize_t write(const std::string& data) {
    return ::write(fd_.get(), data.c_str(), data.size());
  }

private:
  FileDescriptor fd_;
  std::string address_;
  int port_;
};

Epoll 类:epoll 操作封装

将 epoll 相关操作封装为类,提供更清晰的接口和更好的资源管理。

// Wrapper class for epoll operations
// epoll 操作的封装类
class Epoll {
public:
  Epoll() = default;

  // Create epoll instance
  // 创建 epoll 实例
  bool init() {
    // epoll_create1(0) creates an epoll instance
    // Returns a file descriptor referring to the new epoll instance
    // epoll_create1(0) 创建 epoll 实例
    // 返回引用新 epoll 实例的文件描述符
    int fd = epoll_create1(0);
    if (fd == -1) {
      std::cerr << "epoll_create1 failed: " << strerror(errno) << "\n";
      return false;
    }
    epfd_ = FileDescriptor(fd);
    return true;
  }

  // Add fd to epoll interest list
  // 将 fd 添加到 epoll 兴趣列表
  bool add(int fd, uint32_t events) {
    epoll_event ev{};
    ev.events = events;
    ev.data.fd = fd;

    if (epoll_ctl(epfd_.get(), EPOLL_CTL_ADD, fd, &ev) == -1) {
      std::cerr << "epoll_ctl ADD failed: " << strerror(errno) << "\n";
      return false;
    }
    return true;
  }

  // Modify events for existing fd
  // 修改已有 fd 的事件
  bool modify(int fd, uint32_t events) {
    epoll_event ev{};
    ev.events = events;
    ev.data.fd = fd;

    if (epoll_ctl(epfd_.get(), EPOLL_CTL_MOD, fd, &ev) == -1) {
      std::cerr << "epoll_ctl MOD failed: " << strerror(errno) << "\n";
      return false;
    }
    return true;
  }

  // Remove fd from epoll
  // 从 epoll 移除 fd
  bool remove(int fd) {
    // For EPOLL_CTL_DEL, the event parameter is ignored
    // 对于 EPOLL_CTL_DEL,event 参数被忽略
    if (epoll_ctl(epfd_.get(), EPOLL_CTL_DEL, fd, nullptr) == -1) {
      std::cerr << "epoll_ctl DEL failed: " << strerror(errno) << "\n";
      return false;
    }
    return true;
  }

  // Wait for events
  // timeout: -1 block forever, 0 return immediately, >0 wait ms
  // 等待事件
  // timeout: -1 永久阻塞, 0 立即返回, >0 等待毫秒
  int wait(std::vector<epoll_event>& events, int timeout = -1) {
    int n = epoll_wait(epfd_.get(), events.data(), events.size(), timeout);

    if (n == -1) {
      if (errno == EINTR) {
        // Interrupted by signal, not an error
        // 被信号中断,不是错误
        return 0;
      }
      std::cerr << "epoll_wait failed: " << strerror(errno) << "\n";
    }
    return n;
  }

private:
  FileDescriptor epfd_;
};

TcpServer 类:完整服务器实现

这是服务器的核心实现,整合了上述所有组件。

// Main TCP server class using epoll
// 使用 epoll 的主 TCP 服务器类
class TcpServer {
public:
  explicit TcpServer(int port) : port_(port), running_(false) {}

  ~TcpServer() {
    stop();
  }

  // Initialize server: create socket, bind, listen, setup epoll
  // 初始化服务器:创建套接字,绑定,监听,设置 epoll
  bool init() {
    if (!create_listen_socket()) {
      return false;
    }

    if (!epoll_.init()) {
      return false;
    }

    // Add listening socket to epoll
    // Only need EPOLLIN for accepting new connections
    // 将监听套接字添加到 epoll
    // 只需要 EPOLLIN 来接受新连接
    if (!epoll_.add(listen_fd_.get(), EPOLLIN)) {
      return false;
    }

    std::cout << "[Server] Listening on port " << port_ << "\n";
    return true;
  }

  // Main event loop
  // 主事件循环
  void run() {
    running_ = true;
    std::vector<epoll_event> events(MAX_EVENTS);

    while (running_) {
      // Block until events are ready
      // 阻塞直到有事件就绪
      int nready = epoll_.wait(events, -1);

      if (nready < 0) {
        break;
      }

      // Process all ready events
      // Note: only first nready elements are valid
      // 处理所有就绪事件
      // 注意:只有前 nready 个元素有效
      for (int i = 0; i < nready; ++i) {
        int fd = events[i].data.fd;
        uint32_t ev = events[i].events;

        if (fd == listen_fd_.get()) {
          // Event on listening socket means new connection
          // 监听套接字上的事件意味着新连接
          handle_accept();
        } else {
          // Event on client socket
          // 客户端套接字上的事件
          handle_client_event(fd, ev);
        }
      }
    }
  }

  void stop() {
    running_ = false;
  }

private:
  // Create and configure the listening socket
  // 创建并配置监听套接字
  bool create_listen_socket() {
    // Create TCP socket
    // AF_INET: IPv4, SOCK_STREAM: TCP
    // 创建 TCP 套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1) {
      std::cerr << "socket failed: " << strerror(errno) << "\n";
      return false;
    }
    listen_fd_ = FileDescriptor(fd);

    // Enable address reuse to avoid "Address already in use" error
    // when restarting server quickly
    // 启用地址重用,避免快速重启服务器时出现
    // "Address already in use" 错误
    int opt = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
      std::cerr << "setsockopt failed: " << strerror(errno) << "\n";
      return false;
    }

    // Configure server address
    // 配置服务器地址
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;  // Accept on all interfaces
    addr.sin_port = htons(port_);        // Convert to network byte order

    // Bind socket to address
    // 绑定套接字到地址
    if (bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == -1) {
      std::cerr << "bind failed: " << strerror(errno) << "\n";
      return false;
    }

    // Start listening for connections
    // SOMAXCONN: use system maximum backlog
    // 开始监听连接
    // SOMAXCONN: 使用系统最大积压队列
    if (listen(fd, SOMAXCONN) == -1) {
      std::cerr << "listen failed: " << strerror(errno) << "\n";
      return false;
    }

    // Set non-blocking mode for edge-triggered epoll
    // 为边缘触发 epoll 设置非阻塞模式
    if (!set_nonblocking(fd)) {
      return false;
    }

    return true;
  }

  // Accept all pending connections
  // Must loop in edge-triggered mode
  // 接受所有等待的连接
  // 边缘触发模式下必须循环
  void handle_accept() {
    while (true) {
      sockaddr_in client_addr{};
      socklen_t client_len = sizeof(client_addr);

      int client_fd = accept(
        listen_fd_.get(),
        reinterpret_cast<sockaddr*>(&client_addr),
        &client_len
      );

      if (client_fd == -1) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
          // All pending connections have been accepted
          // 所有等待的连接都已被接受
          break;
        }
        std::cerr << "accept failed: " << strerror(errno) << "\n";
        break;
      }

      // Get client information for logging
      // 获取客户端信息用于日志
      std::string client_ip = inet_ntoa(client_addr.sin_addr);
      int client_port = ntohs(client_addr.sin_port);

      std::cout << "[+] New connection: fd=" << client_fd
                << ", ip=" << client_ip
                << ", port=" << client_port << "\n";

      // Set non-blocking mode
      // 设置非阻塞模式
      if (!set_nonblocking(client_fd)) {
        ::close(client_fd);
        continue;
      }

      // Add to epoll with edge-triggered mode
      // EPOLLIN: monitor for readable events
      // EPOLLET: edge-triggered mode
      // EPOLLRDHUP: monitor for peer close
      // 使用边缘触发模式添加到 epoll
      if (!epoll_.add(client_fd, EPOLLIN | EPOLLET | EPOLLRDHUP)) {
        ::close(client_fd);
        continue;
      }

      // Store connection in our map
      // 将连接保存到 map 中
      connections_.emplace(
        client_fd,
        std::make_unique<Connection>(client_fd, client_ip, client_port)
      );
    }
  }

  // Handle events on client socket
  // 处理客户端套接字上的事件
  void handle_client_event(int fd, uint32_t events) {
    auto it = connections_.find(fd);
    if (it == connections_.end()) {
      return;
    }

    Connection* conn = it->second.get();

    // Check for errors or connection close
    // EPOLLERR: error condition
    // EPOLLHUP: hang up
    // EPOLLRDHUP: peer closed or shutdown write
    // 检查错误或连接关闭
    if (events & (EPOLLERR | EPOLLHUP | EPOLLRDHUP)) {
      close_connection(fd);
      return;
    }

    // Handle readable event
    // 处理可读事件
    if (events & EPOLLIN) {
      handle_read(conn);
    }
  }

  // Read all available data from client
  // CRITICAL: Must loop until EAGAIN in edge-triggered mode!
  // 从客户端读取所有可用数据
  // 关键:边缘触发模式下必须循环直到 EAGAIN!
  void handle_read(Connection* conn) {
    std::string data;

    while (true) {
      ssize_t result = conn->read(data);

      if (result > 0) {
        // More data might be available, continue reading
        // 可能还有更多数据,继续读取
        continue;
      } else if (result == 0) {
        // Connection closed by peer
        // 对端关闭连接
        close_connection(conn->fd());
        return;
      } else if (result == -1) {
        // EAGAIN: no more data available
        // EAGAIN: 没有更多数据
        break;
      } else {
        // Error occurred
        // 发生错误
        close_connection(conn->fd());
        return;
      }
    }

    // Echo data back to client
    // 将数据回显给客户端
    if (!data.empty()) {
      std::cout << "[fd=" << conn->fd() << "] Received "
                << data.size() << " bytes\n";
      conn->write(data);
    }
  }

  // Close connection and cleanup resources
  // 关闭连接并清理资源
  void close_connection(int fd) {
    auto it = connections_.find(fd);
    if (it != connections_.end()) {
      std::cout << "[-] Connection closed: fd=" << fd << "\n";

      // Remove from epoll before closing fd
      // 在关闭 fd 之前先从 epoll 移除
      epoll_.remove(fd);

      // Remove from map, destructor closes fd
      // 从 map 移除,析构函数关闭 fd
      connections_.erase(it);
    }
  }

private:
  int port_;
  bool running_;
  FileDescriptor listen_fd_;
  Epoll epoll_;
  std::unordered_map<int, std::unique_ptr<Connection>> connections_;
};

主函数

int main(int argc, char* argv[]) {
  int port = PORT;

  if (argc > 1) {
    port = std::stoi(argv[1]);
  }

  TcpServer server(port);

  if (!server.init()) {
    std::cerr << "Failed to initialize server\n";
    return 1;
  }

  std::cout << "Echo server started. Press Ctrl+C to stop.\n";
  std::cout << "Test with: nc localhost " << port << "\n";

  server.run();

  return 0;
}

七、编译与测试

编译

g++ -std=c++17 -O2 -Wall -o echo_server echo_server.cpp

运行服务器

./echo_server 8080

测试连接

在另一个终端使用 netcat 测试:

nc localhost 8080

输入任意文本并按回车,服务器会将输入内容原样返回。

多客户端测试

可以打开多个终端同时连接,验证服务器能够正确处理多个并发连接:

# Terminal 2
nc localhost 8080

# Terminal 3
nc localhost 8080

# Terminal 4
nc localhost 8080

八、实现注意点详解

注意点一:边缘触发模式必须循环读取直到 EAGAIN

这是使用边缘触发模式最容易犯的错误,也是最致命的错误。

┌─────────────────────────────────────────────────────────────────┐
│            边缘触发必须循环读取 (Must Loop in ET Mode)           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ❌ 错误做法 (Wrong):                                           │
│                                                                 │
│  void handle_read(int fd) {                                     │
│    char buf[1024];                                              │
│    int n = read(fd, buf, sizeof(buf));  // Only read once!      │
│    // process...                         // 只读一次!          │
│  }                                                              │
│                                                                 │
│  问题 (Problem):                                                │
│  If 5000 bytes arrive, only 1024 are read.                      │
│  如果到达 5000 字节,只读取了 1024 字节。                       │
│  Remaining 3976 bytes are LOST forever (no more notifications)! │
│  剩余 3976 字节永久丢失(不会再有通知)!                       │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  ✅ 正确做法 (Correct):                                         │
│                                                                 │
│  void handle_read(int fd) {                                     │
│    char buf[1024];                                              │
│    while (true) {                        // Loop!               │
│      int n = read(fd, buf, sizeof(buf)); // 循环!              │
│                                                                 │
│      if (n > 0) {                                               │
│        // Process data, continue loop                           │
│        // 处理数据,继续循环                                    │
│        process(buf, n);                                         │
│        continue;                                                │
│      }                                                          │
│      else if (n == 0) {                                         │
│        // EOF - peer closed                                     │
│        // EOF - 对端关闭                                        │
│        close_connection(fd);                                    │
│        return;                                                  │
│      }                                                          │
│      else { // n == -1                                          │
│        if (errno == EAGAIN || errno == EWOULDBLOCK) {           │
│          // All data read, exit loop                            │
│          // 所有数据已读取,退出循环                            │
│          break;  // ← This is the key!                          │
│        }         //   这是关键!                                │
│        // Other error                                           │
│        // 其他错误                                              │
│        handle_error(fd);                                        │
│        return;                                                  │
│      }                                                          │
│    }                                                            │
│  }                                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

在示例代码中的实现:

// handle_read 函数中的循环
// Loop in handle_read function
void handle_read(Connection* conn) {
  std::string data;

  // CRITICAL: Must loop until EAGAIN!
  // 关键:必须循环直到 EAGAIN!
  while (true) {
    ssize_t result = conn->read(data);

    if (result > 0) {
      // More data might be available
      // Continue reading to ensure we get all data
      // 可能还有更多数据
      // 继续读取以确保获取所有数据
      continue;
    } else if (result == 0) {
      // EOF - connection closed by peer
      // EOF - 对端关闭连接
      close_connection(conn->fd());
      return;
    } else if (result == -1) {
      // EAGAIN - no more data available right now
      // This is the signal to exit the loop
      // EAGAIN - 当前没有更多数据
      // 这是退出循环的信号
      break;
    } else {
      // Actual error
      // 实际错误
      close_connection(conn->fd());
      return;
    }
  }

  // Now we have ALL available data
  // 现在我们拥有所有可用数据
  if (!data.empty()) {
    conn->write(data);
  }
}

注意点二:非阻塞 I/O 是边缘触发的前提

边缘触发模式必须配合非阻塞 I/O 使用,这是一个硬性要求。

┌─────────────────────────────────────────────────────────────────┐
│        非阻塞 I/O 的必要性 (Why Non-blocking I/O is Required)    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  场景 (Scenario):                                               │
│  Using blocking I/O + ET mode                                   │
│  使用阻塞 I/O + ET 模式                                         │
│                                                                 │
│  ┌────────────────────────────────────────────────────────┐     │
│  │ Time  Action              Result                       │     │
│  │ 时间  动作                结果                         │     │
│  ├────────────────────────────────────────────────────────┤     │
│  │ T1    1000 bytes arrive   epoll_wait returns EPOLLIN   │     │
│  │       1000 字节到达       epoll_wait 返回 EPOLLIN      │     │
│  │                                                        │     │
│  │ T2    read() 1000 bytes   Success, got all data        │     │
│  │       read() 1000 字节    成功,获取所有数据           │     │
│  │                                                        │     │
│  │ T3    read() again        BLOCKS! (blocking I/O)       │     │
│  │       再次 read()         阻塞!(阻塞 I/O)           │     │
│  │                           Waiting for more data...     │     │
│  │                           等待更多数据...              │     │
│  │                                                        │     │
│  │ T4    Other fd has event  CANNOT process!              │     │
│  │       其他 fd 有事件      无法处理!                   │     │
│  │                           Thread is blocked on read()  │     │
│  │                           线程在 read() 上阻塞         │     │
│  │                                                        │     │
│  │       💀 SERVER STUCK! 服务器卡住!                   │     │
│  └────────────────────────────────────────────────────────┘     │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  解决方案 (Solution): Non-blocking I/O                          │
│                                                                 │
│  ┌────────────────────────────────────────────────────────┐     │
│  │ Time  Action              Result                       │     │
│  ├────────────────────────────────────────────────────────┤     │
│  │ T1    1000 bytes arrive   epoll_wait returns EPOLLIN   │     │
│  │                                                        │     │
│  │ T2    read() 1000 bytes   Success                      │     │
│  │                                                        │     │
│  │ T3    read() again        Returns -1, errno=EAGAIN     │     │
│  │                           返回 -1, errno=EAGAIN        │     │
│  │                           No blocking!                 │     │
│  │                           不阻塞!                     │     │
│  │                                                        │     │
│  │ T4    Back to epoll_wait  Can process other events     │     │
│  │       返回 epoll_wait     可以处理其他事件             │     │
│  │                                                        │     │
│  │       ✅ SERVER RESPONSIVE! 服务器响应正常!          │     │
│  └────────────────────────────────────────────────────────┘     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

设置非阻塞模式的代码:

bool set_nonblocking(int fd) {
  // fcntl - file control operations
  // F_GETFL - get file status flags
  // fcntl - 文件控制操作
  // F_GETFL - 获取文件状态标志
  int flags = fcntl(fd, F_GETFL, 0);
  if (flags == -1) {
    return false;
  }

  // F_SETFL - set file status flags
  // O_NONBLOCK - enable non-blocking mode
  // F_SETFL - 设置文件状态标志
  // O_NONBLOCK - 启用非阻塞模式
  //
  // After this:
  // - read() returns -1 with errno=EAGAIN when no data
  // - write() returns -1 with errno=EAGAIN when buffer full
  // - accept() returns -1 with errno=EAGAIN when no connections
  // 设置后:
  // - read() 无数据时返回 -1,errno=EAGAIN
  // - write() 缓冲区满时返回 -1,errno=EAGAIN
  // - accept() 无连接时返回 -1,errno=EAGAIN
  if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
    return false;
  }

  return true;
}

注意点三:正确处理 EINTR

epoll_wait 可能被信号中断,此时返回 -1 且 errnoEINTR。这不是错误,应该重新调用。

┌─────────────────────────────────────────────────────────────────┐
│              处理 EINTR (Handling EINTR)                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  什么是 EINTR?(What is EINTR?)                                 │
│                                                                 │
│  When a signal (like SIGINT from Ctrl+C) arrives while          │
│  epoll_wait is blocking, the system call is interrupted.        │
│                                                                 │
│  当信号(如 Ctrl+C 的 SIGINT)在 epoll_wait 阻塞时到达,        │
│  系统调用会被中断。                                             │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │                                                         │    │
│  │   Thread blocked in                Signal arrives       │    │
│  │   epoll_wait()                     信号到达             │    │
│  │        │                              │                 │    │
│  │        │          ┌───────────────────┘                 │    │
│  │        │          │                                     │    │
│  │        │          ▼                                     │    │
│  │        │    ┌───────────┐                               │    │
│  │        └───▶│ Interrupt │                               │    │
│  │             │   中断    │                               │    │
│  │             └─────┬─────┘                               │    │
│  │                   │                                     │    │
│  │                   ▼                                     │    │
│  │             Returns -1                                  │    │
│  │             errno = EINTR                               │    │
│  │                                                         │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  正确处理 (Correct Handling):                                   │
│                                                                 │
│  int wait(std::vector<epoll_event>& events, int timeout) {      │
│    int n = epoll_wait(epfd, events.data(), events.size(),       │
│                       timeout);                                  │
│                                                                 │
│    if (n == -1) {                                               │
│      if (errno == EINTR) {                                      │
│        // Signal interrupted the call                           │
│        // This is NOT an error!                                 │
│        // 信号中断了调用                                        │
│        // 这不是错误!                                          │
│        return 0;  // Return 0 to indicate "retry"               │
│                   // 返回 0 表示"重试"                          │
│      }                                                          │
│      // Actual error                                            │
│      // 实际错误                                                │
│      std::cerr << "epoll_wait error: " << strerror(errno);      │
│      return -1;                                                 │
│    }                                                            │
│    return n;                                                    │
│  }                                                              │
│                                                                 │
│  // In main loop:                                               │
│  // 在主循环中:                                                │
│  while (running_) {                                             │
│    int nready = epoll_.wait(events, -1);                        │
│                                                                 │
│    if (nready < 0) {                                            │
│      break;  // Real error, exit                                │
│    }                                                            │
│    if (nready == 0) {                                           │
│      continue;  // EINTR, just retry                            │
│    }                                                            │
│                                                                 │
│    // Process events...                                         │
│  }                                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

注意点四:关闭连接时的清理顺序

关闭连接时,应该先从 epoll 移除文件描述符,再关闭它。虽然关闭文件描述符会自动将其从 epoll 中移除,但显式移除是更好的实践。

┌─────────────────────────────────────────────────────────────────┐
│            关闭连接的正确顺序 (Correct Cleanup Order)            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ⚠️ 潜在问题场景 (Potential Problem Scenario):                  │
│                                                                 │
│  Thread A:                    Thread B:                         │
│  ┌─────────────┐              ┌─────────────┐                   │
│  │ close(fd=5) │              │ fd=5 reused │                   │
│  │             │              │ by accept() │                   │
│  │   (auto     │              │             │                   │
│  │   removed   │              │ epoll_ctl   │                   │
│  │   from      │──────────────│ ADD fd=5    │                   │
│  │   epoll)    │              │             │                   │
│  │             │              │ (same fd!)  │                   │
│  │ connections │              │             │                   │
│  │ .erase(5)   │              │             │                   │
│  └─────────────┘              └─────────────┘                   │
│                                                                 │
│  Race condition possible!                                       │
│  可能发生竞态条件!                                             │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  ✅ 正确做法 (Correct Approach):                                │
│                                                                 │
│  void close_connection(int fd) {                                │
│    auto it = connections_.find(fd);                             │
│    if (it != connections_.end()) {                              │
│                                                                 │
│      // Step 1: Remove from epoll FIRST                         │
│      // 第一步:先从 epoll 移除                                 │
│      epoll_.remove(fd);                                         │
│                                                                 │
│      // Step 2: Remove from our data structure                  │
│      // This also closes the fd via RAII                        │
│      // 第二步:从数据结构移除                                  │
│      // 同时通过 RAII 关闭 fd                                   │
│      connections_.erase(it);                                    │
│                                                                 │
│      // Now fd is fully cleaned up                              │
│      // 现在 fd 已完全清理                                      │
│    }                                                            │
│  }                                                              │
│                                                                 │
│  原因 (Reasons):                                                │
│  1. Explicit removal makes intent clear                         │
│     显式移除使意图清晰                                          │
│  2. Avoids subtle race conditions                               │
│     避免微妙的竞态条件                                          │
│  3. Better for debugging and logging                            │
│     更便于调试和日志记录                                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

注意点五:使用 RAII 管理资源

文件描述符是系统资源,泄漏会导致服务器无法接受新连接。使用 RAII 封装确保资源在任何情况下都能正确释放。

┌─────────────────────────────────────────────────────────────────┐
│              RAII 资源管理 (RAII Resource Management)            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ❌ 不使用 RAII 的问题 (Problems without RAII):                 │
│                                                                 │
│  void process_client(int listen_fd) {                           │
│    int client_fd = accept(listen_fd, ...);                      │
│    char* buffer = new char[4096];                               │
│                                                                 │
│    if (some_error) {                                            │
│      return;  // LEAK! fd not closed, memory not freed          │
│    }          // 泄漏!fd 未关闭,内存未释放                    │
│                                                                 │
│    read(client_fd, buffer, 4096);                               │
│                                                                 │
│    if (another_error) {                                         │
│      close(client_fd);                                          │
│      return;  // LEAK! memory not freed                         │
│    }          // 泄漏!内存未释放                               │
│                                                                 │
│    delete[] buffer;                                             │
│    close(client_fd);                                            │
│  }                                                              │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  ✅ 使用 RAII (With RAII):                                      │
│                                                                 │
│  void process_client(int listen_fd) {                           │
│    FileDescriptor client_fd(accept(listen_fd, ...));            │
│    std::vector<char> buffer(4096);                              │
│                                                                 │
│    if (some_error) {                                            │
│      return;  // OK! Destructor closes fd, vector freed         │
│    }          // 没问题!析构函数关闭 fd,vector 释放           │
│                                                                 │
│    read(client_fd.get(), buffer.data(), buffer.size());         │
│                                                                 │
│    if (another_error) {                                         │
│      return;  // OK! Same as above                              │
│    }          // 没问题!同上                                   │
│                                                                 │
│    // Automatic cleanup when function returns                   │
│    // 函数返回时自动清理                                        │
│  }                                                              │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  FileDescriptor 类设计要点 (FileDescriptor Design Points):      │
│                                                                 │
│  class FileDescriptor {                                         │
│  public:                                                        │
│    explicit FileDescriptor(int fd);   // Take ownership         │
│                                        // 获取所有权            │
│                                                                 │
│    ~FileDescriptor() { close(); }     // Auto cleanup           │
│                                        // 自动清理              │
│                                                                 │
│    // Move semantics - transfer ownership                       │
│    // 移动语义 - 转移所有权                                     │
│    FileDescriptor(FileDescriptor&&) noexcept;                   │
│    FileDescriptor& operator=(FileDescriptor&&) noexcept;        │
│                                                                 │
│    // Delete copy - prevent double close                        │
│    // 删除拷贝 - 防止重复关闭                                   │
│    FileDescriptor(const FileDescriptor&) = delete;              │
│    FileDescriptor& operator=(const FileDescriptor&) = delete;   │
│                                                                 │
│    int get() const;     // Access raw fd                        │
│    int release();       // Release without closing              │
│  };                                                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

注意点六:SO_REUSEADDR 选项

服务器重启时,之前的 TCP 连接可能还处于 TIME_WAIT 状态,导致 bind 失败。

┌─────────────────────────────────────────────────────────────────┐
│           SO_REUSEADDR 的作用 (Purpose of SO_REUSEADDR)          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  TCP TIME_WAIT 状态 (TCP TIME_WAIT State):                      │
│                                                                 │
│  ┌────────────────────────────────────────────────────────┐     │
│  │                                                        │     │
│  │    Server              Network              Client     │     │
│  │       │                                        │       │     │
│  │       │◀─────────── FIN ──────────────────────│       │     │
│  │       │                                        │       │     │
│  │       │────────────  ACK  ────────────────────▶│       │     │
│  │       │                                        │       │     │
│  │       │────────────  FIN  ────────────────────▶│       │     │
│  │       │                                        │       │     │
│  │       │◀───────────  ACK  ────────────────────│       │     │
│  │       │                                        │       │     │
│  │  TIME_WAIT                                     │       │     │
│  │  (2*MSL,                                       │       │     │
│  │   ~60 sec)                                     │       │     │
│  │       │                                        │       │     │
│  │                                                        │     │
│  └────────────────────────────────────────────────────────┘     │
│                                                                 │
│  问题 (Problem):                                                │
│                                                                 │
│  $ ./server                                                     │
│  [Server] Listening on port 8080                                │
│  ^C  # Stop server                                              │
│                                                                 │
│  $ ./server                                                     │
│  bind failed: Address already in use    ← TIME_WAIT 阻止绑定    │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  解决方案 (Solution):                                           │
│                                                                 │
│  int opt = 1;                                                   │
│  setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));   │
│                                                                 │
│  // What SO_REUSEADDR does:                                     │
│  // SO_REUSEADDR 的作用:                                       │
│  //                                                             │
│  // 1. Allow binding to address in TIME_WAIT state              │
│  //    允许绑定到 TIME_WAIT 状态的地址                          │
│  //                                                             │
│  // 2. Allow multiple sockets to bind to same port              │
│  //    (with different local addresses)                         │
│  //    允许多个套接字绑定到同一端口(使用不同本地地址)         │
│  //                                                             │
│  // 3. Essential for server restart without waiting             │
│  //    服务器不等待立即重启的必要选项                           │
│                                                                 │
│  ⚠️ 注意 (Note):                                                │
│  Also consider SO_REUSEPORT for load balancing scenarios        │
│  负载均衡场景下也考虑 SO_REUSEPORT                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

注意点七:EPOLLRDHUP 事件的使用

EPOLLRDHUP 提供了更快检测连接关闭的方式。

┌─────────────────────────────────────────────────────────────────┐
│              EPOLLRDHUP 的优势 (Benefits of EPOLLRDHUP)          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ❌ 不使用 EPOLLRDHUP (Without EPOLLRDHUP):                     │
│                                                                 │
│  检测对端关闭的唯一方式是 read() 返回 0                         │
│  The only way to detect peer close is read() returning 0        │
│                                                                 │
│  // Add to epoll                                                │
│  epoll_ctl(epfd, EPOLL_CTL_ADD, fd, {EPOLLIN | EPOLLET});       │
│                                                                 │
│  // In event loop                                               │
│  if (events & EPOLLIN) {                                        │
│    int n = read(fd, buf, size);                                 │
│    if (n == 0) {                                                │
│      // Peer closed - but we had to call read() to know         │
│      // 对端关闭 - 但我们必须调用 read() 才知道                 │
│      close_connection(fd);                                      │
│    }                                                            │
│  }                                                              │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  ✅ 使用 EPOLLRDHUP (With EPOLLRDHUP):                          │
│                                                                 │
│  // Add to epoll                                                │
│  epoll_ctl(epfd, EPOLL_CTL_ADD, fd,                             │
│            {EPOLLIN | EPOLLET | EPOLLRDHUP});                   │
│                      ─────────────                              │
│                         │                                       │
│                         └──── Monitor peer close explicitly     │
│                              显式监控对端关闭                   │
│                                                                 │
│  // In event loop                                               │
│  if (events & EPOLLRDHUP) {                                     │
│    // Peer closed - detected immediately without read()        │
│    // 对端关闭 - 无需 read() 立即检测到                         │
│    close_connection(fd);                                        │
│    return;                                                      │
│  }                                                              │
│                                                                 │
│  if (events & EPOLLIN) {                                        │
│    // Normal read handling                                      │
│    // 正常读取处理                                              │
│  }                                                              │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  EPOLLRDHUP 触发场景 (When EPOLLRDHUP triggers):                │
│                                                                 │
│  1. Peer calls close()                                          │
│     对端调用 close()                                            │
│                                                                 │
│  2. Peer calls shutdown(fd, SHUT_WR)                            │
│     对端调用 shutdown(fd, SHUT_WR)                              │
│                                                                 │
│  3. Peer process crashes                                        │
│     对端进程崩溃                                                │
│                                                                 │
│  4. Network disconnection detected                              │
│     检测到网络断开                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

注意点八:accept 也需要循环

在边缘触发模式下,当监听套接字可读时,可能有多个连接同时到达,必须循环调用 accept 直到返回 EAGAIN

┌─────────────────────────────────────────────────────────────────┐
│              accept 循环 (Loop in accept)                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  场景 (Scenario):                                               │
│  Multiple clients connect simultaneously                        │
│  多个客户端同时连接                                             │
│                                                                 │
│  ┌────────────────────────────────────────────────────────┐     │
│  │                                                        │     │
│  │   Client A ──┐                                         │     │
│  │              │                                         │     │
│  │   Client B ──┼──────▶ Server (listening)               │     │
│  │              │                                         │     │
│  │   Client C ──┘                                         │     │
│  │                                                        │     │
│  │   All 3 connect requests arrive before server         │     │
│  │   calls epoll_wait                                     │     │
│  │   在服务器调用 epoll_wait 之前,3 个连接请求都到达    │     │
│  │                                                        │     │
│  └────────────────────────────────────────────────────────┘     │
│                                                                 │
│  ❌ 错误做法 (Wrong):                                           │
│                                                                 │
│  void handle_accept() {                                         │
│    int client_fd = accept(listen_fd, ...);  // Gets client A    │
│    // Add to epoll...                                           │
│  }                                                              │
│  // Client B and C are LOST! 客户端 B 和 C 丢失!              │
│  // No more EPOLLIN notification until NEW connection!          │
│  // 直到新连接才会有 EPOLLIN 通知!                            │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  ✅ 正确做法 (Correct):                                         │
│                                                                 │
│  void handle_accept() {                                         │
│    while (true) {  // Loop until EAGAIN!                        │
│                    // 循环直到 EAGAIN!                         │
│      int client_fd = accept(listen_fd, ...);                    │
│                                                                 │
│      if (client_fd == -1) {                                     │
│        if (errno == EAGAIN || errno == EWOULDBLOCK) {           │
│          // All pending connections accepted                    │
│          // 所有等待的连接都已接受                              │
│          break;                                                 │
│        }                                                        │
│        // Handle error                                          │
│        break;                                                   │
│      }                                                          │
│                                                                 │
│      // Process client_fd...                                    │
│      set_nonblocking(client_fd);                                │
│      epoll_.add(client_fd, EPOLLIN | EPOLLET | EPOLLRDHUP);     │
│    }                                                            │
│  }                                                              │
│  // All 3 clients properly accepted!                            │
│  // 3 个客户端都正确接受!                                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

九、常见错误与调试

┌─────────────────────────────────────────────────────────────────┐
│                    常见错误汇总 (Common Mistakes)                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  错误 1: 忘记设置非阻塞模式                                     │
│  Mistake 1: Forgetting to set non-blocking mode                 │
│  症状: 服务器在某些时候无响应                                   │
│  Symptom: Server becomes unresponsive                           │
│  解决: 确保所有 fd 都调用 set_nonblocking()                     │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  错误 2: ET 模式下不循环读取                                    │
│  Mistake 2: Not looping read in ET mode                         │
│  症状: 数据丢失,消息不完整                                     │
│  Symptom: Data loss, incomplete messages                        │
│  解决: while 循环读取直到 EAGAIN                                │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  错误 3: 重复关闭文件描述符                                     │
│  Mistake 3: Double-closing file descriptors                     │
│  症状: 随机崩溃,文件描述符错乱                                 │
│  Symptom: Random crashes, fd confusion                          │
│  解决: 使用 RAII 封装,禁用拷贝                                 │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  错误 4: 忽略 EINTR                                             │
│  Mistake 4: Ignoring EINTR                                      │
│  症状: Ctrl+C 后服务器异常退出                                  │
│  Symptom: Server exits abnormally after Ctrl+C                  │
│  解决: 检查 errno == EINTR 并重试                               │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  错误 5: 文件描述符泄漏                                         │
│  Mistake 5: File descriptor leaks                               │
│  症状: "Too many open files" 错误                               │
│  Symptom: "Too many open files" error                           │
│  解决: 使用 RAII,确保所有路径都关闭 fd                         │
│  调试: lsof -p <pid> | wc -l 查看打开的 fd 数量                │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  错误 6: 没有处理 partial write                                 │
│  Mistake 6: Not handling partial write                          │
│  症状: 大数据发送不完整                                         │
│  Symptom: Large data sent incompletely                          │
│  解决: 检查 write 返回值,使用写缓冲区                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

十、性能优化建议

在实际生产环境中,还可以考虑以下优化:

第一,使用线程池处理业务逻辑。I/O 事件循环只负责接收和发送数据,实际的业务处理交给线程池,避免阻塞事件循环。

第二,使用对象池减少内存分配。频繁的 new/deletemalloc/free 会产生内存碎片,使用对象池可以显著提高性能。

第三,正确处理写操作。本文的示例简化了写操作,实际应用中写操作也可能返回 EAGAIN,需要使用写缓冲区并监听 EPOLLOUT 事件。

第四,考虑使用 EPOLLONESHOT。在多线程环境下,使用 EPOLLONESHOT 可以避免同一个文件描述符的事件被多个线程同时处理。

第五,合理设置 TCP 参数。根据应用场景设置 TCP_NODELAYSO_SNDBUFSO_RCVBUF 等参数。

第六,使用 EPOLLEXCLUSIVE(Linux 4.5+)。在多线程 accept 场景下,避免惊群效应。


十一、总结

epoll 是 Linux 下高性能网络编程的基础,理解其工作原理和正确使用方式对于开发高并发服务器至关重要。

┌─────────────────────────────────────────────────────────────────┐
│                        关键要点总结                              │
│                        Key Takeaways                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. epoll 三个核心 API:                                         │
│     epoll_create1() - 创建实例                                  │
│     epoll_ctl()     - 管理文件描述符                            │
│     epoll_wait()    - 等待事件                                  │
│                                                                 │
│  2. 两种触发模式:                                               │
│     LT (Level Triggered)  - 简单可靠,性能稍低                  │
│     ET (Edge Triggered)   - 高性能,编程复杂                    │
│                                                                 │
│  3. ET 模式的铁律:                                              │
│     • 必须使用非阻塞 I/O                                        │
│     • 必须循环读/写直到 EAGAIN                                  │
│     • accept 也要循环                                           │
│                                                                 │
│  4. 资源管理:                                                   │
│     • 使用 RAII 管理文件描述符                                  │
│     • 先从 epoll 移除,再关闭 fd                                │
│     • 设置 SO_REUSEADDR 便于重启                                │
│                                                                 │
│  5. 错误处理:                                                   │
│     • EINTR 不是错误,重试即可                                  │
│     • EAGAIN/EWOULDBLOCK 表示操作完成                           │
│     • 使用 EPOLLRDHUP 检测对端关闭                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

掌握 epoll 后,你就拥有了构建高性能网络服务的基础能力。在此基础上,可以进一步学习 Reactor 模式、Proactor 模式,以及 libevent、libev、libuv 等事件库的实现原理。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐