Skip to content

第 5 章 · epoll 深度解析 ⭐⭐⭐⭐⭐

学完本章你将知道:epoll 凭什么把 select/poll 的 O(n) 干到 O(1)?红黑树 + 就绪链表是怎么协同工作的?水平触发(LT)和边沿触发(ET)该怎么选?为什么 ET 必须配非阻塞 socket?这是面试重灾区,也是工业界事实标准


1. 一句话开场

epoll 是 2002 年 Linux 2.5.44 引入的多路复用接口。 它通过「注册一次、长期复用」+「内核维护就绪链表」两个杀招,把 select/poll 每次 O(n) 的开销降到了 O(就绪 fd 数)——这才是真正撑起 Nginx、Redis 百万并发的发动机。

生活类比

  • select / poll:每次问前台"我这一摞 1 万张登记卡里,谁有动静?"——前台每次都要把 1 万张卡翻一遍回答你。
  • epoll:你先把所有卡贴墙上(注册到红黑树),再告诉前台"出事了你主动把卡放到这个就绪盒子里"。你之后每次只要看就绪盒子——盒子里有几张拿几张,不用再翻墙了

2. epoll 三剑客:三个系统调用

2.1 函数签名

c
#include <sys/epoll.h>

// ① 创建 epoll 实例,返回 fd
int epoll_create1(int flags);     // flags = 0 或 EPOLL_CLOEXEC

// ② 注册 / 修改 / 删除监听项
int epoll_ctl(int epfd,
              int op,             // EPOLL_CTL_ADD / MOD / DEL
              int fd,
              struct epoll_event *event);

// ③ 等待事件
int epoll_wait(int epfd,
               struct epoll_event *events,
               int maxevents,
               int timeout);

// 事件结构体
struct epoll_event {
    uint32_t     events;      // 事件类型 (EPOLLIN | EPOLLOUT | EPOLLET | ...)
    epoll_data_t data;        // 用户自定义数据 (union)
};

