【IO模型系列】一、同步、异步、阻塞、非阻塞

1,061 阅读6分钟

前言

  想象我们正在做一个聊天服务器,我们对一个socket执行read操作,会返回另一个端对这个socket的写入数据。上层用户看来, read是一个简单的系统调用,但是对底层操作系统来说却是一个复杂的过程。从等待数据在网络上就绪,到把网卡上的就绪数据拷贝到内核recv buffer,最后把内核buffer的就绪数据拷贝到用户进程,这是这个read系统调用在底层操作系统执行的过程。
我们可以把read系统调用的过程简单的分为两个阶段:

  1. 等待数据就绪(步骤一/阶段一)

想象你在代码里调用了System.in 终端等待用户输入。在网络世界里,就是等待客户端往这个socket里面写入数据

  1. 把就绪的数据从内核空间拷贝到用户的进程空间(步骤二/阶段二)

当客户端write数据之后,服务端的网卡最先收到这个数据,然后调用中断通知操作系统取走,操作系统取走之后把数据拷贝到服务端的进程

针对上面两个步骤的阻塞、非阻塞以及回调,就有了下面五种IO模型。

当我们谈论阻塞和非阻塞的时候,其实指的是需不需要经历阶段一:等待数据就绪

五种IO模型

BIO Blocking IO Model 阻塞IO

BIO是最常用的IO模型,当我们调用read时,我们的线程会被阻塞,直到数据就绪并且内核把数据拷贝给了我们的进程。 BIO recvfrom是我们执行read时,底层的系统调用。 每次调用read都要阻塞,这样效率很低,能不能不阻塞?可以!我们接着看下面的NIO。

NIO Nonblocking IO Model 非阻塞IO

当使用NIO时,执行read操作,会立马返回。 从上图可以看到,前三次系统调用立马返回了,由于没有数据准备所以返回的是EWOULDBLOCK的错误,第四次调用,由于数据已经就绪,那么在内核把数据拷贝到用户进程后,用户线程返回。 在数据准备好之前,应用一直轮询看数据是否准备好,这通常比较浪费CPU。

IO Multiplexing Model IO多路复用

IO多路复用是当前比较主流的高性能IO网络模型,像Netty,Nginx,Redis等的底层网络模型都是基于IO多路复用。

  如上图所示,我们调用select(或者其它pollepoll)而不是直接read。这个调用同样会被阻塞,直到我们注册在select中fd事件被触发才会返回。然后我们根据返回的触发事件,去做相应的IO操作,比如read操作。这个时候的read操作,数据已经准备好,所以不会阻塞。
  表面上看,IO多路复用这种方式虽然用了两个系统调用,但是它可以让我们在一个线程中去监听多个fd。试想下,在前面的BIO中,我们作为一个服务器,要处理n个客户端连接的读写请求,为了不让一个客户端的处理阻塞其它客户端的响应,我们需要每个线程对应一个客户端连接。然而如果是IO多路复用,对于一个select我们可以在上面监听多个客户端连接,这也就意味着我们可以用一个线程处理多个客户端连接。
当然在客户端连接数量不多的情况下,是看不出差别的,甚至BIO性能还要优于IO多路复用,但是当连接数量上来,如果是BIO1:1的线程方式(一个线程对应一个客户端连接),那么线程的数量将非常多。线程是要占用资源的(在java中一般是分配512K到4M的内存),线程数量太多会导致应用OOM,并且线程间的频繁切换对CPU来说也是一项巨大的负担。
  其实前人已经讨论过BIO的这些问题了,并把它称为C10KC100K问题,有兴趣的可以搜索下。

Signal-Driven IO Model信号驱动IO模型

在信号驱动IO中,我们通过sigaction往感兴趣的fd上注册回调函数,这个系统调用会立即返回,这样在主线程中我们可以继续处理业务逻辑。当数据准备好了的时候,系统会回调我们刚才注册的handler,这样我们可以在handler中非阻塞的读取到达的数据。
信号驱动IO的优势是我们既不需要阻塞着等待数据,也不需要轮询去查看数据是否就绪,我们只需要事先注册一个回调函数,然后就可以干自己的事情了。

Asynchronous IO Model 异步IO

异步IO中,aio_read接受fd,buffer,handler等参数,一经调用,立马返回。当数据准备好并且已经从内核复制到用户进程(在这里就是刚刚的buffer参数)之后,我们的回调函数handler被触发。
可以看到异步IO和信号驱动IO还是非常类似的,但是唯一一个同样也是非常重要的不同就是信号驱动IO是在数据就绪(步骤一完成 就会被触发,触发后读取还是需要内核把数据拷贝到用户进程,而异步IO则是在数据就绪(步骤一)+ 数据从内核拷贝到用户进程(步骤二 都完成后才会被触发,触发时读取操作已经完成。

IO模型比较与总结

从上图可以看出前四种IO模型主要是在第一阶段不同,在阶段二前四种IO模型都是相同的。然而异步IO在阶段一和阶段二都和前面四种IO模型不同。

异步vs同步

在POSIX 标准中,同步和异步是这样定义的:

  • 同步I/O操作会阻塞请求进程直到I/O操作完成(阶段一和阶段二都完成)
  • 异步I/O操作不会阻塞请求进程

如果用上面这个定义的话,那么前面的四种IO模型(BIO\NIO\IO Mutiplexing\Signal-Driven IO)都是同步I/O,只有Asynchronous IO Model 是异步I/O

后记

这篇文章只是IO模型系列的开胃菜,后面会陆续更新select\poll\epoll以及Java中的具体BIO、NIO、IO多路复用的案例。
另外,这篇文章多是来源于《Unix Network Programming》第三版 6.2 IO模型章节。假期之前也一直搞不懂IO多路复用与各种IO模型的关系,找网上的博客,质量也是参差不齐,互相抄袭,直到看到了UNP这本书之后,才豁然开朗。
在这里和各位读者同时也和自己说一句:有时间多读读一手资料,从源头学习知识,减少信息的失真。
当然我这篇博客一点都不水哈~~

REF

参考资料:

  1. 《unix network programming》第三版 6.2IO模型

文章中图片与排版多是来源于此章节

  1. 《The Linux Programming Interface》 63 Alternative I/O Models