计算机通常包括输入设备、输出设备、CPU(控制器、运算单元)、存储设备等组成。当我们运行一段程序,这个程序所创建的进程是在用户空间的,而用户空间是无法操作计算机内核,因此,必须借助内核空间的帮助。例如我们的进程负责读取一个文件,执行read操作,我们的请求会从用户空间到内核空间,由内核空间负责准备数据,当内核数据准备完成后,数据再由内核空间流转到用户空间,进行输出。
常用的Linux IO模式分为阻塞IO(BIO)、非阻塞IO(NIO)、异步IO(AIO)和多路复用IO。
1、同步、异步
同步和异步的区别的核心点是消息的最终通知机制。
同步是指当客户端发起一个请求后,需要等待内核空间把数据准备充分,然后主动去内核空间获取数据的过程,也就是说由请求的调用方主动去获取消息的过程。
异步是指当客户端发起一个请求后,不在主动等待内核空间把数据准备充分,而是在内核空间把数据准备充分后,自行写入到用户空间,应用程序拿到这个消息通知后,进行消息的处理过程。
二、IO模式
对于一次IO访问(以read举例),用户空间发起请求后,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的缓冲区,最后交给进程。
所以说,当一个read操作发生时,它会经历两个阶段:
1、等待数据准备 (Waiting for the data to be ready)
2、将数据从内核拷贝到用户空间(进程)中 (Copying the data from the kernel to the process)
这两个阶段的存在,在Linux系统产生了阻塞IO、非阻塞IO、异步IO、信号驱动IO、多路复用IO,本文将主要讲述阻塞IO、非阻塞IO、异步IO和多路复用IO。
1、阻塞IO(BIO)
阻塞IO是一种非常常见的IO模式,Java中的ServerSocket就是阻塞IO的一种实现方式。阻塞IO是指当应用程序发起一个请求后,该请求会从用户空间传递到内核空间,在内核空间准备数据的过程中,用户的进程没有切换,一直等待着内核空间把数据准备好。这种形式的IO就是阻塞IO。
举个例子:下载一个文件,页面上会有一个进度条提示文件的下载进度。当文件未下载完成的时候,用户一直关注着这个进度条的状态,而且这个期间不去干任何其他的事情。这种过程就类似阻塞IO的过程。

以read为例:
1)用户进程发起read请求,进行recvfrom系统调用;
2)内核开始准备数据(从磁盘拷贝到缓冲区),而进程请求的数据在内核中并不是一下就能准备好,准备数据的过程是要消耗时间的;
3)与此同时,进程阻塞,不再做其他的操作,等待内核准备好数据(用户进程控制);
4)内核准备好数据后,数据从内核拷贝到了用户空间;
5)数据拷贝完成后,内核返回结果,进程解除阻塞。
因此,阻塞IO从根本上来看,内核准备数据阶段和数据从内核拷贝到进程内存地址这两个阶段都是阻塞的。
而用户进程是可以主动主动选择在内核空间准备数据的阶段的阻塞或者非阻塞状态的。
2、非阻塞IO(NIO)
Linux下,可以设置用户进程的Socket为非阻塞(non-blocking)状态。非阻塞IO是指当应用程序发起一个请求后,该请求同样会从用户空间传递到内核空间,在内核空间准备数据的过程中,用户的进程切换,去做其他的事情,与此同时,用户进程会不断的去判断内核空间是否已经把数据准备充分。这种形式的IO就是非阻塞IO。
非阻塞IO与阻塞IO最直观的区别是用户进程在等待内核空间准备数据的阶段,会切换进程去做其他的事情,而不是如阻塞IO一般一直等待内核空间的数据准备完成。
举个例子:下载一个文件,页面上会有一个进度条提示文件的下载进度。当文件未下载完成的时候,用户去完成其他的页面浏览等工作,但是在这个等待的过程中会重复性去查看这个进度条的状态,也就是在这个等待文件下载完成的过程中,会切换自己的工作内容,也会主动关注文件的下载情况。这种过程就类似非阻塞IO的过程。

