I/O概念
众所周知,在 Linux 的世界中一切皆为文件,而文件的本质其实就是一串二进制流,不管是 socket 还是 FIFO、管道、终端等,对于Linux来说,一切都是文件、一切都是流。在信息交换过程中,我们对这些流进行收/发操作,这一过程简称为 I/O 操作(input and output)。
I/O模型
在了解了什么是 I/O 后,接下来介绍五种 I/O 模型:阻塞I/O(blocking I/O)、非阻塞I/O(non-blocking I/O)、多路复用I/O( I/O multiplexing )、信号驱动I/O(signal blocking I/O) 以及 异步I/O(Asynchronous I/O)。
阻塞I/O
顾名思义,当进程在等待数据时,若该数据一直没有产生,则该进程将会一直等待,直到产生数据为止,这一过程中,进程的状态是阻塞的。
在 Linux 中,默认情况下所有的 socket 都是 blocking,一个典型的操作流程大概是这样的:
当用户态进程调用了 recvfrom 这个系统调用接收数据,内核(kernel)就开始了 I/O 的第一个阶段:准备数据。对于网络 I/O 来说,很多数据一开始还没有到达,这个时候内核就要等待足够的数据到达。
该用户态进程将一直在此等待,不会进行其他的操作,直到内核态准备好了数据后,将数据从内核态拷贝到用户态内存空间,然后 recvfrom 成功返回,用户进程才解除阻塞的状态,开始处理收到的数据。
所以,阻塞 I/O 的特点就是在 I/O 执行的两个阶段(等待数据和拷贝数据两个阶段)都被 block 了。
非阻塞I/O
在非阻塞I/O模型中,用户态进程等待内核的数据,当内核数据尚未准备好的时候,用户态进程将会不断的询问内核,直到内核准备好数据。
Linux下,可以通过设置 socket 使其变为non-blocking。
当用户态进程调用了 recvfrom 这个系统调用接收数据,当前内核并没有数据报文产生,此时 recvfrom 返回 EWOULDBLOCK,用户态进程会一直调用 recvfrom 询问内核,待内核准备好数据后,将数据从内核态拷贝到用户态内存空间,recvfrom 成功返回,用户态进程才开始处理收到的数据。
多路复用I/O
如果一个进程需要处理多种不同的消息,那么可能的做法是开启多条线程,每条线程接收处理一类消息,如果每条线程都采用阻塞 I/O 模型,那么也就是多线程中使用阻塞式 I/O。
对于上述的场景中,其实可以使用多路复用 I/O 模型处理,无需采用多线程监听消息的方式,这其中涉及到 select、poll、epoll 等不同的方法。
如上图所示,用户态进程采用 select 的方法,通过 select 可以等待多个不同类型的消息,如果其中有一个类型的消息准备好,则 select 会返回信息,然后用户态进程调用 recvfrom 接收数据。
select、poll、epoll 的区别和用法会单独开一篇文章来讲。
信号驱动I/O
在信号驱动式 I/O 模型中,与阻塞式和非阻塞式有了一个本质性的区别,那就是用户态不再等待内核态的数据准备好,就可以直接去做别的事了。
如上图所示,当需要等待数据的时候,首先用户态进程会向内核发送一个信号,告诉内核需要什么数据,然后用户态就可以去做别的事了。而当内核态数据准备好后,内核立刻发给用户态一个信号,告知数据已准备好,用户态进程收到后,立马调用 recvfrom,等数据从内核空间拷贝到用户内存空间,待完成之后,recvfrom 成功返回,用户态进程开始处理数据。
异步I/O
异步 I/O 模型相对于信号驱动 I/O 模型就更彻底了。
首先用户态进程会告诉内核需要什么数据(上图中通过 aio_read),然后用户态进程就不管了,内核等待用户态需要的数据准备好,然后将数据拷贝到用户态空间,此时才通知用户态进程数据已准备好,然后用户态进程直接处理用户态空间中的数据。
I/O模型比较
一张图解释所有: