IO多路复用中的select poll和epoll

  1. Select ,Poll 和Epoll
    1. select
    2. poll
    3. epoll
    4. 触发方式
  2. 总的对比
  3. 使用情景
    1. poll 应用场景
    2. epoll 应用场景
  4. 参考

Select ,Poll 和Epoll

目前流程的多路复用IO实现主要包括四种: selectpollepoll

select

函数签名与参数

1
2
3
4
5
6
int select(int nfds,
fd_set *restrict readfds,
fd_set *restrict writefds,
fd_set *restrict errorfds,
struct timeval *restrict timeout);

readfdswritefdserrorfds 是三个文件描述符集(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_createepoll_ctlepoll_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 详解


转载无需注明来源,放弃所有权利