特点
上面的图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。(select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO 多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的进程其实是一直被block的。只不过进程是被select这个函数block,而不是被socket IO给block。所以IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。
在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源。
虽然多路复用产生的效果,完全可以由用户态去遍历文件描述符并调用其非阻塞的 read 函数实现。但是多路复用快的原因在于,操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,变成了一次系统调用 + 内核层遍历这些文件描述符。
I/O多路复用的主要应用场景如下:
- 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。
- 服务器需要同时处理多种网络协议的套接字。
阻塞IO、非阻塞IO、IO多路复用的异同
在用户进程进行系统调用的时候,他们在等待数据到来的时候,处理的方式不一样。有直接等待,轮询,select、poll或epoll轮询这三种方式。
IO交互两个阶段过程:
- 第一个阶段有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。
- 第二个阶段都是阻塞的。
从整个IO过程来看,他们都是顺序执行的,因此可以归为同步模型(synchronous),都是进程主动等待且向内核检查状态。
同时,高并发的程序一般使用同步非阻塞方式而非多线程 + 同步阻塞方式。
信号驱动IO模型(signal blocking I/O)
使用信号,让内核在文件描述符就绪的时候使用 SIGIO 信号来通知我们。我们将这种模式称为信号驱动 I/O 模式。
允许Socket使用信号驱动 I/O ,还要注册一个 SIGIO 的处理函数,这时的系统调用将会立即返回。然后我们的程序可以继续做其他的事情,当数据就绪时,进程收到系统发送一个 SIGIO 信号,可以在信号处理函数中调用IO操作函数处理数据。
信号驱动IO在实际中并不常用。
异步IO模型(asynchronous I/O)
场景描述
女友即不想逛街,又觉得餐厅太吵了,因此,决定回家好好休息一下。于是我们叫外卖,打个电话点餐,然后我和女友可以在家好好休息一下了,当饭好了,送货员送到家里来。这就是典型的异步,只需要打个电话说一下,然后可以做自己的事情,饭好了就送来。
网络模型
相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO的两个阶段,进程都是非阻塞的。
Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。
用户进程发起aio_read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal或执行一个基于线程的回调函数来完成这次 IO 处理过程,告诉它read操作完成了。
在 Linux 中,通知的方式是 “信号”:
- 如果这个进程正在用户态忙着做别的事(例如在计算两个矩阵的乘积),那就强行打断,调用事先注册的信号处理函数,这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然闯进来的,因此跟中断处理程序一样,有很多事情是不能做的,因此保险起见,一般是把事件 “登记” 一下放进队列,然后返回该进程原来在做的事。
- 如果这个进程正在内核态忙着做别的事,例如以同步阻塞方式读写磁盘,那就只好把这个通知挂起来了,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
- 如果这个进程现在被挂起了,例如,无事可做 sleep 了,那就把这个进程唤醒,下次有 CPU 空闲的时候,就会调度到这个进程,触发信号通知。
Linux下的异步IO其实用得很少 ,著名的高性能网络框架netty 5.0版本被废弃的原因便是:使用异步IO提升效率,增加了复杂性,却并且没有显示出明显的性能优势。
异步IO的两个阶段:
第一阶段:当在异步 I/O 模型下时,用户进程如果想进行 I/O 操作,只需进行系统调用,告知内核要进行 I/O 操作,此时内核会马上返回, 用户进程 就可以去处理其他的逻辑了 。
第二阶段:当内核完成所有的 I/O 操作和数据拷贝后,内核将通知我们的程序,此时数据已经在用户空间了,可以对数据进行处理了。
非阻塞IO和异步IO的区别
在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。
而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。
总结
前四种I/O模型都是同步I/O操作,他们的区别在于第一阶段,而他们的第二阶段是一样的。在数据从内核复制到应用缓冲区期间(用户空间),进程阻塞于系统调用。
相反,异步I/O模型在这等待数据和接收数据的这两个阶段里面都是非阻塞的,可以处理其他的逻辑,用户进程将整个IO操作交由内核完成,内核完成后会发送通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。