Unix网络编程中的五种IO模型:

  • Blocking IO - 阻塞IO
  • NoneBlocking IO - 非阻塞IO
  • IO multiplexing - IO多路复用
  • signal driven IO - 信号驱动IO
  • asynchronous IO - 异步IO

1.阻塞IO - Blocking IO

最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。

当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。
image-1649870550886

也许有人会说,可以采用多线程+ 阻塞IO 来解决效率问题,但是由于在多线程 + 阻塞IO 中,每个socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。

2.非阻塞IO - NoneBlocking IO

当用户线程发起一个 IO 操作后,并不需要等待,而是马上就得到一个结果。如果结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 IO 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。

在非阻塞IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。

对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。
image-1649870657204

  1. 非阻塞式主要体现在用户进程发起recvfrom系统调用的时候,这个时候系统内核还没有接收到数据报,直接返回错误给用户进程,告诉“当前还没有数据报可达,晚点再来”
  2. 用户进程接收到信息,但是用户进程不知道什么时候数据报可达,于是就开始不断轮询(polling)向系统内核发起recvfrom的系统调用“询问数据来了没”,如果没有则继续返回错误
  3. 用户进程轮询发起recvfrom系统调用直至数据报可达,这个时候需要等待系统内核复制数据报到用户进程的缓冲区,复制完成之后将返回成功提示

3. IO多路复用 - IO multiplexing

所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。

在多路复用IO模型中,会有一个内核线程不断地去轮询多个 socket 的状态,只有当真正读写事件发送时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有真正有读写事件进行时,才会使用IO资源,所以它大大减少来资源占用。
image-1649870742468

  1. IO复用模式是使用select或者poll函数向系统内核发起调用,阻塞在这两个系统函数调用,而不是真正阻塞于实际的IO操作(recvfrom调用才是实际阻塞IO操作的系统调用)
  2. 阻塞于select函数的调用,等待数据报套接字变为可读状态
  3. 当select套接字返回可读状态的时候,就可以发起recvfrom调用把数据报复制到用户空间的缓冲区

4.信号驱动IO - signal driven IO

在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。这个一般用于UDP中,对TCP套接字几乎没用,原因是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么请求。
image-1649870811205

用户进程可以使用信号方式,当系统内核描述符就绪时将会发送SIGNO给到用户空间,这个时候再发起recvfrom的系统调用等待返回成功提示,流程如下:

  1. 先开启套接字的信号IO启动功能,并通过一个内置安装信号处理函数的signaction系统调用,当发起调用之后会直接返回;
  2. 其次,等待内核从网络中接收数据报之后,向用户空间发送当前数据可达的信号给信号处理函数;
  3. 信号处理函数接收到信息就发起recvfrom系统调用等待内核数据复制数据报到用户空间的缓冲区;
  4. 接收到复制完成的返回成功提示之后,应用进程就可以开始从网络中读取数据。

5. 异步IO - asynchronous IO

前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
image-1649870884727

  1. 由POSIX规范定义,告知系统内核启动某个操作,并让内核在整个操作包含数据等待以及数据复制过程的完成之后通知用户进程数据已经准备完成,可以进行读取数据;
  2. 与上述的信号IO模型区分在于异步是通知我们何时IO操作完成,而信号IO是通知我们何时可以启动一个IO操作

同步与异步的定义

  • 同步:发起一个fn的调用,需要等待调用结果返回,该调用结果要么是期望的结果要么是异常抛出的结果,可以说是原子性操作(要么成功要么失败返回)
  • 异步: 发起一个fn调用,无需等待结果就直接返回,只有当被调用者执行处理程序之后通过“唤醒”手段通知调用方获取结果(唤醒的方式有回调,事件通知等)
  • 小结: 同步和异步关注的是消息通信机制,处理方程序是否等待结果再返回

阻塞与非阻塞的定义

  • 阻塞: 类比线程阻塞来说明,在并发多线程争抢资源的竞态条件下,如果有一个线程已持有锁,那么当前线程将无法获取锁而被挂起,处于等待状态
  • 非阻塞: 一旦线程释放锁,其他线程将会进入就绪状态,具备争抢锁的资格
  • 小结: 阻塞和非阻塞关注的是调用方程序在等待调用结果(消息,返回值)时的状态

由此可知,同步异步与阻塞非阻塞之间不存在关联,关注的目标是不一样的

同步IO与异步IO(基于POSIX规范)

  • 同步IO: 表示应用进程发起真实的IO操作请求(recvfrom)导致进程一直处于等待状态,这时候进程被阻塞,直到IO操作完成返回成功提示
  • 异步IO: 表示应用进程发起真实的IO操作请求(recvfrom)导致进程将直接返回一个错误信息,“相当于告诉进程还没有处理好,好了会通知你”
  • 阻塞IO: 主要是体现发起IO操作请求通知内核并且内核接收到信号之后如果让进程等待,那么就是阻塞
  • 非阻塞IO: 发起IO操作请求的时候不论结果直接告诉进程“不用等待,晚点再来”,那就是非阻塞

image-1649870979966

  • 根基上述的同步与异步IO定义并结合上述的模型可知,只有异步IO模型符合POSIX规范的异步IO,其他IO模型都存在recvfrom系统调用被内核阻塞,属于同步IO操作

  • 由此可知,阻塞IO与非阻塞IO可总结如下:

  • 也就是说,要么称为同步与异步IO,要么称为上述5种模型的IO说法,注意上述的同步与异步的概念

  • 大部分操作系统都是基于同步IO的方式实现,对于支持异步IO模型的操作系统还不确定,在实际工作我们经常会说Blocking-IO(阻塞IO)和Non-Blocking-IO(非阻塞IO),极少称同步IO与异步IO

小结:

同步与异步针对通信机制,阻塞与非阻塞针对程序调用等待结果的状态

原文:

I/O 多路复用底层原理前篇 - 五种IO模型