typedef union epoll_data {
    void    *ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

2.2 三个调用的分工

调用频率作用类比
epoll_create1一次(启动时)在内核建一个 epoll 实例(红黑树 + 就绪链表 + 等待队列)"开个事件中心"
epoll_ctl(ADD/MOD/DEL)偶尔(连接来去时)增删改要监听的 fd"贴/撕墙上的卡"
epoll_wait频繁(事件循环每轮)拿走就绪 fd 列表"翻就绪盒子"

核心思想差异:select/poll 是每次都告诉内核"这一万个 fd 你帮我看一下";epoll 是先把一万个 fd 贴上墙,之后只问"墙上的卡谁有动静"

2.3 常用事件类型

含义
EPOLLIN可读
EPOLLOUT可写
EPOLLRDHUP对端关闭连接(半关闭也算,强烈推荐监听
EPOLLPRI紧急数据
EPOLLERR错误(默认监听,不用显式加)
EPOLLHUP挂断(默认监听
EPOLLET边沿触发模式(默认是水平触发)
EPOLLONESHOT触发一次后自动从监听集移除
EPOLLEXCLUSIVELinux 4.5+,避免惊群

3. 内核数据结构:红黑树 + 就绪链表

3.1 epoll 实例的内部长什么样?

text
              用户进程

      epoll_create1()


        ┌────────────────────┐
        │   eventpoll  (内核)  │
        │ ┌────────────────┐ │
        │ │ ① 红黑树 RB    │ │ ← 存所有"被监听的 fd" (key=fd)
        │ │   epi₁  epi₂   │ │   epoll_ctl ADD/MOD/DEL 操作它
        │ │  /  \   /  \   │ │
        │ │ ...  ...  ...  │ │
        │ └────────────────┘ │
        │ ┌────────────────┐ │
        │ │ ② 就绪链表 RDL │ │ ← 存"已就绪的 fd"
        │ │  epi_a → epi_b │ │   epoll_wait 从这里拿
        │ └────────────────┘ │
        │ ┌────────────────┐ │
        │ │ ③ 等待队列     │ │ ← epoll_wait 的进程睡这里
        │ └────────────────┘ │
        └────────────────────┘

                 │ 数据到达时回调
        ┌────────────────────┐
        │ socket fd 等待队列  │
        │ (内核网络栈触发)    │
        └────────────────────┘

图解说明

  • 红黑树 RB:存"我关心哪些 fd"——为什么用红黑树?因为 add/del/find 都是 O(log n),比哈希更适合"既要快查又要遍历"
  • 就绪链表 RDL:存"实际发生了事件的 fd"——这是 epoll 的杀手锏!数据来了内核主动放进来,不需要遍历
  • 等待队列:调 epoll_wait 的进程在没事件时挂这里睡觉

3.2 整个流程的时序

关键洞察

  • select/poll 是用户问内核:"我这堆 fd 谁就绪了?"内核 O(n) 找
  • epoll 是内核回调用户:每个 fd 早就被注册了回调,数据来了直接放进就绪链表——用户来取就行,O(就绪数)

3.3 ep_poll_callback:epoll 的灵魂

当 socket 收到数据时,内核网络栈会遍历这个 socket 的等待队列——所有挂在上面的进程都会被处理。如果发现 epoll 也挂在等待队列上,就调用 ep_poll_callback

c
// 内核源码简化版(fs/eventpoll.c)
static int ep_poll_callback(...) {
    struct epitem *epi = ...;
    struct eventpoll *ep = epi->ep;

    // 把这个 epi 放进就绪链表
    list_add_tail(&epi->rdllink, &ep->rdllist);

    // 唤醒在 epoll_wait 上睡觉的进程
    if (waitqueue_active(&ep->wq))
        wake_up(&ep->wq);

    return 1;
}

这就是 epoll 把 O(n) 干到 O(1) 的全部秘密——用回调把"主动找"变成"被动等"


4. 标准用法模板

4.1 最小骨架(LT 模式,默认)

c
int epfd = epoll_create1(EPOLL_CLOEXEC);

struct epoll_event ev = {.events = EPOLLIN, .data.fd = listen_fd};
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

struct epoll_event events[64];
while (1) {
    int n = epoll_wait(epfd, events, 64, -1);
    for (int i = 0; i < n; i++) {
        int fd = events[i].data.fd;
        if (fd == listen_fd) {
            int conn = accept(listen_fd, NULL, NULL);
            ev.events = EPOLLIN | EPOLLRDHUP;
            ev.data.fd = conn;
            epoll_ctl(epfd, EPOLL_CTL_ADD, conn, &ev);
        } else {
            char buf[1024];
            ssize_t r = read(fd, buf, sizeof(buf));
            if (r <= 0) {
                close(fd);
                /* 不需要 epoll_ctl(DEL) —— close fd 会自动从 epoll 摘除 */
            } else {
                write(fd, buf, r);
            }
        }
    }
}

细节 1:close fd 时会自动从 epoll 摘除——你不需要手动 EPOLL_CTL_DEL细节 2events[i].data 是 union——你可以塞 fd,也可以塞自己的连接结构体指针,这是事件循环里关联上下文的常用技巧

4.2 ET 模式骨架(必须非阻塞 + 循环读到 EAGAIN)

c
/* 1. fd 必须设非阻塞 */
fcntl(conn, F_SETFL, fcntl(conn, F_GETFL) | O_NONBLOCK);

/* 2. 注册时加 EPOLLET */
ev.events = EPOLLIN | EPOLLET | EPOLLRDHUP;

/* 3. 触发时必须循环读到 EAGAIN */
while (1) {
    char buf[1024];
    ssize_t r = read(fd, buf, sizeof(buf));
    if (r > 0) {
        process(buf, r);
        continue;
    }
    if (r == 0) { close(fd); break; }              // 对端关闭
    if (r < 0 && errno == EAGAIN) break;           // ET 必须读到这里
    if (r < 0) { perror("read"); close(fd); break; }
}

ET 模式三大铁律违反任何一条都会卡死/丢数据):

  1. ✅ socket 必须设为非阻塞——否则最后一次 read 会卡死
  2. ✅ 必须循环读到 EAGAIN——否则下次 epoll_wait 不会再通知
  3. ✅ 写也要循环写到 EAGAIN——同理

5. LT vs ET:水平触发 vs 边沿触发 ⭐⭐⭐

5.1 概念辨析

模式触发条件类比
LT (Level Triggered) 默认只要 fd 缓冲区里还有数据,就一直通知闹钟一直响到你按掉
ET (Edge Triggered)状态变化的那一刻通知一次门铃响一声你没听见就错过

5.2 用具体例子理解

场景:socket 缓冲区里有 10 字节数据。你 read 了 4 字节。

LT 模式

text
事件 1:  [10B 数据到] → epoll_wait 返回 EPOLLIN
你 read 4B               → 缓冲区还剩 6B
下一次 epoll_wait 立刻返回 EPOLLIN  ← 因为还有数据"高位"
你又 read 6B              → 缓冲区空
下一次 epoll_wait 阻塞    ← 没数据了

ET 模式

text
事件 1:  [10B 数据到]    → epoll_wait 返回 EPOLLIN(这是状态变化点!)
你 read 4B               → 缓冲区还剩 6B
下一次 epoll_wait 阻塞   ← 状态没"再变化"!🚨 6B 数据被你忘了!

核心理解

  • LT = "只要有水就通知" → 你忘了喝也不怕
  • ET = "水开始来的瞬间通知一次" → 你不喝完就完蛋

5.3 时序图对比

5.4 LT vs ET 优缺点对比

维度LT(默认)ET
写法难度✅ 简单,少写一次也没事❌ 必须循环到 EAGAIN,配非阻塞
唤醒次数多(有数据就反复醒)少(只在状态变化时)
CPU 效率一般更高(少 syscall)
是否易丢事件❌ 不会✅ 写错就丢
典型用户Redis、Java NIO 默认Nginx、libev、libuv

5.5 该选谁?

场景推荐
新手 / 简单业务LT
极致性能(Nginx 级别)ET
Redis / 缓存类(事件少且简单)LT
框架/库(用户感知不到)ET(性能极致)

业界数据:Nginx 用 ET 比 LT 大约少 30~40% syscall 次数——但代价是代码复杂度高很多。


6. EPOLLONESHOT:避免多线程并发问题

6.1 问题场景

多线程 worker 模型下,同一个 fd 可能被多个线程同时处理——线程 A 在 read,B 也来 read,数据顺序乱了。

6.2 解决方案

c
ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);

// 触发后这个 fd 不会再触发,直到你重新 ARM
worker_thread() {
    epoll_wait(...);
    handle(fd);
    // 处理完毕,重新挂回 epoll
    ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
    epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);   // ⚠️ 必须 MOD 再 ARM
}

