IO模型

原文链接https://www.jianshu.com/p/486b0965c296

写在前面:缓存IO又称为标准IO,大多数文件系统的默认IO操作都是缓存IO。在LINUX的缓存IO机制中,操作系统会将IO的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先拷贝到操作系统内核的缓冲区中,然后才会从操作系统的内核缓冲区拷贝到应用程序的地址空间。

网络IO的本质是socket的读取,socket在Linux系统中被抽象为流,IO可以理解成对流的操作。对于一次IO访问,数据先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统的内核缓冲区拷贝到应用程序的地址空间。所以分为两步:

1.第一阶段 等待数据准备

2.第二阶段 将数据从内核拷贝到进程中

对于socket流而言:

第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区

第二步:把数据从内核缓冲区复制到应用程序缓冲区

网络IO模型分为:

同步模型:1.阻塞IO 2.非阻塞IO 3. 多路复用IO 4.信号驱动式IO

异步IO

信号驱动IO在实际中是不常用的。

以一个生动形象的例子来说明这四个概念。周末我和女友去逛街,中午饿了,我们准备去吃饭。周末人多,吃饭需要排队,我和女友有以下几种方案。

2.1 阻塞IO

我和女友点完餐后,不知道什么时候能做好,只好坐在餐厅里面等,直到做好,然后吃完才离开。女友本想还和我一起逛街的,但是不知道饭能什么时候做好,只好和我一起在餐厅等,而不能去逛街,直到吃完饭才能去逛街,中间等待做饭的时间浪费掉了。
这是典型的阻塞IO模型
 
网络模型::
同步阻塞IO是最常见的一个模型,也是最简单的模型。在LINUX中,默认情况下所有的socket都是blocking。阻塞就是进程被休息了,CPU去处理其他进程了。
在这个IO模型中,用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞,什么也不干,直到数据准备好,并且将数据从内核复制到应用进程,最后进程处理数据。在等待数据和将数据从内核缓冲区拷贝到应用程序地址空间两个阶段,整个进程都是被阻塞的,不能处理别的网络IO。调用应用程序处于一种不再消费CPU而只是简单等待响应的阶段,因此从处理的角度来看,这是非常有效的。在调用recv等时,发生在内核的等待数据和复制数据的过程,大致如下图所示

 

 同步阻塞IO流程描述

 
当用户进程调用了recv()/recvfrom()这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。在用户进程这边,整个进程会被阻塞。第二阶段:当内核一直等待到数据准备好了,它就会将数据从内核拷贝到用户地址空间,然后内核返回结果,用户进程解除阻塞状态。
阻塞IO的特点是:IO 执行的两个阶段都被block了
 
优点:1 能及时返回数据,无延迟  2.对内核开发者来说这是省事了
缺点:对于用户来说,处于等待就要付出性能上的代价
 
2.2同步阻塞IO
场景描述:我女友不甘心白白在这等,又想去逛商场,又担心饭好了。所以我们逛一会,回来询问服务员饭好了没有,来来回回好多次,饭都还没吃都快累死了啦。这就是非阻塞。需要不断的询问,是否准备好了。

网络模型:

同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式。在这种模型中,设备是以非阻塞的形式打开的。这意味着 IO 操作不会立即完成,read 操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN 或 EWOULDBLOCK)。
非阻塞的recvform系统调用被调用之后,进程并没有阻塞,内核会马上返回给进程,如果数据还没有准备好,此时会返回一个error。进程返回之后,可以干点别的事情,然后再重新发起recvform系统调用,重复以上过程。这个过程通常称为轮询。轮询检查内核数据,直到数据真被好,再拷贝数据给进程,进行数据处理。拷贝数据的整个过程,进程仍然是处于阻塞状态的。
再linux下,通过设置socket使其变为non-blocking。

 

当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
非阻塞的特点是:用户进程需要不断的主动询问内核是否准备好了数据
同阻塞方式相比:
优点:能够再等待任务完成的时间里干其他活(后台可以有多个任务同时执行)
缺点:任务的响应延迟大了,因为每隔一段时间采取轮询一次read操作,而任务可能再两次轮询之间的任意时间完成,这导致整体数据的吞吐量降低。
 
2.3 IO多路复用(select poll epoll)
与第二个方案差不多,餐厅安装了电子屏幕用来显示点餐的状态,这样我和女友逛街一会,回来就不用去询问服务员了,直接看电子屏幕就可以了。这样每个人的餐是否好了,都直接看电子屏幕就可以了,这就是典型的IO多路复用


select相对非阻塞的轮询区别在于:前者可以等待多个socket,能实现对多个IO端口进行监听,当其中的任何一个socket的数据准备好了,就能返回进行读操作,然后再将数据由内核拷贝到用户线程,当然,整个过程是阻塞的。
 
select poll与阻塞IO不同的是:select不是等到数据全部到达之后再处理,而是有了一部分数据就会调用用户进程来处理。,如何直到有一部分数据到达呢? 这个还是交给内核进行处理
I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用I/O操作函数。

对于多路复用,也就是轮询多个socket。多路复用既然可以处理多个IO,也就带来了新的问题,多个IO之间的顺序变得不确定了,当然也可以针对不同的编号。具体流程,如下图所示:

 

 

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。(select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。所以IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。

在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源,I/O多路复用的主要应用场景如下:

服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。

服务器需要同时处理多种网络协议的套接字。

了解了前面三种IO模式,在用户进程进行系统调用的时候,他们在等待数据到来的时候,处理的方式不一样,直接等待,轮询,select或poll轮询,两个阶段过程:

第一个阶段有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。

第二个阶段都是阻塞的。

从整个IO过程来看,他们都是顺序执行的,因此可以归为同步模型(synchronous)。都是进程主动等待且向内核检查状态。【此句很重要!!!】

高并发的程序一般使用同步非阻塞方式而非多线程 + 同步阻塞方式。要理解这一点,首先要扯到并发和并行的区别。比如去某部门办事需要依次去几个窗口,办事大厅里的人数就是并发数,而窗口个数就是并行度。也就是说并发数是指同时进行的任务数(如同时服务的 HTTP 请求),而并行数是可以同时工作的物理资源数量(如 CPU 核数)。通过合理调度任务的不同阶段,并发数可以远远大于并行度,这就是区区几个 CPU 可以支持上万个用户并发请求的奥秘。在这种高并发的情况下,为每个任务(用户请求)创建一个进程或线程的开销非常大。而同步非阻塞方式可以把多个 IO 请求丢到后台去,这就可以在一个进程里服务大量的并发 IO 请求

异步IO
女友不想逛街,又餐厅太吵了,回家好好休息一下。于是我们叫外卖,打个电话点餐,然后我和女友可以在家好好休息一下,饭好了送货员送到家里来。这就是典型的异步,只需要打个电话说一下,然后可以做自己的事情,饭好了就送来了。
相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知IO两个阶段,进程都是非阻塞的

Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。异步过程如下图所示:

 

 

 

用户进程发起aio_read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal或执行一个基于线程的回调函数来完成这次 IO 处理过程,告诉它read操作完成了。

五种IO模型总结#

3.1 blocking和non-blocking区别##

调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。

3.2 synchronous IO和asynchronous IO区别##

在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

An asynchronous I/O operation does not cause the requesting process to be blocked;

两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。

有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。

而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

各个IO Model的比较如图所示:

输入图片说明
输入图片说明

通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据



posted @ 2020-05-15 23:39  任仁人  阅读(158)  评论(0编辑  收藏  举报