网络IO模型

Published: by

IO发生时涉及的对象和步骤:对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:

  1. 等待数据准备到内核中
  2. 将数据从内核拷贝到进程中

同步阻塞I/O模型:

  • 在进程空间中调用recvfrom,kernel(内核)就开始了IO的第一个阶段:准备数据,对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误再返回,在此期间一直会等待,进程从调用recvfrom开始到它返回的整段时间内都是阻塞的。所以阻塞IO的特点是在IO执行的两个阶段(等待数据和拷贝数据的两个阶段)都被阻塞了。

  • 在阻塞期间,线程是无法执行任何运算或者响应任何的网络请求,采用此类IO模型的系统为保持多个连接,通常会使用多线程技术,但是一旦需要同时响应大量的连接请求,多线程会严重占用系统资源,降低系统对外界的响应效率。再次基础上,也可以使用线程池技术,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务,用以减少线程频繁创建以及销毁带来的开销,但是线程池也只是在一定程度上缓解了大量IO操作带来的资源占用问题。

同步非阻塞I/O模型:

  • 当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,就直接返回错误,从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

  • 一般对于非阻塞I/O模型进行轮训检查这个状态,查看内核是不是有数据到来。所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。

  • 使用此类iO模型的服务器可以通过循环调用recv()接口,可以在单个线程内实现对所有连接的数据接收工作。但是上述模型绝不被推荐。因为,循环调用recv()将大幅度推高CPU 占用率;此外,在这个方案中recv()更多的是起到检测到某一个连接中“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成“作用的接口,例如select()多路复用模式,可以一次检测多个连接是否活跃。

I/O复用模型:

  • Linux提供select/poll,在读取文件过程中,进程通过将一个或多个fd(文件描述符)传递给select/poll系统调用,阻塞的select操作上,这样select/poll可以帮我们检测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否准备就绪,而且支持的fd有限。Linux还而外提供了epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,当有fd准备就绪时,立即回调函数rollback

  • 当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

  • 这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

  • 优点:相比其他IO模型,该模型只用单线程执行,占用CPU资源少,同时能够为多个客户端提供服务。

  • 缺点:1.当需要探测的句柄值较大时,select事件探测操作会花费大量的时间去顺序轮训句柄【linux提供epoll来替代轮训,通过事件驱动来告知进程数据准备就绪】;2.IO多路复用模型在每一个执行周期都会探测一组事件,一个特定的事件会触发某个特定的响应,一旦数据报过大,或者接受数据包并处理的过程很长,导致事件响应的执行体庞大,会验证影响到下一次执行select的实时性

PS:I/O复用模型一般也被划分为同步非阻塞IO模型,虽然整个用户进程在执行select时会被阻塞,但是在整个过程中,数据从内核准备就绪的IO过程并未阻塞住:当某个连接的数据未准备至内核中时,IO并未阻塞住而是将空闲时间用于在其它连接上执行IO操作【多个连接之间的IO操作互相不阻塞】

信号驱动I/O模型:

  • 通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据

异步I/O模型:

  • 用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们合适可以开始一个I/O操作;而异步I/O模型由内核通知我们I/O操作何时完成。

阻塞与非阻塞

  • 阻塞与非阻塞关注的是程序在等待调用结果时的状态:阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在拿到结果之后才返回;非阻塞调用指在不能立刻获得结果之前,该调用不会阻塞当前线程。

  • 调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还在准备数据的情况下会立刻返回。

同步与异步

  • 同步和异步关注的是消息通信机制。所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回,但是一旦调用返回了,就得到返回值了。换句话说,就是调用者在主动等待这个调用的结果。而异步则相反,调用在发出之后,这个调用就直接返回了,此时没有返回结果。换句话说,当一个异步过程调用发出之后,调用者不会立刻得到结果,而是在调用发出之后,【被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用】。

  • 图中的IO操作其实是指真实的IO操作,是指当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中这个过程。异步的不同之处在于当进程发起IO操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

总结

  • 总的来说,IO模型有5种之多:阻塞IO,非阻塞IO,IO复用,信号驱动IO,异步IO。前四种都属于同步IO。阻塞IO不必说了。非阻塞IO ,IO请求后立刻返回,IO没有就绪会返回错误,需要请求进程主动轮询不断发IO请求直到返回正确。IO复用同非阻塞IO本质一样,不过利用了新的select系统调用,由内核来负责本来是请求进程该做的轮询操作。看似比非阻塞IO还多了一个系统调用开销,不过因为可以支持多路IO,才算提高了效率。信号驱动IO,调用sigaltion系统调用,当内核中IO数据就绪时以SIGIO信号通知请求进程,请求进程再把数据从内核读入到用户空间,这一步是阻塞的。

  • 异步IO,如定义所说,不会因为IO操作阻塞,IO操作全部完成才通知请求进程。

  • 经过上面的介绍,会发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。