Linux IO 多路复用 epoll 机制

一、什么是 IO 多路复用

在 Linux 中:

  • IO 就是对文件的读写操作
  • 多路是指同时读写多个文件
  • 复用是指使用一个线程处理多个文件的同时读写

问题来了: 为什么需要多路复用?

为了快,要给每一个 fd 通道最快的感受,要让每一个 fd 觉得,你只在给他一个人跑腿。

为了更快的处理多路 IO,大体有两种方案:

  • 一种方案是:一个 IO 请求(比如 write )对应一个线程来处理,但是线程数多了,性能反倒会差。
  • 另外一种方案是:IO 多路复用

接下来,我们就来看看 IO 多路复用

我不用任何其他系统调用,能否实现IO 多路复用

可以的,写个 for 循环,每次都尝试 IO 一下,读/写到了就处理,读/写不到就 sleep 下。

while(true) 
{
    foreach fd数组 
    {
        read/write(fd, /* 参数 */)
    }

    sleep(1s)
}
  • 默认情况下,我们没有加任何参数 create 出的 fd 是阻塞类型的。我们读数据的时候,如果数据还没准备好,是会需要等待的,当我们写数据的时候,如果还没准备好,默认也会卡住等待。所以,在上面伪代码中的 read/write 是可能被直接卡死,而导致整个线程都得到不到运行。只需要把 fd 都设置成非阻塞模式。这样 read/write 的时候,如果数据没准备好,返回 EAGIN 的错误即可,不会卡住线程,从而整个系统就运转起来了。

  • 这个实现只是为了帮助我们理解 IO 多路复用,实际上,上面的实现在性能上有很大的缺陷。for 循环每次要定期 sleep 1s,这个会导致吞吐能力极差,因为很可能在刚好要 sleep 的时候,所有的 fd 都准备好 IO 数据,而这个时候却要硬生生的等待 1s

  • IO 多路复用 就是 1 个线程处理 多个 fd 的模式。我们的要求是:这个 “1” 就要尽可能的快,避免一切无效工作,要把所有的时间都用在处理句柄的 IO 上,不能有任何空转,sleep 的时间浪费。

为了实现上诉的功能,内核提供了 3 种系统调用 select,poll,epoll

这 3 种系统调用都能够管理 fd 的可读可写事件, 在所有 fd 不可读不可写无所事事的时候,可以阻塞线程,切走 cpu 。fd 可读写的时候,对应线程会被唤醒

三者的差异主要是在性能上, epoll 的性能是强于 select 和 poll 的,我们接下来就来看看 epoll 以及它的具体使用。


二、epoll简介

2.1 什么是 epoll?

epoll Linux 内核提供的高性能 IO 多路复用机制,是传统 select 和 poll 机制的升级版,专门解决高并发场景下的多 IO 事件管理性能瓶颈,也是Android Native 层(Framework/HAL/ 底层驱动)处理多 IO 事件的核心技术(比如 WiFi/Bluetooth 的 socket 通信、Binder 事件、native loop 消息循环、各硬件 HAL 的 IO 事件监听,均基于 epoll 实现)。

简单来说:epoll 能让一个进程高效监听成千上万个文件描述符(fd)的 IO 事件(如可读、可写、异常)当某个 fd 有事件发生时,epoll 会主动通知进程处理,无需进程轮询检测所有 fd,这是它比 select/poll 高效的核心原因。

IO 多路复用的本质:一个进程同时监听多个 fd 的 IO 状态,避免为每个 fd 创建独立进程 / 线程(减少资源开销),实现单进程高效处理多 IO 任务。epoll 是 Linux 下 IO 多路复用的工业级方案,也是 Android 底层开发的必备基础。

2.1 为什么需要 epoll?(select/poll 的痛点)

epoll 是为了解决传统 select/poll 的三大致命问题而生,这也是 Android 底层放弃 select/poll、全面使用 epoll 的原因:

  1. fd 数量限制:select 有最大 fd 数量限制(默认 1024),无法满足 Android 底层多硬件、多 socket 的高并发需求;
  2. 轮询效率极低:select/poll 每次都需要轮询所有监听的 fd判断是否有事件,即使只有 1 个 fd 有事件,也要遍历全部,fd 越多效率越低;
  3. 内核 / 用户态拷贝开销:select/poll 每次调用都需要将监听的 fd 集合从用户态拷贝到内核态,事件返回后又要拷贝回用户态,高并发下拷贝开销极大。

