Linux磁盘IO
前言
做存储开发,必然绑定在 I/O 路径上。现代高级语言和标准库往往将底层细节封装得很好,降低了开发门槛,但也让我们对数据从应用到磁盘这条链路上的每一层缓冲缺乏清晰认知。本文从用户态到内核态,逐层梳理 Linux 磁盘 I/O 涉及的关键系统调用及其语义差异,帮助理解"数据到底什么时候才算安全落盘"。
I/O 路径概览
一次典型的写操作,数据会经过以下层次:
应用程序 → C 标准库 IO Buffer → 内核 Page Cache → 磁盘控制器 Write Cache → 持久化介质每一层缓冲都是为了提升性能而存在的,但也意味着在任何一层发生故障时,尚未向下刷写的数据都可能丢失。理解各层的边界和刷写机制,是保证数据可靠性的基础。
用户态 I/O 接口
fwrite
fwrite 是 C 标准库(libc)提供的缓冲写入函数。调用 fwrite 后,数据仅被写入用户态的 stdio 缓冲区(即 FILE 结构体内部维护的 buffer),并不会立即触发系统调用。这意味着:如果进程在 fwrite 之后、fflush 之前崩溃,这部分数据将彻底丢失——因为它从未离开过进程的地址空间。
fflush
fflush 的作用是将 stdio 缓冲区中的数据通过 write(2) 系统调用提交给内核。调用成功后,数据进入内核的 Page Cache。此时进程崩溃不会导致数据丢失,但如果发生内核 panic 或断电,Page Cache 中尚未回写的数据仍然会丢失。
需要注意的是,fflush 只保证数据从用户态缓冲区交付给内核,不保证数据落盘。
write
write(2) 是 POSIX 系统调用,直接将数据从用户空间拷贝到内核的 Page Cache。与 fwrite 不同,write 不经过 stdio 缓冲区,但默认情况下同样不保证数据立即写入磁盘——内核会在合适的时机(如 dirty page 超时、内存压力等)异步回写。
内核态刷盘机制
fsync
fsync(2) 将指定文件描述符关联的所有已修改数据(包括文件内容和元数据,如文件大小、修改时间等)从 Page Cache 刷写到持久化存储设备,并等待设备确认写入完成后才返回。这是最严格的落盘保证。
int fd = open("data.log", O_WRONLY | O_CREAT, 0644);
write(fd, buf, len);
fsync(fd); // 返回后,数据和元数据均已持久化
fdatasync
fdatasync(2) 与 fsync 类似,但只保证文件数据落盘,不强制刷写元数据——除非元数据的变更会影响后续数据读取(例如文件大小发生了变化)。对于频繁追加写入的场景,fdatasync 可以减少不必要的元数据 I/O,性能优于 fsync。
sync
sync(2) 将系统中所有文件的脏页提交回写,但不等待 I/O 完成即返回。它是一个全局操作,粒度粗,通常不适合应用程序使用。Linux 内核的 pdflush/writeback 线程也会周期性地执行类似操作。
特殊打开标志
O_SYNC
以 O_SYNC 标志打开文件后,每次 write 调用都等价于 write + fsync:数据和元数据写入持久化存储后 write 才返回。语义上最安全,但性能开销最大,因为每次写入都是同步 I/O。
O_DSYNC
与 O_SYNC 类似,但语义等价于 write + fdatasync:只保证数据落盘,不强制刷写非必要的元数据。性能略优于 O_SYNC。
O_DIRECT
O_DIRECT 绕过内核 Page Cache,数据直接在用户空间缓冲区和块设备之间传输(DMA)。使用 O_DIRECT 时需要注意:内存缓冲区、文件偏移量和传输长度通常需要按扇区大小(一般为 512 字节)对齐。
重要区别:O_DIRECT 绕过了 Page Cache,但并不保证数据落盘。数据仍然可能停留在磁盘控制器的 Write Cache 中。要真正保证持久化,仍需配合 fsync 或 O_SYNC 使用。
mmap
mmap(2) 将文件映射到进程的虚拟地址空间,之后对该内存区域的读写操作等价于对文件的读写。写入映射区域的数据直接进入内核 Page Cache(因为映射的页本身就是 Page Cache 中的页),不经过 stdio 缓冲区,也不需要显式调用 write。
mmap 写入的数据同样不会立即落盘,内核会异步回写。如果需要保证持久化,应调用 msync(2):
msync(addr, length, MS_SYNC); // 等待映射区域的数据落盘
各层关系总结
上图清晰地展示了 Linux I/O 各层之间的数据流向。总结如下:
fwrite将数据写入用户态 stdio 缓冲区。进程崩溃则数据丢失。fflush通过write系统调用将 stdio 缓冲区的数据提交到内核 Page Cache。进程崩溃不丢数据,但内核崩溃或断电会丢。mmap映射的文件,写操作直接作用于 Page Cache,效果等同于第 2 步之后的状态。fsync/fdatasync将 Page Cache 中的脏页刷写到持久化存储设备并等待完成。这是应用层能获得的最强落盘保证。
关于磁盘 Write Cache
需要特别指出的是,即使 fsync 返回成功,数据是否真正写入了非易失性介质,还取决于磁盘控制器的 Write Cache 策略。如果磁盘开启了 Write Cache 且没有电池保护(BBU),断电时 Write Cache 中的数据仍可能丢失。对于数据库等对持久性要求极高的场景,通常需要关闭磁盘 Write Cache,或使用带 BBU 的 RAID 卡来保证 fsync 的语义完整性。
可以通过以下命令查看和设置磁盘 Write Cache:
# 查看 Write Cache 状态
hdparm -W /dev/sda
# 关闭 Write Cache
hdparm -W 0 /dev/sda