概述
本文从I/O网络模型出发,介绍目前主流的几种网络模型,然后对同步阻塞I/O、同步非阻塞I/O、I/O多路复用的机制和流程做了详细的阐述,最后通过一个生活中的例子加深对这3中网络模型机制的理解。
I/O操作
网络IO的本质是socket的读取,socket在linux中被抽象为流,IO操作可以理解为对流的操作。为了操作系统的安全性等考虑,进程是无法直接操作I/O设备的,其必须通过系统调用请求内核来协助完成I/O动作,而内核会为每个I/O设备维护一个buffer。整个请求过程可以概括为:用户进程发起请求,内核接受到请求后,从I/O设备中获取数据到buffer中,再将buffer中的数据copy到用户进程的地址空间,该用户进程获取到数据后再响应客户端。如下图所示:
在整个请求过程中,数据从IO设备输入至kernel buffer需要时间,而从kernel buffer复制到用户进程也需要时间(从IO设备到kernel比从kernel到process需要花更多的时间)。因此根据在这两段时间内等待方式的不同,I/O动作可以分为以下五种模式:
- 阻塞I/O (Blocking I/O)
- 非阻塞I/O (Non-Blocking I/O)
- I/O复用(I/O Multiplexing)
- 信号驱动的I/O (Signal Driven I/O)
- 异步I/O (Asynchrnous I/O)
这里为了更好的理解I/O复用,主要前3种I/O网络模型
同步阻塞I/O
同步阻塞I/O流程图如下:
当用户进程调用了recv()/recvfrom()这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。第二个阶段:当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,同步阻塞(blocking IO)的特点就是在IO执行的两个阶段都被block了。
同步非阻塞I/O
同步非阻塞I/O流程图如下:
非阻塞IO也会进行recvform系统调用,检查数据是否准备好。非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
所以,同步阻塞(nonblocking IO)的特点就是在IO执行的第1个阶段没有被block,第2个阶段被block了。
I/O多路复用
I/O多路复用的基本原理是:select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。实现单个process就可以同时处理多个网络连接的IO。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
I/O多路复用的流程图如下:
I/O多路复用和阻塞I/O类似,不同的是这里使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。I/O多路复用用户进程阻塞的不是recvfrom,而是select/epoll。但是,用select的优势在于它可以同时处理多个connection。(select/epoll的详细原理可以参考大话 Select、Poll、Epoll)
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
场景举例
为了更好的理解以上3个I/O 模型,我们举个生活中的例子。前段时间踢球把膝盖扭了,需要去医院拍核磁共振,然后让专家看看到底如何恢复,我们就拿这个做为例子来讲解。
首先对齐几个概念
- 病人:看作是用户进程
- 专家:看作是内核进程
- 核磁共振片子:I/O请求中的数据
- 医院:看作操作系统,可以调度专家来处理病人的请求
拍过核磁共振的兄弟都知道,片子一般需要3个工作日才能出来。然后等片子出来后,去医院取完片子再看专家,一般一个病人,专家花10-20分钟就处理完了。对比I/O操作的2个阶段,等待数据接收 <--> 等待片子出来,数据从内核复制到用户态 <--> 取片子后到专家处咨询。然后可以把专家看病看作出是数据处理。
有了以上的概念后,我们再看3个I/O模型分别是个什么情况(为了更好的说明,我们把拍片时间缩短1小时)
同步阻塞I/O
病人去医院找到专家,然后拍核磁共振,然后干等,此时这个专家无法再处理其他病人的请求,医院想要接待更多病人,必须找更多的专家。这个时候,对于医院,一个专家处理一个病人需要 1小时 + 10分钟。对于病人,在这1小时内,只能傻傻等待,做不了其他事。
同步非阻塞I/O
和同步阻塞I/O不同的是,病人拍完片后就可以立即回去,不需要在医院干等1小时。这个时候对于病人来说,在这个1小时中可以去干其他事,但是病人必须每隔一段时间来问专家片子是不是已经ready(因为片子随时可能准备好,并非严格1小时)。这就类似轮询机制。所以同步非阻塞I/O机制收益的是病人,理解此机制的主要视角是从应用程序出发。
I/O多路复用
以上两种场景,医院想要接待更多的病人,只能通过增加专家人数。当病人数量不多的情况下,这种模式没问题。但是当病人数不断增多后,医院的处理能力就跟不上了。这时候,医院就想出了一种机制:将核磁共振拍摄单独拎出来,有专门的医生处理(简称片子医生)。每个病人拍完片子后都会记录下来,这个片子医生会定期去看哪个病人的片子已经出来(select轮询)/ 医院建立了erp系统,一旦某个病人的片子出来,就会通知片子医生,同时通知到对应的病人(epoll)。从医院角度看,这样的机制下,一个专家 + 一个片子医生就可以处理多个病人(拍片子的时间远远少于片子产出的时间)。当然医院为了达到更大的规模,还是可以招聘更多的专家,多线程和IO多路复用并不冲突。
所以以上机制的一个核心思想:将集中(中央)处理变为分散(分布式)处理。原本是一个专家负责 拍片+诊断,现在变为拍片医生+诊断专家,将耗时的I/O等待剥离出来,实现了高吞吐量。