对常用I/O模型进行比较说明

丈夫志四海,万里犹比邻。这篇文章主要讲述对常用I/O模型进行比较说明相关的知识,希望能为你提供帮助。
      I/O在计算机中指Input/Output,IOPS(Input/Output Per Second)即每秒的输入输出量(或读写次数),是衡量磁盘性能的主要指标之一。IOPS是指单位时间内系统能处理的I/O请求数量,一般以每秒处理的I/O请求数量为单位,I/O请求通常为读或写数据操作请求。
      一次完整的I/O是用户空间的进程数据与内核空间的内核数据的报文的完整交换,但是由于内核空间与用户空间是严格隔离的,所以其数据交换过程中不能由用户空间的进程直接调用内核空间的内存数据,而是需要经历一次从内核空间中的内存数据copy到用户空间的进程内存当中,所以简单说I/O就是把数据从内核空间中的内存数据复制到用户空间中进程的内存当中。


  1. I/O模型相关概念
1.1 同步/异步
      同步(synchronous)是指被调用者并不提供事件的处理结果相关的通知消息,需要调用者主动询问事情是否处理完成;异步(asynchronous)指的是被调用者通过状态、通知或回调机制主动通知调用者被调用者的运行状态。
    同步/异步关注的是消息通信机制,即调用者在等待一件事情的处理结果时,被调用者是否提供完成状态的通知。

1.2 阻塞/非阻塞
      阻塞(blocking)指IO操作需要彻底完成后才返回到用户空间,调用结果返回之前,调用者被挂起,干不了别的事情;非阻塞(nonblocking)指IO操作被调用后立即返回给用户一个状态值,而无需等到IO操作彻底完成,在最终的调用结果返回之前,调用者不会被挂起,可以去做别的事情。
      阻塞/非阻塞关注调用者在等待结果返回之前所处的状态。



  1. 网络I/O模型
    网络I/O模型包括阻塞型、非阻塞型、复用型、信号驱动型和异步五种。
2.1  阻塞型I/O模型(blocking IO)
      阻塞型I/O模型是最简单的I/O模型,用户线程在内核进行IO操作时被阻塞。用户线程通过系统调用read发起I/O读操作,由用户空间转到内核空间,内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作;用户需要等待read将数据读取到buffer后,才继续处理接收的数据。整个I/O请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。

    阻塞型I/O模型的优缺点表现为:
    ①优点:程序简单,在阻塞等待数据期间进程/线程挂起,基本不会占用 CPU 资源。
    ②缺点:每个连接需要独立的进程/线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销较大,apache的preforck使用的是这种模式。
2.2  非阻塞型I/O模型 (nonblocking IO)
      在非阻塞型I/O模型中,用户线程发起IO请求时立即返回,但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。这会带来“轮询”机制中所存在两个问题:如果有大量文件描述符都要等,那么就得一个一个的read,这会带来大量的Context Switch(read是系统调用,每调用一次就得在用户态和核心态切换一次);此外轮询的时间也不好把握,需要猜多久之后数据才能到,如果等待时间设的太长,程序响应延迟就过大,如果设的太短,就会造成过于频繁的重试,干耗CPU而已,是比较浪费CPU的方式,一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。

      由上图可知,在设置连接为非阻塞时,当应用进程系统调用recvfrom没有数据返回时,内核会立即返回一个EWOULDBLOCK错误,而不会一直阻塞到数据准备好。如上图在第四次调用时有一个数据报准备好了,所以这时数据会被复制到 应用进程缓冲区 ,于是recvfrom成功返回数据。当一个应用进程这样循环调用recvfrom时,称之为轮询polling,这么做往往会耗费大量CPU时间,实际使用很少。
