目录

IO

计算机 IO

IO,英文全称是 Input/Output,翻译过来就是输入/输出。比如磁盘 IO,网络 IO。

输入设备是向计算机输入数据和信息的设备,键盘,鼠标都属于输入设备;输出设备是计算机硬件系统的终端设备,用于接收计算机数据的输出显示,一般显示器、打印机属于输出设备。

鼠标、显示器这只是直观表面的输入输出,回到计算机架构来说,涉及计算机核心与其他设备间数据迁移的过程,就是 IO。如磁盘 IO,就是从磁盘读取数据到内存,这算一次输入,对应的,将内存中的数据写入磁盘,就算输出。这就是 IO 的本质。

操作系统 IO

将内存中的数据写入到磁盘的话,主体会可能是一个应用程序,比如一个 Java 进程或者 Go 进程。

以 32 位操作系统为例,它为每一个进程都分配了 4G(2 的 32 次方)的内存空间。这 4G 可访问的内存空间分为二部分,一部分是用户空间,一部分是内核空间。

内核空间是操作系统内核访问的区域,是受保护的内存空间,而用户空间是用户应用程序访问的内存区域。

应用程序是跑在用户空间的,它不存在实质的 IO 过程,真正的 IO 是在操作系统执行的。即应用程序的 IO 操作分为两种动作:IO 调用和 IO 执行。

  • IO 调用:应用程序进程向操作系统内核发起调用。
  • IO 执行:操作系统内核完成 IO 操作。

IO 调用是由进程(应用程序的运行态)发起,而 IO 执行是操作系统内核的工作。此时所说的 IO 是应用程序对操作系统 IO 功能的一次触发,即 IO 调用。

操作系统内核完成 IO 操作还包括两个过程:

  • 准备数据阶段:内核等待 I/O 设备准备好数据。
  • 拷贝数据阶段:将数据从内核缓冲区拷贝到用户进程缓冲区。

IO 就是把进程的内部数据转移到外部设备,或者把外部设备的数据迁移到进程内部。外部设备一般指硬盘、socket 通讯的网卡。一个完整的 IO 过程包括以下几个步骤:

  1. 应用程序进程向操作系统发起 IO 调用请求
  2. 操作系统准备数据,把 IO 外部设备的数据,加载到内核缓冲区
  3. 操作系统拷贝数据,即将内核缓冲区的数据,拷贝到用户进程缓冲区

阻塞 IO 模型

用程序的进程发起 IO 调用,但是如果内核的数据还没准备好的话,那应用程序进程就一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次 IO 操作,称之为阻塞 IO。

阻塞 IO 的缺点就是:如果内核数据一直没准备好,那用户进程将一直阻塞,浪费性能,可以使用非阻塞 IO 优化。

非阻塞 IO 模型

如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求。这就是非阻塞 IO。

相对于阻塞 IO,虽然大幅提升了性能,但是它依然存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的 CPU 资源。可以考虑 IO 复用模型,去解决这个问题。

IO 多路复用模型

IO 复用模型核心思路:操作系统提供了一类函数(比如 select、poll、epoll 函数),它们可以同时监控多个 fd(socket 或者文件)的操作,任何一个返回内核数据就绪,应用进程再发起 recvfrom 系统调用。

select

应用进程通过调用 select 函数,可以同时监控多个 fd,在 select 函数监控的 fd 中,只要有任何一个数据状态准备就绪了,select 函数就会返回可读状态,这时应用进程再发起 recvfrom 请求去读取数据。

非阻塞 IO 模型(NIO)中,需要 N(N>=1)次轮询系统调用,然而借助 select 的 IO 多路复用模型,只需要发起一次询问就够了,大大优化了性能。

但是呢,select 有几个缺点:

  • 监听的 IO 最大连接数有限,在 Linux 系统上一般为 1024。
  • select 函数返回后,是通过遍历 fdset,找到就绪的描述符 fd。(仅知道有 I/O 事件发生,却不知是哪几个流,所以遍历所有流)

因为存在连接数限制,所以后来又提出了 poll。与 select 相比,poll 解决了连接数限制问题。但是呢,select 和 poll 一样,还是需要通过遍历文件描述符来获取已经就绪的 socket。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。

epoll

epoll 先通过 epoll_ctl()来注册一个 fd(文件描述符),一旦基于某个 fd 就绪时,内核会采用回调机制,迅速激活这个 fd,当进程调用 epoll_wait()时便得到通知。这里去掉了遍历文件描述符的操作,而是采用监听事件回调的机制。这就是 epoll 的亮点。

select、poll、epoll 的区别

select poll epoll
底层数据结构 数组 链表 红黑树和双链表
获取就绪的 fd 遍历 遍历 事件回调
时间复杂度 O(n) O(n) O(1)
最大连接数 1024 无限制 无限制
fd 数据拷贝 每次调用 select,需要将 fd 数据从用户空间拷贝到内核空间 每次调用 poll,需要将 fd 数据从用户空间拷贝到内核空间 使用内存映射(mmap),不需要从用户空间频繁拷贝 fd 数据到内核空间

epoll 明显优化了 IO 的执行效率,但在进程调用 epoll_wait()时,仍然可能被阻塞。能不能不用老是去问你数据是否准备就绪,等发出请求后,数据准备好了通知程序就行了,这就诞生了信号驱动 IO 模型。

信号驱动 IO 模型

信号驱动 IO 不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号(调用 sigaction 的时候建立一个 SIGIO 的信号),然后应用用户进程可以去做别的事,不用阻塞。当内核数据准备好后,再通过 SIGIO 信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用 recvfrom,去读取数据。

信号驱动 IO 模型,在应用进程发出信号后,是立即返回的,不会阻塞进程。它已经有异步操作的感觉了。但是你细看上面的流程图,发现数据复制到应用缓冲的时候,应用进程还是阻塞的。回过头来看下,不管是 BIO,还是 NIO,还是信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的。

异步 IO 模型(AIO)

AIO 实现了 IO 全流程的非阻塞,就是应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是表示提交成功类似的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程 IO 操作执行完毕。

异步 IO 的优化思路很简单,只需要向内核发送一次请求,就可以完成数据状态询问和数据拷贝的所有操作,并且不用阻塞等待结果。日常开发中,有类似思想的业务场景:

比如发起一笔批量转账,但是批量转账处理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。

阻塞、非阻塞、同步、异步 IO 划分

IO 模型
阻塞 I/O 模型 同步阻塞
非阻塞 I/O 模型 同步非阻塞
I/O 多路复用模型 同步阻塞
信号驱动 I/O 模型 同步非阻塞
异步 IO(AIO)模型 异步非阻塞

参考

https://www.51cto.com/article/693213.html