Skip to content

I/O 基础介绍

I/O(Input/Output)意思是输入输出,其实就是数据传递的一个过程,作为后台服务需要更多地与外部进行数据交互,那么就免不了 I/O 操作。一般来说, I/O 在系统层面会有经过一下两个阶段:

  • 第一个阶段是读取文件,将文件放入操作系统内核缓冲区;
  • 第二阶段是将内核缓冲区拷贝到应用程序地址空间

I/O 的 5 种模型

1. 阻塞 I/O

例如调用 read 函数读取一个文件,我们必须要等待文件读取完成后,也就是完成上面所说的两个阶段,才能执行其他逻辑,而当前是无法释放 CPU 的,因此无法去处理其他逻辑。

2. 非阻塞 I/O

非阻塞的意思是,我们发起了一个读取文件的指令,系统会返回正在处理中,然后这时候如果要释放进程中的 CPU 去处理其他逻辑,你就必须间隔一段时间,然后不停地去询问操作系统,使用轮询的判断方法看是否读取完成了

3. 多路复用 I/O

这一模型主要是为了解决轮询调度的问题,我们可以将这些 I/O Socket 处理的结果统一交给一个独立线程来处理,当 I/O Socket 处理完成后,就主动告诉业务,处理完成了,这样不需要每个业务都来进行轮询查询了。多路复用包括包括目前常见的三种类型:select 、poll 和 epoll。他们之间的区别如下:

  1. select :使用数组来保存 I/O Socket(文件操作符 fd_set 结构) 数据,会有上限 1024 位,属于比较旧的模型
  2. poll:使用链表来保存 I/O Socket 数据
  3. epoll:以红黑树的方式保存 需要注意的是这三者只是用来处理第一阶段告知文件读取进入了操作系统内核缓冲区,在第二阶段从内核拷贝到应用程序地址空间还是同步等待的。所以本质上他们都是同步 I/O

::: primary 三者区别 前两者每次调用 select/poll,都需要把 fd_set 集合从用户态拷贝到内核态,并且在内核中会去遍历轮询传递进来的 fd_set 集合中 fd 的状态(I/O 操作是否完成),将已完成的 fd 返回到用户态集合,如果 fd_set 集合很大时,这个开销会很大,而 epoll 通过红黑树以 O (LogN) 的方式定位到那些被内核 IO 事件异步唤醒(I/O 操作完成)的 fd,避免了轮询,之后遍历所有返回到用户态加入 Ready 队列的描述符集合就行了,epoll 能显著提高程序在大量并发连接中只有少量活跃的情况下的系统 CPU 利用率。但是 epoll 目前只支持 pipe, 网络等操作产生的 fd,暂不支持文件系统产生的 fd。 :::

4. 信号驱动 I/O

指进程预先告知内核,使得当某个描述符上发生某事时,内核使用信号通知相关进程。和多路复用的区别在于不需要有其他线程来处理,在完成了读取进入操作系统内核缓冲区后,立马通知,也就是第一阶段可以由系统层面来处理,不需要独立线程来管理,但是第二阶段还是和多路复用一样。

异步 I/O

和信号驱动不同的是,异步 I/O 是两个阶段都完成了以后,才会通知,并不是第一阶段完成。 Node.js 是其 libuv 库自行实现的一种类似异步 I/O 的模型,对于 Node.js 应用来说是一个异步 I/O,因此无须处理两个过程,而在 libuv 内部实现,则是多线程的一个 epoll 模型(多线程用来处理 epoll 不能支持文件系统产生的 fd 问题)。

libuv I/O 模型

libuv 使用 epoll 来构建 event-loop 的主体,其中:

  1. socket, pipe 等能通过 epoll 方式监听的 fd 类型,通过 epoll_wait 的方式进行监听;
  2. 文件处理 / DNS 解析 / 解压、压缩等操作,使用工作线程的进行处理,将请求和结果通过两个队列建立联系,由一个 pipe 与主线程进行通信, epoll 监听该 fd 的方式来确定读取队列的时机。