2.3  多路复用I/O模型(I/O multiplexing)
      多路复用I/O模型主要包括select、poll和epoll三种系统调用,这三种系统调用的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/poll/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,当用户进程调用了select,那么整个进程会被block,同时kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

      多路复用I/O模型的优缺点表现为:
    ①优点:可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程),这样可以大大节省系统资源;
    ②缺点:当连接数较少时效率相比多线程+阻塞 I/O 模型效率较低,可能延迟更大,因为单个连接处理需要2次系统调用,占用时间会有增加。
      多路复用I/O模型适用于以下场合:
    ①当客户端处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用;
    ②当一个客户端同时处理多个套接字时,此情况可能的但很少出现;
    ③当一个服务器既要处理监听套接字,又要处理已连接套接字,一般也要用到I/O复用;
    ④当一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用;
    ⑤当一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
2.4  信号驱动式I/O模型(signal-driven IO)
      信号驱动I/O的意思就是进程现在不用傻等着,也不用去轮询,而是让内核在数据就绪时,发送信号通知进程。

    调用的步骤是:通过系统调用sigaction,并注册一个信号处理的回调函数,该调用会立即返回,然后主程序可以继续向下执行,当有I/O操作准备就绪,即内核数据就绪时,内核会为该进程产生一个SIGIO信号,并回调注册的信号回调函数,这样就可以在信号回调函数中系统调用recvfrom获取数据,将用户进程所需要的数据从内核空间拷贝到用户空间。
      此模型的优势在于等待数据报到达期间进程不被阻塞,用户主程序可以继续执行,只要等待来自信号处理函数的通知。在信号驱动式I/O模型中,应用程序使用套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
      信号驱动式I/O模型的优缺点表现为:
【对常用I/O模型进行比较说明】    ①优点:线程并没有在等待数据时被阻塞,内核直接返回调用接收信号,不影响进程继续处理其他请求因此可以提高资源的利用率;
    ②缺点:信号I/O在大量IO操作时可能会因为信号队列溢出导致没法通知。
2.5  异步I/O模型(asynchronous IO)
      异步I/O与信号驱动I/O最大区别在于,信号驱动是内核通知用户进程何时开始一个I/O操作,而异步I/O是由内核通知用户进程I/O操作何时完成,两者有本质区别,相当于不用去饭店场吃饭,直接点个外卖,把等待上菜的时间也给省了。
      相对于同步I/O,异步I/O不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
      在信号驱动IO中,当内核通知触发信号处理程序时,信号处理程序还需要阻塞在从内核空间缓冲区拷贝数据到用户空间缓冲区这个阶段,而异步IO直接是在第二个阶段完成后,内核直接通知用户线程可以进行后续操作了。

      异步I/O模型优缺点表现为:
    ①优点:异步I/O能够充分利用DMA特性,让I/O操作与计算重叠;
    ②缺点:要实现真正的异步I/O,操作系统需要做大量的工作。目前Windows下通过IOCP实现了真正的异步I/O,在Linux系统下,Linux 2.6才引入,目前AIO并不完善,因此在Linux下实现高并发网络编程时以IO复用模型模式+多线程任务的架构基本可以满足需求。


  1. I/O模型对比
      在以上列出的五种常用IO模型中,按次序越往后阻塞越少,理论上效率也是最优。其中前四种属于同步I/O模型,因为其中真正的I/O 操作(recvfrom)将阻塞进程/线程,只有异步I/O模型才与POSIX定义的异步I/O相匹配。



  1. I/O的具体实现方式
