Linux IO 多路复用 epoll 机制
Linux IO多路复用epoll机制是一种高效管理多文件描述符(fd)的技术,通过单线程同时监听多个fd的IO事件,避免资源浪费。相比select/poll,epoll解决了fd数量限制、轮询效率低和内核态拷贝开销等问题。其核心包括epoll实例(epfd)、事件注册(epoll_ctl)和事件等待(epoll_wait)三个操作,采用红黑树和就绪链表数据结构实现高效管理。epoll支持水平触发
目录
| 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 的原因:
- fd 数量限制:select 有最大 fd 数量限制(默认 1024),无法满足 Android 底层多硬件、多 socket 的高并发需求;
- 轮询效率极低:select/poll 每次都需要轮询所有监听的 fd判断是否有事件,即使只有 1 个 fd 有事件,也要遍历全部,fd 越多效率越低;
- 内核 / 用户态拷贝开销:select/poll 每次调用都需要将监听的 fd 集合从用户态拷贝到内核态,事件返回后又要拷贝回用户态,高并发下拷贝开销极大。
而 epoll 从底层设计上彻底解决了这三个问题,是百万级 fd 高并发场景的最优解(Android 设备中底层监听的 fd 数量通常在千级,epoll 能轻松处理)。
epoll 之所以做到了高效,最关键的两点:
- 内部管理 fd 使用了高效的红黑树结构管理,做到了增删改之后性能的优化和平衡;
- epoll 池添加 fd 的时候,调用
file_operations->poll,把这个 fd 就绪之后的回调路径安排好。通过事件通知的形式,做到最高效的运行; - epoll 池核心的两个数据结构:红黑树和就绪列表。红黑树是为了应对用户的增删改需求,就绪列表是 fd 事件就绪之后放置的特殊地点,epoll 池只需要遍历这个就绪链表,就能给用户返回所有已经就绪的 fd 数组;
2.3 epoll 的核心概念(3 个核心要素)
epoll 的使用围绕3 个核心对象 / 操作展开,概念简单且固定,Android Native 层代码中也完全遵循这个模型,先理解概念再看代码会更清晰:
- epoll 实例(epfd)
调用epoll_create()创建,返回一个专属的文件描述符(epfd),代表一个 epoll 实例;
每个 epoll 实例独立管理一组「监听的 fd + 对应事件」,进程可以创建多个 epoll 实例(Android 底层通常单实例管理所有相关 fd);
本质:内核为 epfd 维护两棵核心数据结构——红黑树(管理所有注册的 fd 和事件,支持快速增删改)、就绪链表(仅存放有 IO 事件发生的 fd,无需轮询)。 - 事件注册(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 集合,一次注册永久有效(直到主动删除)。
- 事件等待(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 对稳定性要求更高),了解一下即可。
- 水平触发(LT,默认模式)
触发规则:只要 fd 的 IO 缓冲区中有数据 / 空间(如可读缓冲区有数据、可写缓冲区有空位),epoll_wait 就会持续通知进程,直到缓冲区的事件被处理完毕;
特点:容错性高,不会漏事件,即使进程一次没处理完数据,下次 epoll_wait 仍会通知,适合底层驱动 /hal 的稳定场景(Android 首选);
示例:socket 的可读缓冲区有 1024 字节数据,进程第一次只读取了 512 字节,剩余 512 字节,下一次 epoll_wait 仍会触发该 socket 的 EPOLLIN 事件。 - 边缘触发(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 的核心事件):
-
- timerfd:超时后变为可读(内核写入超时次数,触发 EPOLLIN);
-
- binder_fd:有外部跨进程调用请求时变为可读(触发 EPOLLIN);
-
- 普通文件 / 套接字:有数据可读取时触发 EPOLLIN。
其他如 EPOLLOUT(可写)、EPOLLERR(错误)、EPOLLHUP(挂起),暂不讲解
3.1.2 epoll_create
创建 epoll 实例,获取 epoll 文件描述符
-
- 函数原型
#include <sys/epoll.h>
#include <sys/types.h>
// 创建 epoll 实例,返回该实例的文件描述符(epfd)
int epoll_create(int size);
-
- 参数详解
| 参数 | 类型 | 含义 | 关键注意点 |
|---|---|---|---|
| size | int | 历史兼容参数,当前已被内核忽略 | ① 必须传入大于 0 的整数(否则报错);② 早期内核用于指定「监听的最大 fd 数」,现在内核会动态扩容,传任意正数即可(惯例传 1024/512)。 |
-
- 返回值
- 成功:返回一个非负的 epoll 文件描述符(epfd),后续 epoll_ctl/epoll_wait 都通过这个 epfd 操作对应的 epoll 实例;
- 失败:返回 -1,并设置全局变量 errno,常见错误码:
EINVAL:size ≤ 0(参数非法);ENOMEM:内核内存不足,无法创建 epoll 实例(极少出现)。
-
- 核心特性
epoll_create 创建的 epfd 是普通的 Linux 文件描述符,使用完成后必须调用 close(epfd) 释放,否则会造成文件描述符泄漏;
每个 epoll 实例对应内核中的一块独立内存区域,用于存储监听的 fd 集合、事件配置、就绪事件队列,用户态通过 epfd 操作这块内存。
3.1.3 epoll_ctl
操作 epoll 实例,管理监听的 fd(增 / 删 / 改)
-
- 函数原型
#include <sys/epoll.h>
// 操作 epoll 实例:向 epoll 中添加/删除/修改要监听的 fd 及对应事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-
- 参数详解
| 参数 | 类型 | 含义 | 关键注意点(结合 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 触发事件后调用指定回调」 |
-
- 返回值
- 成功:返回 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(极少出现)。
-
- 核心特性
一次性添加,持续监听: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 事件
-
- 函数原型
#include <sys/epoll.h>
// 阻塞等待 epoll 实例中已添加的 fd 触发 I/O 事件,返回就绪的 fd 数量,并将就绪事件存入 events 数组
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-
- 参数详解
| 参数 | 类型 | 含义 | 关键注意点(结合 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:返回就绪的 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)。
- 核心特性
- 水平触发(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 模式详解
更多推荐


所有评论(0)