以read为例:
1)用户进程发起read请求,进行recvfrom系统调用;
2)内核空间的数据还没有准备好,它不会阻塞用户进程,会立刻返回一个error,也就是说当发起一个read操作后,如果内核空间的数据没有准备完成,用户进程并不需要等待,而是马上就得到了一个结果;
3)用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它会再次发送read操作,而在发送read操作的其他时间,用户进程可以去做其他的事情;
4)一旦内核空间的数据准备好了,又再次收到了用户进程的system call,内核空间会将数据拷贝到了用户内存,然后返回。
所以,非阻塞IO的特点是用户进程在内核准备数据的阶段不会阻塞自己的进程,但是需要不断的主动询问数据准备好了没有。
3、异步IO(AIO)
Linux下的异步IO其实用的比较少,在实现上相对阻塞IO和非阻塞IO的难度大了很多。异步IO是指当应用程序发起一个请求后,不需要任何的等待,立刻去做其他的事情;另一方面该请求同样会从用户空间传递到内核空间,内核会立刻返回结果。然后在数据准备完成后,主动将数据拷贝到用户空间,等待完成后,给用户进程发出一个信号,告诉用户工作已经完成了。这种形式的IO就是异步IO。
举个例子:作为996的一份子,晚上下班后,精疲力竭,不想做饭,想先休息,等休息后再吃晚饭。于是,点了份外卖,留了电话,登记了地址,付了款,愉快的回家睡大觉。当外卖送到家门口的时候,外卖员给你打电话,你再出来取饭,就餐。这个过程就类似异步IO过程。

1)用户进程发起read操作之后,立刻就去做其它的事;
2)而另一方面,从kernel的角度,当它收到一个asynchronous read之后,首先内核会立刻返回,所以不会对用户进程产生任何阻塞;
3)然后,内核会准备数据,并等待数据准备完成后将数据拷贝到用户内存;
4)当这一切都完成之后,内核会给用户进程发送一个信号(signal),告诉它read操作完成了。
所以,异步IO的特点是不会阻塞用户进程,也不需要用户进程去关注内核何时准备好数据,是一种高性能的IO模式。
4、多路复用IO
异步IO虽然性能很高,但是实现困难,多路复用是非阻塞IO和异步IO之间的一种权衡,既能简单实现,又具有较好的性能指标。在Linux中多路复用IO实际上就是常接触的select、poll、epoll。多路复用可以满足单个进程(线程)同时监听多个IO的状态,当有已经准备好的IO后,会主动通知用户进程进程数据的获取工作。它的基本原理是select、poll、epoll函数不断的轮询所负责的Socket,当有已经准备好的Socket后,通知用户进程。
举个例子:作为996福报的一员,辛苦工作一天后,想下班后去犒赏一下自己,于是约上小伙伴一起去餐厅吃大餐。到达餐厅后,在餐厅的自助点单系统上下单,厨房做完菜后,会在餐厅系统上标注什么菜已经做好,并播报给服务员后上菜。在这个案例中,餐厅系统相当于是select、poll、epoll程序,轮询监听我们的餐品是否做好,服务员只需要关注自己收到的播报信息来进行上菜。这个过程就类似多路复用IO过程。

1)当用户进程调用了select,那么整个进程会被阻塞;
2)而同时,内核(kernel)会监视所有select负责的socket;
3)当任何一个socket中的数据准备好了,select就会返回;
4)这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符中的任意一个进入读就绪状态,select()函数就可以返回。
与阻塞IO相比,实际是多了一次select的system call (recvfrom)操作,如果只是很少的连接进行IO操作,因为多了一次操作,此时的多路复用IO性能可能比阻塞IO还要差,但是多路复用IO在请求较多的时候,往往性能好于阻塞IO,因为它可以同时处理多个连接,而且只需要关注已经准备好的连接。
另外,select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
5、小结
1、同步IO(synchronous IO)和异步IO(asynchronous IO)的区别
同步IO和非同步IO的判断基础是进行”IO operation”的时候是否会阻塞用户进程,也就是说在内核空间准备数据和拷贝数据的过程是否会阻塞用户进程。同步IO因为没有收到内核的通知会阻塞,而异步IO因为已经收到了内核的通知不会阻塞。因此,之前所述的阻塞IO,非阻塞IO,多路复用IO都属于同步IO。
这里有一点需要提醒:非阻塞IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这段时间内,进程是被block的。而异步IO则不一样,当进程发起IO 操作之后,内核会直接返回,进程不需要再做任何工作,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
2、阻塞(blocking)和非阻塞(non-blocking)的区别
调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO不会阻塞用户进程,而是在内核(kernel)准备数据的情况下会立刻返回,但是会不断的去check数据的状态。
3、异步IO和非阻塞IO的区别
非阻塞IO是用户进程的表现,虽然进程不会被block,但是需要进程去主动的check内核数据准备是否完成,当数据准备完成以后,需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。
异步IO则完全不同,它不需要关注内核数据的准备情况,也不需要主动的去拷贝数据,只需要关注内核在数据完成后的信号。
因作者能力有限,文中可能出现描述不清的地方,不足之处请指出!希望与你一起共进步^




