Linux IO 多路复用 epoll 机制
目录
- 一、什么是 IO 多路复用
- 二、epoll简介
- 2.1 什么是 epoll?
- 2.1 为什么需要 epoll?(select/poll 的痛点)
- 2.3 epoll 的核心概念(3 个核心要素)
- 2.4 epoll 的两种触发模式
- 2.5 哪些 fd 可以用 epoll 来管理?
- 三、epoll 的使用
- 四、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
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 调度,但是只要有“事”,立马会从这里唤醒;
接下来我们看个示例程序:
使用 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
#include
#include
#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
", 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 模式详解






