多路复用是一种单线程/进程管理多个Socket连接的技术,核心是通过系统调用监听多个文件描述符(FD),当某个FD就绪(可读/可写/异常)时通知程序处理,避免阻塞等待。常见的实现方式有:
- select
- poll
- epoll(Linux特有,高性能)
1. select/poll/epoll对比
特性 | select | poll | epoll |
---|---|---|---|
时间复杂度 | O(n) | O(n) | O(1) |
FD数量限制 | 1024(默认) | 无限制 | 无限制 |
工作模式 | 轮询 | 轮询 | 回调(事件驱动) |
内核态/用户态拷贝 | 每次需拷贝全部FD集合 | 同select | 仅传递就绪的FD(mmap优化) |
适用场景 | 跨平台、少量连接 | 少量连接 | 高并发(如10万+连接) |
2. epoll的核心优势
epoll是Linux下高性能多路复用的实现,解决了select/poll的缺陷:
- 无需线性扫描FD:通过红黑树管理FD,就绪时直接回调通知。
- 无FD数量限制:仅受系统最大文件描述符数限制。
- 边缘触发(ET)模式:减少事件重复触发的开销。
epoll的关键函数
int epoll_create(int size); // 创建epoll实例
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 注册/修改FD
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 等待事件
3. epoll的触发模式
(1) 水平触发(LT,Level-Triggered)
- 默认模式,只要FD处于就绪状态(如缓冲区有数据),就会持续触发事件。
- 特点:
- 类似select/poll的行为。
- 如果未一次性读完数据,下次
epoll_wait
会再次通知。
- 优点:编程简单,不易遗漏事件。
- 缺点:可能引发多次无意义的唤醒(如数据未读完时)。
LT示例代码
struct epoll_event event;
event.events = EPOLLIN | EPOLLLT; // 水平触发(默认)
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
(2) 边缘触发(ET,Edge-Triggered)
- 仅在FD状态变化时触发一次(如从不可读变为可读)。
- 特点:
- 必须非阻塞IO + 循环读写直到EAGAIN,否则会丢失后续数据。
- 高性能,减少重复事件。
- 优点:减少无效唤醒,适合高并发。
- 缺点:编程复杂,需处理不全读/写的情况。
ET示例代码
struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // 边缘触发
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
// 必须非阻塞读取!
while (read(fd, buf, sizeof(buf)) > 0); // 读到EAGAIN为止
4. 多路复用的优缺点
优点
- 高并发:单线程可管理数万连接(相比多线程/进程更节省资源)。
- 避免阻塞:无需为每个连接创建独立线程。
- 精准事件通知(epoll):仅处理就绪的FD。
缺点
- 编程复杂(尤其是ET模式需非阻塞IO + 循环读写)。
- 仅适用于IO密集型场景:CPU密集型任务仍需多线程。
- 平台依赖性(如epoll仅限Linux)。
5. 面试常见问题
Q1: 为什么epoll比select/poll高效?
- select/poll需轮询所有FD,而epoll仅回调就绪的FD。
- select/poll每次调用需全量拷贝FD集合,epoll通过
mmap
共享内存减少拷贝。
Q2: ET模式下为什么要用非阻塞IO?
- ET模式只通知一次,如果阻塞读取且未读完,后续数据到达时不会再次触发,导致数据滞留。
Q3: LT和ET如何选择?
- LT:适合简单场景(如HTTP服务器)。
- ET:适合高性能场景(如Redis、Nginx),但需确保正确处理读写。
总结
要点 | 说明 |
---|---|
多路复用核心 | 单线程管理多个Socket,避免阻塞等待。 |
epoll优势 | 事件驱动、O(1)复杂度、无FD限制。 |
LT vs ET | LT持续触发,ET仅状态变化时触发(需非阻塞IO)。 |
适用场景 | 高并发网络编程(如Web服务器、即时通讯)。 |