本文通过「情景再现」的方式解释 Linux 中的 IO 模型,这对于学习 Java 高并发 还是有些许帮助的。

网络 IO 模型有以下几类:

  • 阻塞 IO(bloking IO)
  • 非阻塞 IO(non-bloking IO)
  • 多路复用 IO(multiplexing IO)
  • 同步 IO(synchronous IO)
  • 异步 IO(asynchronous IO)
  • 信号驱动式 IO(signal-driven IO)

这里仅对以上 IO 模型做一个浅显的认识与说明,也算是自己在学习过程中的一个记录,若有错误或者描述不准确的地方,还请留言指出。

阻塞 IO(bloking IO)

  阻塞 IO 就是说:当我去银行办理业务的时候,前面有很多人,这时我需要排队,然后就一直等着前面的人办完业务后才轮到我(在等待的过程中不能去办其他的事情,比如去吃饭),最后我才能办理业务。IO 模型如下图所示:

image.png

  你可以看到 应用进程 在进行 系统调用 的时候,由于 内核 中的 数据报 还没有准备好,所以这时一直处于 等待数据 的状态,这种状态就对应我去银行办理业务时的 等待 情况。然后直到 数据报 准备好了,这时才会 将数据从内核复制到用户空间,最后返回给 应用进程

  所以对于这两个阶段(红色方框中的内容),由于需要 等待数据,所以对于等待中的用户来说性能低下,因为它在一直等着,也不能做别的事情。

非阻塞 IO(non-bloking IO)

  非阻塞 IO 就是说:我还是要去银行办理业务,但是这次不会一直等着了,而是做其他的事情(比如,吃饭),在吃饭的过程中我来来回回地往银行跑,去 询问 业务员什么时候轮到我,这种方式就属于非阻塞。IO 模型如下所示:

image.png

  正如所描述的那样,该 IO 模型采用的是 轮询 的方式。也就是说,当 应用进程 进行 系统调用 的时候,会发现内核中的数据没有准备好,然后这时会返回一个信息,即 EWOULDBLOCK应用进程 不需要等待,而是马上得到了一个信息。得到信息后就会知道数据还没有准备好,于是就会再次进行 系统调用,直到 数据报准备好 后,通过 将数据报从内核复制到用户空间 才会返回成功。

  所以,通过以上两种不同 IO 方式的对比你可以看出:非阻塞 IO 可以去干别的事情,但是整体数据的吞吐量降低了,因为需要不断的来回进行 系统调用,也就是 轮询 操作,任务完成的响应延迟增大了。

多路复用 IO(multiplexing IO)

  多路复用 IO 就是说:我在去银行办理业务的时候,还是想要做其他的事情(还是去吃饭),但是现在可以使用银行提供的 应用软件 查看当前是否轮到我了,这样我就不用再来来回回地跑到银行问业务员了。IO 模型如下所示:

image.png

  由于这部分涉及到许多知识点,详细内容可以查看 参考 部分的文章,这里只是简单的解释一下。我的理解是这样的:图中的 应用进程 中的 select 就相当于银行提供的 应用软件 ,通过 select 可以看到内核中的数据有没有准备好,一旦准备好后就会 返回可读条件,也就是通知 应用进程 可以去进行 系统调用 了(相当于我查看手机上的软件看到终于轮到我了),这时再从内核中 复制数据报,然后将数据从内核复制到用户空间,最后复制完成返回结果。

  通过以上三种 IO 模型可以看到,在 等待数据 过程中,这几种 IO 模型处理的方式是不同的。有的是直接等待,有的是轮询,还有的是使用 select 方式。从整个 IO 过程来看,由于都是 顺序执行 的,所以都属于 同步模型,即都是通过进程主动等待且向内核检查状态的方式进行的。

信号驱动式 IO(signal-driven IO)

  对于信号驱动式 IO,这里仅给出IO 模型,然后简单解释一下。如下所示:

image.png

  这种方式是先建立一个 sigio 信号处理程序,通过 系统调用 的方式交给内核,然后当 数据报准备好 时再将 sigio 提交给 信号处理程序。这时就会告知 应用程序 可以进行 recvfrom 系统调用了,然后在内核中 将数据从内核拷贝到用户空间,最后返回。你可以看到,后半段的处理方式和前几种方式是一样的。

异步 IO(asynchronous IO)

  异步 IO 就是说:我仍然得去银行办理业务,但是我太懒了,不想去,我就找人帮我去,这个时候我就自由了,闲下来想干嘛就干嘛,他帮我办完业务后就把银行提供的那些材料再交给我。IO 模型图如下所示:

image.png

  这种方式比较有意思,当 应用进程 进行 系统调用 的时候,无论内核中的数据准没准备好,都会给 应用进程 一个 返回,然后 应用进程 可以去做别的事情。等到数据准备好后,内核直接将数据复制给进程,然后再从内核发送消息给 应用程序

模型对比

  最后将这几种 IO 模型进行对比,如下所示:

image.png

总结

阻塞非阻塞 指的是执行一个操作是等操作结束再返回,还是马上返回。

比如餐馆的服务员为用户点菜,当有用户点完菜后,服务员将菜单给后台厨师,此时有两种方式:

  • 阻塞方式:就在出菜窗口等待,直到厨师炒完菜后将菜送到窗口,然后服务员再将菜送到用户手中;
  • 非阻塞方式:等一会再到窗口来问厨师,某个菜好了没?如果没有先处理其他事情,等会再去问一次。

同步异步 又是另外一个概念,它是事件本身的一个属性。还拿前面点菜为例,服务员直接跟厨师打交道,菜出来没出来,服务员直接指导,但只有当厨师将菜送到服务员手上,这个过程才算正常完成,这就是 同步 的事件。同样是点菜,有些餐馆有专门的传菜人员,当厨师炒好菜后,传菜员将菜送到传菜窗口,并通知服务员,这就变成 异步的了。其实异步还可以分为两种:带通知不带通知。前面说的那种属于带通知的。有些传菜员干活可能主动性不是很够,不会主动通知你,你就需要时不时的去关注一下状态,这种就是不带通知的异步。

对于 同步 的事件,你只能以 阻塞 的方式去做。而对于 异步 的事件,阻塞非阻塞都是可以的。

非阻塞 又有两种方式:主动查询被动接收消息。被动不意味着一定不好,在这里它恰恰是效率更高的,因为在主动查询里绝大部分的查询是在做无用功。对于带通知的异步事件,两者皆可。而对于不带通知的,则只能用主动查询。

但是对于 非阻塞异步 的概念有点混淆,非阻塞 只是意味着方法调用不阻塞,就是说作为服务员的你不用一直在窗口等,非阻塞 的逻辑是 “等可以读(写)了告诉你”,但是完成读(写)工作的还是调用者(线程)服务员的你等菜到窗口了还是要你亲自去拿。而 异步 意味这你可以不用亲自去做读(写)这件事,你的工作让别人(别的线程)来做,你只需要发起调用,别人把工作做完以后,或许再通知你,它的逻辑是 “我做完了之后告诉或不告诉你”,它和非阻塞的区别在于一个是“已经做完”另一个是“可以去做”。

参考

  非常推荐这两篇文章,看完之后受益匪浅。