IO复用详解——C/C++
IO复用
在Linux网络编程中,I/O复用是处理高并发连接的核心技术。它允许单个线程/进程同时监控多个文件描述符(如Socket)的可读/可写状态,避免为每个连接创建独立线程的资源浪费。以下是 select
、poll
和 epoll
的详细对比与实现方法:
1. I/O复用核心概念
- 阻塞I/O:线程等待数据就绪,期间无法执行其他操作。
- 非阻塞I/O:轮询检查数据是否就绪,浪费CPU资源。
- I/O复用:通过系统调用统一监控多个I/O事件,仅处理就绪的I/O。
2. select
原理
-
使用**位图(fd_set)**表示监控的文件描述符集合。
typedef struct {unsigned long fds_bits[FD_SETSIZE / (8 * sizeof(unsigned long))]; } fd_set;
-
FD_SETSIZE
是一个宏,定义了fd_set
可容纳的最大文件描述符数量,默认值通常为 1024。 -
fds_bits
-
每个文件描述符对应一个位。
-
位为 1:表示文件描述符在集合中。
-
位为 0:表示文件描述符不在集合中。
宏 功能描述 FD_ZERO(fd_set *set)
清空集合,将所有位设为 0。 FD_SET(int fd, fd_set *set)
将文件描述符 fd
添加到集合中(对应位设为 1)。FD_CLR(int fd, fd_set *set)
从集合中移除文件描述符 fd
(对应位设为 0)。FD_ISSET(int fd, fd_set *set)
检查 fd
是否在集合中,返回值为1
(存在)或0
(不存在)。 -
-
每次调用时需将所有fd集合从用户态拷贝到内核态,内核通过线性扫描判断就绪状态。
-
支持跨平台(Windows/Linux)。
代码示例
#include <sys/select.h>int main() {fd_set read_fds;FD_ZERO(&read_fds); // 清空集合FD_SET(sock_fd, &read_fds); // 添加sock_fd到监控集合struct timeval timeout;timeout.tv_sec = 5; // 超时时间5秒timeout.tv_usec = 0;// 调用select,监控可读事件int ret = select(sock_fd + 1, &read_fds, NULL, NULL, &timeout);if (ret > 0) {if (FD_ISSET(sock_fd, &read_fds)) {// 处理数据读取}}
}
优缺点
- 优点:跨平台兼容。
- 缺点:
- 最大支持
FD_SETSIZE
(通常1024)个文件描述符。 - 每次调用需重置fd集合,存在内存拷贝开销。
- 线性扫描效率低(时间复杂度O(n))。
- 最大支持
3. poll
原理
- 使用**结构体数组(pollfd)**替代select的位图,突破文件描述符数量限制。
- 仍需要遍历所有fd检查就绪状态。
代码示例
#include <poll.h>int main() {struct pollfd fds[1];fds[0].fd = sock_fd; // 监控的fdfds[0].events = POLLIN; // 监控可读事件// 调用poll,超时时间5秒int ret = poll(fds, 1, 5000);if (ret > 0) {if (fds[0].revents & POLLIN) {// 处理数据读取}}
}
优缺点
- 优点:无文件描述符数量限制。
- 缺点:
- 仍需遍历所有fd,效率随连接数线性下降。
- 每次调用需传递完整的fd数组。
4. epoll(Linux特有)
原理
- 使用红黑树管理待监控的fd,事件驱动机制。
- **边缘触发(ET)和水平触发(LT)**两种模式。
- LT模式(默认):只要fd处于就绪状态,每次epoll_wait都会通知。
- ET模式:仅当fd状态变化时通知一次,需一次性处理完数据。
代码示例
#include <sys/epoll.h>int main() {int epoll_fd = epoll_create1(0); // 创建epoll实例struct epoll_event event;event.events = EPOLLIN; // 监控可读事件event.data.fd = sock_fd;// 添加sock_fd到epoll监控epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &event);struct epoll_event events[10];// 等待事件,超时时间5秒int ret = epoll_wait(epoll_fd, events, 10, 5000);if (ret > 0) {for (int i = 0; i < ret; i++) {if (events[i].data.fd == sock_fd) {// 处理数据读取}}}
}
优缺点
- 优点:
- 支持海量连接(时间复杂度O(1))。
- 无需每次传递全部fd,通过事件回调机制高效通知。
- 缺点:仅适用于Linux系统。
5. 对比总结
特性 | select | poll | epoll |
---|---|---|---|
最大连接数 | 1024 | 无限制 | 无限制 |
效率 | O(n) 线性扫描 | O(n) 线性扫描 | O(1) 事件驱动 |
内存拷贝 | 每次拷贝全部fd集合 | 每次拷贝全部fd数组 | 仅注册时拷贝一次 |
触发模式 | 水平触发(LT) | 水平触发(LT) | 支持LT/ET模式 |
跨平台 | 是 | 是(多数系统) | 仅Linux |
6. 应用场景建议
- select/poll:低并发场景或需要跨平台时使用。
- epoll:Linux高并发服务器(如Web服务器、即时通讯)。
- Windows替代方案:IOCP(I/O Completion Ports)。
7. 注意事项
- ET模式需一次性读取所有数据(循环读取直到
EAGAIN
错误)。 - fd管理:epoll需手动管理fd的增删(
EPOLL_CTL_ADD/DEL/MOD
)。 - 多线程:epoll实例本身非线程安全,需加锁或每个线程独立实例。
I/O复用技术是构建高性能服务器的关键!