Linux 中的多路转接技术(IO 多路复用 / I/O Multiplexing) 是高并发网络服务器最核心的技术手段之一。
它解决的核心问题:一个线程/进程如何高效地同时“监视”大量 socket(文件描述符),在其中任意一个或多个“就绪”(可读、可写、异常)时才去处理,而不是每个 socket 都单独开线程或反复轮询。
Linux 目前主流提供了三种实现方式:select → poll → epoll(从老到新、从差到好)。
一、核心概念对比表(2025-2026 视角)
| 特性 | select | poll | epoll (主流) |
|---|---|---|---|
| 系统调用 | select() | poll() | epoll_create1() + epoll_ctl() + epoll_wait() |
| 最大文件描述符数量 | 通常 1024(FD_SETSIZE) | 无硬限(取决于内存) | 无硬限(通常几十万) |
| 内核-用户态拷贝开销 | 每次调用拷贝整个 fd_set(O(n)) | 每次拷贝整个 pollfd 数组(O(n)) | 只拷贝就绪事件(O(1) 均摊) |
| 就绪事件返回方式 | 修改传入的 fd_set | 修改 pollfd.revents | 返回独立的事件链表(epoll_event 数组) |
| 是否支持边缘触发 | 水平触发(LT) | 水平触发(LT) | 支持 LT / ET(边缘触发) |
| 是否支持 O(1) 获取就绪数 | 否(需遍历位图) | 否(需遍历数组) | 是(只返回就绪个数) |
| 性能(1w 连接,100 活跃) | ★☆☆☆☆ | ★★☆☆☆ | ★★★★★ |
| 跨平台性 | 极好(POSIX) | 好(POSIX) | 仅 Linux |
| 当前主流使用场景 | 遗留系统、小型服务 | 中型服务、跨平台需求 | Nginx、Redis、libevent、Go runtime 等 |
| 引入内核版本 | 很早 | 较早 | Linux 2.5.44(2002) |
二、三者底层原理与实现差异
1. select(最古老、最简单)
工作流程:
- 用户把关心的 fd 集合放入三个 fd_set(读、写、异常)
- 调用 select(nfds, &rfds, &wfds, &efds, &timeout)
- 内核遍历所有 fd,检查是否就绪 → 修改 fd_set 位图
- 返回就绪个数,用户自己遍历 fd_set 找哪些位置为1
致命缺点:
- 每次调用都要把整个 fd 集合从用户态 → 内核态拷贝(O(n))
- 内核每次都要线性扫描整个集合(O(n))
- fd 上限 1024(位图大小固定,可改但不推荐)
- 返回后用户还需再次遍历找就绪 fd(O(n))
2. poll(select 的改进版)
改进点:
- 用 pollfd 数组代替 fd_set:{fd, events, revents}
- 没有 1024 限制(只受内存限制)
- 事件和结果分离(events 传入,revents 返回)
仍存在的缺点:
- 每次调用仍需把整个 pollfd 数组拷贝到内核(O(n))
- 内核仍需遍历整个数组检查(O(n))
- 返回后用户仍需遍历数组找 revents != 0 的 fd(O(n))
3. epoll(目前 Linux 高并发的事实标准)
革命性设计:事件驱动 + 内核维护就绪链表
三大核心函数:
// 1. 创建 epoll 实例(红黑树 + 就绪链表)
int epoll_fd = epoll_create1(0); // 或 epoll_create(size) 已废弃
// 2. 注册/修改/删除 监控的 fd 和事件
int epoll_ctl(epoll_fd, EPOLL_CTL_ADD/DEL/MOD, fd, &event);
// event 示例
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 可读 + 边缘触发
ev.data.fd = client_fd; // 或 ev.data.ptr = 自定义结构体
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
// 3. 等待就绪事件(阻塞或超时)
int n = epoll_wait(epoll_fd, events, maxevents, timeout);
// events 数组中就是已经就绪的 fd 和事件
关键优势:
- 红黑树存储所有监控 fd(增删改 O(log n))
- 内核回调机制:当 fd 就绪时,内核主动把事件加入就绪链表(callback 机制)
- epoll_wait 只返回就绪的 fd(O(1) 均摊获取就绪数)
- 支持边缘触发(ET) vs 水平触发(LT):
- LT(默认):只要缓冲区有数据就一直通知(类似 select/poll)
- ET:只在状态变化时通知一次(高性能,但需一次性读完)
ET 模式经典写法(非阻塞 + 循环读写):
while (1) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
if (events[i].events & EPOLLIN) {
char buf[1024];
while (1) { // 必须循环读到 EAGAIN
int len = read(fd, buf, sizeof(buf));
if (len <= 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
// 错误处理
}
// 处理数据
}
}
}
}
三、实际选型建议(2025-2026)
| 场景 | 推荐选择 | 理由简述 |
|---|---|---|
| 连接数 < 1000,活跃连接少 | select / poll | 实现最简单,跨平台 |
| 跨平台需求(Linux + BSD + Windows) | poll | 比 select 更灵活,无 1024 限制 |
| 高并发服务器(1w+ 连接) | epoll | 性能碾压,Nginx/Redis 标配 |
| 需要边缘触发 + 极致性能 | epoll ET | 减少唤醒次数,但代码复杂度高 |
| 想跨平台又要高性能 | libevent / libev / io_uring | 封装了 epoll/kqueue 等 |
一句话总结:
select 和 poll 已经过时,除非你有跨平台强需求,否则现代 Linux 高并发网络服务一律首选 epoll。
如果你想看 epoll 的完整服务器示例代码(C语言)、与 io_uring 的对比、ET vs LT 的详细实验、或者 Nginx 是如何用 epoll 的,都可以直接告诉我,我再给你展开更具体的代码和分析。