4.1 I/O常见实现
      nginx支持在多种不同的操作系统实现不同的事件驱动模型,但是其在不同的操作系统甚至是不同的系统版本上面的实现方式不尽相同,主要有以下实现方式:
      ①select
      select库是在linux和windows平台都基本支持的 事件驱动模型库,并且在接口的定义也基本相同,只是部分参数的含义略有差异,最大并发限制1024,是最早期的事件驱动模型。
      ②poll
      在Linux中的基本驱动模型,windows不支持此驱动模型,是select的升级版,取消了最大的并发限制,在编译nginx的时候可以使用--with-poll_module和--without-poll_module这两个指定是否编译select库。
      ③epoll
      epoll是库是Nginx服务器支持的最高性能的事件驱动库之一,是公认的非常优秀的事件驱动模型。它和select以及poll有很大的区别,epoll是poll的升级版,但是与poll有很大的区别在于epoll的处理方式是创建一个待处理的事件列表,然后把这个列表发给内核,返回的时候在去轮训检查这个表,以判断事件是否发生。epoll支持一个进程打开的最大事件描述符的上限是系统可以打开的文件的最大数,同时epoll库的I/O效率不随描述符数目增加而线性下降,因为它只会对内核上报的“活跃”的描述符进行操作。
      ④kqueue
      用于支持BSD系列平台的高校事件驱动模型,主要用在FreeBSD 4.1及以上版本、OpenBSD2.0级以上版本、NetBSD级以上版本及MacOS X平台上,该模型也是poll库的变种,因此和epoll没有本质上的区别,都是通过避免轮训操作提供效率。
      ⑤rtsig
      不是一个常用事件驱动,最大队列1024,不是很常用。
      ⑥/dev/poll
      用于支持unix衍生平台的高效事件驱动模型,主要在Solaris 平台、HP/UX,该模型是sun公司在开发Solaris系列平台的时候提出的用于完成事件驱动机制的方案,它使用了虚拟的/dev/poll设备,开发人员将要见识的文件描述符加入这个设备,然后通过ioctl()调用来获取事件通知,因此运行在以上系列平台的时候请使用/dev/poll事件驱动机制。
      ⑦eventport
      该方案也是sun公司在开发Solaris的时候提出的事件驱动库,只是Solaris 10以上的版本,该驱动库看防止内核崩溃等情况的发生。
      ⑧Iocp
      Windows系统上的实现方式,对应第5种(异步I/O)模型。
4.2  常用I/O模型比较
4.2.1 性能对比
    下图是从性能的角度对常用I/O进行比较,其中横坐标表示的是用户的访问量,纵坐标表示的处理时间。绿色实线的Select主要在apache中使用,橙色虚线的Epoll主要在Nginx中使用,对比可见,当用户访问量逐渐上涨时,Epoll的处理速度仍然很快,响应能力并未受到太大的影响。

4.2.2 select、poll和epoll
    下图主要从操作方式、底层展现和IO效果等方面来select、poll和epoll进行对比。
?
4.2.2.1 select
      POSIX所规定,目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。
    select的缺点表现为:
    ①单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义FD_SETSIZE,再重新编译内核实现,但是这样也会造成效率的降低;
    ②单个进程可监视的fd数量被限制,默认是1024,修改此值需要重新编译内核;
    ③对socket是线性扫描,即采用轮询的方法,效率较低;
    ④select采取了内存拷贝方法来实现内核将FD消息通知给用户空间的这样一个用来存放大量fd的数据结构,会使得用户空间和内核空间在传递该结构时复制开销大。
4.2.2.2 poll
      本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态。
    poll没有最大连接数的限制,原因是它是基于链表来存储的。
    大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
      poll特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
4.2.2.3 epoll
      在Linux2.6内核中提出的select和poll的增强版本,支持水平触发LT和边缘触发ET,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。
    epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
    epoll的优点表现为:
    ①没有最大并发连接的限制:能打开的FD的上限远大于1024(1G的内存能监听约10万个端口),具体查看/proc/sys/fs/file-max,此值和系统内存大小相关;
    ②效率提升:非轮询的方式,不会随着FD数目的增加而效率下降,只有活跃可用的FD才会调用callback函数,即epoll最大的优点就在于它只管理“活跃”的连接,而跟连接总数无关;
    ③内存拷贝,利用mmap(Memory Mapping)加速与内核空间的消息传递,即epoll使用mmap减少复制开销。
4.2.2.4 总结
    ①epoll只是一组API,比起select这种扫描全部的文件描述符,epoll只读取就绪的文件描述符,再加入基于事件的就绪通知机制,所以性能比较好;
    ②基于epoll的事件多路复用减少了进程间切换的次数,使得操作系统少做了相对于用户任务来说的无用功;
    ③epoll比select等多路复用方式来说,减少了遍历循环及内存拷贝的工作量,因为活跃连接只占总并发连接的很小一部分。

    推荐阅读