加了 EPOLLONESHOT 后,一个 fd 一次只会触发到一个 worker,避免线程竞争。


7. 内核版本演进与惊群修复

7.1 epoll 演进时间线

text
2.5.44 (2002)    epoll 三剑客诞生
2.6.x            进入主线,性能优化
3.7              加入 EPOLLEXCLUSIVE 的雏形
4.5  (2016)      正式 EPOLLEXCLUSIVE → 单触发,避免惊群
5.1  (2019)      io_uring 出现,开始威胁 epoll 的地位

7.2 惊群问题与解决

惊群(Thundering Herd):多个进程/线程都在等同一个 fd(如 listen_fd),来一个连接所有人都被唤醒,但只有一个能 accept 成功——其他都白醒。

解决方案

  1. EPOLLEXCLUSIVE(4.5+):内核保证只唤醒一个等待者
  2. SO_REUSEPORT(3.9+):每个 worker 有独立 listen_fd,内核负责负载均衡
  3. Nginx 默认用 accept_mutex 锁强行串行化

8. 动手实践

请直接看本页底部 [💻 示例代码] 区,包含 4 个示例:

文件内容
01_epoll_basic.c入门:epoll 三剑客最小演示
02_epoll_lt_server.cLT 模式 echo 服务器
03_epoll_et_server.cET 模式 echo 服务器(非阻塞 + 循环读)
04_epoll_oneshot.cEPOLLONESHOT 多线程 worker 模板

实践 1:观察 LT/ET 触发差异

bash
# 编译
gcc 02_epoll_lt_server.c -o lt_srv
gcc 03_epoll_et_server.c -o et_srv

# 终端 1 跑 LT 模式(带打印)
./lt_srv 9001

# 终端 2 跑 ET 模式
./et_srv 9002