epoll 从底层设计上彻底解决了这三个问题,是百万级 fd 高并发场景的最优解(Android 设备中底层监听的 fd 数量通常在千级,epoll 能轻松处理)。

epoll 之所以做到了高效,最关键的两点:

  1. 内部管理 fd 使用了高效的红黑树结构管理,做到了增删改之后性能的优化和平衡;
  2. epoll 池添加 fd 的时候,调用 file_operations->poll ,把这个 fd 就绪之后的回调路径安排好。通过事件通知的形式,做到最高效的运行;
  3. epoll 池核心的两个数据结构:红黑树和就绪列表。红黑树是为了应对用户的增删改需求,就绪列表是 fd 事件就绪之后放置的特殊地点,epoll 池只需要遍历这个就绪链表,就能给用户返回所有已经就绪的 fd 数组;

2.3 epoll 的核心概念(3 个核心要素)

epoll 的使用围绕3 个核心对象 / 操作展开,概念简单且固定,Android Native 层代码中也完全遵循这个模型,先理解概念再看代码会更清晰:

  1. epoll 实例(epfd)
    调用epoll_create()创建,返回一个专属的文件描述符(epfd),代表一个 epoll 实例;
    每个 epoll 实例独立管理一组「监听的 fd + 对应事件」,进程可以创建多个 epoll 实例(Android 底层通常单实例管理所有相关 fd);
    本质:内核为 epfd 维护两棵核心数据结构——红黑树(管理所有注册的 fd 和事件,支持快速增删改)、就绪链表(仅存放有 IO 事件发生的 fd,无需轮询)。
  2. 事件注册(epoll_ctl)
    调用epoll_ctl(epfd, 操作类型, fd, 事件结构体),将需要监听的 fd和要监听的 IO 事件(如可读 EPOLLIN、可写 EPOLLOUT、异常 EPOLLERR)注册到 epoll 实例中;
    操作类型分 3 种:
  • EPOLL_CTL_ADD:添加 fd 和事件到 epoll 实例;
  • EPOLL_CTL_DEL:从 epoll 实例中删除 fd;
  • EPOLL_CTL_MOD:修改已注册 fd 的监听事件;
    特点:仅在增删改时操作内核红黑树,无需重复拷贝 fd 集合,一次注册永久有效(直到主动删除)。
  1. 事件等待(epoll_wait)
    调用epoll_wait(epfd, 就绪事件数组, 数组大小, 超时时间),阻塞等待epoll 实例中注册的 fd 发生 IO 事件;
    当有 fd 触发事件时,内核会将就绪的 fd 和事件从「就绪链表」拷贝到用户态的就绪事件数组中,进程直接遍历该数组即可处理事件;
    特点:只拷贝有事件的 fd,无事件时不拷贝,且通过mmap实现内核 / 用户态内存共享,彻底避免频繁拷贝开销。

2.4 epoll 的两种触发模式

epoll 支持水平触发(LT,Level Trigger)和边缘触发(ET,Edge Trigger),决定了「fd 有事件时,epoll 如何通知进程」,是 epoll 使用的核心,不过Android Native 层几乎全部使用 LT 模式(避免漏事件,底层驱动 /hal 对稳定性要求更高),了解一下即可

  1. 水平触发(LT,默认模式)
    触发规则:只要 fd 的 IO 缓冲区中有数据 / 空间(如可读缓冲区有数据、可写缓冲区有空位),epoll_wait 就会持续通知进程,直到缓冲区的事件被处理完毕;
    特点:容错性高,不会漏事件,即使进程一次没处理完数据,下次 epoll_wait 仍会通知,适合底层驱动 /hal 的稳定场景(Android 首选);
    示例:socket 的可读缓冲区有 1024 字节数据,进程第一次只读取了 512 字节,剩余 512 字节,下一次 epoll_wait 仍会触发该 socket 的 EPOLLIN 事件。
  2. 边缘触发(ET)
    触发规则:仅在 fd 的 IO 状态发生变化的瞬间通知一次(如从无数据→有数据、从满缓冲区→有空位),后续即使缓冲区还有数据 / 空间,也不会再通知,直到有新的 IO 状态变化;
    特点:效率更高,通知次数少,但要求进程一次性处理完缓冲区的所有数据,否则会漏事件,适合高并发 socket 服务器(如网络框架);
    示例:socket 的可读缓冲区从无到有 1024 字节,epoll_wait 仅通知一次,若进程只读取 512 字节,剩余 512 字节不会再被通知,直到有新的数据写入缓冲区。

