IO 多路复用(I/O Multiplexing)详细讲解
1. IO 多路复用是什么?
IO 多路复用是一种 同步非阻塞 的 IO 处理方式,它允许 单个进程(或线程)同时监听多个 IO 事件,并在有数据可读/可写时进行相应的操作。这样可以避免传统的 一个连接对应一个线程/进程 的方式,减少系统资源消耗,提高并发处理能力。
它的核心思想是:
- 进程只需等待一个“事件通知器”(如
select
、poll
、epoll
),而不是轮询所有的文件描述符(FD)。 - 当任意 FD 就绪(可读、可写)时,通知进程进行相应操作,从而避免无效的 CPU 轮询,提高系统吞吐量。
2. IO 多路复用的工作原理
通常,IO 读写操作会阻塞进程,而多路复用提供了一种 高效的事件监听机制,允许进程同时监听多个文件描述符(FD),只有在 IO 事件就绪时才进行真正的读写操作。
传统的 IO 处理方式
- 阻塞 IO:
read()
调用会一直阻塞,直到数据可读。 - 非阻塞 IO:
read()
立即返回,如果没有数据,返回EAGAIN
,进程需要不断轮询(CPU 开销大)。
IO 多路复用方式
- 进程调用
select
/poll
/epoll
,让内核监听多个 FD。 - 当任意 FD 就绪(可读/可写),调用返回,进程进行 IO 操作。
- 进程处理完成后,继续监听,形成 事件驱动 模型。
3. 常见的 IO 多路复用方式
目前 Linux 提供了三种主要的 IO 多路复用机制:
select
poll
epoll
(Linux 推荐)
(1)select
select
使用 固定大小的位图数组(fd_set) 来存储监听的 FD,最大支持 1024 个 FD(Linux 默认)。
使用步骤
- 创建
fd_set
,并把需要监听的 FD 加入集合。 - 调用
select()
,让内核监听这些 FD 的状态变化。 select()
返回时,遍历fd_set
,找到可用的 FD 并进行 IO 操作。
代码示例
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(socket_fd, &rfds);
struct timeval timeout = {5, 0}; // 5 秒超时
int ret = select(socket_fd + 1, &rfds, NULL, NULL, &timeout);
if (ret > 0) {
if (FD_ISSET(socket_fd, &rfds)) {
// 处理 socket_fd 的数据
}
}
缺点
- 最大支持 1024 个 FD(
FD_SETSIZE
限制)。 - 每次调用都要拷贝整个
fd_set
,CPU 开销大。 - 每次返回都要遍历所有 FD,复杂度 O(N)。
(2)poll
poll
通过 动态数组(pollfd
结构)存储 FD,克服了 select
的 1024 限制。
使用步骤
- 将监听的 FD 放入
pollfd
数组。 - 调用
poll()
,让内核监听所有 FD 的状态。 poll()
返回后,遍历pollfd
,找到就绪的 FD 进行 IO 操作。
代码示例
struct pollfd fds[2];
fds[0].fd = socket_fd;
fds[0].events = POLLIN;
int ret = poll(fds, 2, 5000); // 5 秒超时
if (ret > 0 && (fds[0].revents & POLLIN)) {
// 处理 socket_fd 的数据
}
缺点
- 时间复杂度 O(N)(仍然需要遍历所有 FD)。
- 没有事件通知机制,需要每次轮询整个数组。
(3)epoll
(Linux 推荐)
epoll
是 事件驱动 方式,只有发生事件的 FD 才会被返回,避免 select/poll
需要轮询所有 FD 的问题。
epoll
的数据结构
- 红黑树:用于存储所有监听的 FD,提供 快速插入/删除(O(logN))。
- 双向链表:存储 活跃 FD(有事件的 FD),返回
epoll_wait
时只需要遍历链表(O(1))。
主要系统调用
epoll_create()
:创建 epoll 实例。epoll_ctl()
:添加、修改、删除监听的 FD。epoll_wait()
:等待事件,返回活跃的 FD。
代码示例
int epfd = epoll_create(1);
struct epoll_event ev, events[10];
ev.events = EPOLLIN;
ev.data.fd = socket_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &ev);
int ret = epoll_wait(epfd, events, 10, 5000);
for (int i = 0; i < ret; i++) {
if (events[i].events & EPOLLIN) {
// 处理 socket_fd 的数据
}
}
优势
- O(1) 事件通知,只返回有事件的 FD。
- 支持上百万个 FD,适用于 高并发 服务器。
- 采用
mmap
共享内存减少用户态与内核态的拷贝开销。
4. IO 多路复用的应用场景
- 高并发网络服务器(如 Nginx、Redis、Kafka)
- 即时通信(如 WebSocket)
- 事件驱动框架(如 Netty、libevent)
- 消息队列系统
- 数据库连接池管理
5. IO 多路复用 vs 其他 IO 模型
IO 模型 | 说明 | 适用场景 |
---|---|---|
阻塞 IO | read() 直接阻塞 |
适合简单同步任务 |
非阻塞 IO | read() 立即返回,无数据时返回 EAGAIN |
轮询开销大,少用 |
IO 多路复用 | select/poll/epoll 监听多个 FD |
高并发服务器 |
信号驱动 IO | SIGIO 信号通知 |
较少使用 |
异步 IO(AIO) | io_submit() ,数据准备好后,系统通知应用 |
适合高吞吐应用 |
6. 结论
select
和poll
适用于小规模并发,但不适合高并发。epoll
适用于大规模并发,推荐在 Linux 下使用。- IO 多路复用 避免了“一个连接一个线程”的传统模型,减少线程切换开销,提高系统吞吐量。
epoll
是 Linux 服务器 高性能 IO 的关键技术,Nginx、Redis、Netty 等广泛使用。
如果面试官深入追问,可以结合 Nginx 的 epoll
模型、Redis 事件驱动、Java Selector
机制(NIO) 进一步讨论。