Select ,Poll 和Epoll
目前流程的多路复用IO实现主要包括四种: select
、poll
、epoll
select
函数签名与参数
1 | int select(int nfds, |
readfds
、writefds
、errorfds
是三个文件描述符集(fd)合。select
会遍历每个集合的前 nfds
个描述符,分别找到可以读取、可以写入、发生错误的描述符,统称为“就绪”的描述符。然后用找到的子集替换参数中的对应集合,返回所有就绪描述符的总数。
timeout
参数表示调用 select
时的阻塞时长。如果所有文件描述符都未就绪,就阻塞调用进程,直到某个描述符就绪,或者阻塞超过设置的 timeout 后,返回。如果 timeout
参数设为 NULL,会无限阻塞直到某个描述符就绪;如果 timeout
参数设为 0,会立即返回,不阻塞。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。
select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。
对socket进行扫描时是线性扫描,性能开销大,采用轮询的方法,调用
select
时会陷入内核,这时需要将参数中的fd_set
从用户空间拷贝到内核空间,内核需要遍历传递进来的所有fd_set
的每一位,不管它们是否就绪。
poll
1 | int poll(struct pollfd *fds, nfds_t nfds, int timeout); |
其中 fds
是一个 pollfd
结构体类型的数组,调用 poll()
时必须通过 nfds
指出数组 fds
的大小,即文件描述符的数量。
poll没有最大连接数的限制,原因是它是基于链表来存储的,poll 和 select 几乎没有区别。poll 在用户态通过数组方式传递文件描述符,在内核会转为链表方式存储,没有最大数量的限制
从性能开销上看,poll 和 select 的差别不大。
- 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
- poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
epoll
select、poll 模型都只使用一个函数,而 epoll 模型使用三个函数:epoll_create
、epoll_ctl
和 epoll_wait
。
epoll 是对 select 和 poll 的改进,避免了“性能开销大”和“文件描述符数量少”两个缺点。
简而言之,epoll 有以下几个特点:
- 使用红黑树存储文件描述符集合
- 使用队列存储就绪的文件描述符
- 每个文件描述符只需在添加时传入一次;通过事件更改文件描述符状态
一开始说,epoll 是对 select 和 poll 的改进,避免了“性能开销大”和“文件描述符数量少”两个缺点。
对于“文件描述符数量少”,select 使用整型数组存储文件描述符集合,而 epoll 使用红黑树存储,数量较大。
对于“性能开销大”,
epoll_ctl
中为每个文件描述符指定了回调函数,并在就绪时将其加入到就绪列表,因此 epoll 不需要像select
那样遍历检测每个文件描述符,只需要判断就绪列表是否为空即可。这样,在没有描述符就绪时,epoll 能更早地让出系统资源。
相当于时间复杂度从 $O(n)$ 降为 $O(1)$
此外,每次调用 select
时都需要向内核拷贝所有要监听的描述符集合,而 epoll 对于每个描述符,只需要在 epoll_ctl
传递一次,之后 epoll_wait
不需要再次传递。这也大大提高了效率。
select
只支持水平触发,epoll
支持水平触发和边缘触发。
触发方式
水平触发(LT,Level Trigger):当文件描述符就绪时,会触发通知,如果用户程序没有一次性把数据读/写完,下次还会发出可读/可写信号进行通知。
当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。#
边缘触发(ET,Edge Trigger):仅当描述符从未就绪变为就绪时,通知一次,之后不会再通知。
和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。
很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
区别:边缘触发效率更高,减少了事件被重复触发的次数,函数不会返回大量用户程序可能不需要的文件描述符。
水平触发、边缘触发的名称来源:
数字电路当中的电位水平,高低电平切换瞬间的触发动作叫边缘触发,而处于高电平的触发动作叫做水平触发。
总的对比
-
select
:调用开销大(需要复制集合);集合大小有限制;需要遍历整个集合找到就绪的描述符 -
poll
:poll 采用数组的方式存储文件描述符,没有最大存储数量的限制,其他方面和 select 没有区别 -
epoll
:调用开销小(不需要复制);集合大小无限制;采用回调机制,不需要遍历整个集合
使用情景
##select 应用场景
select 的 timeout 参数精度为 1ns,而 poll 和 epoll 为 1ms,因此 select 更加适用于实时要求更高的场景,比如反应堆的控制,
其移植性更好,几乎被所有主流平台所支持。
poll 应用场景
poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。
需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。
需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。
因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且epoll 的描述符存储在内核,不容易调试。
epoll 应用场景
只需要运行在 Linux 平台上,并且有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。
参考
1.【操作系统】I/O 多路复用,select / poll / epoll 详解
转载无需注明来源,放弃所有权利