补充:ET 模式必须搭配非阻塞 fd使用,否则进程一次处理不完数据会被阻塞,导致其他 fd 无法处理;LT 模式支持阻塞 / 非阻塞 fd。

2.5 哪些 fd 可以用 epoll 来管理?

再来思考另外一个问题:由于并不是所有的 fd 对应的文件系统都实现了poll接口,所以自然并不是所有的 fd 都可以放进 epoll 池,那么有哪些文件系统的 file_operations 实现了 poll 接口?

首先说,类似 ext2,ext4,xfs 这种常规的文件系统是没有实现的,换句话说,这些你最常见的、真的是文件的文件系统反倒是用不了 epoll 机制的。

那谁支持呢?

最常见的就是网络套接字:socket 。网络也是 epoll 池最常见的应用地点。Linux 下万物皆文件,socket 实现了一套 socket_file_operations 的逻辑( net/socket.c ):

static const struct file_operations socket_file_ops = {
    .read_iter =    sock_read_iter,
    .write_iter =   sock_write_iter,
    .poll =     sock_poll,
    // ...
};

我们看到 socket 实现了 poll 调用,所以 socket fd 是天然可以放到 epoll 池管理的。

还有吗?

有的,其实 Linux 下还有两个很典型的 fd ,常常也会放到 epoll 池里。

  • eventfd:eventfd 实现非常简单,故名思义就是专门用来做事件通知用的。使用系统调用 eventfd 创建,这种文件 fd 无法传输数据,只用来传输事件,常常用于生产消费者模式的事件实现;
  • timerfd:这是一种定时器 fd,使用 timerfd_create 创建,到时间点触发可读事件;

小结一下

ext2,ext4,xfs 等这种真正的文件系统的 fd ,无法使用 epoll 管理;
socket fd,eventfd,timerfd 这些实现了 poll 调用的可以放到 epoll 池进行管理;
其实,在 Linux 的模块划分中,eventfd,timerfd,epoll 池都是文件系统的一种模块实现。


三、epoll 的使用

使用 epoll 需要以下三个系统调用:

//头文件
#include <sys/epoll.h>

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • epollcreate 负责创建一个池子,一个监控和管理句柄 fd 的池子;
  • epollctl 负责管理这个池子里的 fd 增、删、改;
  • epollwait 就是负责打盹的,让出 CPU 调度,但是只要有“事”,立马会从这里唤醒;

3.1 epoll API

3.1.1 epoll_event

前置结构体:struct epoll_event(三个 API 的核心纽带)

这是 epoll 所有 API 的「事件载体」,用于告诉内核要监听什么事件(epoll_ctl),以及内核通知用户态什么事件发生了(epoll_wait),定义在 <sys/epoll.h>,先吃透这个结构体,才能理解后续 API:

#include <sys/epoll.h>

// epoll 事件结构体:核心是 events 事件类型 + data 附加数据
struct epoll_event {
    uint32_t events;  // 要监听/已发生的事件类型(按位或组合)
    epoll_data_t data;// 附加数据:联合体,存储与 fd 关联的信息,最常用 fd 成员
};

