1. I/O多路复用概述
I/O多路复用(Multiplexing)是一种"单线程/进程管理多个连接"的技术,其核心价值在于:
- 通过单次系统调用监听多个文件描述符的状态变化
- 当某个或多个文件描述符就绪(可读、可写或出现异常)时,系统调用返回
- 应用程序可以非阻塞地处理这些就绪的文件描述符
这种机制避免了为每个连接创建线程/进程的开销,同时解决了阻塞I/O模型无法处理多连接的问题。
2. select模型
2.1 工作原理
select是最早出现的I/O多路复用实现,其基本工作流程如下:
- 应用程序创建一个fd_set(文件描述符集合),并设置需要监控的文件描述符
- 调用select函数,将fd_set传递给内核
- 内核遍历fd_set中的所有文件描述符,检查它们的就绪状态
- 如果有文件描述符就绪,内核修改对应的标志位
- select返回,应用程序遍历fd_set找出就绪的文件描述符进行处理
2.2 主要缺点
- 文件描述符数量限制:默认最大1024个(由FD_SETSIZE定义)
- 性能问题:
- 每次调用都需要将整个fd_set从用户空间拷贝到内核空间
- 内核需要线性扫描所有文件描述符(时间复杂度O(n))
- 返回后应用程序也需要线性扫描所有文件描述符
- 重复初始化:每次调用select前都需要重新设置fd_set
2.3 代码示例
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int ret = select(sockfd+1, &readfds, NULL, NULL, &timeout);
if (ret > 0) {
if (FD_ISSET(sockfd, &readfds)) {
// 处理就绪的socket
}
}
3. poll模型
3.1 改进之处
poll是对select的改进,主要解决了以下问题:
- 解除数量限制:使用动态数组而非固定大小的fd_set,理论上只受系统最大文件描述符数限制
- 更精细的事件区分:引入POLLIN、POLLOUT等事件类型,可以更精确地指定和获取事件类型
3.2 工作原理
poll使用pollfd结构体数组来管理文件描述符:
struct pollfd {
int fd; // 文件描述符
short events; // 等待的事件
short revents; // 实际发生的事件
};
工作流程与select类似:
- 应用程序准备pollfd数组
- 调用poll函数
- 内核遍历所有pollfd,检查就绪状态
- 返回就绪的文件描述符数量
- 应用程序遍历pollfd数组找出就绪的文件描述符
3.3 仍然存在的问题
- 性能问题:与select一样需要全量轮询(时间复杂度O(n))
- 内存拷贝:每次调用仍需将整个pollfd数组从用户空间拷贝到内核空间
3.4 代码示例
struct pollfd fds[1];
fds[0].fd = sockfd;
fds[0].events = POLLIN;
int ret = poll(fds, 1, 5000); // 5秒超时
if (ret > 0) {
if (fds[0].revents & POLLIN) {
// 处理就绪的socket
}
}
4. epoll模型
4.1 革命性改进
epoll是Linux特有的I/O多路复用机制,针对select/poll的缺点进行了彻底革新:
- 高效的事件通知机制:使用回调机制而非轮询,只有就绪的文件描述符才会被处理
- 共享内存:通过mmap实现用户空间和内核空间共享内存,避免数据拷贝
- 可扩展性强:支持大量并发连接(仅受系统资源限制)
4.2 核心数据结构
epoll使用三个主要系统调用:
epoll_create():创建epoll实例,返回一个文件描述符,这里的创建实例其实是创建一个eventpoll的数据结构,这个结构是内核维护的,后续有新的连接也是添加到内核的红黑树中epoll_ctl():向epoll实例中添加、修改或删除监控的文件描述符epoll_wait():等待I/O事件发生,epollwait会将就绪链表中的fd通过epoll_event的结构体的形式传出
内核内部使用两个关键数据结构:
- 红黑树:高效地存储和查找所有被监控的文件描述符
- 就绪链表:存储所有就绪的文件描述符
struct eventpoll {
/* 保护该结构的自旋锁 */
spinlock_t lock;
/* 用于sys_epoll_wait()的等待队列 */
wait_queue_head_t wq;
/* 用于file->poll()的等待队列 */
wait_queue_head_t poll_wait;
/* 就绪文件描述符链表 */
struct list_head rdllist;
/* 红黑树的根节点,存储所有监控的文件描述符 */
struct rb_root_cached rbr;
/*
* 当将就绪事件传输到用户空间时,
* 将同时生成的文件描述符链入该链表
*/
struct epitem *ovflist;
/* wakeup_source用于epoll的PM(电源管理) */
struct wakeup_source *ws;
/* 该eventpoll文件描述符 */
struct file *file;
/* 用于环形缓冲区优化的用户空间使用的标志 */
int visited;
struct list_head visited_list_link;
};
4.3 工作流程
- 调用
epoll_create()创建epoll实例 - 调用
epoll_ctl()将需要监控的文件描述符添加到红黑树中 - 调用
epoll_wait()等待事件发生 - 当某个文件描述符就绪时,内核将其添加到就绪链表
epoll_wait()返回,应用程序只需处理就绪链表中的文件描述符
4.4 性能优势
- 时间复杂度:
- 添加/删除文件描述符:O(log n)
- 等待事件:O(1)(仅处理就绪的文件描述符)
- 内存效率:无需每次调用都传递完整的文件描述符集合
- 可扩展性:支持数十万级别的并发连接
4.5 代码示例
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
struct epoll_event events[10];
int n = epoll_wait(epfd, events, 10, 5000);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == sockfd) {
// 处理就绪的socket
}
}
5. 三种模型对比
| 特性 | select | poll | epoll |
|---|---|---|---|
| 最大连接数 | 1024 | 无限制 | 数十万 |
| 效率 | O(n) | O(n) | O(1) |
| 内存拷贝 | 每次调用都需要 | 每次调用都需要 | 共享内存,无需拷贝 |
| 事件触发方式 | 轮询 | 轮询 | 回调 |
| 复杂度 | 低 | 中 | 高 |
| 跨平台性 | 几乎所有平台 | 大多数Unix-like系统 | Linux特有 |
6. 适用场景建议
-
select:
- 需要跨平台兼容的简单应用
- 连接数较少(<1024)的场景
- 对性能要求不高的场景
-
poll:
- 需要监控超过1024个文件描述符
- 不需要Linux特有特性的场景
- 连接数中等(几千)的场景
-
epoll:
- Linux平台下的高性能服务器
- 需要处理数万甚至数十万并发连接
- 对延迟和吞吐量有严格要求的场景

被折叠的 条评论
为什么被折叠?



