浅谈Linux五种IO模型

612 阅读7分钟

用户空间和内核空间

Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间。 当我们在用户空间想要实现对内核的操作,比如使用open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做 “系统调用”的方法来实现从用户空间陷入到内核空间,这样才能实现对底层驱动的操作。

image.png

在Linux系统中,虚拟地址布局如下:

image.png

进程的用户空间中存放的是用户程序的代码和数据,内核空间中存放的是内核代码和数据。不管是内核空间还是用户空间,它们都处于虚拟内存中,都是对物理地址的映射。

当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。

进程在内核空间可以访问受保护的内存空间,也可以访问底层硬件设备。也就是可以执行任意命令,调用系统的一切资源。在用户空间只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称system call),才能向内核发出指令。这样划分的目的是为了避免用户进程直接操作内核,保证内核的安全。

同步和异步

同步请求,A调用B,B的处理是同步的,在处理完之前他不会通知A,只有处理完之后才会明确的通知A。

异步请求,A调用B,B的处理是异步的,B在接到请求后先告诉A我已经接到请求了,然后异步去处理,处理完之后通过回调等方式再通知A。

所以说,同步和异步最大的区别就是被调用方的执行方式和返回时机。同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方。

阻塞和非阻塞

阻塞请求,A调用B,A一直等着B的返回,别的事情什么也不干。

非阻塞请求,A调用B,A不用一直等着B的返回,先去忙别的事情了。

所以说,阻塞非阻塞最大的区别就是在被调用方返回结果之前的这段时间内,调用方是否一直等待。阻塞指的是调用方一直等待别的事情什么都不做。非阻塞指的是调用方先去忙别的事情。

阻塞、非阻塞和同步、异步其实针对的对象是不一样的。阻塞、非阻塞说的是调用者,同步、异步说的是被调用者。

IO

IO一般分为磁盘IO网络IO,这里我们以网络IO为例。一次完整的网络IO过程如下所示:

image.png

当我们要取数据的时候,如果这块数据已经存在于用户进程的页内存中,就直接从内存中读取数据。如果数据不存在,则先将数据从磁盘/网络加载到内核缓冲区,再从内核缓冲区拷贝到用户进程的页内存中。

从上图可以看出,数据无论从网卡到用户空间还是从用户空间到网卡都需要经过内核。

Linux五种IO模型

首先提一下,在Java中,主要有三种IO模型,分别是阻塞IO(BIO)、非阻塞IO(NIO)和 异步IO(AIO)。可以理解为是Java语言对操作系统的各种IO模型的封装。程序员在使用这些API的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码,只需要使用Java的API就可以了。

所以接下来讲的是操作系统层面的IO,不是Java层面的IO,不要混淆哦。 在Linux中一共有五种IO模型:

  • 阻塞IO模型
  • 非阻塞IO模型
  • IO复用模型
  • 信号驱动IO模型
  • 异步IO模型 下面分别来讲解下。

阻塞IO模型

应用进程通过系统调用 recvfrom 接收数据,但由于内核还未准备好数据报,应用进程就会阻塞住,直到内核准备好数据报,从内核拷贝到用户空间,recvfrom 返回成功,应用进程才能结束阻塞状态。 image.png

阻塞IO模型的优点是编程简单,但缺点是需要配合大量线程/线程池使用。应用进程每接收一个连接,就需要为此连接创建一个线程来处理该连接上的读写任务。

非阻塞IO模型

调用进程在等待数据的过程中不会被阻塞,而是会不断地轮询查看数据有没有准备好。如果某一次轮询发现数据已经准备好了,将数据从内核空间拷贝到用户空间。

应用进程通过 recvfrom 调用不停的去和内核交互,直到内核准备好数据。如果没有准备好,内核会返回error,应用进程在得到error后,过一段时间再发送recvfrom请求。在两次发送请求的时间段,进程可以先做别的事情,等待数据的过程是非阻塞的,但数据拷贝时仍是阻塞的。

image.png

非阻塞io的优点在于可以实现使用一个线程同时处理多个连接的需求,减少线程的大量使用。缺点在于要不断地去轮询检查数据是否准备好,比较耗费CPU,还有可能存在延迟。

IO复用模型

为了解决非阻塞IO不断轮询导致CPU占用升高的问题,出现了IO复用模型。 Linux中提供了select、poll和epoll三种方式来实现IO复用。

管道统一和内核进行交互。下图以select函数为例,多个进程的IO可以注册到同一个select上,当用户进程调用该select,select会监听所有注册好的IO,如果所有被监听的IO需要的数据没有准备好时,select调用进程会阻塞。当任意一个IO所需的数据准备好之后,select调用就会返回,然后进程再通过recvfrom来把对应的数据拷贝到用户空间中。

image.png

IO复用只需要阻塞在select,poll或者epoll,可以同时处理和管理多个连接。缺点是当 select、poll或者epoll 管理的连接数过少时,这种模型将退化成阻塞 IO 模型。并且还多了一次系统调用:一次 select、poll或者epoll 和一次 recvfrom。

信号驱动IO模型

应用程序创建一个信号驱动程序SIGIO,向内核注册一个信号处理函数,然后用户进程返回继续运行,不会被阻塞。当内核数据准备就绪时会发送一个信号给应用进程,之后信号驱动程序就会执行,用户进程便在信号处理函数中开始把数据拷贝到用户空间中。

image.png

信号驱动IO模型的优点在于非阻塞,缺点在于串行处理信号驱动程序,当前一个SIGIO没有被处理的情况下,后一个信号也不能被处理。在信号量大的时候会导致后面的信号不能被及时感知。

异步IO模型

相比于同步IO,异步IO不是顺序执行的。应用进程在执行aio_read系统调用之后,给内核传递描述符、缓冲区指针、缓冲区大小等,告诉内核当整个操作完成时,如何通知进程,当内核收到aio_read后,会立刻返回,然后应用进程就去做其他事情了。

然后内核开始等待数据准备,当数据准备好之后,内核直接复制数据给用户进程,内核完成相关操作后,然后发出信号告诉应用进程本次IO已经完成。

image.png

在异步IO模型中,应用进程调用aio_read以及数据被拷贝到用户空间这两个过程都是非阻塞的。很完美。

总结

到这里Linux中五种IO模型全部讲完,前四种模型区别在于第一部分,即系统调用,但是第二部分都是一样的,即将数据从内核空间拷贝到用户空间这个过程,进程阻塞于redvfrom的调用。 而最后一种,异步IO模型,在系统调用和数据拷贝过程都是非阻塞的。

image.png