// 附加数据联合体:按需使用,Looper 中仅用 fd 成员(标识哪个 fd 触发了事件)
typedef union epoll_data {
    void        *ptr;  // 自定义指针(如回调函数地址,Looper 中用这个)
    int          fd;   // 要监听的文件描述符(最基础、最常用)
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

重点:常用事件类型(events 取值)

  • EPOLLIN:表示fd 处于可读状态(Looper 中监听 binder_fd、timerfd 的核心事件):
    1. timerfd:超时后变为可读(内核写入超时次数,触发 EPOLLIN);
    1. binder_fd:有外部跨进程调用请求时变为可读(触发 EPOLLIN);
    1. 普通文件 / 套接字:有数据可读取时触发 EPOLLIN。

其他如 EPOLLOUT(可写)、EPOLLERR(错误)、EPOLLHUP(挂起),暂不讲解

3.1.2 epoll_create

创建 epoll 实例,获取 epoll 文件描述符

    1. 函数原型
#include <sys/epoll.h>
#include <sys/types.h>

// 创建 epoll 实例,返回该实例的文件描述符(epfd)
int epoll_create(int size);
    1. 参数详解
参数 类型 含义 关键注意点
size int 历史兼容参数,当前已被内核忽略 ① 必须传入大于 0 的整数(否则报错);② 早期内核用于指定「监听的最大 fd 数」,现在内核会动态扩容,传任意正数即可(惯例传 1024/512)。
    1. 返回值
    1. 成功:返回一个非负的 epoll 文件描述符(epfd),后续 epoll_ctl/epoll_wait 都通过这个 epfd 操作对应的 epoll 实例;
    1. 失败:返回 -1,并设置全局变量 errno,常见错误码:
      EINVAL:size ≤ 0(参数非法);
      ENOMEM:内核内存不足,无法创建 epoll 实例(极少出现)。
    1. 核心特性

epoll_create 创建的 epfd 是普通的 Linux 文件描述符,使用完成后必须调用 close(epfd) 释放,否则会造成文件描述符泄漏;
每个 epoll 实例对应内核中的一块独立内存区域,用于存储监听的 fd 集合、事件配置、就绪事件队列,用户态通过 epfd 操作这块内存。

3.1.3 epoll_ctl

操作 epoll 实例,管理监听的 fd(增 / 删 / 改)

    1. 函数原型
#include <sys/epoll.h>

// 操作 epoll 实例:向 epoll 中添加/删除/修改要监听的 fd 及对应事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    1. 参数详解
参数 类型 含义 关键注意点(结合 timerfd/binder_fd)
epfd int epoll 实例的文件描述符 由 epoll_create 成功返回,必须是有效未关闭的 fd
op int 对 fd 的操作类型(三选一) EPOLL_CTL_ADD:添加 fd 到 epoll 实例,监听 event 事件; ② EPOLL_CTL_DEL:从 epoll 实例中删除 fd,不再监听; ③ EPOLL_CTL_MOD:修改 epoll 中已存在的 fd 的监听事件;Looper 中最常用 EPOLL_CTL_ADD(addFd 方法底层就是这个)
fd int 要监听的目标文件描述符 比如 timerfd、binder_fd,必须是有效未关闭的 fd; ⚠️ 同一个 fd 不能重复添加到同一个 epoll 实例(会报错 EEXIST)
event struct epoll_event * 监听事件配置 添加 / 修改(ADD/MOD):event 不能为 NULL,需设置 events(如 EPOLLIN)和 data(如 data.fd = fd);② 删除(DEL):event 可以为 NULL(内核忽略,只需指定 fd 即可);Looper 中:设置 events = EPOLLIN,data.ptr 存储回调函数(而非 data.fd),实现「fd 触发事件后调用指定回调」
    1. 返回值
  • 成功:返回 0(fd 已成功添加 / 删除 / 修改);
  • 失败:返回 -1,设置 errno,常见错误码(贴合你的场景):
    • EBADF:epfd 或 fd 不是有效未关闭的文件描述符;
    • EEXIST:op=EPOLL_CTL_ADD,但 fd 已存在于该 epoll 实例中(重复添加);
    • ENOENT:op=EPOLL_CTL_DEL/MOD,但 fd 不存在于该 epoll 实例中;
    • EINVAL:epfd 不是 epoll 实例的 fd,或 op 是非法值;
    • ENOMEM:内核内存不足,无法添加 fd(极少出现)。
    1. 核心特性
      一次性添加,持续监听:fd 被 EPOLL_CTL_ADD 添加到 epoll 后,会被持续监听,直到调用 EPOLL_CTL_DEL 删除,或 fd 被关闭(内核会自动从 epoll 中移除已关闭的 fd);
      事件与 fd 绑定:每个 fd 在 epoll 中对应唯一的事件配置,修改需用 EPOLL_CTL_MOD

3.1.4 epoll_wait

阻塞等待 epoll 实例中的 fd 触发 I/O 事件

    1. 函数原型
#include <sys/epoll.h>

// 阻塞等待 epoll 实例中已添加的 fd 触发 I/O 事件,返回就绪的 fd 数量,并将就绪事件存入 events 数组
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    1. 参数详解
参数 类型 含义 关键注意点(结合 Looper/pollAll)
epfd int epoll 实例的文件描述符 同 epoll_ctl,必须是有效未关闭的 epfd
events struct epoll_event * 输出参数:就绪事件数组 ① 由用户态提前分配内存,内核会将「触发了 I/O 事件的 fd 对应的 epoll_event」按顺序存入该数组;② 每个元素的 events 表示该 fd 触发的事件,data 表示该 fd 的关联信息(Looper 中取 data.ptr 执行回调);⚠️ 不能传 NULL
maxevents int 就绪事件数组的最大长度 ① 必须传入大于 0 的整数(否则报错);② 表示本次 epoll_wait 最多能返回多少个就绪事件(内核不会超出这个数);Looper 中:会根据已添加的 fd 数量动态设置该值,确保能容纳所有就绪事件
timeout int 超时时间(单位:毫秒,ms) 核心参数,控制 epoll_wait 的阻塞行为,三种取值(与 Looper::pollAll 完全对应):① timeout = -1:永久阻塞,直到有至少一个 fd 触发事件,或被信号中断;② timeout = 0:非阻塞,立即返回,不管是否有就绪事件;③ timeout > 0:超时阻塞,最多阻塞 timeout 毫秒,超时后若无就绪事件则返回 0;Looper 中:pollAll(-1) 就是调用 epoll_wait (epfd, …, -1),永久阻塞等待事件
    1. 返回值
  • 成功 ≥ 1:返回就绪的 fd 数量(即 events 数组中实际有效元素的个数,≤ maxevents),用户态可遍历 events 数组处理每个就绪事件;
  • 成功 = 0:超时返回(仅当 timeout > 0 时可能发生),此时没有 fd 触发事件;
  • 失败 = -1:调用失败,设置 errno,常见错误码:
    EBADF:epfd 不是有效未关闭的文件描述符;
    EINVAL:epfd 不是 epoll 实例的 fd,或 maxevents ≤ 0;
    EINTR:阻塞等待时被信号中断(如 Ctrl+C),可重新调用 epoll_wait(Looper 中会自动处理该情况,重新进入 pollAll)。
  1. 核心特性
  • 水平触发(LT)默认模式:只要 fd 处于「事件就绪状态」(如 timerfd 超时未 read、binder_fd 有消息未处理),每次调用 epoll_wait 都会返回该 fd 的就绪事件,直到用户态处理完事件(这是 Looper 能稳定处理事件的关键);
    比如 timerfd 超时后,若不执行 read,epoll_wait 会持续返回 EPOLLIN 事件,直到 read 重置 timerfd 为不可读;
  • 就绪事件有序返回:内核会将就绪事件按「触发时间」或「fd 编号」排序后存入 events 数组,用户态按顺序遍历即可;
  • 无 fd 数量限制:epoll 对监听的 fd 数量无硬限制(仅受内核内存和文件描述符上限限制),这也是比 select/poll 高效的核心原因(select 最多监听 1024 个 fd)。

3.2 epoll示例

接下来我们看个示例程序:

使用 epoll_create 创建一个管理 fd 的池子

epollfd = epoll_create(1024);
if (epollfd == -1) {
    perror("epoll_create");
    exit(EXIT_FAILURE);
}

这个池子对我们来说是黑盒,这个黑盒是用来装 fd 的,我们暂不纠结其中细节。我们拿到了一个 epollfd ,这个 epollfd 就能唯一代表这个 epoll 池。

然后,我们就要往这个 epoll 池里放 fd 了,这就要用到 epoll_ctl 了

if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
    perror("epoll_ctl: listen_sock");
    exit(EXIT_FAILURE);
}

