逐步实现TCP服务端Step04-6:I/O多路复用

I/O多路复用是一种I/O模式,由OS提供支持。基于该方式,可对多个I/O实施管理。

由进程发起的一次I/O操作,其执行包含两个阶段:1. 准备数据;2. 复制数据。处于用户空间的进程没有直接操作设备的权限,这两步操作都由内核完成。第一步所谓的准备数据,指的是内核将设备中的数据取到内核缓冲中。比如当收到一个完整TCP报文段时,其携带的数据会被存到内核缓冲中,此时的状态就是数据准备已就绪。第一步就绪后,第二步则会将内核缓冲中的数据拷贝到进程空间中的指定区域(用户缓冲)。

显然,这两步操作是一个顺序的过程,只有第一步完成了才能执行第二步。而数据是否准备就绪又取决于I/O设备上是否有符合要求的数据。总之,设备上没有合规的数据,内核缓冲中就没有准备就绪的数据,内核就无法将数据复制给提出I/O要求的那个进程。若该I/O的类型为阻塞式的,那么发起此次I/O的那个线程将会阻塞在系统调用处。若该I/O为非阻塞式的,在没有数据准备就绪的时候,将不会引起阻塞。

目前在netio中所用到的网络I/O类型都是非阻塞式的。

不过,不论阻塞还是非阻塞,进程在发起一次I/O时,发起的线程总是免不了“等”。不同的是,非阻塞I/O可以让线程少等一部分时间。对于非阻塞I/O,若数据未准备就绪,内核会直接告诉线程,具体表现就是系统调用立即返回,不阻塞。若系统调用发生时,数据已准备就绪,则内核就会执行第二步,将数据复制给进程空间。在执行第二步的过程中,线程其实是阻塞的,只不过时间很短。

非阻塞I/O免去了线程等待数据就绪的那部分时间,线程仍要等待数据的复制,虽然它很短暂。因此,所谓的非阻塞,其实是部分非阻塞。不过,若对性能没有很高的要求,这个问题没必要关注。

上面讨论了I/O的两个关键步骤,以及阻塞和非阻塞I/O在执行这两步时的区别。接下来说一下负责网络通信的netio存在的问题。

之前的设定,netio使用的是单进程单线程的并发模型,即,要在单个线程中处理多路I/O操作。当时在做应对多客户的改造时,我们使用了一种简单的方式,就是用一个List去存放每个客户所对应的socket信息。然后,通过遍历List,逐个发起I/O操作。

在现实当中,并非每个客户都是时刻不停地在与netio进行通信。而就算客户们都在频繁地与netio交互,以程序中循环执行的速度来看,这个“频繁“其实根本不算频繁,其中的间隙还是很大。

简单说,netio用一个循环,不停地对每个socket发起的I/O操作,会有相当一部分因数据尚未准备就绪而没产生任何实际效果。大量无实际效果的动作,对应了大量的系统调用。

这就是netio的问题:为了完成一些有效的I/O操作,进行了大量对有效I/O无任何助益的系统调用,浪费了时间。

之所以使用这种方式,其原因在于没法直接知道哪些socket准备就绪了。而这正是I/O多路复用机制可以解决的问题,内核把查询I/O状态的工作给做了,这免去了在进程中做的那些无用功。

不过,更好的做法是,图中的商家完全不理会客户准备钱的“进度”。商家主动查询客户的进度,试图在其准备好钱的时候,对其实施收钱动作,这其实是一种“同步”行为。这种做法实际是主动寻求与客户步调一致。打破这种同步,让准备好钱的客户直接把钱给商家,最后给商家打声招呼就行了。这是一种异步I/O机制,现阶段我们只关注同步I/O,有关于异步的话题,以后再说。

常见的I/O多路复用接口有:selectpollepoll等。另外,Windows平台所特有的IOCP并非多路复用机制,不过,做为一种高性能模型常与epoll放在一起讨论。后续将在netio中分别使用这些机制。


<==  index  ==>