一、IO 多路复用:定义与核心作用
1. 核心定义
IO 多路复用是指单线程或单进程同时监测若干个文件描述符是否可以执行 IO 操作的能力。简单来说,就是让一个 "管理者" 同时盯着多个 IO 通道,哪个通道数据就绪,就优先处理哪个通道的读写任务。
2. 两大核心作用
- 支撑高并发 TCP 服务器:在 TCP 服务端场景中,一台服务器需要同时应对成百上千个客户端连接。IO 多路复用可高效管理所有连接的读写事件,避免为每个连接创建线程 / 进程带来的巨大资源开销。
- 优化多阻塞设备 IO 处理:对于磁盘、串口等会产生阻塞的设备,IO 多路复用能实时监测多个设备的就绪状态,数据就绪后立即触发处理逻辑,无需对单个设备进行低效轮询等待。
二、Linux 下的 IO 模型全景
Linux 提供了 5 种经典 IO 模型,不同模型适用于不同业务场景,IO 多路复用是其中的高性能代表。
| IO 模型 | 核心特点 | 适用场景 |
|---|---|---|
| 阻塞 IO(默认) | fd 未就绪时,进程 / 线程阻塞挂起,直到数据到达 | 连接数少、逻辑简单的场景 |
| 非阻塞 IO | 设置 fd 为非阻塞状态,无数据时立即返回错误,需外部循环轮询(忙等待) | 需实时响应,且轮询开销可控的场景 |
| 信号驱动 IO(SIGIO) | 通过信号通知 fd 就绪,无需主动轮询 | 小众场景,实际项目中使用频率低 |
| 多进程 / 多线程模型 | 为每个连接创建独立进程 / 线程处理 IO | 连接数少,对资源开销不敏感的场景 |
| IO 多路复用(select/poll/epoll) | 单线程监测多个 fd,仅在 fd 就绪时触发 IO 操作 | 高并发场景,如百万级连接的服务器 |
非阻塞 IO 的设置方法
要将 fd 设置为非阻塞状态,需借助fcntl函数修改其属性,核心代码如下:
c
运行
// 获取fd原有属性
int flag = fcntl(fd, F_GETFL, 0);
// 添加非阻塞属性
flag |= O_NONBLOCK;
// 使新属性生效
fcntl(fd, F_SETFL, flag);
三、IO 多路复用实现:select 详解

select 是 Linux 早期的 IO 多路复用方案,基于线性数组管理 fd 集合,采用内核轮询的方式检测就绪事件。
1. select 处理流程
- 创建 fd 集合 :使用
fd_set结构体存储需要监测的 fd。 - 添加关心的 fd :通过
FD_SET宏将目标 fd 加入读、写或异常事件集合。 - 阻塞等待事件 :调用
select函数,内核轮询所有已注册的 fd。若无 fd 就绪,进程阻塞;若超时或 fd 就绪,函数返回。 - 查找就绪 fd :通过
FD_ISSET宏遍历 fd 集合,判断哪些 fd 已就绪。 - 执行 IO 操作 :对就绪的 fd 执行
read/write操作。 - 清除标志位 :每次处理完后,需通过
FD_ZERO/FD_CLR清空或删除 fd,避免下次误判。
2. select 核心函数与宏
核心函数:select
c
运行
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 参数说明
nfds:需监测的最大 fd 值 + 1,内核以此为边界进行轮询。readfds/writefds/exceptfds:分别对应读、写、异常事件的 fd 集合。timeout:超时时间,NULL表示永久阻塞。
- 返回值:超时返回 0,失败返回 - 1,成功返回就绪的 fd 数量。
配套宏函数
| 宏函数 | 功能 |
|---|---|
FD_ZERO(fd_set *set) |
清空 fd 集合 |
FD_SET(int fd, fd_set *set) |
将 fd 添加到集合 |
FD_CLR(int fd, fd_set *set) |
将 fd 从集合中删除 |
FD_ISSET(int fd, fd_set *set) |
判断 fd 是否在就绪集合中 |
四、epoll 深度解析
epoll 是 Linux 特有的高性能 IO 多路复用方案,专为高并发场景设计,基于红黑树 + 就绪事件链表 实现,采用事件驱动而非轮询方式。

