来自于《软件架构设计:大型网站技术架构与业务架构融合之道》中第 4 章内容。

缓冲 I/O 和直接 I/O

缓冲 I/O 与直接 I/O 对应的 API 接口列表:

类型对应 API 接口
缓冲 I/OC 语言的库函数:
fopen, fclose, fsee, fflush, fread, fwrite, fprintf, fscanf, …
直接 I/OLinux 系统 API:
open, close, lseek, fsync, read, write, pread, pwrite

先解释几个关键概念:

  • 应用程序内存:代码中通过 malloc/free、new/delete 等分配出来的内存。
  • 用户缓冲区:C 语言的 FILE 结构体里面的 buffer。FILE 结构体的定义如下,可以看到里面有定义的 buffer:
    typedef struct
    {
    	short level;
    	short token;
    	short bsize;
    	char fd;
    	unsigned flags;
    	unsigned char hold;
    	unsigned char *buffer;
    	unsigned char * curp;
    	unsigned istemp;
    }FILE;
  • 内核缓冲区:Linux 操作系统的 Page Cache。为了加快磁盘的 I/O,Linux 系统会把磁盘上的数据以 Page 为单位缓存在操作系统的内存里,这里的 Page 是 Linux 系统定义的一个逻辑概念,一个 Page 一般为 4K。

对于缓冲 I/O,一个读操作会有 3 次数据拷贝,一个写操作,有反向的 3 次数据拷贝:

  • 读:磁盘→内核缓冲区→用户缓冲区→应用程序内存;
  • 写:应用程序内存→用户缓冲区→内核缓冲区→磁盘。

对于直接 I/O,一个读操作,会有 2 次数据拷贝,一个写操作,有反向的 2 次数据拷贝:

  • 读:磁盘→内核缓冲区→应用程序内存;
  • 写:应用程序内存→内核缓冲区→磁盘。

所以,所谓的“直接 I/O”,其中直接的意思是指没有用户级的缓冲,但操作系统本身的缓冲还是有的,两者的原理对比如图 4-1 所示。

600|600

关于缓冲 I/O 和直接 I/O,有几点需要特别说明:

  1. fflush 和 fsync 的区别:fflush 是缓冲 I/O 中的一个 API,它只是把数据从用户缓冲区刷到内核缓冲区而已,fsync 则是把数据从内核缓冲区刷到磁盘里。这意味着无论缓冲 I/O,还是直接 I/O,如果在写数据之后不调用 fsync,此时系统断电重启,最新的部分数据会丢失,因为数据只是在内核缓冲区里面,操作系统还没来得及刷到磁盘。后面讲数据库、数据一致性,会反复提到这个 fsync 函数。
  2. 对于直接 I/O,也有 read/write 和 pread/pwrite 两组不同的 API。pread/pwrite 在多线程读写同一个文件的时候很有用。关于这两组 API 更为详细的区别,读者可以查阅相关资料,此处不再进一步展开。

DMA 技术

DMA,英文全称是 Direct Memory Access,即直接内存访问。DMA 本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行 IO 数据传输,其过程不需要 CPU 的参与。

操作系统 IO 概念 & 文件拷贝.png|600

  • 用户应用进程调用 read 函数,向操作系统发起 IO 调用,进入阻塞状态,等待数据返回。
  • CPU 收到指令后,对 DMA 控制器发起指令调度。
  • DMA 收到 IO 请求后,将请求发送给磁盘;
  • 磁盘将数据放入磁盘控制缓冲区,并通知 DMA
  • DMA 将数据从磁盘控制器缓冲区拷⻉到内核缓冲区。
  • DMA 向 CPU 发出数据读完的信号,把工作交换给 CPU,由 CPU 负责将数据从内核缓冲区拷⻉到用户缓冲区。
  • 用户应用进程由内核态切换回用户态,解除阻塞状态

可以发现,DMA 做的事情很清晰啦,它主要就是帮忙 CPU 转发一下 IO 请求,以及拷⻉数据。为什么需要它的?
主要就是效率,它帮忙 CPU 做事情,这时候,CPU 就可以闲下来去做别的事情,提高了 CPU 的利用效率。大白话解释就是,CPU 老哥太忙太累啦,所以他找了个小弟(名叫 DMA) ,替他完成一部分的拷⻉工作,这样 CPU 老哥就能着手去做其他事情。

内存映射文件与零拷贝

内存映射文件

相比于直接 I/O,内存映射文件往前更进了一步。如图 4-2 所示,当用户空间不再有物理内存,直接拿应用程序的逻辑内存地址映射到 Linux 操作系统的内核缓冲区,应用程序虽然读写的是自己的内存,但这个内存只是一个“逻辑地址”,实际读写的是内核缓冲区!

数据拷贝次数从缓冲 I/O 的 3 次,到直接 I/O 的 2 次,再到内存映射文件,变成了 1 次。

  • 读:磁盘→内核缓冲区;
  • 写:内核缓冲区→磁盘。

    600|600

在 Linux 系统中,内存映射文件对应的系统 API 是:

void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);

在 Java 中,用 MappedByteBuffer 类可以实现同样的目的。

零拷贝

零拷贝的内容存放在零拷贝 中。