我们就把 fd 放到这个池子里了,EPOLL_CTL_ADD 表明操作是增加 fd,最后一个参数是 epoll_event 结构体:

struct epoll_event {
  __uint32_t events;  /* Epoll 事件 */
  epoll_data_t data;  /* 用户数据 */
};

epoll_event 的第一个成员 events 用于指定我们监听的 fd 事件类型,常见的值有:

  • EPOLLIN:可读事件
  • EPOLLOUT:可写事件

多个值可以通过或操作同时生效:

epoll_event event;
// 同时监听可读可写事件
event.events = EPOLLIN | EPOLLOUT;

最后,我们需要调用 epoll_wait 进入休眠状态,可读或可写事件到来时,醒休眠中的程序从 epoll_wait 处被唤醒。

其使用方法,通常如下:

while (true)
{   
    // epollfd 是 epoll_create 的返回值
    // events 是一个 epoll_event 的数组,用于存储收到的多个事件
    // EPOLL_SIZE 用于设定最多监听多少个事件
    // 最后一个参数 -1 用于指定阻塞时间上限,-1:表示调用将一直阻塞
    int count = epoll_wait(epollfd, events, EPOLL_SIZE, -1);
    if (count < 0)
    {
        perror("epoll failed");
        break;
    }
    for (int i=0;i < count;i++)
    {
        //处理可读或可写事件
    }
}

