对简单操作模型的理解
堵塞与非堵塞
堵塞(blocking):
调用者(caller)必须等待操作完成才能返回。操作完成可能是异常、失败、返回空数据、返回不完整数据等。
非堵塞(non-blocking):
被调用者(call)立即返回,不会因为数据未到而等待。一般是指内核等待数据到达。
无论是堵塞或者非堵塞,都可能返回空的数据,都需要数据从内核缓存区到用户缓存区的拷贝时间。堵塞操作可能因为信号、异常而失败,也可能因为对方关闭连接返回空的数据。非堵塞操作会直接返回空的数据或者已经存在的(部分)数据,不会等待要求的数据全部到达内核后才返回。
同步与异步
同步模型(synchronous)
同步模型是指操作顺序执行,前续操作和后续操作顺序执行,后续操作发生时前序逻辑已经完成。
异步模型(asynchronous)
异步模型是指操作顺序非顺序执行,后续操作执行时前序操作还未完成,前序操作可能在后台处理,可能挂起。
同步与异步的主要区别是,在后续操作开始时前续操作是否真正完成,或者说在后续操作和前续操作之间是否能够执行其他的操作。
对简单模型的类比
生活中我们会在医院取药,利用该场景做一个类比。药单是操作的对象,类比fd;药是数据,类比data;取药是操作,类比recv;取药系统、药剂师(护士)等类比操作系统。
堵塞模型
你拿着药单去药剂室取药5盒,把药单交给药剂师发起取药的操作,药剂师并不立刻回应你。你在窗口一直等(堵塞),药剂师拿到药单发现手头没有对应的药,于是自己或者让护士去库房取,等在库房取回后交给你,此次取药堵塞操作完成。但是你发现药剂师只给了你三盒,于是你又重复取药2盒的操作,这个时候药剂师拿到药单发现手头就有2盒,于是立马给你,此次堵塞操作也完成。或者药剂师拿到药单之后,突然出现意外你被赶跑了,此次堵塞取药操也完成,虽然你没有拿到药,但是被告知出意外了,你自己看着办吧。
非堵塞模型
你又来取药,把药单交给药剂师发起取药操作,药剂师拿到药单发现手头没有对应的药,立马(非堵塞)告诉你没有药了,此次非堵塞取药操作完成。或者,药剂师手头正好有3盒立即给了你,此次非堵塞操作也完成。
可见,堵塞和非堵塞的重要区别是药剂师手头没有药时,他会让护士去取等回来再给你,还是立马告诉你没有。
同步模型
你再次来取药,无论是那种方式,你必须在取到药之间不能做其他任何的操作,即使你被堵塞在窗口等待药剂师去库房取药的时候。
异步模型
你最后一次取药,在把药单给药剂师之后和药拿到手之间,你可以干点别的事情,玩玩手机、听听音乐或者去其他窗口取别的药。
可见,同步与异步的区别在于你发起取药的操作和药到手之间,能不能干其他的事情。
同步堵塞
你在取药的过程中执行堵塞操作,直到药到手为止。
同步非堵塞
同步非堵塞几乎没什么用,你把药单给药剂师,但是药剂师手头没有药立马告诉你,你需要再次发起取药操作直到拿到药。这个就是轮询模式,还不如同步堵塞,因为浪费了药剂师(系统)不断回应你的时间。
异步堵塞
现实中几乎没用的模型,因为你总是等待取药,在这期间不能干别的,因为只有1个你。但是在计算机的世界中你会分身术(线程),你的分身可以去做别的事情。
异步非堵塞
异步非阻塞模型通常是(框架给)用户使用的模型,你去取药,把药单交给药剂师,然后不等待结果自己就走了,待药从库房拿到后药剂师再想法设法给你。
异步的几种接口
药剂师的想方设法
就是不同的表现形式,异步接口:
事件通知
医院很先进,你把药单给药剂师后,自己去干别的,等广播你的名字后,你再去取药。这个是事件通知。
回调
医院护士很多,你把药单给药剂师并且把自己喝多少药之类的也一并交代。等药取到后,药剂师派个小护士直接把药给你灌下去。这个是回调,需要先定义后续操作逻辑(喝药),再注册(在取药的时候告诉药剂师)。
占位符
你把药单给药剂师,药剂师给你的是药盒子,你以为拿到了真实的药,然后去进行打热水、打开盒子、拿药的一些列操作,等你真正喝药的时候,药会被药剂师偷偷摸摸的放入盒子里,这需要特殊的药盒子。
状态
类似事件通知,但不是直接通知你,而是有个大屏幕。当药剂师从库房拿到药时会在大屏幕打出你的名字,于是你再次跑到窗口拿到药。
如果你同时需要取药、打针两件事请,你分别在医药窗口和注射窗口提交了单据。当轮到你的时候,大屏幕只出现你的名字,你不得已只好挨个问哪个窗口准备好了(select)。
但是另外一个医院很先进,不仅打出你的名字还打出窗口号,于是你直接去已经准备好的窗口即可(epoll)。
Unix下的几种IO模型
说完简单的类比回到Unix平台。在Unix(linux)平台下有五种I/O模型:
- 堵塞I/O模型(blocking I/O)
- 非堵塞I/O模型(un-blocking I/O)
- I/O多路复用模型(select, poll, epoll)
- 信号驱动I/O模型(SIGIO)
- 异步I/O模型(posix aio_functions)
I/O操作一般分为两个阶段:
- 等待数据达到内核缓存区
- 将数据从内核拷贝到用户进程
我们用recvfrom
定义操作,并且将数据分为在内核缓冲区和用户进程缓冲区。
堵塞I/O模型
堵塞I/O模型中,先在内核中等待数据到来,再等待数据从内核拷贝到用户进程,两个阶段都被堵塞。下图是简单的一问一答堵塞型的服务器。
非堵塞I/O模型
非堵塞I/O模型中操作recvfrom
立即返回,为了得到完整数据需要不断的发起操作,这个就是轮询。recvfrom
实际上有两个作用,第一询问数据是否到来,第二得到数据。
I/O多路复用
I/O多路复用是同步还是异步比较模糊。像上面那样轮询一个fd效率比较底下,多路复用一次可以询问多个fd的数据是否到来。于是,将recvfrom
的两个作用拆分成2个函数ask和read,ask堵塞(非堵塞没有意义)的一次询问多个fd,任何一个fd的数据到达就返回,然后再发起非堵塞read操作。如果ask的返回不能确定哪个fd的数据准备好了,就是select模型;如果同时返回确定的已准备好数据的fd,就是epoll模型。
站在处理所有fd的角度,多路复用I/O依然是同步堵塞模型;但是站在每个fd的角度(每个IO操作的角度),处理单个fd的整个I/O操作时也处理了其他fd的I/O操作,因此是异步;而且每个fd都需要等待数据从内核拷贝到用户缓冲区,因此也是堵塞操作。所以一般认为I/O多路复用是异步堵塞模型。(通过把这种模型新起个个名字可以看出确实比较难以用堵塞/非堵塞、同步/异步来定义)
异步I/O
该异步I/O完全符合异步的要求,在调用时立即返回并且在数据到达后通过信号来通知进程。Unix类系统最常见的异步是回调,通过注册回调由内核在事件发生时调用,例如信号。在这里等于是把事件(数据到达)通过信号的方式通知用户让用户去处理了。需要注意异步I/O没有堵塞,发起操作后立即返回,当信号到来时数据已经被拷贝到用户缓冲区中了。
完整的I/O模型定义
- 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;
异步同步通过在一完整的I/O操作时是不是有其他I/O操作来判断。无论是等待数据到达内核,还是等待数据从内核缓冲区拷贝到用户缓冲区都是堵塞模型。
几种不同I/O模型的异同
简单总结:
- 异步非堵塞I/O模型,内核帮用户完成了整个数据的传送。
- 堵塞I/O模型,用户需要至少等待一个阶段的I/O传送。
- 异步模型,用户在完成一次I/O操作期间,可以进行其他的I/O操作。
(完)