# 终端 3 用 nc 一次发大数据
yes "abc" | head -c 100000 | nc localhost 9001
yes "abc" | head -c 100000 | nc localhost 9002

# 对比两个服务器的"epoll_wait 返回次数"——ET 会显著少于 LT

实践 2:strace 观察 epoll 系统调用

bash
strace -e trace=epoll_create1,epoll_ctl,epoll_wait ./lt_srv 9999

# 输出(简化):
# epoll_create1(EPOLL_CLOEXEC)         = 4
# epoll_ctl(4, EPOLL_CTL_ADD, 3, ...)  = 0      ← 只在新连接来时调
# epoll_wait(4, [...], 64, -1)         = 1
# epoll_wait(4, [...], 64, -1)         = 1
# ...

对比 select 版的 strace——epoll_ctl 调用稀疏,epoll_wait 才是循环里的主调用


9. 常见陷阱(必看)

🪤 陷阱 1:ET 模式 + 阻塞 socket = 死锁

c
// ❌ 致命错误
ev.events = EPOLLIN | EPOLLET;
// fd 是阻塞的(默认)
epoll_ctl(...);

// 触发后:
read(fd, ...);   // 读完所有数据
read(fd, ...);   // 没数据了,阻塞 socket 会卡住整个事件循环!

修复:fd 必须 O_NONBLOCK

🪤 陷阱 2:ET 模式只读一次 → 丢事件

c
// ❌ ET 模式下读完一次就完事
read(fd, buf, sizeof(buf));
// 缓冲区可能还有几 KB 数据,但下次 epoll_wait 不会再通知!

修复:循环读到 EAGAIN。

🪤 陷阱 3:忘了监听 EPOLLRDHUP

c
// ❌ 只监 EPOLLIN
ev.events = EPOLLIN;

// 对端 close 后:
// LT 模式:read 返回 0,你能感知
// 但如果你是 ET:可能会有边界条件踩坑

建议:永远加上 EPOLLRDHUP,明确感知半关闭。

🪤 陷阱 4:epoll_wait 数组开太小 → 饥饿

c
struct epoll_event events[10];
epoll_wait(epfd, events, 10, -1);    // 一次最多取 10 个
// 如果有 1000 个就绪,剩下 990 个要等下一次循环
// 风险:某个 fd 一直没机会被处理 → 饥饿

建议:开到 64~256 之间,按业务调整。

🪤 陷阱 5:close fd 后还用旧 fd 数字

c
close(fd);
// fd 数字可能立刻被新连接复用
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);   // ❌ 操作到了新连接!

建议:用 events[i].data.ptr 存指向连接对象的指针,而不是裸 fd——你能验证对象是否还活着。

🪤 陷阱 6:忘了处理 EINTR

c
int n = epoll_wait(...);
if (n < 0) { perror("epoll_wait"); break; }   // ❌ 信号打断也会失败

修复

c
int n = epoll_wait(...);
if (n < 0) {
    if (errno == EINTR) continue;
    perror("epoll_wait");
    break;
}

10. 本章小结

text
epoll 关键事实清单(必背)
───────────────────────────────
✅ 三剑客:epoll_create1 / epoll_ctl / epoll_wait
✅ 数据结构:红黑树(监听集)+ 就绪链表 + 等待队列
✅ 性能:O(就绪 fd 数),与监听总数无关
✅ LT 模式:有数据就反复通知,简单不易错
✅ ET 模式:状态变化时只通知一次,必须非阻塞 + 循环到 EAGAIN
✅ EPOLLRDHUP:对端关闭事件,强烈推荐监听
✅ EPOLLONESHOT:多线程 worker 必备
✅ EPOLLEXCLUSIVE / SO_REUSEPORT:解决惊群

11. 真实面试题(高频)

Q1:epoll 比 select / poll 快在哪?请从内核数据结构层面解释。

A:① select/poll 每次都把全部 fd 传进内核,O(n) 拷贝;epoll 用 epoll_ctl 一次注册到红黑树,后续 epoll_wait 只传回就绪的;② select/poll 内核要 O(n) 遍历查就绪;epoll 内核网络栈在数据到达时通过回调 ep_poll_callback 把 fd 放进就绪链表,O(1) 拿到;③ select/poll 用户态还要再 O(n) 遍历找就绪 fd,epoll 直接拿到就绪数组。3 倍 O(n) → O(就绪数)

