Unix中的几种I/O模型

image

对简单操作模型的理解


堵塞与非堵塞

  • 堵塞(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操作一般分为两个阶段:

  1. 等待数据达到内核缓存区
  2. 将数据从内核拷贝到用户进程

我们用recvfrom定义操作,并且将数据分为在内核缓冲区和用户进程缓冲区。

堵塞I/O模型

image

堵塞I/O模型中,先在内核中等待数据到来,再等待数据从内核拷贝到用户进程,两个阶段都被堵塞。下图是简单的一问一答堵塞型的服务器。

image

非堵塞I/O模型

image

非堵塞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多路复用是异步堵塞模型。(通过把这种模型新起个个名字可以看出确实比较难以用堵塞/非堵塞、同步/异步来定义)

image

异步I/O

该异步I/O完全符合异步的要求,在调用时立即返回并且在数据到达后通过信号来通知进程。Unix类系统最常见的异步是回调,通过注册回调由内核在事件发生时调用,例如信号。在这里等于是把事件(数据到达)通过信号的方式通知用户让用户去处理了。需要注意异步I/O没有堵塞,发起操作后立即返回,当信号到来时数据已经被拷贝到用户缓冲区中了。

image

完整的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模型的异同


image

简单总结:

  1. 异步非堵塞I/O模型,内核帮用户完成了整个数据的传送。
  2. 堵塞I/O模型,用户需要至少等待一个阶段的I/O传送。
  3. 异步模型,用户在完成一次I/O操作期间,可以进行其他的I/O操作。

(完)