之前便简单使用过了 Boost.Asio 异步 I/O 库,一直很都好奇诸如 async_read() 之类的异步函数是如何来实现的,于是我就开始了疯狂地找资料。 从重新理解同步,异步,阻塞,非阻塞到重温 Linux 下5种经典的 I/O 模型,然后我找到了 Reactor,Proactor 这两个模式。往下读之前希望你已经认识了各种 I/O 模型。
想要真正实现一个异步操作,OS 必须提供给我们支持异步 I/O(由于异步主要用于解决 I/O 的性能问题,这里暂且只讨论异步 I/O 操作)的 API。
Windows平台为我们提供了 Proactor 模式的异步支持方式,Proactor 模式是什么?
这里我们假设有3个对象,用户进程 a,内核 b,异步读事件 A:
1.用户进程 a 通过内核 b 提供的 API 注册了一个异步读事件 A,同时提供自己的内存空间等参数。
2.内核 b 运行注册好的事件 A,这个异步读事件包括从端口将数据读到内存缓冲区,再将内存缓冲区的数据读到用户进程的内存空间中。
3.内核 b 完成事件 A 后通知用户进程 a,“你要的数据已经读到你的内存里了,你可以用了” ,这种方式通常是调用1里注册的回调函数来实现的。
这整个的过程便是 Proactor 模式了,它使得编写用户进程的程序员完全不用去理会整个异步过程,而只需要静静地等待结果并使用就好了。
而到了 Linux 平台上,似乎就没有这么幸运了,Linux 支持地只是非完全异步的 Reactor 模式,甚至更早的版本上连 Reactor 都不支持。为什么说是非完全异步(这个词是我自己胡乱造的,只是为了更好地说明问题)?先来看一看一个 Reactor 模式下的异步读事件是如何完成的。
4个对象,用户进程 a,内核 b,读事件 A及其 Socket 句柄:
1.用户进程 a 通过内核 b 提供的 API(如epoll_ctr()) 注册了一个 Socket。
2.内核检测到注册过的 Socket 对应的端口有数据可读时通知用户进程 a,"你要的数据已经来了,你可以从端口读取了"(注意和 Proactor 的区别),通常也是通过注册的回调函数来进行读取。
到这里你应该已经发现了2者的区别,Proactor 模式里,用户进程不需要关注数据读取的细节且可以去干别的事情,直接通过提前注册好的回调函数来使用就行了。这就好比你问别人借了一个东西,想让他送到你家来,但是你那段时间却想去干别的事,于是你提前告诉他,“我家的钥匙放在地毯下面,东西送过来了开门放到桌上就行”(回调函数)。所以整个过程包括内核等待数据的到来和内核将数据从内核缓冲区 copy 至用户内存空间用户进程都可以去做别的事,都是非阻塞进行的。
而在 Reactor 模式中,虽然在等待数据到来这一过程内核会帮你完成,但是将数据从端口搬运到用户的内存空间是需要用户来完成的,通常我们可以利用回调函数来完成第二个过程,而回调函数由谁来运行取决于具体实现了。还是用上面的例子,你问别人借了一个东西,想让他送到你家来,但是你那段时间仍然想去干别的事,于是你提前告诉他,“你尽管来吧,等你到的时候会有人拿的”(这个人可能就是指代跑回调函数的线程)。
讲到这里就可以解释一下之前我胡乱编造的名词“非完全异步”了,首先再简单解释一下异步:
将某个过程交于别人来完成,至于别人什么时候才能完成我们管不着也不想管,我们可以安心地干别的,只要完成的时候知会我一声(或者是做我提前安排好的其他事)就可以了。
在 Reactor 模式中,数据等待的过程很明显符合上面的定义,属于异步过程,而当数据到达后,用户进程又不得不自己来完成读取过程,而在 Linux 中,内核是不提供这一功能的。可能你会说:“找一个代理来完成读取的过程不就OK了吗?”,对的,这就是我下面要讲的,通过这种方式来实现从 Reactor 模式到 Proactor 模式的模拟,这也正是 Asio 库最终选择 Proactor 模式的原因,可以跨平台了(从 Windows 到 Linux)。
之后的内容纯属我的推测-。-,等我能力够了再去读 Asio 的源码,到时候回来再补
用过 Asio 库的都会很熟悉 io_service,想要支持异步操作就必须通过 io_service 实例的支持,那么它便是上面所说到的代理了。
io_service 用来管理所有的异步操作,在 Windows 平台上,它的任务可能就是在内核将数据 copy 到用户进程空间后将回调函数加入到任务队列中并按顺序执行这些回调函数。而在 Linux 平台上,它还需要完成再 Windows上本该属于内核的工作,也就是将数据 copy 到用户进程空间,然后再执行回调函数了。能力有限,先讲到这。。。