来自于《软件架构设计:大型网站技术架构与业务架构融合之道》中第 4 章内容。
缓冲 I/O 和直接 I/O
缓冲 I/O 与直接 I/O 对应的 API 接口列表:
| 类型 | 对应 API 接口 |
|---|---|
| 缓冲 I/O | C 语言的库函数: fopen, fclose, fsee, fflush, fread, fwrite, fprintf, fscanf, … |
| 直接 I/O | Linux 系统 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 所示。

关于缓冲 I/O 和直接 I/O,有几点需要特别说明:
- fflush 和 fsync 的区别:fflush 是缓冲 I/O 中的一个 API,它只是把数据从用户缓冲区刷到内核缓冲区而已,fsync 则是把数据从内核缓冲区刷到磁盘里。这意味着无论缓冲 I/O,还是直接 I/O,如果在写数据之后不调用 fsync,此时系统断电重启,最新的部分数据会丢失,因为数据只是在内核缓冲区里面,操作系统还没来得及刷到磁盘。后面讲数据库、数据一致性,会反复提到这个 fsync 函数。
- 对于直接 I/O,也有 read/write 和 pread/pwrite 两组不同的 API。pread/pwrite 在多线程读写同一个文件的时候很有用。关于这两组 API 更为详细的区别,读者可以查阅相关资料,此处不再进一步展开。
DMA 技术
DMA,英文全称是 Direct Memory Access,即直接内存访问。DMA 本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行 IO 数据传输,其过程不需要 CPU 的参与。

- 用户应用进程调用 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 次。
- 读:磁盘→内核缓冲区;
- 写:内核缓冲区→磁盘。


在 Linux 系统中,内存映射文件对应的系统 API 是:
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);在 Java 中,用 MappedByteBuffer 类可以实现同样的目的。
零拷贝
零拷贝的内容存放在零拷贝 中。