多路复用IO概念
如何管理 fd 的可读可写事件?
IO 多路:指同时对多个文件进行读写操作。大体有两种方案:
-
阻塞IO模式
-
一个线程只能处理一个流的IO事件
-
缺点:若线程数增多性能会变差
多个线程处理多个IO(浪费CPU资源,效率低)—— 单个线程while循环。
-
while(true) {
select(stream[]);
for (i->stream[]) {
if i has data {
read data until unavailable
}
}
}
-
多路复用IO。复用是指:使用一个线程处理多个文件fd的读写 的模式。
-
让每一个 fd 觉得,当前线程只在给他一个fd跑腿。
-
要尽可能的少的空转,要把所有的时间都用在处理句柄的 IO 上。
-
为了实现上诉的功能,内核提供了 3 种系统调用 :select
,poll
,epoll
。
系统调用
select poll epoll比较
相同:这 3 种系统调用都能够管理 fd 的可读可写事件,
- 所有 fd 不可读不可写时,阻塞线程切走 cpu
- 存在 fd 可读写时,唤醒对应线程。
适用场景:
-
select/poll:适合连接数少、跨平台需求或兼容性要求高的场景。
-
优先选择
poll
:在多数现代场景中,poll
的灵活性(无 fd 数量限制、独立事件设置)更优。 -
仅在特定场景用
select
:如兼容性要求、极低并发且需多事件监控,或依赖剩余超时时间时。
-
-
高并发场景选
epoll
/kqueue
:若在 Linux 下,直接使用epoll
;BSD 系用kqueue
,彻底超越select
/poll
。适合 Linux 下高并发、长连接的服务器(如 Web 服务器、实时通信系统)。短连接频繁的场景可能因频繁调用epoll_ctl
影响性能。
特性 | select | poll | epoll |
---|---|---|---|
文件描述符数量限制 | 有(1024) | 无(基于链表实现) | 无 |
内核实现 | 基于轮询(polling),内核遍历 fd 检查状态。 | 基于轮询:同select | 基于回调(事件驱动) |
遍历效率 | 低(O(n) 遍历)返回后需遍历fd检查状态。 | 低:同select | 高(O(1) 回调) |
拷贝开销 | 开销大:每次调用都需要把fd集合从用户态拷贝到内核态。 | 开销大:同select | 开销小:在内核中维护了一个事件表。 |
数据拷贝 | 每次复制 fd 集合 | 每次复制 fd 数组 | 内核维护事件表,无需全拷贝 |
可移植性 | POSIX 标准,跨平台 | POSIX 标准,跨平台 | Linux 特有 |
触发模式 | 水平触发(LT) | 水平触发(LT) | 水平触发(LT)、边缘触发(ET) |
水平触发(LT)、边缘触发(ET)
- 水平触发是只要fd处于就绪状态,每次调用都会通知;
- 边缘触发只在状态变化时通知一次,需要程序处理完所有数据,否则可能丢失事件。
epoll
简介
总而言之,epoll是
-
linux内核提供的一种系统调用,性能好于另外两个系统调用 poll,select。
-
用于处理IO 多路复用(管理 fd 的可读可写事件)
-
在所有 fd 不可读不可写无所事事的时候,可以阻塞线程,切走 cpu
-
fd 可读写的时候,对应线程会被唤醒
-
使用事件驱动的方式,当某个fd就绪时,内核会采用回调机制直接将该fd加入到就绪列表,不需要每次都扫描所有fd。epoll_wait返回时只提供就绪的事件。会把哪个流发生的什么样的IO事件进行通知,时间复杂度O(1),对流的操作都是有意义的
while(true) {
active_stream[] = epoll_wait();
for i in active_stream[] {
read or write till unavailable
}
}
epoll 的使用
使用 epoll 需要以下三个系统调用:
//头文件
#include <sys/epoll.h>
// 创建一个epoll句柄,size用来告诉内核需要监听的数目
int epoll_creat(int size);
// epoll的事件注册函数
int epoll_ctl(int epfd, int op, int fd, struct