Q2:epoll 的红黑树为什么用红黑树而不是哈希表?

A:① 红黑树插入/查找/删除都是 O(log n),worst case 稳定——哈希表最坏 O(n);② epoll 内部还需要"按 fd 顺序遍历或区间操作",红黑树天然有序;③ 红黑树不需要扩容,没有 rehash 卡顿。Redis 用跳表也是出于类似理由。

Q3:水平触发(LT)和边沿触发(ET)的区别是什么?为什么 ET 必须非阻塞?

A:LT 只要 fd 缓冲区有数据/有空间,就反复通知;ET 只在状态变化的瞬间通知一次。ET 必须非阻塞是因为——ET 通知一次后必须把数据全读完(否则丢事件),所以代码里要 while (read != EAGAIN),而循环里最后一次 read 在没数据时:阻塞 socket 会卡死,非阻塞 socket 才会返回 EAGAIN 让你跳出循环。

Q4:什么是惊群?epoll 怎么解决?

A:多个进程/线程都在 epoll_wait 同一个 listen_fd——一个连接来了全被唤醒,但只有一个能 accept 成功,其他白醒(开销)。解决方案:① Linux 4.5+ 用 EPOLLEXCLUSIVE 标志,内核保证只唤醒一个;② 用 SO_REUSEPORT 让每个 worker 有独立 listen_fd,内核内置负载均衡;③ 应用层加锁(如 Nginx 的 accept_mutex)。

Q5:close 一个 fd 后,需要 epoll_ctl(DEL) 吗?

A:不需要——内核在 fd 真正释放时会自动把它从所有 epoll 实例的红黑树中摘除。但有个例外:如果有 fork 后子进程持有这个 fd,父进程 close 不会真释放,这时 epoll 仍可能触发——所以多进程时要小心。

Q6:epoll_wait 的 maxevents 参数填多少合适?

A:业界经验值 64~256。太小会饥饿(一次取不完,慢的 fd 没机会处理);太大会一次性占用过多用户内存且让单次循环过长,影响延迟。Nginx 默认 512,Redis 默认 1024。

Q7:epoll 自己也是 fd 吗?能监听另一个 epoll 吗?

A:——epoll_create1 返回的就是 fd(指向 anon_inode:[eventpoll])。可以用一个 epoll 监听另一个 epoll(嵌套),libev 等库就用这个特性做层级化事件循环——但是嵌套深度有限制,一般不建议超过 5 层。

Q8:水平触发模式下,如果 EPOLLIN 一直不读,会怎么样?

A:每次 epoll_wait 都会立刻返回这个 fd——等于退化成"忙循环",CPU 100%。这种事件叫做 "starving"——必须要么读走数据,要么从 epoll 摘除(DEL 或 MOD 去掉 EPOLLIN)。


下一站第 6 章:横向对比,把 select / poll / epoll 三者的所有维度做最终汇总,给出明确的选型决策。

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

