IO多路复用技术
一、核心概念
1.1 定义
IO多路复用(I/O Multiplexing)是单线程或单进程同时监测多个文件描述符是否可以执行IO操作的能力。
1.2 主要作用
-
TCP服务器中处理多个客户端连接请求
-
对多个可能阻塞的设备进行IO操作,哪个设备数据先就绪就先处理哪个
-
解决传统阻塞IO模型并发能力差的问题
二、Linux IO模型分类
2.1 阻塞IO(默认)
-
进程在等待IO操作时被挂起
-
直到数据准备好或超时才恢复执行
2.2 非阻塞IO
-
通过
fcntl()设置O_NONBLOCK标志 -
立即返回,不会阻塞进程
-
需要循环检查(忙等待),消耗CPU资源
int flag;
flag = fcntl(fd, F_GETFL, 0); // 获取当前文件状态标志
flag = flag | O_NONBLOCK; // 添加非阻塞标志
fcntl(fd, F_SETFL, flag); // 设置新的文件状态标志
2.3 信号驱动IO(SIGIO)
-
为文件描述符设置信号处理函数
-
当数据就绪时内核发送SIGIO信号
-
实际应用较少
2.4 并行模型
-
使用多进程或多线程
-
每个连接一个进程/线程
-
资源消耗大,上下文切换开销高
2.5 IO多路复用(重点)
-
select 、poll 、epoll
-
单个线程监控多个文件描述符
-
高效处理大量并发连接
三、select 模型详解
3.1 select 函数原型
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
3.2 参数说明
| 参数 | 说明 |
|---|---|
nfds |
监控的文件描述符最大值+1 |
readfds |
监控读就绪的文件描述符集合 |
writefds |
监控写就绪的文件描述符集合 |
exceptfds |
监控异常的文件描述符集合 |
timeout |
超时时间,NULL表示无限等待 |
3.3 相关宏函数
void FD_ZERO(fd_set *set); // 清空集合
void FD_SET(int fd, fd_set *set); // 添加fd到集合
void FD_CLR(int fd, fd_set *set); // 从集合移除fd
int FD_ISSET(int fd, fd_set *set); // 检查fd是否在集合中
3.4 select 工作流程

1. 创建fd集合
2. 添加关心的fd到集合
3. 调用select()阻塞等待(内核轮询检查)
4. select返回后,遍历集合找到就绪的fd
5. 对就绪的fd进行读写操作
6. 清理标志位,准备下一次select
3.5 select 的缺点
-
文件描述符数量限制:默认1024
-
每次调用都需要重新设置fd集合
-
需要遍历所有fd检查就绪状态(O(n)复杂度)
-
fd集合需要在用户态和内核态之间拷贝
四、epoll 模型详解
4.1 epoll 核心优势
-
无文件描述符数量限制
-
事件驱动,不随监听数增加而性能下降(O(1)复杂度)
-
共享内存,减少数据拷贝
-
只返回就绪的事件列表
4.2 epoll 相关函数
4.2.1 epoll_create()
int epoll_create(int size);
-
创建epoll实例
-
size:建议的监控数量(Linux 2.6.8后忽略,但需>0) -
返回:epoll文件描述符(成功),-1(失败)
4.2.2 epoll_ctl()
int epoll_ctl(int epfd,
int op,
int fd,
struct epoll_event *event);
- 管理epoll监控列表
op参数:
-
EPOLL_CTL_ADD:添加文件描述符 -
EPOLL_CTL_MOD:修改监控事件 -
EPOLL_CTL_DEL:删除文件描述符
epoll_event结构:
struct epoll_event {
uint32_t events; // 监控的事件类型
epoll_data_t data; // 用户数据
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
常用事件类型:
-
EPOLLIN:可读事件 -
EPOLLOUT:可写事件 -
EPOLLET:边缘触发模式 -
EPOLLONESHOT:单次事件
4.2.3 epoll_wait()
int epoll_wait(int epfd,
struct epoll_event *events,
int maxevents,
int timeout);
-
等待事件发生
-
events:输出参数,存放就绪的事件 -
maxevents:events数组的大小 -
timeout:超时时间(毫秒),-1表示阻塞
4.3 epoll 工作模式
4.3.1 水平触发(LT,默认)
-
只要文件描述符可读/可写,就会不断通知
-
类似select/poll的工作方式
-
编程简单,不易遗漏事件
4.3.2 边缘触发(ET)
-
只有状态变化时才通知一次
-
需要一次读取所有数据
-
效率更高,但编程复杂
-
必须使用非阻塞IO
4.4 epoll 工作流程

1. 创建epoll实例 (epoll_create)
2. 添加关心的fd到epoll (epoll_ctl)
3. 等待事件发生 (epoll_wait)
4. 处理就绪的事件(直接遍历返回的events数组)
5. 继续等待下一次事件
五、select vs epoll 对比
| 特性 | select | epoll |
|---|---|---|
| 最大连接数 | 1024(可调整但有限制) | 无限制(系统可打开文件数) |
| 时间复杂度 | O(n),需要遍历所有fd | O(1),只处理就绪的fd |
| 工作方式 | 轮询所有fd | 事件驱动,回调机制 |
| 数据拷贝 | 每次调用都要拷贝fd集合 | 内核和用户空间共享内存 |
| 触发模式 | 只支持水平触发 | 支持水平触发和边缘触发 |
| 编程复杂度 | 简单 | 相对复杂 |
| 适用场景 | 连接数少,跨平台 | 连接数多,Linux专用 |
六、实际应用建议
6.1 选择标准
-
select:连接数少(<1000),需要跨平台
-
poll:连接数中等,无平台限制
-
epoll:连接数多(>1000),Linux平台
6.2 性能优化建议
-
非阻塞IO:与epoll ET模式配合使用
-
事件循环:单线程处理所有连接
-
连接池:复用已建立的连接
-
缓冲区管理:合理设置缓冲区大小
6.3 常见应用
-
Web服务器:Nginx, Apache
-
数据库连接池
-
聊天服务器
-
游戏服务器
-
代理服务器
七、示例代码框架
// epoll 服务器框架示例
int main() {
// 1. 创建socket,绑定,监听
int listen_fd = socket(...);
bind(listen_fd, ...);
listen(listen_fd, ...);
// 2. 创建epoll实例
int epfd = epoll_create(1024);
// 3. 添加监听socket到epoll
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
struct epoll_event events[MAX_EVENTS];
while (1) {
// 4. 等待事件
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listen_fd) {
// 5. 接受新连接
int conn_fd = accept(listen_fd, ...);
// 6. 设置非阻塞
fcntl(conn_fd, F_SETFL, O_NONBLOCK);
// 7. 添加到epoll
ev.events = EPOLLIN | EPOLLET; // ET模式
ev.data.fd = conn_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);
} else {
// 8. 处理客户端数据
handle_client(events[i].data.fd);
}
}
}
close(epfd);
close(listen_fd);
return 0;
}
八、总结
IO多路复用是现代高性能网络编程的核心技术,通过select/poll/epoll等机制,实现了单线程处理大量并发连接的能力。其中epoll在Linux平台下性能最优,广泛应用于各种高性能服务器中。掌握这些技术对于开发高并发网络应用至关重要。