理解Linux五种I/O模型、同步I/O与异步I/O、阻塞与非阻塞

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/ZYZMZM_/article/details/97814160


IO模型描述的是出现I/O等待时进程的状态以及处理数据的方式。围绕着进程的状态、数据准备到kernel buffer再到app buffer的两个阶段展开。其中数据复制到kernel buffer的过程称为数据准备阶段,数据从kernel buffer复制到user buffer的过程称为数据复制阶段

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

  • 等待数据准备 (Waiting for the data to be ready)
  • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

这两点很重要,因为这些IO Model的区别就是在两个阶段上各有不同的情况


五种IO模型

5种IO模型分别是阻塞式IO非阻塞式IOIO复用(如select和wpoll)信号驱动IO(SIGIO)异步IO(POSIX的aio_系列函数);前4种为同步IO操作,只有异步IO模型是异步IO操作


Blocking I/O模型

进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。

当调用recvfrom()函数时,系统首先查是否有准备好的数据。如果数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户空间,然后该函数返回。在socket应用程序中,当调用recvfrom()函数时,未必用户空间就已经存在数据,那么此时recvfrom()函数就会处于等待状态。

上图中,进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。最常见的错误时系统调用被中断。进程在从调用recvfrom函数开始到它返回的整段时间内是被阻塞的。revefrom成功返回后,应用进程开始处理数据报。

阻塞模式给网络编程带来了一个很大的问题,如在调用 recvfrom()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。这给多客户机、多业务逻辑的网络编程带来了挑战。这时,我们可能会选择多线程的方式来解决这个问题。

特点:

  • 进程阻塞挂起不消耗CPU资源,及时响应每个操作;
  • 实现难度低、开发应用较容易;
  • 适用并发量小的网络应用开发;
  • 不适用并发量大的应用:因为一个请求IO会阻塞进程,所以,得为每请求分配一个处理进程(线程)以及时响应,系统开销大。

Non-Blocking I/O模型

从上图中可以看出,前三次调用recvfrom时没有数据可返回,因此内核转而立刻返回一个EWOULDBLOCK错误。第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回,我们接着处理数据(注意处理数据的过程是阻塞的)。

当用户进程发出recvfrom操作时,如果kernel中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个error从用户进程角度讲 ,它发起一个recvfrom操作后,并不需要等待,而是马上就得到了一个结果。用户进程收到返回结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送recvfrom操作不断的轮询)。一旦kernel中的数据准备好了(上图中的第四次操作),并且又再次收到了用户进程的系统调用,那么它马上就将数据拷贝到了用户内存,然后返回。

特点:

  • 进程轮询(重复)调用,消耗CPU的资源;
  • 实现难度高、开发应用相对阻塞IO模式较难;
  • 适用并发量较小、且不需要及时响应的网络应用开发;

I/O Multiplexing模型

I/O Multiplexing 称为多路IO模型IO复用,意思是可以检查多个IO等待的状态。有三种IO复用模型:select、poll和epoll。其实它们都是一种函数,用于监控指定文件描述符的数据是否就绪,就绪指的是对某个系统调用不再阻塞了,例如对于read()来说,就是数据准备好了就是就绪状态。就绪种类包括是否可读、是否可写以及是否异常,其中可读条件中就包括了数据是否准备好。

当数据就绪之后,将通知进程,进程再发送对数据操作的系统调用,如read()。所以,这三个函数仅仅只是处理了数据是否准备好以及如何通知进程的问题。可以将这几个函数结合阻塞和非阻塞IO模式使用,例如设置为非阻塞时,select()/poll()/epoll将不会阻塞在对应的描述符上,调用函数的进程/线程也就不会被阻塞。

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

这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection,多路复用模型中,对于每一个socket,一般都设置成为non-blocking

特点:

  • 专一进程解决多个进程I/O的阻塞问题,性能好;Reactor模式;
  • 实现、开发应用难度较大;
  • 适用高并发服务应用开发:一个进程(线程)响应多个请求;

Signal-driven I/O模型

