旧话重提 -- 同步、异步、阻塞、非阻塞
前言
本文以大雄妈妈喊他写作业为例子展开讨论。
大雄写作业
从大雄写作业开始–同步、异步
话说某天,大雄的妈妈又喊他写作业了。(调用)
此时,大雄会有几种反应。
- 好的,我立马
做
。 (同步调用) - 我待会再去
做
。 (异步调用)
异步本质上是一种延迟过程调用。
大雄的妈妈如何知道作业做完了没有–阻塞、非阻塞
- 大雄的妈妈坐在旁边等。 (阻塞)
- 大雄的妈妈每隔一段时间过来看一下。 (非阻塞)
大雄的妈妈如何知道作业做完了没有–完成函数
我们看出来了,上面两种方式都不是很好,其实大雄的妈妈只需要告诉大雄,你做完作业来通知我。
这个做完并通知的函数即完成函数
。
在现代分时操作系统中讨论
在现代分时操作系统中谈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
并返回,没有时等待
多路复用:
select、poll
系统方 : 能IO操作时,设置相关标识
调用方 : 轮询所有fd的IO标识,碰到相关标识为相关事件时返回,没有时等待epoll
系统方 : 能IO操作时,加入就绪队列
调用方 : 取回就绪队列内容,没有时等待
小结:
从效率上说,完成端口直接往提供的 buffer 中写入数据,要比多路复用少一次系统调用,
代价是灵活性以及buffer开销(因为需要预先分配一个足够大的buffer,否则得不偿失)
具体可参见 WSARecv 函数 (winsock2.h) - Win32 apps | Microsoft Learn 有关 WSAEMSGSIZE 的说明
同时在多路复用中,epoll 又相对于 select、poll 少了一个轮询过程。
总结
关键是要明白,所谓的 同步、异步、阻塞、非阻塞到底是在哪个层面去说的,否则容易出现理解偏差。
说明
在上次讨论本话题时,是对着内核源码多角度分析的,而本文仅凭回忆及一些片面的参考资料作出。
这也导致了本文描述的不够细致,难免出现错误,如果有什么错误欢迎留言指出。