四、epoll 进阶使用

#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>

#define MAX_EVENTS 1024  // 一次最多处理的就绪事件数
#define EPOLL_TIMEOUT -1 // 永久阻塞(直到有事件发生),0为非阻塞,>0为超时时间(ms)

int main() {
    // 1. 创建epoll实例,返回epfd(参数size已废弃,传>0即可)
    int epfd = epoll_create(1);
    if (epfd < 0) {
        perror("epoll_create failed");
        return -1;
    }

    // 待监听的fd(示例:socket fd、设备fd、管道fd等,这里用socket fd举例)
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    // (可选)设置fd为非阻塞(ET模式必须,LT模式可选)
    fcntl(sock_fd, F_SETFL, fcntl(sock_fd, F_GETFL) | O_NONBLOCK);

    // 2. 定义事件结构体:指定要监听的fd和事件
    struct epoll_event ev;
    ev.data.fd = sock_fd;                // 绑定要监听的fd
    ev.events = EPOLLIN | EPOLLRDHUP;    // 监听:可读事件 + 对方关闭连接事件(LT模式,默认)
    // ev.events = EPOLLIN | EPOLLRDHUP | EPOLLET; // 若用ET模式,添加EPOLLET

    // 3. 将fd和事件注册到epoll实例中
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ev) < 0) {
        perror("epoll_ctl add failed");
        close(sock_fd);
        close(epfd);
        return -1;
    }

    // 4. 定义就绪事件数组:存放epoll_wait返回的有事件的fd
    struct epoll_event ready_ev[MAX_EVENTS];

    // 核心循环(native loop的核心,Android底层的事件循环均基于此)
    while (1) {
        // 5. 阻塞等待就绪事件,返回值为就绪的fd数量
        int nfds = epoll_wait(epfd, ready_ev, MAX_EVENTS, EPOLL_TIMEOUT);
        if (nfds < 0) {
            perror("epoll_wait failed");
            break;
        }

        // 遍历就绪事件数组,处理每个有事件的fd
        for (int i = 0; i < nfds; i++) {
            int fd = ready_ev[i].data.fd;
            uint32_t events = ready_ev[i].events;

            // 处理可读事件(如socket接收数据)
            if (events & EPOLLIN) {
                char buf[1024] = {0};
                int len = read(fd, buf, sizeof(buf));
                if (len > 0) {
                    printf("read data: %s\n", buf);
                } else if (len == 0 || (events & EPOLLRDHUP)) {
                    // 对方关闭连接,删除fd并关闭
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    close(fd);
                }
            }

            // 处理可写事件(如socket发送数据)
            if (events & EPOLLOUT) {
                const char* data = "hello epoll";
                write(fd, data, strlen(data));
                // 可选:写完后取消可写监听(避免持续触发LT模式的可写事件)
                ev.events = EPOLLIN | EPOLLRDHUP;
                epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
            }

            // 处理异常事件
            if (events & EPOLLERR || events & EPOLLHUP) {
                epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                close(fd);
            }
        }
    }

    // 释放资源
    close(sock_fd);
    close(epfd);
    return 0;
}

五、参考

深入理解 Linux 的 epoll 机制
Linux下的I/O复用技术 之 epoll为什么更高效
Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析
epoll LT 模式和 ET 模式详解

Logo

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

更多推荐