Signal-driven I/O即信号驱动IO模型。当开启了信号驱动功能时,首先发起一个信号处理的系统调用,如sigaction(),这个系统调用会立即返回。但数据在准备好时,会发送SIGIO信号,进程收到这个信号就知道数据准备好了,于是发起操作数据的系统调用,如read()。

当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。

无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。

在发起信号处理的系统调用后,进程不会被阻塞,但是在read()将数据从kernel buffer复制到app buffer时,进程是被阻塞的

特点:

  • 回调机制,实现、开发应用难度大;

Asynchronous I/O模型

当进程发起一个IO操作,进程返回(不阻塞),但也不能返回结果;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据。

工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。这种模型与之前的信号驱动模型的主要区别在于:信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。

上图中,我们调用aio_read函数(POSIX异步I/O函数以aio_或lio_开头),给内核传递描述符、缓冲区指针、缓冲区大小(与read相同的三个参数)和文件偏移(与lseek类似),并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,而且在等待I/O完成期间,我们的进程不会被阻塞。我们假设要求内核在操作完成时产生某个信号。该信号直到数据已复制到应用进程缓冲区才产生,这一点不同于信号驱动式I/O模型。

特点:

  • 不阻塞,数据一步到位;Proactor模式;
  • 需要操作系统的底层支持,LINUX 2.5 版本内核首现,2.6 版本产品的内核标准特性;
  • 实现、开发应用难度大;
  • 非常适合高性能高并发应用;

五种I/O模型的比较

如上图,对比了5种不同的I/O模型,可以看出,前4种模型的主要区别在于第一阶段,即数据准备阶段,因为它们的第二阶段是一样的:在数据从内核复制到缓冲区期间,进程阻塞于recvfrom系统调用。相反,异步I/O在这两个阶段都要处理,从而不同于其他4种模型。

阻塞和非阻塞的区别在哪?调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回


同步I/O与异步I/O

  • 同步I/O操作(synchronous IO):导致请求进程阻塞,直到I/O操作完成
  • 异步I/O操作(asynchronous IO):不导致请求进程阻塞

先说说同步和异步,同步和异步关注的是双方的消息通信机制

  • 同步:双方的动作是经过双方协调的,步调一致的。
  • 异步:双方并不需要协调,都可以随意进行各自的操作。

这里我们的双方是指 用户进程和IO设备;明确同步和异步之后,我们进行扩展定义:

  • 同步IO:用户进程发出IO调用,去获取IO设备数据,双方的数据要经过内核缓冲区同步,完全准备好后,再复制返回到用户进程。而复制返回到用户进程会导致请求进程阻塞,直到I/O操作完成。

  • 异步IO:用户进程发出IO调用,去获取IO设备数据,并不需要同步,内核直接复制到进程,整个过程不导致请求进程阻塞。

根据上述定义,上述五种I/O模型中的四种模型:阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型和信号驱动式I/O模型都是同步I/O模型,因为其中真正的I/O操作(recvfrom)将阻塞进程。只有异步I/O模型与POSIX定义的异步I/O相匹配


总结

明确I/O请求的两个阶段:

  • 数据准备阶段:I/O请求一般需要请求特殊的资源(如磁盘、RAM、文件),当资源被上一个使用者使用没有被释放时,IO请求就会被阻塞,直到能够使用这个资源。
  • 数据复制阶段:真正进行数据接收和发生。

数据准备阶段,IO分为阻塞IO非阻塞IO

  • 阻塞IO:资源不可用时,IO请求一直阻塞,直到反馈结果(有数据或超时)。
  • 非阻塞IO:资源不可用时,IO请求离开返回,返回数据标识资源不可用。

数据复制阶段,IO分为同步IO异步IO

  • 同步IO:应用阻塞在发送或接收数据的状态,直到数据成功传输或返回失败,用户需要自行处理数据
  • 异步IO:应用发送或接收数据后立刻返回(不会阻塞),数据写入OS缓存,由OS完成数据发送或接收,并返回成功或失败的信息给应用,用户无需自行处理数据,由内核处理完毕直接通知用户

本文主要来源于《UNIX网络编程 卷1》、《Linux高性能服务器编程》和 其他优秀博客,加之自己的理解。

猜你喜欢

转载自blog.csdn.net/ZYZMZM_/article/details/97814160