c
/*
 * 01_epoll_basic.c —— epoll 三剑客最小演示
 *
 * 编译: gcc 01_epoll_basic.c -o epoll_basic
 * 运行: ./epoll_basic
 * 玩法: 5 秒内输入任意文字回车
 *
 * 仅监听 stdin,目的是看清 create/ctl/wait 的最小调用形态。
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <errno.h>

int main(void) {
    /* 1) 创建 epoll 实例(这本身也是个 fd,会随进程退出关闭) */
    int epfd = epoll_create1(EPOLL_CLOEXEC);
    if (epfd < 0) { perror("epoll_create1"); return 1; }

    /* 2) 把 stdin 注册进去,关心 EPOLLIN 事件 */
    struct epoll_event ev;
    ev.events  = EPOLLIN;
    ev.data.fd = STDIN_FILENO;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev) < 0) {
        perror("epoll_ctl"); return 1;
    }

    printf("⏰ 5 秒内输入任意文字...(epoll 等着)\n");
    fflush(stdout);

    /* 3) 等事件 */
    struct epoll_event events[8];
    int n = epoll_wait(epfd, events, 8, 5000);   /* timeout = 5000 ms */

    if (n < 0)      perror("epoll_wait");
    else if (n == 0) printf("😴 5 秒到了,啥也没等到\n");
    else {
        for (int i = 0; i < n; i++) {
            if (events[i].data.fd == STDIN_FILENO) {
                char buf[256];
                ssize_t r = read(STDIN_FILENO, buf, sizeof(buf) - 1);
                if (r > 0) {
                    buf[r] = '\0';
                    printf("✅ epoll 通知 stdin 可读,读到: %s", buf);
                }
            }
        }
    }

    close(epfd);    /* 关闭 epoll 实例 */
    return 0;
}
c
/*
 * 02_epoll_lt_server.c —— epoll LT (水平触发) 模式 echo 服务器
 *
 * 编译: gcc 02_epoll_lt_server.c -o lt_srv
 * 运行: ./lt_srv 9001
 * 测试: nc localhost 9001
 *
 * LT 模式特点:
 *   - 不需要 fd 设非阻塞
 *   - 不需要循环 read 到 EAGAIN(少读一次也没事,下次 epoll_wait 还会通知)
 *   - 写法接近 select/poll,最容易上手
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#define MAX_EVENTS 64
#define BUF_SIZE   1024

static int make_listen_socket(int port) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    int yes = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0 || listen(fd, 64) < 0) {
        perror("bind/listen"); exit(1);
    }
    printf("🚀 epoll [LT] 服务器已启动,端口 %d\n", port);
    return fd;
}

int main(int argc, char *argv[]) {
    int port = (argc > 1) ? atoi(argv[1]) : 9001;
    int lfd  = make_listen_socket(port);

    int epfd = epoll_create1(EPOLL_CLOEXEC);

    /* 注册监听 socket(LT 模式不加 EPOLLET) */
    struct epoll_event ev = {.events = EPOLLIN, .data.fd = lfd};
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    struct epoll_event events[MAX_EVENTS];
    long wait_count = 0;

    while (1) {
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (n < 0) {
            if (errno == EINTR) continue;
            perror("epoll_wait"); break;
        }
        wait_count++;
        printf("🔔 [LT] epoll_wait 第 %ld 次返回, 就绪 %d 个 fd\n", wait_count, n);

        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;
            uint32_t evs = events[i].events;

            if (fd == lfd) {
                /* 新连接 */
                struct sockaddr_in cli;
                socklen_t cl = sizeof(cli);
                int conn = accept(lfd, (struct sockaddr*)&cli, &cl);
                if (conn >= 0) {
                    ev.events  = EPOLLIN | EPOLLRDHUP;     /* LT 模式 */
                    ev.data.fd = conn;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, conn, &ev);
                    printf("    ➕ 新客户端 fd=%d, 来自 %s:%d\n",
                           conn, inet_ntoa(cli.sin_addr), ntohs(cli.sin_port));
                }
            } else {
                if (evs & (EPOLLERR | EPOLLHUP | EPOLLRDHUP)) {
                    printf("    🗑  fd=%d 错误/挂断, 关闭\n", fd);
                    close(fd);
                    /* close 会自动从 epoll 摘除,无需 EPOLL_CTL_DEL */
                    continue;
                }
                if (evs & EPOLLIN) {
                    char buf[BUF_SIZE];
                    /* LT 模式:读一次就行,剩下的下次 epoll_wait 还会通知 */
                    ssize_t r = read(fd, buf, sizeof(buf));
                    if (r <= 0) {
                        if (r == 0) printf("    👋 fd=%d 对端关闭\n", fd);
                        close(fd);
                    } else {
                        write(fd, buf, r);
                        printf("    📨 fd=%d echo %ld 字节\n", fd, r);
                    }
                }
            }
        }
    }
    close(epfd);
    close(lfd);
    return 0;
}
c
/*
 * 03_epoll_et_server.c —— epoll ET (边沿触发) 模式 echo 服务器
 *
 * 编译: gcc 03_epoll_et_server.c -o et_srv
 * 运行: ./et_srv 9002
 * 测试: yes "abc" | head -c 100000 | nc localhost 9002
 *
 * ET 模式三大铁律:
 *   1) socket 必须 O_NONBLOCK
 *   2) read/accept 必须循环到 EAGAIN
 *   3) 否则丢事件 / 卡死
 *
 * 跟 LT 版对比"epoll_wait 调用次数",能直观看到 ET 节省 syscall。
 */

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

