IO模型

什么是IO

在Linux世界里,一切皆文件。文件就是一串二进制流,不管是socket、FIFO、管道还是终端,对我们来说一切都是文件,一切都是流。在信息交换的过程中,我们都是对这些流进行数据的收发操作,简称为I/O操作(Input and Output)。

计算机里的所有流都是通过文件描述符(File Descriptor,简称fd)来表示。一个fd就是一个整数,所以对这个整数的操作,就是对这个文件(流)的操作。我们创建一个socket,通过系统调用会返回一个fd,对socket的操作就会转化为对这个fd的操作。

IO交互

Linux操作系统将用户空间和内核空间进行了隔离,用户空间也称为用户态,内核空间也称为内核态。通常用户进程中的一个完整IO分为两个阶段:

  • 用户空间和内核空间之间的交互
    用户空间用来存放的是用户程序的代码和数据,应用程序运行在用户空间。内核空间用来存放的是内核代码和数据,操作系统和驱动程序运行在内核空间。两者不能简单地使用指针传递数据,因为Linux使用的虚拟内存机制,用户空间中的程序必须通过系统调用请求内核空间中的kernel来协助完成IO动作。

  • 内核空间和设备空间之间的交互
    设备空间用来缓冲设备中将被读写操作的数据。以网卡设备为例,当发起一次网络请求时,将内核空间中的数据复制到网卡,然后再将请求发送出去。当接收一次网球请求时,等待网络数据到达网卡,然后内核将网卡中的数据读取到内核缓冲区。由于IO设备一般速度比较慢,需要等待,所以内核会为每一个IO设备维护一个缓冲区。

从广义的角度来讲,只要是比内核态慢的交互,都可以看作是IO操作。如:内存IO、网络IO和磁盘IO。通常我们说的IO指的是网络IO和磁盘IO。

IO模型

阻塞IO(Blocking I/O,BIO)

阻塞IO(Blocking I/O,BIO)

Application发起一个IO请求,Kernel等待IO设备数据ready,然后将ready的数据从内核空间copy到用户空间,最后返回给Application。在这整个过程当中Application一直是被block住的。

非阻塞IO(Nonblocking I/O)

非阻塞IO(Nonblocking I/O)

Application发起一个IO请求,Kernel立刻返回EWOULDBLOCK(用于非阻塞模式,不需要重新读或者写)。Application不停的轮询Kernel,直到IO设备数据ready被Kernel读取到内核空间,再从内核空间copy到用户空间,最后返回给Application为止。在这整个过程当中Application是可以执行其它操作的,是一种非阻塞状态。这种方式会让Application由于轮询导致CPU过高。

IO多路复用(I/O Multiplexing,NIO)

IO多路复用(I/O Multiplexing,NIO)

在IO多路复用中,Application会通过select取轮询一个fd集合中的状态,只有当fd有读、写或者异常事件时,才真正调用recvfrom实际的IO读写操作(IO设备数据ready被Kernel读取到内核空间,再从内核空间copy到用户空间,最后返回给Application)。

IO多路复用是通过一个线程就可以管理多个fd,只有当fd真正有读、写或者异常事件发生才会占用资源来进行实际的操作。因此,IO多路复用比较适合连接数比较多的情况。IO多路复用非阻塞IO的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在IO多路复用中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。

不过要注意的是,IO多路复用是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于IO多路复用来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新事件的轮询。

信号驱动IO(Signal—Driven I/O)

信号驱动IO(Signal—Driven I/O)

Application发起一个IO请求,Kernel会给对应的socket注册一个信号函数,然后Application继续执行其它操作,当Kernel中数据就绪时会发送一个信号给Application,Application接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。这个一般用于UDP中,对TCP套接口几乎是没用的,原因是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么事情。

异步IO(Asynchronous I/O,AIO)

异步IO(Asynchronous I/O,AIO)

异步IO模型才是最理想的IO模型,在异步IO模型中,当Application发起aio_read操作之后,立刻就可以开始去做其它的事。当Kernel收到一个aio_read请求之后会立刻返回,说明read请求已经成功发起了,因此不会对Application产生任何block。然后,Kernel会等待IO数据准备完成,然后将数据从内核空间copy到用户空间,当这一切都完成之后,Kernel会给Application发送一个信号,告诉它read操作完成了。也就说Application完全不需要关心实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收Kernel返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。

也就说在异步IO模型中,IO操作的两个阶段都不会阻塞Application,这两个阶段都是由Kernel自动完成,然后发送一个信号告知Application操作已完成。Application中不需要再次调用IO函数(recvfrom)进行具体的读写。这点是和信号驱动IO模型有所不同的,在信号驱动IO模型中,当Application接收到信号表示数据已经就绪,然后需要Application调用IO函数(recvfrom)进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在Application中调用IO函数(recvfrom)进行实际的读写操作。

很少有Linux系统支持异步IO模型,Windows的IOCP就是该模型

IO模型对比

IO模型对比(Comparison of the I/O Models)

结论

  • 阻塞程度:Blocking I/O>Nonblocking I/O>I/O Multiplexing>Signal-Driven I/O >Asynchronous I/O,效率是由低到高的
  • 对于Blocking I/O、Nonblocking I/O、I/O Multiplexing和Signal-Driven I/O 而言,第一阶段的处理方式不同,第二阶段的处理是相同的(阻塞的调用recvfrom
posted @ 2020-07-12 13:37  PinXiong  阅读(321)  评论(0编辑  收藏  举报