select, poll, epoll
I/O 多路复用是一种使得程序能够同时监听多个文件描述符的技术,从而提高程序的性能。能够在单个线程内,监视多个 I/O 流的状态来同时管理多个 I/O 流。一旦检测到某个文件描述符上关心的事情发生,就能通知程序进行相应的操作。
Linux 下实现 I/O 复用的方法主要有 select
, poll
和 epoll
。
I/O 多路复用一般与 非阻塞 IO 协同使用,因为 I/O 多路复用能够获取所“监视”的 文件描述符 中 处于 准备状态的也即读取成功、写成功的 所有文件描述符。相当于代替了非阻塞 IO 中判断是否读写完毕的操作。
Select
Select 要求首先构造一个文件描述符的列表,将要监听的文件描述符添加到该列表中,类型为 fd_set
,该数组的最大长度为 1024。调用 select 后,会发生阻塞,直到所监听列表中的文件描述符发生事件,返回发生 I/O 操作的描述符的数量。然后,我们对该列表进行遍历,判断是否需要对该文件描述符进行操作。
缺点:
- 每次调用 select 都需要拷贝一次 fd 列表,到内核
- 每次调用 select 都需要在内核遍历 fd 列表中的所有内容
- 默认仅支持 1024 个文件描述符
- select 返回后,还需要遍历 fd 列表进行判断
select 使用固定长度的 BitsMap,表示文件描述符集合,默认最大值为 1024。
Poll
Poll 的原理和 select 类似,但支持的文件描述符没有限制。
Poll 内部使用链表组织描述符,突破了 select 的文件描述符个数限制。
Epoll
epoll 指 event poll,允许进程监控多个 file descriptors,当 I/O 不阻塞时,获得通知。
epoll 不像 select 或者 poll,并不需要遍历文件描述符,而是由内核通过类似回调函数的机制,通知 epoll 实例,返回发生变动的文件描述符列表。
epoll 内部使用红黑树数据结构保存待检测的文件描述符,使用链表记录就绪事件,当某个 socket 有事件发生时,内核会将其加入到就绪事件链表中,用户调用 epoll_wait
时,只会返回有事件发生的文件描述符。
epoll_create
int epoll_create(int size);
size
表示要监控的文件标识符(file descriptor)的个数。返回一个 file descriptor,指向新创建的 epoll 实例。
这会在内核中创建一个 epoll 实例。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// epfd: returned by `epoll_create`
// op: 操作,Register(EPOLL_CTL_ADD), Delete(EPOLL_CTL_DEL), Modify(EPOLL_CTL_MOD)
// fd: 要添加的 file descriptor
// event: 一些事件处理
添加 file descriptors 到该 epoll 实例。
epoll_wait
int epoll_wait(int epfd, struct epoll_event * evlist, int maxevents, int timeout);
// epfd: the file descriptor returned by epoll_create which identifies the epoll instance in the kernel.
// evlist: evlist is allocated by the calling process and when epoll_wait returns, this array is modified to indicate information about the subset of file descriptors in the interest list that are in the ready state (this is called the ready list)
// maxevents: evlist 数组的长度
// timeout: This value specifies for how long the epoll_wait system call will block.
阻塞,直到 epoll set 中有文件标识符变为 ready 状态。
LT 模式 vs ET 模式
LT 指 Level - Triggered,是默认的工作方式,支持 阻塞 或 非阻塞 方式,这种方式下,内核检测到文件描述符就绪后,会发出通知,如果没有响应该通知,会继续通知。
ET 指 Edge - Triggered,是更加高效的工作方式,仅支持 非阻塞 方式,这种方式下,内核检测到文件描述符就绪后,仅发出一次通知。使用边缘触发模式时,I/O 事件发生时只会通知一次,所以我们应该尽可能的读写数据,以免错失机会,因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数中,程序就没办法继续往下执行,所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 错误,直到出现系统调用错误,返回 EAGAIN 或 EWOULDBLOCK。
select 和 poll 只有 LT 模式,而 epoll 默认是 ET 模式。
为什么使用 LT 模式?
惊群效应: 指在高并发场景下,当多个进程或线程等待同一个事件时,如果该事件发生,所有等待该事件的进程或线程都会被唤醒,导致大量的竞争和资源浪费。
就绪列表
epoll 中的就绪列表是使用链表结构实现的,实际使用中,通过 fs.epoll.max_user_watches
控制一个用户能够注册的文件描述符的数量。