#define MAX_EVENTS 64
#define BUF_SIZE   1024

static void set_nonblock(int fd) {
    int fl = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

static int make_listen_socket(int port) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    int yes = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
    set_nonblock(fd);    /* 监听 socket 也设非阻塞 */
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0 || listen(fd, 64) < 0) {
        perror("bind/listen"); exit(1);
    }
    printf("🚀 epoll [ET] 服务器已启动,端口 %d\n", port);
    return fd;
}

int main(int argc, char *argv[]) {
    int port = (argc > 1) ? atoi(argv[1]) : 9002;
    int lfd = make_listen_socket(port);

    int epfd = epoll_create1(EPOLL_CLOEXEC);

    struct epoll_event ev = {.events = EPOLLIN | EPOLLET, .data.fd = lfd};
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    struct epoll_event events[MAX_EVENTS];
    long wait_count = 0, total_bytes = 0;

    while (1) {
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (n < 0) { if (errno == EINTR) continue; perror("epoll_wait"); break; }
        wait_count++;
        printf("🔔 [ET] epoll_wait 第 %ld 次返回, 就绪 %d 个 fd, 累计接收 %ld 字节\n",
               wait_count, n, total_bytes);

        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;
            uint32_t evs = events[i].events;

            if (fd == lfd) {
                /* ET 模式: 必须循环 accept 到 EAGAIN,否则会漏新连接 */
                while (1) {
                    struct sockaddr_in cli;
                    socklen_t cl = sizeof(cli);
                    int conn = accept(lfd, (struct sockaddr*)&cli, &cl);
                    if (conn < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) break;
                        perror("accept"); break;
                    }
                    set_nonblock(conn);
                    ev.events  = EPOLLIN | EPOLLET | EPOLLRDHUP;
                    ev.data.fd = conn;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, conn, &ev);
                    printf("    ➕ 新客户端 fd=%d, 来自 %s:%d\n",
                           conn, inet_ntoa(cli.sin_addr), ntohs(cli.sin_port));
                }
            } else {
                if (evs & (EPOLLERR | EPOLLHUP | EPOLLRDHUP)) {
                    printf("    🗑  fd=%d 关闭\n", fd);
                    close(fd);
                    continue;
                }
                if (evs & EPOLLIN) {
                    /* ET 模式: 必须循环 read 到 EAGAIN */
                    while (1) {
                        char buf[BUF_SIZE];
                        ssize_t r = read(fd, buf, sizeof(buf));
                        if (r > 0) {
                            total_bytes += r;
                            write(fd, buf, r);
                            continue;
                        }
                        if (r == 0) {
                            printf("    👋 fd=%d 对端关闭\n", fd);
                            close(fd);
                            break;
                        }
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            /* 数据读完了,正常退出循环 */
                            break;
                        }
                        perror("read"); close(fd); break;
                    }
                }
            }
        }
    }
    close(epfd);
    close(lfd);
    return 0;
}
c
/*
 * 04_epoll_oneshot.c —— EPOLLONESHOT + 多线程 worker 模板
 *
 * 编译: gcc 04_epoll_oneshot.c -o oneshot_srv -pthread
 * 运行: ./oneshot_srv 9003
 * 测试: 同时多个 nc localhost 9003,会被多个 worker 线程公平消费
 *
 * 思路:主线程 epoll_wait → 把就绪的 fd 派给一个 worker 线程
 *      EPOLLONESHOT 保证同一个 fd 一次只被一个 worker 处理
 *      worker 处理完必须重新 EPOLL_CTL_MOD 把它"挂回去"
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <pthread.h>
#include <errno.h>

#define MAX_EVENTS 64
#define WORKERS    4
#define BUF_SIZE   1024

static int epfd;

/* 简单的有锁队列 */
typedef struct {
    int items[1024];
    int head, tail, size;
    pthread_mutex_t mu;
    pthread_cond_t  cv;
} job_queue_t;

static job_queue_t Q;

