网络IO: 同步、异步、阻塞、非阻塞、IO复用

2015-08-27 Li Shuai 更多博文 » 博客 » GitHub »

网络IO 技术 网络编程 Tornado

原文链接 https://cyrusin.github.io/2015/08/27/TCPIO-20150827/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


同步IO

Linux IO的两阶段

同步, 异步, 阻塞, 非阻塞, 是网络IO中经常被提到的概念, 刚接触Tornado服务器的时候也知道Tornado是异步非阻塞的高性能web服务器, 直到最近看了一些资料才对同步异步的概念有了一些自己的想法。

HTTP协议是构建在TCP协议上的, TCP通信的底层本质上是socket的IO, 在Linux上, 以读socket为例, 数据首先到达的是内核缓冲区, 其次才会从内核缓冲区拷贝到用户进程, 所以负责通信的进程去读写socket的时候(也即recvfrom调用), 一般是两个阶段:

  1. 等待数据准备好, 此时数据暂存在内核缓冲区
  2. 数据准备好, 从内核缓冲区拷贝到用户进程

以上的两阶段就是进程调recvfrom读socket的过程, 而四种不同的IO的提法, 实质上是根据这两阶段的过程中进程所处的不同状态来区分的。

阻塞式IO

对进程而言, 就绪(Ready)、运行(Running)、阻塞(Blocked)是三种基本状态, 阻塞一般意味着等待某些条件或资源, IO等待就是一种原因, 所以阻塞常常是被动的, 阻塞的进程是无法被直接调度运行的, 即使CPU空闲也无法去拥有CPU, 无法获取CPU也就意味着不能做等待之外的任何事情, 只有等待的条件满足, 阻塞的进程才会转为就绪态, 等待获取CPU调度运行。

结合IO的两个阶段来看, 当应用进程在网络IO中发起recvfrom的系统调用的时候, kernel开始进入准备数据阶段, 此时进程由于IO等待进入阻塞状态。直到数据准备好且从内核缓冲区拷贝到用户进程里, 用户进程才解除阻塞状态恢复就绪态, 所以说, 阻塞式IO的两个阶段用户进程一直处于阻塞状态, 除了等待不能干其他的。

非阻塞式IO

相对于阻塞式IO, 非阻塞式IO在发起recvfrom的系统调用的时候, 若此时数据还未到达, 那么recvfrom的系统调用就会返回一个错误信息(EWOULDBLOCK), 有了这个错误信息, 用户进程知道内核还没有把数据准备好, 就可以通过不断轮询的方式一直去问内核:数据准备好了没? 每次都返回这个错误信息, 直到内核里的数据准备好了, 返回OK给用户进程。

注意: 用户进程不断轮询的时候, 是完全没有阻塞的, 这也就是称之为非阻塞IO的原因。 然后就可以进入第二个阶段, 内核把数据拷贝到用户进程里, 这个阶段, 进程阻塞了。

简而言之, 非阻塞式IO的第一阶段, 用户进程是不阻塞的, 而在数据拷贝阶段, 用户进程才是阻塞的。

IO复用

之前讲的阻塞式IO、非阻塞式IO都是针对单次IO单个socket来讲, 但是在实际应用中, web服务器要处理的是一个多连接问题, 也就是用户进程绑定了多个socket对象以供读写(同一个时刻, 多个连接请求到达, 三次握手之后, 加入一个连接成功的队列, 每次执行accept()调用之后, 从建立连接的队列中取出一个, 生成一个新的socket对象用于和客户端通信, 服务器进程面对着多个client socket要读写), 此时, 为了提高性能, 引入了select、epoll, 称为IO复用, 或者叫event driving式的IO。这种IO复用技术其实就是在调用recvfrom之前, 使进程阻塞在select/epoll这个系统调用上, 之所以先加这么个调用, 就是要更好的处理多连接的情况, select/epoll可以看作是可以很好的管理多个待读写的socket的东东, 当其中有可读的socket的时候, 可以唤醒用户进程来recvfrom, 也就是执行之前介绍的IO两阶段, 由于此时socket的数据已经准备好, 实际内核可以直接进行数据拷贝。这就是事件驱动的IO。

IO复用的技术很适合web server这种需要读写多socket的情况。但是很明显这种IO方式下, 进程也难免阻塞, 先是阻塞在select/epoll上, 后又阻塞在内核给自己拷贝数据的过程中。

之前介绍了三种IO的方式, 其共同点显而易见: 当内核中的数据准备好后, 用户进程真正去执行recvfrom操作也就是内核给自己拷贝数据的时候, 用户进程都是阻塞状态, recvfrom调用不返回, 进程就无法执行其他操作。这就是同步的IO。

异步IO

Asynchronous IO

Linux下的异步IO是aio_read这个系统调用, 当进程发起aio_read这个请求的时候, 不管数据有没有准备好, 这个调用都可以立即返回, 这样用户进程可以完全不care了, 一切交给内核就行了, 他可以继续干其他的。而内核接到aio_read的请求之后, 知道进程要做异步IO, 就会自己等待数据准备好, 然后自己把数据拷贝到用户进程, 完事儿发个singal给用户进程就OK了, IO的两个阶段里, 用户进程完全没被IO的系统调用阻塞掉。

总结

  1. 同步IO异步IO的主要区别就在于用户进程在进行真正的IO操作时的状态, 此时阻塞是同步, 此时非阻塞则为异步。
  2. 阻塞IO非阻塞IO的主要区别在于用户进程在等待内核准备数据时的状态, 此时阻塞是阻塞式IO, 此时非阻塞是非阻塞式IO。