Direct & Sync I/O¶
是什么 / 解决什么问题¶
- O_DIRECT:绕过 page cache,直接在用户缓冲区和磁盘间传输数据
- O_SYNC / fsync:保证数据持久化到物理存储
这两者服务于不同目的,可以独立或组合使用。
使用模式¶
O_SYNC — 每次写入都等落盘¶
int fd = open("data", O_WRONLY | O_SYNC);
write(fd, data, len);
// 返回时数据已经在磁盘上(含文件元数据)
// 等价于每次 write 后都调 fsync
变体:
| 标志 | 保证 |
|---|---|
| O_SYNC | 数据 + 元数据都落盘 |
| O_DSYNC | 只保证数据落盘(不含不影响读取的元数据如 mtime) |
fsync / fdatasync — 按需刷盘¶
int fd = open("data", O_WRONLY);
write(fd, data, len); // 快,只到 page cache
write(fd, data2, len2); // 快
fsync(fd); // 慢,等待上面所有写入落盘
// fdatasync:只刷数据,不刷文件大小未变的元数据
fdatasync(fd);
典型使用:WAL (Write-Ahead Log)
O_DIRECT — 绕过 page cache¶
int fd = open("data", O_RDWR | O_DIRECT);
// 关键约束:缓冲区必须对齐!
void *buf;
posix_memalign(&buf, 4096, buf_size); // 4KB 对齐
// 或 aligned_alloc(4096, buf_size);
// 读写的大小和偏移也必须是扇区大小(通常 512 或 4096)的整数倍
pread(fd, buf, 4096, 0); // OK
pread(fd, buf, 1000, 0); // 可能失败 (EINVAL)
pread(fd, buf, 4096, 100); // 可能失败 (EINVAL)
free(buf);
O_DIRECT + O_SYNC 组合¶
行为与语义¶
普通 write 的路径¶
write(fd, buf, 4096)
→ 用户 buf 拷贝到 page cache (CPU 拷贝)
→ 标记页为 dirty
→ 立即返回
→ ... 后台 writeback 线程异步写磁盘 ...
O_DIRECT write 的路径¶
fsync 的语义细节¶
fsync(fd) 保证:
1. fd 关联文件的所有 dirty 页写入磁盘
2. 文件元数据(大小、权限等)写入磁盘
3. 上述操作完成后才返回
但 fsync 不保证目录项(dentry)的持久化!
新创建文件后需要 fsync 父目录:
// 安全地创建新文件
int fd = open("dir/newfile", O_WRONLY | O_CREAT, 0644);
write(fd, data, len);
fsync(fd);
close(fd);
// 还需要 fsync 目录,确保 "newfile" 这个目录项持久化
int dirfd = open("dir", O_RDONLY);
fsync(dirfd);
close(dirfd);
rename + fsync 的安全写入模式¶
// 原子安全写入文件(数据库、配置文件常用)
int fd = open("file.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, data, len);
fsync(fd);
close(fd);
rename("file.tmp", "file"); // 原子替换
int dirfd = open(".", O_RDONLY);
fsync(dirfd); // 持久化 rename 操作
close(dirfd);
性能考量¶
O_DIRECT 的使用场景¶
适合: - 应用层自己管理缓存(数据库:MySQL InnoDB、RocksDB) - 避免 page cache 被大量一次性数据污染 - 超大文件顺序写(cache 反而是负担)
不适合: - 小文件随机读(page cache 的预读和缓存能力被浪费) - 需要 mmap 的场景 - 读取量远大于物理内存的场景(O_DIRECT 没有缓存命中的可能)
fsync 的性能影响¶
优化策略:
// 1. 批量 fsync:攒一批写入后一次 fsync
for (int i = 0; i < batch_size; i++) {
write(fd, entries[i], entry_len);
}
fsync(fd); // 一次 fsync 覆盖整批
// 2. group commit:多个线程的写入合并一次 fsync
// 数据库常用:收集多个事务,一次 fsync 提交
// 3. fdatasync 代替 fsync(如果不关心元数据)
fdatasync(fd); // 通常比 fsync 快(少刷一次 inode)
sync_file_range(精细控制)¶
// Linux 特有:只刷文件的指定范围
sync_file_range(fd, offset, length,
SYNC_FILE_RANGE_WRITE); // 开始回写但不等待
sync_file_range(fd, offset, length,
SYNC_FILE_RANGE_WAIT_BEFORE |
SYNC_FILE_RANGE_WRITE |
SYNC_FILE_RANGE_WAIT_AFTER); // 等价于 fdatasync 该范围
注意:sync_file_range 不保证元数据持久化,不能替代 fsync 做崩溃一致性。
常见陷阱与 FAQ¶
1. O_DIRECT 对齐要求¶
// 必须满足三个对齐:
// 1. 用户缓冲区地址对齐(通常 512 或 4096)
// 2. 读写偏移对齐
// 3. 读写长度对齐
// 不对齐 → EINVAL
// 查询设备的对齐要求
struct statfs sfs;
fstatfs(fd, &sfs);
// sfs.f_bsize 即文件系统块大小
2. fsync 可能失败(且状态不可恢复)¶
if (fsync(fd) == -1) {
// EIO: 写回失败
// 此时脏页可能已被丢弃!重试 fsync 不会重新写入
// PostgreSQL 因此问题修改了崩溃恢复策略
// 正确做法:视为数据损坏,从 WAL 恢复
}
3. O_DIRECT 和 page cache 不一致¶
// 进程 A: O_DIRECT 写入
// 进程 B: 普通 read(从 page cache 读)
// B 可能读到过期数据!
// O_DIRECT 写绕过 page cache,不会更新 cache 中的旧页
// 同一文件不要混用 O_DIRECT 和 buffered I/O
4. O_SYNC 不等于 O_DIRECT¶
| O_SYNC | O_DIRECT | |
|---|---|---|
| 绕过 cache | 否(写入 page cache 后再刷盘) | 是 |
| 保证落盘 | 是 | 否(只保证绕过 cache,磁盘可能有写缓存) |
两者解决不同问题,需要根据需求组合。
观测与调试¶
# 区分 buffered 和 direct I/O
strace -e trace=openat ./app 2>&1 | grep O_DIRECT
# 查看 page cache 写回活动
cat /proc/vmstat | grep -E "nr_dirty|nr_writeback"
# 查看 fsync 延迟
bpftrace -e '
tracepoint:syscalls:sys_enter_fsync { @start[tid] = nsecs; }
tracepoint:syscalls:sys_exit_fsync /@start[tid]/ {
@fsync_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# 查看磁盘实际写入量(区分 cache 写和真实磁盘写)
iostat -x 1
# wkB/s: 实际写入磁盘的 KB/s
延伸阅读¶
man 2 open— O_DIRECT, O_SYNC, O_DSYNC 说明man 2 fsync/man 2 fdatasyncman 2 sync_file_range- LWN: Ensuring data reaches disk