I/O多路复用器之隐秘的角落

时间:2022-07-23
本文章向大家介绍I/O多路复用器之隐秘的角落,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

上一篇文章讲到了Unix的I/O模型,以及在java中的具体实现,其中在java中我们最为关注的就是 I/O 复用了,这篇主要总结下I/O多路复用器。

概念:文件描述符fd

Linux的内核将所有外部设备都可以看做一个文件来操作。那么我们对与外部设备的操作都可以看做对文件进行操作。我们对一个文件的读写,都通过调用内核提供的系统调用;内核给我们返回一个filede scriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符)。描述符就是一个数字,指向内核中一个结构体(文件路径,数据区,等一些属性)。那么我们的应用程序对文件的读写就通过对描述符的读写完成。

复用器

1. select

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

参数解析
  1. readset、writeset、exceptset 指定我们要让内核测试读、写和异常条件的描述符。它们的类型fd_set是一个BitMap的设计,32位操作系统内核设置它1024位,64位操作系统则设置为2048位,拿readset为例,如果描述符为3的读时间被关注,那么readset的第三位会被置为1。
  2. maxfdp1 指定待测试对的描述符个数,它的值是待测试的最大描述符加1(如其名),它就是在readset、writeset、exceptset三个描述符集中找出最大描述符编号值,然后加1。表示0~(maxfgp1 - 1)位均将被内核关注。
  3. timeout 就是超时时间,它的结构精确到了微秒,相对更为精准;
  4. 返回 若有就绪描述符则返回其数目,若超时则返回0,若出错则返回-1;
操作过程
  1. select执行,CPU由用户态转为内核态,同时这些参数也拷贝到内核态,由内核进行判断所关注的0~(maxfgp1 - 1)描述符是否有事件,如果没有,则阻塞,如果有,则对有事件发生的FD,进行置位(其实是对没有发生事件的置位为0),select返回;
  2. 程序接下来还需要重新遍历0~(maxfgp1 - 1)的FD,对已经被置位的FD进行处理;
  3. readset、writeset、exceptset 3个数据对关注的FD置位,因为已经被内核修改了,所以需要每次都要重新设置一下;

缺点

  1. FD的数目有上限,意味着最大连接数:x86机器为1024,x64为2048;
  2. 每次调用,都会发生上下文切换,而且都需要将3个fd_set数据结构传入内核;
  3. fd_set不可重用,每次都需重新置位;
  4. 2次对关注FD进行遍历,时间复杂度为O(n);
2. poll

int poll(struct pollfd *fdarray, unsigned nfds,int timeout);

参数解析
  1. fdarray

fdarray是一个链表的结构,数据结构如下:

struct pollfd{
    int fd; // 需要关注的文件描述符
    short events;// 关注的事件类型
    short revents;// 发生的事件
}

poll相对于select的改进主要是在这个结构体上,由数组到链表,解决了描述符有上限的问题,并且将结果和参数分离,只需要重置revents就可以了,而不需要重新申请整个结构;

  1. nfds 代表元素的个数
  2. timeout 超时时间
  3. 返回 若有就绪描述符则返回其数目,若超时则返回0,若出错则返回-1;

缺点

经过select->poll的改进,还剩下select的2个缺点、

  1. 每次调用,都会发生上下文切换,而且都需要将3个fd_set数据结构传入内核;
  2. 2次对关注FD进行遍历,时间复杂度为O(n);
3. epoll

epoll在poll和select的缺点之上做了重大改进,但是逻辑也更为复杂。它有三个函数:

int epoll_create(int size)直接在内核创建保存文件描述符的空间。epoll的结构采用红黑树保存,epoll_create可视为初始化root节点。调用epoll_create所创建的文件描述符保存空间称为“epoll例程”。size值可视为建议值,并非用来决定例程大小;

int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)向控件注册并注销文件描述符,可视为向红黑树注册描述符,并注册相关事件的相关回调;

int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout):与select函数类似,等待文件描述符发生变化;

参数解析
  1. epfd:表示事件发生监视范围的epoll例程的文件描述符
  2. events:用于保存发生事件的文件描述符集合的链表
  3. maxevents:告诉内核第二个参数events的大小
  4. timeout:超时时间
操作过程

存储FD的数据结构直接改由在内核的维护,我们便不再需要重复多次从用户态copy到内核态,只需要实时维护发生变化的FD到内核就可以了;用events来存储发生事件的fd,则无需再遍历整个被关注的描述符集合。

操作模式

epoll对文件描述符的操作有2种模式,默认模式是水平触发。

  1. 水平触发(level trigger) 当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。水平触发同时支持block和none-blocking
  2. 边缘触发(edge trigger) 当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。边缘触发是高速工作方式,只支持none-blocking
4. 总结

select

poll

epoll

操作方式

遍历

遍历

回调

底层实现

数组

链表

红黑树

IO效率

每次调用都进行线性遍历,时间复杂度为O(n)

每次调用都进行线性遍历,时间复杂度为O(n)

事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)

最大连接数

1024(x86)或2048(x64)

无上限

无上限

fd拷贝

每次调用select,都需要把fd集合从用户态拷贝到内核态

每次调用poll,都需要把fd集合从用户态拷贝到内核态

调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