有关I/O多路复用的总结

什么是IO多路复用呢?

IO多路复用的实现有哪些呢?

它们的区别是什么呢?

为了回答上面三个问题,我总结得到了这篇文章。

什么是IO多路复用呢?

IO多路复用是一种可以监视多个描述符,一旦某个描述符就绪(读、写就绪),能够通知进程进行相应的读写操作的机制。

描述符:内核(kernel)利用文件描述符(file descriptor)来访问文件。 文件描述符是非负整数。 打开现存文件或新建文件时,内核会返回一个文件描述符。 读写文件也需要使用文件描述符来指定待读写的文件。

IO多路复用是同步非阻塞方式,由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,而 “后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。如果轮询不是进程的用户态,而是有人帮忙就好了。那么这就是所谓的 “IO 多路复用”。UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,做的事情是一样的)。

其最大优势就系统开销小,系统不必要创建过多的进程/线程,减少了上下文切换的消耗和系统资源。

主要适用如下场合:

  • 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用;
  • 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用;
  • 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用;
  • 当客户端需要处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用。

IO多路复用的实现有哪些呢?

在内核中的实现有 select、poll、epoll ,其出现顺序也是如此。

select

用户线程会启动一个监视Socket调用select函数以后会阻塞,直到有描述符就绪,或超时,函数才返回。当函数返回以后,再遍历fdset来寻找就绪的描述符。select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。

有以下缺点:

  • 每次调用都需要把fd集合从用户态拷贝到内核态,这样会使得用户空间和内核空间在传递该结构时复制开销大;
  • 每次扫描时采取线性扫描内核中的所有fd,效率很低;
  • 单个线程处理的IO数量由fd限制,默认值为1024。

然而,它有良好的跨平台性,几乎所有平台都可以支持。

poll

poll和select的实现是类似的,区别在于它是基于链表存储的,没有最大连接的限制。

epoll

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait:

  • epoll_create是创建一个epoll句柄;
  • epoll_ctl是向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理;
  • epoll_wait则是等待事件的产生。

epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。

对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

如果没有大量的空闲连接或者“死”连接,epoll的效率并不会比select/poll高很多,但是当有大量空闲连接的时候,就会发现epoll的效率会大大提高。

它们的区别是什么呢?

三者的区别体现在三个方面:

1. 支持的最大连接数

  • select:最大连接数由FD_SIZE决定,默认值为1024;
  • poll:基于链表存储,没有最大连接数的限制;
  • epoll:最大连接数由内存决定,1G的内存可以打开约10万个连接。

2. FD剧增后带来的IO效率问题

  • select:每次调用都会对连接进行线性遍历,随着FD的增加使得遍历效率降低;
  • poll:同上;
  • epoll:fd上是利用回调函数,只有活跃的socket才会主动调用callback,在活跃socket较少的情况下,不存在前者的线性下降的性能问题,当所有socket都很活跃的时候,可能会出现性能问题。

3. 消息传递方式

  • select:通过拷贝的方式将内核的消息传递给用户空间;
  • poll:同上;
  • epoll:通过共享内存的方式,实现内核态和用户态的通信。

参考资料