I/O 多路复用实现方式
I/O 多路复用(I/O Multiplexing)的实现方式
I/O 多路复用是一种 通过单个线程监控多个 I/O 事件(如可读、可写) 的技术,核心目标是 用最少的资源管理大量并发连接。以下是主要的实现方式及其对比:
1. 主要实现方式
(1) select
特点
- 跨平台支持:几乎在所有操作系统(Linux/Windows/macOS)上可用。
- 基于位图(fd_set):监听的文件描述符(fd)数量有限(通常 1024)。
- 线性扫描:每次调用需遍历所有 fd,时间复杂度 O(n)。
- 触发方式:水平触发(LT,Level-Triggered)。
代码示例(C)
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(sockfd, &read_fds);struct timeval timeout = {5, 0}; // 5秒超时
int ret = select(sockfd + 1, &read_fds, NULL, NULL, &timeout);
if (ret > 0 && FD_ISSET(sockfd, &read_fds)) {// 可读事件就绪
}
缺点
- 效率低(高并发时遍历开销大)。
- 无法动态扩展 fd 数量。
(2) poll
特点
- 改进
select
的 fd 数量限制:使用链表存储 fd,理论无上限(但性能仍受限制)。 - 仍为线性扫描:时间复杂度 O(n)。
- 触发方式:水平触发(LT)。
代码示例(C)
struct pollfd fds[1];
fds[0].fd = sockfd;
fds[0].events = POLLIN; // 监听可读事件int ret = poll(fds, 1, 5000); // 5秒超时
if (ret > 0 && (fds[0].revents & POLLIN)) {// 可读事件就绪
}
缺点
- 和
select
一样有遍历开销,性能不高。
(3) epoll
(Linux 专属)
特点
- 高效事件通知:基于事件回调,仅返回就绪的 fd,时间复杂度 O(1)。
- 支持高并发:可管理数十万连接。
- 触发方式:
- 水平触发(LT,默认):只要 fd 可读/可写,就会重复通知。
- 边缘触发(ET):仅在状态变化时通知一次(需一次性处理完数据)。
- 核心函数:
epoll_create()
:创建 epoll 实例。epoll_ctl()
:注册/修改/删除 fd 监听事件。epoll_wait()
:等待事件就绪。
代码示例(C)
int epfd = epoll_create1(0);
struct epoll_event ev, events[10];
ev.events = EPOLLIN | EPOLLET; // 监听可读 + 边缘触发
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);while (1) {int n = epoll_wait(epfd, events, 10, 5000); // 5秒超时for (int i = 0; i < n; i++) {if (events[i].data.fd == sockfd) {// 处理可读事件(ET 模式需循环 read() 直到 EAGAIN)}}
}
优点
- 高性能,适合高并发场景(如 Nginx、Redis)。
- 支持边缘触发(ET),减少事件重复通知。
缺点
- 仅限 Linux 系统。
(4) kqueue
(FreeBSD/macOS 专属)
特点
- 类似
epoll
,但用于 BSD/macOS 系统。 - 支持更多事件类型(如文件系统事件)。
- 核心函数:
kqueue()
:创建 kqueue 实例。kevent()
:注册/等待事件。
代码示例(C)
int kq = kqueue();
struct kevent ev, events[10];
EV_SET(&ev, sockfd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, NULL);
kevent(kq, &ev, 1, NULL, 0, NULL);while (1) {int n = kevent(kq, NULL, 0, events, 10, &(struct timespec){5, 0}); // 5秒超时for (int i = 0; i < n; i++) {if (events[i].ident == sockfd) {// 处理可读事件}}
}
优点
- 高性能,功能比
epoll
更丰富。
缺点
- 仅限 BSD/macOS。
(5) IOCP
(Windows 专属)
特点
- 异步 I/O 模型:与
epoll
/kqueue
不同,IOCP 是真正的异步 I/O(非事件驱动)。 - 核心函数:
CreateIoCompletionPort()
:创建 IOCP 实例。GetQueuedCompletionStatus()
:获取已完成的操作。
适用场景
- Windows 高性能服务器(如 IIS)。
2. 对比总结
实现方式 | 操作系统 | 时间复杂度 | 最大 fd 数量 | 触发模式 | 特点 |
---|---|---|---|---|---|
select | 跨平台 | O(n) | 1024(默认) | LT | 兼容性好,效率低 |
poll | 跨平台 | O(n) | 无硬限制 | LT | 改进 select 的 fd 数量限制 |
epoll | Linux | O(1) | 数十万 | LT/ET | 高性能,边缘触发优化 |
kqueue | FreeBSD/macOS | O(1) | 数十万 | LT/ET | 功能丰富,类似 epoll |
IOCP | Windows | 异步回调 | 无硬限制 | 完成通知(非事件) | 真正的异步 I/O |
3. 如何选择?
- Linux 服务器:优先用
epoll
(Nginx、Redis、Netty)。 - BSD/macOS:用
kqueue
。 - Windows:用
IOCP
。 - 跨平台需求:
- 低并发 →
select
/poll
。 - 高并发 → 封装不同系统的多路复用(如 Libevent、Libuv)。
- 低并发 →
4. 常见问题
Q:为什么 epoll
比 select
/poll
快?
select
/poll
每次调用需遍历所有 fd,而epoll
仅返回就绪的 fd。epoll
使用内核事件表(红黑树 + 就绪链表),避免重复拷贝 fd 集合。
Q:水平触发(LT) vs 边缘触发(ET)?
- LT:只要 fd 可读/可写,会重复通知(编程简单,但可能重复触发)。
- ET:仅在状态变化时通知一次(需一次性处理完数据,性能更高)。
Q:epoll
的惊群问题?
- 多个线程/进程监听同一
epoll
,事件可能被所有线程唤醒(Linux 4.5+ 已修复)。
5. 现代应用
- Nginx:
epoll
(Linux)/kqueue
(BSD)。 - Redis:单线程
epoll
。 - Netty:封装
epoll
/kqueue
实现跨平台 Reactor 模式。