系统调用 fsync、fwrite、fflush、mmap、write barriers
1、各系统调用介绍
大部分内容来自百度百科。
1.1、fsync
调用 fsync 可以保证文件的修改时间也被更新。fsync 系统调用可以使您精确的强制每次写入都被更新到磁盘中。您也可以使用同步(synchronous)I/O 操作打开一个文件,这将引起所有写数据都立刻被提交到磁盘中。通过在 open 中指定 O_SYNC 标志启用同步I/O。
1.2、fwrite
fwrite()
是 C 语言标准库中的一个文件处理函数,功能是向指定的文件中写入若干数据块,如成功执行则返回实际写入的数据块数目。该函数以二进制形式对文件进行操作,不局限于文本文件。
1.3、write
write 和 fwrite 不一样。write 用来直接将文件从应用程序直接写入到内核缓冲区。fwrite 适用于需要处理大量数据的情况,并且希望通过标准 I/O 库提供的高级功能来管理数据写入。而 write 适用于需要直接将数据写入文件或设备,更加底层和高效的场景。
1.4、fclose
fclose 会将缓冲区的数据刷新到内核缓冲区,并释放相关资源。内核会负责将数据从内核缓冲区写入磁盘。因此 fclose 不会将数据直接写入到文件。
1.5、fflush
fflush 是一个在C语言标准输入输出库中的函数,功能是冲洗流中的信息,该函数通常用于处理磁盘文件。fflush 会强迫将缓冲区内的数据写回参数 stream 指定的文件中。
总结
read/write/fsync:
- linux 底层操作;
- 内核调用,涉及到进程上下文的切换,即用户态到核心态的转换,这是个比较消耗性能的操作。
fread/fwrite/fflush:
- c 语言标准规定的 IO 流操作,建立在read/write/fsync之上
- 在用户层,又增加了一层缓冲机制,用于减少内核调用次数,但是增加了一次内存拷贝。
1.6、mmap
mmap 将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。mmap 在用户空间映射调用系统中作用很大。
2、各系统调用之间的区别
其实熟悉的朋友都知道每个调用的作用是什么,但是他们在底层到底是一个什么关系,估计很多同学还是无法说清楚。为了搞清楚这些问题,我参考网上大牛的资料,画了下面这张图:
通过上图,我们可以清楚的了解到,在整个文件写入的过程中,其需要经过很多个 buffer 缓存。如 IO Buffer、Page Cache、驱动缓存、Disk cache。所有这些缓存存在的意义都是为了提升我们的文件读写速度。但是在我们需要确保数据百分之百安全的场景下(如WAL),这些 buffer 就成了一个一个的障碍。为了让数据从应用层直接万无一失的写入磁盘,我们需要合理的利用上面提到的各类方法调用。根据不同的业务需求我们可以采用不同的方法组合。
2.1、允许应用奔溃的写操作
通过上图可知只有数据写入内核的 Page Cache 中之后,应用崩溃才不会导致数据丢失。通常我们有两个种方式可以将数据写入到内核中。
A、普通写入(write/flush/close)
咱们平时调用 fwrite 的时候,数据仅仅是从应用写入到了C标准库的IO Buffer。此时数据还在用户空间。如果此时我们就调用 close 关闭操作。那么数据通常不会理解写入内核,更不用说磁盘了。通常需要等到C标准库的 IO Buffer 满了之后才能被主动写入内核缓存的 Page Cache。通过上图我可以看到,我们可以通过 flush 将数据主动写入到内核的 Page Cache 中。这就是为什么我们通常被建议在关闭之前 flush 文件。因为当数据进入内核之后相对于应用来说,该数据就是安全的。此时如果应用挂了,咱们的数据还是安全的。且能够被内部后续写入磁盘。
B、mmap
在持久化中经常被提及的mmap的数据其实也只是在Application Cache和内核Page Cache中建立了映射关系。这样所有在应用层对数据的操作实际是映射到内核的Page Cache中的。因此使用mmap我们不用调用flush,也不用担心数据会因为应用崩溃而丢失。
当然mmap除了能够直接在应用层操作内核中的数据,同时也因此减少了不必要的上下文切换。比如普通写入中,我们调用flush是需要相应的上下文切换呢,这里会有一定的开销。这也是为什么在持久化场景中,我们通常使用mmap的主要原因。
2、允许操作系统崩溃的写操作
通过上图可知,只有当数据被写入到磁盘缓存或者磁盘介质中之后,才能够保证当系统崩溃之后,数据不会丢失(如果数据在磁盘缓存中,则需要磁盘具有备份电源)。
那么需要将内核中的Page Cache中的数据写入到磁盘(缓存)中,我们只需要调用fsync()即可。此时就算机器宕机了,咱们的数据还是安全的。这也就是很多WAL都是fsync刷盘的原因。
3、磁盘缓存中的数据落盘
通过上面1和2的操作后,数据就已经进入磁盘了。但是却并不能保证数据百分之百的落盘成功。有可能数据在磁盘的缓存中。此时如果机器掉电了,那我们的数据也就可能会丢失。针对该问题,目前主要有两种解决办法:备用电源和开启OS的 Write Barriers。
A、备用电源
商用磁盘很多都自带有备用电源,当机器断电后,能够根据依靠备用电源将缓存中的数据落盘。
B、Write Barriers
在linux中文件系统ext3或者ext4又被称为日志文件系统。原因是因为其写数据的的时候也有个类似WAL的操作。
如上图,在日志文件系统中,磁盘大概是上述结构。数据在写入的时候,首先会写入缓存,然后将这次数据写操作的元数据(根据该数据记录了数据的所有修改记录)先写入到磁盘介质中,最后在写入一个commit record标记表示日志已经写完了,此时数据已经安全了。这个时候写入指令就返回了。如fsync指令,当commit record标记写入后,就返回了。但此时真实的数据还在缓存中。但是就算此时磁盘掉电,重启之后磁盘也能够根据记录的日志恢复该数据。同时记录日志和commit record的空间都是连续的,因此写入速度回很快。这也就是日志文件系统如何做到快速写入且数据不会因掉电而丢失的。其实也是我们平常的WAL思想。
但是这里有个小问题,那就是日志和commit record都是交给驱动执行的写操作,而现代驱动基本都会对所有写入进行重排序从而提高写入性能。此时就可能就将日志和commit record重排序,导致commit record先落盘,日志再后面。这样的处理会导致,如果commit record落盘后,磁盘掉电,此时由于日志没有写入,导致数据无法恢复。
于是文件系统采用了write barriers。在每次写入commit record之前加入write barriers,该barriers可以确保其后面的数据在写入前,其前面的数据都已经落盘了。这样就保证了日志和commit record不会被重排序,且能够正确落盘。
四、疑问解答
fsync 和 fwrite/fflush 组合的区别是啥?
fwrite 和 fllush 组合是将数据从应用层写入C标准库 Buffer,然后刷新到内核的 Page Cache 中。
fsync 是将内核 Page Cache 中的数据写入磁盘(并不一定会落盘到介质中)。
mmap 和 fsync 有什么关系?
mmap:在 application 和内核的 Page Cache 之前建立映射。使得应用程序在应用层可以直接操作内核 Page Cache 中的数据。
fsync:将内核 Page Cache 中的数据刷入磁盘。
为什么都说 fsync 之后数据就不会丢失了,真的不会丢失么?
我们知道 fsync 的功能,是将内核中的数据直接刷盘。但是其刷盘之后数据并不一定就安全了。首先如果文件系统没有 Write Barriers,或者没有开启 Write Barriers。且磁盘也没有备用电源。那么如果系统宕机(掉电)之后,还在磁盘缓存中的数据就会丢失。所以 fsync 并不一定能确保数据不丢失。
数据写入磁盘就安全了么?
这具体要看数据写入到磁盘的哪个位置。如果只是在磁盘缓存中,则可能存在风险。如果已经落盘或者成功写日志和 Commit record 则是安全的。
其实就算数据真正落盘到介质了,也不一定是安全的。因为磁盘可能会损坏什么的。但这已经超出了本文的讨论范围,我们就不深入讨论了。
为什么不能直接 close 文件,而需要先 flush
因为通过 write 写入的数据实际还在应用的缓存中,此时如果 flcose 文件。则可能由于应用崩溃导致数据丢失。所以在 close 之前,需要通过flush将数据刷新到内核的 page cache 中。
Reference
Linux IO流程:Linux IO流程-CSDN博客
Linux OS: Write Barriers:Linux OS: Write Barriers_罗索
Barriers and journaling filesystems:Barriers and journaling filesystems [LWN.net]