摘要 :IO 多路复用是高并发网络编程核心,本文聚焦
select、poll、epoll三组核心系统调用,梳理函数接口、底层行为、优缺点与关键注意点,适合快速复习与面试。
前言
IO 分为两个阶段:等待事件就绪 、数据拷贝。IO 多路复用的作用:把多个文件描述符交给内核监控,fd 就绪后通知用户进程,单线程管理大量连接。
注意:IO 多路复用属于同步 IO ,数据拷贝依旧需要用户进程调用
read/recv完成。
1 select
函数原型
cs
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数
nfds:最大监听 fd + 1,内核扫描范围0 ~ nfds‑1。readfds/writefds/exceptfds:可读、可写、异常 fd 集合,位图fd_set。
内核会修改这三个集合:就绪位置 1,未就绪位清零。每次调用 select 前必须重新设置 fd_set。
timeout:超时时间,结构体本身会被 select 修改,每次调用需要重新赋值。NULL:永久阻塞;{0,0}:不阻塞,轮询一次直接返回。
返回值
-
0:就绪 fd 数量;
- =0:超时;
- <0:出错。
fd_set 操作宏
cs
FD_ZERO(set); //清空集合
FD_SET(fd, set); //添加fd
FD_CLR(fd, set); //删除fd
FD_ISSET(fd, set);//判断fd是否就绪
核心缺点
- 默认上限
1024个 fd,受FD_SETSIZE限制; - 每次调用,全部 fd 集合需要用户态↔内核态拷贝;
- O (n) 轮询;返回后需要遍历全部 fd 判断就绪。
优点:跨平台兼容性好。
2 poll
函数原型
cs
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd;
short events; //【入参】用户设置要监听的事件 POLLIN / POLLOUT
short revents; //【出参】内核填充,标记已经发生的事件
};
参数
fds:pollfd结构体数组;events用户填写,内核只读;就绪事件写进revents。nfds:数组有效元素个数。timeout:超时,单位 ms;‑1永久阻塞,0立即返回。
返回值
-
0:就绪 fd 数量;
- =0:超时;
- <0:出错。
改进点
- 取消 1024 的 fd 上限,内核使用链表 + 动态数组存储;
events与revents分离,不需要每次重置监听事件。
遗留问题
- O (n) 轮询全部 fd;
- 每次调用全量拷贝数组到内核;
- 返回后仍然要遍历整个数组查找就绪 fd。
select、poll 本质:内核挨个扫描所有 fd,不知道谁就绪。
3 epoll(Linux 专属,3 个系统调用)
epoll 由epoll_create、epoll_ctl、epoll_wait共同工作;内核维护红黑树(保存监听 fd) 、就绪双向链表(保存就绪事件)。
① epoll_create 创建 epoll 实例
cs
int epoll_create(int size);
size:历史遗留参数,现代内核忽略,传大于 0 即可。- 返回 epoll 实例 fd;失败返回‑1。
作用:在内核创建红黑树、就绪链表。
② epoll_ctl 增删改监听 fd
cs
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event {
uint32_t events; // EPOLLIN可读,EPOLLOUT可写,EPOLLET边缘触发
epoll_data_t data;//联合体,保存fd或者自定义指针
};
op操作:
EPOLL_CTL_ADD:添加 fd 到红黑树,注册回调;EPOLL_CTL_MOD:修改已有 fd 的监听事件;EPOLL_CTL_DEL:从红黑树删除 fd。
关键:fd 就绪触发内核回调,直接把事件节点放入就绪链表,不轮询全部 fd。
③ epoll_wait 获取就绪事件
cs
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
events:用户数组,内核将就绪事件拷贝到此;maxevents:数组最大容量;timeout:超时,单位 ms。
返回就绪事件的数量,用户直接遍历events数组拿到就绪 fd。
LT 水平触发 / ET 边缘触发
- LT(默认,水平触发):缓冲区有未读完数据,每次 epoll_wait 都会上报事件,编程简单。
- ET(EPOLLET,边缘触发) :仅状态变化瞬间上报一次。 ⚠️ET 硬性要求:fd 设置
O_NONBLOCK非阻塞;循环读直到EAGAIN,否则残留数据不会再次通知。
epoll 优缺点
✅ 无 fd 上限;海量空闲连接性能高;仅拷贝就绪事件。
❌ 仅 Linux 支持;少量活跃连接场景,内核额外开销大。
函数简单对比
表格
| 接口 | 拷贝行为 | 时间复杂度 | fd 上限 | 跨平台 |
|---|---|---|---|---|
| select | 每次全量拷贝 fd 集合 | O(n) | 默认 1024 | 是 |
| poll | 每次全量拷贝 pollfd 数组 | O(n) | 无 | 大部分平台 |
| epoll | 监听 fd 只拷贝一次,仅拷贝就绪事件 | O (就绪数) | 无 | 仅 Linux |
高频简短考点
- epoll 红黑树存待监听 fd;就绪链表存放已经就绪事件,
epoll_wait只读取就绪链表。 - ET 边缘触发必须非阻塞 + 循环读,否则会漏数据。
- epoll 不是一定更快,大量活跃短连接场景 select/poll 可能更优。
- IO 多路复用是同步 IO,数据拷贝仍然由用户进程调用 read 完成。