五种 IO 模型
IO 即为 Input、Output,对计算机来说,我们使用键盘鼠标给计算机指令就是一种输入,计算机将我们键盘输入的文字显示到显示器即是一种输出。或者写博客时将计算机从键盘接收到的文字信息发送到平台上即为输出,当我们查阅资料,打开某一篇博客时对计算机来说也可理解为输入。
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间
。
上面所说的 IO 数据通常由 OS 缓存到内核中,而后拷贝到用户空间中,因此 Input 和 Output 的流程可以简化为:
外部输入---》OS 内核空间---》用户空间---》用户进程
用户进程输入---》用户空间---》OS 内核空间---》外部
阻塞 IO
用户进程在获取 IO 数据时,最简单的方式就是串行,即阻塞 IO(BIO)。
例子:好比我们在代码开发完成后,提交 merge 请求,提交后我们就到 commitor 工位,发现 commitor 不在工位,我们一直等到他回工位,然后看着他检视代码,直到他检视完成后我们才放心的做其他事。
jdk1.4 前网络连接都是采用 BIO 的模式,当服务端 Socket 接收到请求后,不能再接收处理其他请求,就只能把这个请求处理完后才能接收其他请求,可以简单的理解为串行。虽然该模型简单,但是我们开发完代码后一直等着 commitor 检视还是有偷懒的嫌疑,可能会被说效率低,从上图中可以看出,我们不一定要一直守在 commitor 工位旁边等他回来,所以就出现了非阻塞 IO。
非阻塞 IO
鉴于 BIO 有串行效率低的缺点,我们做了优化,就是不再一直等待数据准备好,而是用户进程主动多次询问。
例子:好比我们在代码开发完成后,提交 merge 请求,提交后我们看到 commitor 不在工位就知道还不能合代码,此时我们不会守在 commiter 工位旁边等待,而是是不是去他工位看看回来没,直到某一次去看他已经回到工位,才守到他工位旁边,看着他检视代码,直到他检视完成后我们才放心的做其他事。
和 BIO 相比,NIO 内核会立刻返回,返回后应用进程可以做其他事,即应用进程第一阶段不是阻塞的,但是需要主动不断去询问内核数据是否准备好;第二个阶段仍然是阻塞的。NIO 虽然比 BIO 有所提升,但是还是需要应用进程不断去询问,因此产生了 IO 复用模型。
IO 复用
IO 复用模型去掉了应用进程主动询问的过程,而是把数据是否准备好交给了内核处理,内核通过 select/poll 去遍历检查数据是否准备好,或者通过 epoll 回调方式处理。
例子:我们此次需要提交前台和后台的功能代码,由多个 commitor 负责检视,commitor 检视前由秘书记录由哪些代码合入的请求,我们提交 merge 后,发现 commitor 不在,秘书就先记录,我们在 commitor 工位一直等着,秘书遍历去各 commitor 工位看是否回来,一旦看到某个 commitor 在工位,就通知此 commitor 相关的 merge 代码的人来检视代码,程序员看着他检视代码,直到他检视完成后我们才放心的做其他事。
IO 多路复用(IO multiplexing) ,也称事件驱动 IO(event -driven IO),就是在单个线程里同时监控多个套接字,通过 select 或 poll 轮询所负责的所有 socket,当某个 socket 有数据到达了, 就通知用户进程 。 IO 复用同非阻塞 IO 本质一样,不过利用了新的 select 系统调用,由内核来负责本来是请求进程该做的轮询操作。看似比非阻塞 IO 还多了一个系统调用开销,不过支持多路 IO 提高了效率。 进程先是阻塞在 select/poll 上,再是阻塞在读操作的第二个阶段上。
IO 复用主要有 select、poll、epoll 三类,select 通过数组记录应用进程请求,因此监听数量有限;poll 使用链表的方式优化了 select 的监听数量缺陷,epoll 为了减少内核多余的遍历调用,变主动位被动,通过回调实现,如下图:
epoll 相较于 select/poll,多了两次系统调用,其中 epoll_create 建立与内核的连接,epoll_ctl 注册事件,epoll_wait 阻塞用户进程,等待 IO 事件。
select、poll、epoll 区别如下:
IO 复用的缺点是应用进程请求后就会阻塞。
信号驱动 IO
信号驱动 IO 与 IO 多路复用最大的区别就在于,在 IO 执行的数据准备阶段 ,不会阻塞用户进程 。 如图所示:当用户进程需要等待数据的时候,会向内核发送一个信号,告诉内核我要什么数据,然后用户进程 就继续做别的事情去了,而当内核中的数据准备好之后,内核立马发给用户进程一个信号,说 ”数据准备好了, 快来查收“,用户进程收到信号之后,立马调用 recvfrom 去查收数据 。
例子:我们提交代码合入请求后,这次仍然有秘书记录 merge 请求,我们看到 commitor 不在工位就马上回去干自己的活了,一旦 commitor 回到工位,秘书就通知我去 commitor 工位检视代码,我就在他旁边看着他检视代码,直到他检视完成后我才放心的做其他事。
总的就一句话:信号驱动优化了第一阶段阻塞的情况,但是在程序员守着检视代码的阶段还是阻塞的。
异步 IO
AIO,异步 IO 真正实现了 IO 全流程(两个阶段)的非阻塞。用户进程发出系统调用后立即返回,内核等待数据准备完成,然后将数据拷贝到用户进程缓冲区,然后发送信号告诉用户进程 IO 操作执行完毕 (与 SIGIO 相比,一个是发送信号告诉用户进程数据准备完毕,一个是 IO 执行完毕)。
例子:我们提交代码合入请求后,我们看到 commitor 不在工位就马上回去干自己的活了,commitor 回到工位后看到我们提的 merge,就自己开始检视代码,当检视完成后,通知我说代码已经合入了,可以发版本了。
总结
最后总结一下,五种 IO 模型,从上到下应该是逐级改进的,总体分类如下图:
版权声明: 本文为 InfoQ 作者【懒AI患者】的原创文章。
原文链接:【http://xie.infoq.cn/article/42ab0970ce9aa69fc99bbbca2】。文章转载请联系作者。
评论