1. epoll 核心函数
epoll 的操作依赖三个核心函数:epoll_create、epoll_ctl、epoll_wait。
(1)创建 epoll 实例:epoll_create
c
运行
int epoll_create(int size);
- 功能:创建一个 epoll 实例,内核会生成红黑树(存储注册的 fd)和就绪事件链表。
- 参数 :
size指定集合的大小(现代 Linux 中该参数已被忽略,仅需传入大于 0 的值)。 - 返回值:成功返回 epoll 实例的文件描述符,失败返回 - 1。
(2)管理 fd 事件:epoll_ctl
c
运行
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-
功能:向 epoll 实例中添加、删除或修改需要监测的 fd 及事件。
-
参数说明
-
epfd:epoll 实例的文件描述符。 -
op:操作类型,包括EPOLL_CTL_ADD(添加 fd)、EPOLL_CTL_DEL(删除 fd)、EPOLL_CTL_MOD(修改事件)。 -
fd:需要监测的文件描述符。 -
event:需要监测的事件类型,结构体定义如下:c
运行
struct epoll_event { uint32_t events; // 事件类型,如EPOLLIN(读事件)、EPOLLOUT(写事件) epoll_data_t data;// 用户自定义数据,用于关联fd相关信息 };
-
-
返回值:成功返回 0,失败返回 - 1。
(3)等待就绪事件:epoll_wait
c
运行
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 功能:阻塞等待就绪事件,是 epoll 的核心等待函数。
- 参数说明
epfd:epoll 实例的文件描述符。events:用于存储就绪事件的数组,由内核填充。maxevents:events数组的最大容量。timeout:超时时间(单位 ms),-1 表示永久阻塞,0 表示非阻塞。
- 返回值:成功返回就绪的 fd 数量,超时返回 0,失败返回 - 1。
2. epoll 处理流程
- 创建 epoll 实例 :调用
epoll_create创建红黑树和就绪链表。 - 注册 fd 及事件 :通过
epoll_ctl将需要监测的 fd 和事件(如EPOLLIN)注册到红黑树中。 - 阻塞等待事件 :调用
epoll_wait,内核仅在 fd 就绪时,将事件添加到就绪链表并唤醒进程,无需轮询所有 fd。 - 获取就绪 fd :
epoll_wait直接返回就绪事件列表,无需遍历所有注册的 fd。 - 执行 IO 操作:对就绪的 fd 执行读写操作,无需手动清除标志位。
五、select 与 epoll 核心差异对比
select 和 epoll 作为 IO 多路复用的两种实现,性能差异显著,具体对比如下:
| 对比维度 | select | epoll |
|---|---|---|
| 底层数据结构 | 线性数组 | 红黑树 + 就绪事件链表 |
| fd 数量限制 | 受FD_SETSIZE限制,默认最多 1024 个 |
无限制,仅受系统内存影响 |
| 检测方式 | 内核轮询所有注册 fd,时间复杂度 O (n) | 事件驱动,仅处理就绪 fd,时间复杂度 O (1) |
| 数据拷贝 | 每次调用select,fd 集合需从用户态拷贝到内核态 |
仅在epoll_ctl注册时拷贝一次,后续复用共享内存 |
| 就绪 fd 获取 | 需遍历所有 fd,通过FD_ISSET判断是否就绪 |
直接返回就绪事件列表,无需遍历 |
| 适用场景 | 连接数少的简单场景 | 高并发场景,如 Nginx、Redis 等中间件 |
六、总结
IO 多路复用是 Linux 高并发网络编程的基石,select 作为经典实现,兼容度高但性能受限;epoll 则凭借事件驱动、零拷贝、无 fd 数量限制等优势,成为高性能服务器的首选方案。在实际开发中,需根据业务场景选择合适的 IO 模型:低并发场景可使用 select 快速实现,高并发场景则优先使用 epoll,充分发挥服务器的性能潜力。