static void q_init() {
    Q.head = Q.tail = Q.size = 0;
    pthread_mutex_init(&Q.mu, NULL);
    pthread_cond_init(&Q.cv, NULL);
}
static void q_push(int fd) {
    pthread_mutex_lock(&Q.mu);
    if (Q.size < 1024) {
        Q.items[Q.tail] = fd;
        Q.tail = (Q.tail + 1) % 1024;
        Q.size++;
        pthread_cond_signal(&Q.cv);
    }
    pthread_mutex_unlock(&Q.mu);
}
static int q_pop() {
    pthread_mutex_lock(&Q.mu);
    while (Q.size == 0) pthread_cond_wait(&Q.cv, &Q.mu);
    int fd = Q.items[Q.head];
    Q.head = (Q.head + 1) % 1024;
    Q.size--;
    pthread_mutex_unlock(&Q.mu);
    return fd;
}

static void set_nonblock(int fd) {
    int fl = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

static void rearm(int fd) {
    /* 重新挂回 epoll,否则这个 fd 永远不会再触发 */
    struct epoll_event ev = {
        .events = EPOLLIN | EPOLLET | EPOLLONESHOT | EPOLLRDHUP,
        .data.fd = fd,
    };
    if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) < 0) {
        if (errno != ENOENT) perror("EPOLL_CTL_MOD");
    }
}

static void *worker(void *arg) {
    long wid = (long)arg;
    while (1) {
        int fd = q_pop();
        printf("[worker-%ld] 接到任务 fd=%d\n", wid, fd);

        int closed = 0;
        while (1) {
            char buf[BUF_SIZE];
            ssize_t r = read(fd, buf, sizeof(buf));
            if (r > 0) {
                write(fd, buf, r);
                continue;
            }
            if (r == 0) { close(fd); closed = 1; break; }
            if (errno == EAGAIN || errno == EWOULDBLOCK) break;
            close(fd); closed = 1; break;
        }
        if (!closed) rearm(fd);    /* 处理完后必须重新挂回 */
    }
    return NULL;
}

static int make_listen_socket(int port) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    int yes = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
    set_nonblock(fd);
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0 || listen(fd, 64) < 0) {
        perror("bind/listen"); exit(1);
    }
    printf("🚀 ONESHOT + 多线程 worker 服务器启动,端口 %d (workers=%d)\n", port, WORKERS);
    return fd;
}

int main(int argc, char *argv[]) {
    int port = (argc > 1) ? atoi(argv[1]) : 9003;
    int lfd  = make_listen_socket(port);
    epfd = epoll_create1(EPOLL_CLOEXEC);

    /* 监听 socket: 不加 ONESHOT,因为只有主线程操作它 */
    struct epoll_event ev = {.events = EPOLLIN | EPOLLET, .data.fd = lfd};
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    q_init();
    pthread_t tids[WORKERS];
    for (long i = 0; i < WORKERS; i++) {
        pthread_create(&tids[i], NULL, worker, (void*)i);
    }

    struct epoll_event events[MAX_EVENTS];
    while (1) {
        int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (n < 0) { if (errno == EINTR) continue; perror("epoll_wait"); break; }

        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;
            uint32_t evs = events[i].events;

            if (fd == lfd) {
                while (1) {
                    int conn = accept(lfd, NULL, NULL);
                    if (conn < 0) { break; }
                    set_nonblock(conn);
                    struct epoll_event cev = {
                        .events = EPOLLIN | EPOLLET | EPOLLONESHOT | EPOLLRDHUP,
                        .data.fd = conn,
                    };
                    epoll_ctl(epfd, EPOLL_CTL_ADD, conn, &cev);
                    printf("[main] 新连接 fd=%d (已加 ONESHOT)\n", conn);
                }
            } else {
                /* 派给 worker 线程处理 */
                if (evs & (EPOLLERR | EPOLLHUP | EPOLLRDHUP)) {
                    close(fd);
                    continue;
                }
                if (evs & EPOLLIN) {
                    q_push(fd);
                }
            }
        }
    }
    return 0;
}

01_epoll_basic.c ↗ · 02_epoll_lt_server.c ↗ · 03_epoll_et_server.c ↗ · 04_epoll_oneshot.c ↗