UNIX 下的 I/O 模型

RPC 相关第三篇

Posted by ChenJY on October 1, 2018 | Viewed times
本站图床基于新浪微博,图片加载异常请强制刷新或直接访问语雀空间查阅文章备份

开篇

RPC 中很重要的部分就是网络通信,因此这篇叙述一下 Unix 下为解决不同 I/O 问题所设计的 I/O 模型。首先要说明的是,I/O 是个很宽泛的概念,常见的有网络 I/O、磁盘 I/O、内存 I/O 等。

在 Unix 系统下,不论是标准输入还是借助套接字接受网络输入,其实都会有两个步骤,很多文章都提到:

  1. 等待数据准备好(Waiting for the data to be ready)
  2. 从内核向进程复制数据(Copying the data from the kernel to the process)

这两个阶段涉及到用户空间和内核空间

用户空间和内核空间

对 32 位 OS 而言,它的寻址空间(虚拟存储空间)为 4G。OS 的核心是内核,可以访问底层硬件设备,为了保证用户进程不能直接操作内核从而保证内核的安全,OS 将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。

内核空间中存放的是内核代码和数据,例如 Linux 的 OS 和驱动便运行在内核空间,可以操作底层硬件,如果从磁盘读取数据,那么数据会被先载入内核空间的缓冲区中;而进程的用户空间中存放的是用户程序的代码和数据,通常来讲就是应用程序常驻的区域。

因此整个 Linux 内部结构可以分为三部分,从最底层到最上层依次是:硬件、内核空间、用户空间。如下图:

二者间无法直接通信,必须通过系统调用,一般来说系统调用的成本很高。

内核态和用户态

  • 当一个进程经过系统调用而陷入内核代码中执行时,称进程处于内核运行态,简称内核态
  • 当进程在执行用户自己的代码时,则称其处于用户运行态,简称用户态

高性能的Server有什么特点

说完上面的之后,你可能疑惑这和 RPC 的通信设计有什么关系呢?其实正是由于这种内存空间的划分,所以 I/O 一般会在两个地方阻塞,一个是等待数据报到达时,一个是从内核空间拷贝到用户空间时,而阻塞多数情况下我们是无法接受的,因为其损耗性能,而高性能的 server 到底在关注什么?一句话总结:用尽可能少的系统开销处理尽可能多的连接请求。因此诞生了不同的 I/O 模型,它们的不同点总结起来就是对这两个阻塞阶段的处理方式不同

Unix 下的 I/O 模型

Unix 下存在五种 I/O 模型:

  1. 阻塞 I/O
  2. 非阻塞 I/O
  3. I/O 复用(select和poll)
  4. 信号驱动 I/O(SIGIO)
  5. 异步 I/O

以下的例子,我们以 UDP 套接字中的 recvfrom 函数作为系统调用来说明I/O模型。recvfrom 函数类似于标准的 read 函数,它的作用是从指定的套接字中读取数据报。

1 、阻塞 I/O

可以看到阻塞 I/O 在两个步骤阶段都是阻塞的,等到数据报准备好和数据报从内核空间拷贝到用户空间之后,才会向用户侧的进程返回结果,此时用户侧的进程才能继续工作。

2 、非阻塞 I/O

非阻塞 I/O 的优化点在于第一阶段不是阻塞的,而是采取轮询的形式,如果数据报没有准备好,立刻返回一个错误 EWOULDBLOCK,此时用户侧进程不需要等待而是立刻得知此次询问的结果,然后进行重试直到数据报准备好再开始,但是再第二阶段拷贝数据报的时候依旧是阻塞的。

3、 I/O 复用

本质上 I/O 复用的优化点在于让内核来负责非阻塞 I/O 时用户侧进程进行的反复重试操作,当内核发现某个套接字的数据报已经就绪时就通知进程。但是这里细心的你会发现,有两个系统调用,select 和 revfrom,但是由于 I/O 复用可以处理多个连接,性能还是有提升。

4 、信号驱动 I/O

进程先创建一个信号处理 handler,然后内核立刻返回,进程可以去处理其他事情,等到数据报就绪,内核通过发送信号给之前的 handler 通知进程,然后进程在拷贝数据报期间阻塞。

5 、异步 I/O

调用 aio_read 函数发起读取操作时其实是告诉内核 “当整个I/O操作完成后通知我们”。该系统调用会立即返回,进程不会被阻塞。当 I/O 阶段两个步骤完成后,内核会产生一个信号通知应用进程对数据报进行处理。

跟信号驱动 I/O 相比是告知进程何时进行数据拷贝操作,而异步 I/O 则是通知进程何时整个 I/O 操作完毕。

参考资料

  1. Unix五种IO模型
  2. 也谈IO模型
  3. 图解UNIX的I/O模型

License


Comment