IO模型的演变

97 阅读5分钟

用户程序如何从网络中读数据

用户程序如何从网络中读数据,即执行read系统调用,会分为两个阶段

  1. 等待有数据可读,也就是等待数据从网络到达网卡并拷贝到内核

  2. 把数据从内核拷贝到用户空间

    因为用户进程无法访问内核空间,所以必须把数据从内核拷贝到用户空间,只有这样用户进程才能访问数据。

也就是说,网络数据必须经过两次数据拷贝才能被用户程序读取。

不同IO模型的区别就是这两个阶段的处理有所不同

以下都是以读场景来讲述各种IO模型及优缺点,写场景其实是差不多的

BIO(同步阻塞IO)

两个阶段用户线程都会阻塞,如图:

BIO的read流程.png 优点:应用程序开发简单。

缺点:针对BIO的特性,一般都会为每个连接分配一个线程进行处理,在高并发场景下(即大量连接),系统就会创建大量线程,内存、线程上下文切换的开销都非常大。因此,BIO模型无法应对高并发场景

NIO(同步非阻塞IO)

第一个阶段用户线程不会阻塞(若无数据可读会直接返回),第二个阶段还是会发生阻塞,如图:

NIO的read流程.png 优点:没有数据可读时用户线程不会阻塞而是直接返回,可以去干其他事情。

缺点:

对于NIO模型,理论上可以用一个线程处理所有连接:就是轮询所有连接,有数据可读就读取数据再处理下一个连接,无数据可读就直接返回继续处理下一个连接,这种单线程处理架构有些问题:那就是连接的处理不及时(即响应慢)、性能差,因为要读完数据才能处理下一个连接,可能读数据就很耗时,从而导致后面的连接处理不及时

针对上述问题,可以改变架构一个监控线程专门负责轮询所有连接、返回有哪些连接可读,然后多个工作线程处理所有可读连接,即多线程架构,这样就可以大大提高连接处理的及时性、提高性能,这种架构下只需少量线程就能处理大量连接,同时还保证了性能,但还是有不足之处:对于NIO模型,一次系统调用只能判断一个连接是否可读,那监控线程想要监控所有连接就必须执行大量系统调用,从而产生巨大的线程上下文切换开销。

因此,就演变出了IO多路复用模型...

IO多路复用

第一个阶段用户线程可阻塞也可以不阻塞,第二个阶段会发生阻塞,但是在第一阶段时,一次系统调用就可以监控多个连接并返回所有可读连接。相比于NIO一次系统调用只能监控一个连接,IO多路复用模型大大减少了系统调用次数,提高了性能。

如图:

IO多路复用的read流程.png

select系统调用支持阻塞和非阻塞

优点:一次系统调用可以监控多个连接,大大减少了系统调用次数,大大降低了系统开销、提高了性能。

缺点:一次系统调用监控多个连接的能力需要OS支持,在Linux中就是指select、poll和epoll系统调用。

总结:

NIO和IO多路复用模型理论上都可以做到一个线程处理所有连接,即单线程架构

  • 对于NIO,就是轮询所有连接,有数据可读则读完数据再处理下一个连接,无数据可读则直接返回继续处理下一个连接
  • 对于IO多路复用,就是一个线程既监控所有连接,也处理所有可读连接

但是,单线程架构会有连接处理不及时(响应慢)、性能差的问题。

因此,可以采用多线程架构,即一个监控线程负责监控所有连接,多个工作线程负责处理所有可读连接,从而可实现只需少量线程就能处理大量连接。IO多路复用模型相比于NIO模型,在监控方面做了优化:一次系统调用就能监控所有连接,大大减少了系统调用次数,降低了系统开销、提高了性能。

信号驱动IO

第一个阶段用户线程不会阻塞,第二个阶段用户线程会发生阻塞

信号驱动IO,就是在有数据可读的时候,由内核通知用户线程去读取数据,而不是用户线程去轮询是否有数据可读(NIO和IO多路复用模型必须由用户线程轮询),但是用户线程读取数据的时候(即把数据从内核拷贝到用户空间)还是会阻塞。

异步IO

两个阶段用户线程都不会阻塞

异步IO的流程如下:

  1. 用户线程执行read系统调用,会直接返回,然后用户线程就可以去做其他事了
  2. 执行read系统调用后,内核会等待有数据可读,有数据可读后,内核会将数据从内核空间拷贝到用户空间(即这个过程用户线程不再阻塞了
  3. 数据拷贝到用户空间后,内核会通知用户线程(如发送一个信号)读取数据或者回调用户线程注册的回调接口
  4. 用户线程从用户空间读取数据

异步IO是真正的非阻塞IO,用户线程不会阻塞、完全解放了。

总结

IO模型的演变其实就是不断发现问题和解决问题的过程