第 13 章 文件 I/O 缓冲 (File I/O Buffering)
核心结论
-
两层缓冲:内核 buffer cache(page cache)+ stdio 缓冲;两者都会缓冲数据;理解它们是优化 I/O 性能的关键。
-
Buffer Cache 行为:
read/write不直接操作磁盘——数据先进入内核 page cache;内核异步刷盘;多次小写合并为大写。 -
Buffer Cache 大小影响性能:大 buffer 减少系统调用次数,性能显著提升;1 字节 vs 4096 字节差 50 倍(§13.1 表 13-1)。
-
stdio 缓冲:
setvbuf控制缓冲模式——_IONBF(无缓冲)、_IOLBF(行缓冲,终端默认)、_IOFBF(全缓冲,磁盘文件默认);BUFSIZ8192 字节。 -
fflush:强制 stdio 输出缓冲写入内核;
fflush(NULL)刷新所有流;输入流调用则丢弃缓冲。 -
同步 I/O:
fsync(fd)强制数据落盘;fdatasync只同步数据不同步元数据;sync()同步所有;O_SYNC/O_DSYNC/O_SYNCflag 控制写入语义。 -
直接 I/O:
O_DIRECT绕过 buffer cache,直接 DMA 到用户缓冲区;适合数据库等需要完全控制 I/O 的程序;要求对齐(通常 512 字节)和 size 限制。
|
本章主旨
本章是第 4-5 章文件 I/O 的延伸。核心:理解内核 buffer cache + stdio 缓冲的双层结构;理解为何「小写不如大写快」「O_SYNC 影响性能」「何时需要直接 I/O」。本章不展开内存映射 I/O(详见第 49 章)、异步 I/O(详见第 63 章)、IO 调度(详见第 14 章)。 |
一、核心概念
本章围绕 6 个核心概念展开:从「内核缓冲」到「stdio 缓冲」再到「同步与直接 I/O」。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
Buffer Cache (Page Cache) |
内核维护的数据缓存; |
§13.1;性能关键——大 buffer 减少 syscall 50 倍。 |
stdio 缓冲 |
C 标准库的 I/O 缓冲;默认全缓冲(磁盘文件)、行缓冲(终端)、无缓冲(stderr); |
§13.2; |
Buffer 大小与性能 |
大 buffer 显著提升 I/O 性能;4096 字节接近最优;过小(1 字节)性能极差(50-100 倍差距)。 |
§13.1 表 13-1;4096 字节是常见文件系统块大小。 |
同步 I/O |
|
§13.3; |
直接 I/O (O_DIRECT) |
绕过 buffer cache;用户缓冲区直接 DMA 到设备;要求对齐(512/4096 字节)和 size 匹配;数据库常用。 |
§13.6;绕过 OS 缓存——应用程序需自己管理缓存。 |
性能权衡 |
缓冲提升吞吐但增加延迟(崩溃时数据可能丢失);同步保证安全但慢;直接 I/O 提供控制但责任大。 |
§13.1-§13.6;根据场景选择:日志 → fsync;批量处理 → 大 buffer;数据库 → O_DIRECT。 |
二、详细笔记
13.1 内核 Buffer Cache
What:内核为文件 I/O 维护的内存缓存(page cache);read/write 不直接操作磁盘;数据先进入 cache,再异步刷盘。
Why:理解 buffer cache 是理解「为什么 write 立即返回」「为什么 read 第二次快」「为什么崩溃时数据可能丢失」的关键。
How:
| 操作 | 内核动作 | 性能影响 |
|---|---|---|
|
先查 page cache;命中则拷贝到用户 buf;未命中则读磁盘到 cache,再拷贝 |
命中是 μs 级;未命中是 ms 级 |
|
数据从用户 buf 拷贝到 page cache;返回;内核异步刷盘 |
write 立即返回;崩溃时未刷盘数据丢失 |
读命中 |
第二次读同一文件 → 从 cache 返回 |
极快 |
写合并 |
多次小 write → 内核合并为大块异步写 |
减少磁盘 I/O 次数 |
Buffer Cache 行为细节(§13.1):
-
Linux 2.4+:不再有独立的「buffer cache」——文件 I/O 缓存合并到 page cache,与 mmap 文件共享。
-
大小无固定上限——按需扩展;受可用内存限制。
-
内存紧张时——内核刷脏页到磁盘,释放 cache 页。
性能数据(§13.1 表 13-1,100MB 文件复制):
| BUF_SIZE | Elapsed (秒) |
|---|---|
System CPU (秒) |
1 |
107.43 |
99.12 |
16 |
7.50 |
6.63 |
256 |
2.06 |
1.65 |
4096 |
2.05 |
0.38 |
65536 |
2.06 |
0.32 |
观察:
-
1 字节 → 65536 字节,性能提升 50 倍。
-
4096 字节接近最优(与文件系统块大小匹配)。
-
超过 4096 字节后——性能提升微小(主要瓶颈是磁盘)。
When:
-
大数据复制——用 4KB+ buffer。
-
频繁小写——合并为大块 write。
-
实时性要求高——
O_SYNC或fsync。
Example:
// 摘自《The Linux Programming Interface》第 4 章 fileio/copy.c
// 推荐 buffer 大小
#define BUF_SIZE 4096 // 一次读 4KB
char buf[BUF_SIZE];
ssize_t numRead;
while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0)
if (write(outputFd, buf, numRead) != numRead)
fatal("couldn't write whole buffer");
13.2 stdio 缓冲
What:C 标准库为 FILE* 流提供的缓冲层;缓冲模式由 setvbuf 设置;fflush 强制刷出。
Why:stdio 缓冲让 fprintf 等高级 I/O 与裸 read/write 一样高效;理解何时该 fflush 是写交互式程序的关键。
How:
// 摘自《The Linux Programming Interface》第 13 章
#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
// mode: _IONBF / _IOLBF / _IOFBF
// 必须在任何其他 stdio 操作前调用
void setbuf(FILE *stream, char *buf);
// 简化版:buf == NULL → 无缓冲;否则全缓冲
void setbuffer(FILE *stream, char *buf, size_t size);
// 类似 setbuf 但指定 size
int fflush(FILE *stream);
// 强制刷出;stream == NULL 刷所有
缓冲模式:
| 模式 | 行为 |
|---|---|
默认场景 |
|
无缓冲;每次 stdio 调用立即 read/write |
stderr |
|
行缓冲;遇 |
终端(stdin/stdout) |
|
全缓冲;缓冲区满或显式 fflush 才刷出 |
磁盘文件 |
BUFSIZ:默认 buffer 大小;glibc 8192 字节。
When:
-
交互式提示——
printf("Enter: ");后立即fflush(stdout)(行缓冲到终端会自动刷)。 -
关键日志——每次写后
fflush或用setvbuf设为无缓冲。 -
性能——大块用全缓冲;小块用无缓冲避免缓冲开销。
Example:
// 摘自《The Linux Programming Interface》第 13 章
// 配置 stdout 为无缓冲(错误日志)
if (setvbuf(stdout, NULL, _IONBF, 0) != 0)
errExit("setvbuf");
// 自定义缓冲区
static char buf[4096];
if (setvbuf(fp, buf, _IOFBF, sizeof(buf)) != 0)
errExit("setvbuf");
13.3 同步 I/O 完成
What:SUSv3 定义「同步 I/O 完成」——保证数据已被传输(或诊断为失败);区分「data integrity」(数据完整性)和「file integrity」(文件完整性,包括元数据)。
Why:数据库、日志等需要崩溃时数据不丢——必须同步到磁盘。
How:
| 类型 | 含义 |
|---|---|
synchronized I/O data integrity |
read:数据已被读;pending write 已落盘 |
write:数据和检索数据所需元数据已落盘 |
synchronized I/O file integrity |
read:同上 |
write:数据 + 所有元数据(atime/mtime 等)已落盘 |
相关调用(§13.3):
// 摘自《The Linux Programming Interface》第 13 章
#include <unistd.h>
int fsync(int fd);
// 强制 fd 的所有数据 + 元数据落盘;慢(毫秒级)
// 成功返回 0
int fdatasync(int fd);
// 只强制数据落盘;跳过非必要元数据(如 atime)
// 略快于 fsync
void sync(void);
// 同步所有文件的 buffer cache;不等待
O_SYNC flag(§13.3):
// 摘自《The Linux Programming Interface》第 4 章
// 每次 write 都同步落盘
int fd = open("log", O_WRONLY | O_CREAT | O_APPEND | O_SYNC, 0644);
// 每次 write 阻塞直到数据落盘;慢但安全
// O_DSYNC 类似但只同步数据,不同步元数据(更快)
int fd = open("log", O_WRONLY | O_CREAT | O_APPEND | O_DSYNC, 0644);
When:
-
关键日志——
O_DSYNC或定期fsync。 -
数据库事务提交——
fsync保证持久性。 -
普通配置文件——退出前
fsync。 -
临时文件——不需要 fsync(崩溃丢失可接受)。
Example:
// 摘自《The Linux Programming Interface》第 13 章
// 关键日志
int fd = open("important.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
write(fd, "critical event\n", 16);
fsync(fd); // 确保已落盘
close(fd);
13.4 直接 I/O (O_DIRECT)
What:O_DIRECT flag 让 read/write 绕过内核 buffer cache;用户缓冲区直接 DMA 到磁盘(需要硬件支持)。
Why:数据库等需要完全控制缓存——避免「双缓存」(OS 缓存 + 应用缓存)浪费内存。
How:
// 摘自《The Linux Programming Interface》第 13 章
#define _GNU_SOURCE
#include <fcntl.h>
int fd = open("file", O_WRONLY | O_DIRECT | O_SYNC, 0644);
// 绕过 buffer cache
// 要求:
// 1. 用户缓冲区按设备块大小对齐(512/4096 字节)
// 2. write size 是块大小整数倍
// 3. 文件 offset 是块大小整数倍
约束(§13.6):
-
缓冲区对齐:
posix_memalign(&buf, 512, size)或aligned_alloc(512, size)。 -
size 必须是块大小整数倍——否则
EINVAL。 -
offset 必须是块大小整数倍——否则
EINVAL。 -
不支持 mmap。
When:
-
数据库(PostgreSQL、MySQL InnoDB 等)——管理自己的缓存。
-
高性能科学计算——避免数据复制。
-
不适合普通应用——OS 缓冲已经很快。
Example:
// 摘自《The Linux Programming Interface》第 13 章
#define _GNU_SOURCE
#include <fcntl.h>
// 4KB 对齐的缓冲区
void *buf;
if (posix_memalign(&buf, 4096, 4096) != 0) errExit("posix_memalign");
memset(buf, 'A', 4096);
int fd = open("data.bin", O_WRONLY | O_CREAT | O_TRUNC | O_DIRECT, 0644);
if (fd == -1) errExit("open");
if (write(fd, buf, 4096) != 4096) errExit("write");
fsync(fd);
close(fd);
free(buf);
13.5 系统调用开销
What:每次系统调用都有开销(用户态↔内核态切换、参数检查、数据拷贝);系统调用比普通函数调用慢一个数量级。
Why:理解开销能解释「为什么大 buffer 性能好」「为什么 syscall 密集型应用考虑 vDSO/io_uring」。
How:
开销构成(§3.1):
-
CPU 模式切换(保存/恢复寄存器):约 0.1-0.3 μs。
-
参数检查:地址验证等。
-
数据拷贝:用户态↔内核态缓冲区。
-
返回值设置。
减少系统调用的方法:
-
大 buffer 一次读/写(§13.1)。
-
readv/writev:单次调用处理多缓冲区(§5.7)。 -
mmap:减少用户态/内核态拷贝(§49 章)。 -
io_uring:Linux 5.1+ 异步 I/O 框架(超出 TLPI 范围)。
When:
-
性能敏感——
time命令对比不同 buffer 大小。 -
大量小 I/O——
readv/writev或mmap。
Example:从 §3.1 实测:1000 万次 getppid() 约 2.2 秒(≈0.3 μs/次);普通 C 函数返回整数只需 0.11 秒(≈0.01 μs/次)。
13.6 stdio 与系统调用的关系
What:stdio 函数(fopen/fread/fprintf 等)在底层调用 read/write;stdio 缓冲减少系统调用次数。
Why:理解 stdio 与 syscall 的关系能选择合适的 I/O 接口。
How:
-
fopen → open:
fopen调用open;返回 FILE*。 -
fread → read:从 stdio 缓冲读;缓冲空时调
read填满。 -
fwrite → write:写入 stdio 缓冲;缓冲满或 fflush 时调
write。 -
fclose → close:fflush 后 close。
混合使用的问题:
-
用
read(fd, …)绕过 stdio 缓冲——之后fgets看不到这些数据(仍在内核缓冲,但 stdio 缓冲有自己的视图)。 -
用
fileno(fp)获取 FILE* 对应的 fd;fdopen(fd, mode)反向。
When:
-
纯文件复制——
open/read/write/close+ 大 buffer(最高效)。 -
格式化输出——
fprintf(printf 已内置缓冲)。 -
避免混用——同一文件描述符用同一层 API。
三、关键图表
|
非可视化条目(缓冲 API 速查)
|
四、思维导图
mindmap
root((第 13 章 文件 I O 缓冲))
内核 buffer cache
page cache
write 立即返回
异步刷盘
大小无限制
性能
buffer 大小
4096 最优
50 倍差距
stdio 缓冲
_IONBF 无
_IOLBF 行 终端
_IOFBF 全 磁盘
setvbuf fflush
同步 I O
fsync
fdatasync
sync
O_SYNC O_DSYNC
data file integrity
直接 I O
O_DIRECT
对齐要求
块大小倍数
数据库场景
系统调用开销
模式切换
数据拷贝
readv writev
stdio 与 syscall
fopen open
fread read
fwrite write
避免混用
五、重点与易错点
-
write 不等于落盘:数据进入 page cache;崩溃时丢失;用 fsync/O_SYNC 保证。
-
大 buffer 性能显著提升:1 字节 → 4096 字节,性能提升约 50 倍;推荐 4096 字节。
-
stdio 默认缓冲策略:磁盘文件全缓冲、终端行缓冲、stderr 无缓冲。
-
交互式提示需要 fflush:
printf("> ");不立即显示(行缓冲未触发);加fflush(stdout)或用 stderr。 -
O_SYNC 显著降低 write 性能:毫秒级延迟;只在「数据必须落盘」时用。
-
O_DIRECT 不是「更快」:绕过 OS 缓存;数据库自己管理缓存才有意义;普通应用用 OS 缓存更好。
-
O_DIRECT 要求严格对齐:用户缓冲区、offset、size 都必须是设备块大小的整数倍;否则 EINVAL。
-
混用 read 和 fgets:绕过 stdio 缓冲的 read 数据对 fgets 不可见;保持单一 I/O 层。
-
Linux 2.4+ 已无独立 buffer cache:统一为 page cache,与 mmap 共享。
-
崩溃恢复策略:关键数据用 fsync;可重建数据不用 fsync;批量操作后台 fsync。
-
跨章衔接:第 4-5 章是文件 I/O 基础;第 6 章是进程;第 49 章是 mmap;本章是缓冲机制。