旧话重提 -- 同步、异步、阻塞、非阻塞



前言

本文以大雄妈妈喊他写作业为例子展开讨论。

image-20221010171258100

大雄写作业

从大雄写作业开始–同步、异步

话说某天,大雄的妈妈又喊他写作业了。(调用)

此时,大雄会有几种反应。

  1. 好的,我立马。 (同步调用)
  2. 我待会再去。 (异步调用)

异步本质上是一种延迟过程调用。

大雄的妈妈如何知道作业做完了没有–阻塞、非阻塞

  1. 大雄的妈妈坐在旁边等。 (阻塞)
  2. 大雄的妈妈每隔一段时间过来看一下。 (非阻塞)

大雄的妈妈如何知道作业做完了没有–完成函数

我们看出来了,上面两种方式都不是很好,其实大雄的妈妈只需要告诉大雄,你做完作业来通知我。
这个做完并通知的函数即完成函数


在现代分时操作系统中讨论

在现代分时操作系统中谈IO

以上例子对于字面解释来说是正确的,但是在IO的情况下有些容易被忽视的细节:
IO本质上是异步的
例如在调用read write等,他们所作的事情不过是将io任务送到io任务队列中(具体底层实现不一定是队列,这里只是为了方面描述),等待进一步处理。
如果fd处于阻塞模式,将通过事件 信号等方式等待 io操作完成,反之亦然。

对于大多数上层开发者来说,他们只关心到:
大雄妈妈喊大雄写作业这一层面,而不具体去关心大雄到底是如何写作业。
如果放在现代分时操作系统层面来说,得到结果就是大雄可能就没有亲自去做过作业

为何要加现代分时几字,在原始的系统或者实时系统中 IO调用就是一头摸到黑,直接去读写硬件的IO端口,这种就不作讨论。

在在现代分时操作系统中谈完成端口

完成端口的首要条件就是异步(要是同步的,函数调用返回的时候就知道结果了),根据之前的说法,IO本质上是异步的,也就是说,无论哪个系统都是可以写出完成端口的,区别也仅在于内核封装以系统调用的形式提供,还是由上层(应用层)封装以库接口的形式提供。

上层封装又会出现各种开销等问题,肯定是不如内核封装来的好。

在此简单聊几句 windows上原装的完成端口:
完成端口的GetQueuedCompletionStatus内部是一个带锁队列。
其内存放着已经完成的IO请求及附属结构。每次调用时都会弹出队列顶部的I/O completion packet
这个东西并不复杂,只是windows完成端口(IOCP)还存在一些棱模两可(缺乏相关文档解释说明)的东西,用的时候很坑。

完成端口与多路复用

从浅显的层面说明,注意下文提及的系统方和调用方均位于内核层
windows完成端口:
系统方 : IO操作完成时(包括拷贝),加入完成队列
调用方 : 从完成队列取出I/O completion packet并返回,没有时等待

多路复用:

  1. select、poll
    系统方 : 能IO操作时,设置相关标识
    调用方 : 轮询所有fd的IO标识,碰到相关标识为相关事件时返回,没有时等待

  2. epoll
    系统方 : 能IO操作时,加入就绪队列
    调用方 : 取回就绪队列内容,没有时等待

小结:
从效率上说,完成端口直接往提供的 buffer 中写入数据,要比多路复用少一次系统调用,
代价是灵活性以及buffer开销(因为需要预先分配一个足够大的buffer,否则得不偿失)

具体可参见 WSARecv 函数 (winsock2.h) - Win32 apps | Microsoft Learn 有关 WSAEMSGSIZE 的说明

同时在多路复用中,epoll 又相对于 select、poll 少了一个轮询过程。


总结

关键是要明白,所谓的 同步、异步、阻塞、非阻塞到底是在哪个层面去说的,否则容易出现理解偏差。


说明

在上次讨论本话题时,是对着内核源码多角度分析的,而本文仅凭回忆及一些片面的参考资料作出。
这也导致了本文描述的不够细致,难免出现错误,如果有什么错误欢迎留言指出。


参考