Linux I/O操作fsync后数据就安全了么(fsync、fwrite、fflush、mmap、write barriers详解)

1,895 阅读6分钟

这是我参与8月更文挑战的第28天,活动详情查看:8月更文挑战

一、前言

前段时间一直在研究磁盘顺序写和随机写,以及Java直接内存相关的问题,于是在各类资料或者源码中常常看到flush、mmap等概念和相关使用。然后开始各个击破,一个一个去理解其含义。终于都理清楚之后,回来总结却发现,自己越来越糊涂了。于是产生了如下疑问:

1、fsync和fwrite/fflush组合的区别是啥?

2、mmap和fsync有什么关系?

3、为什么都说fsync之后数据就不会丢失了,真的不会丢失么?

4、数据写入磁盘就安全了么?

5、为什么不能直接close文件,而需要先flush?

带着这些问题我又开始了漫长的研究之路,终于对这些方法和调用有了全新的认识。接下来我们就一起来慢慢分析,并最终解答上述问题。

PS:由于网上很难找到这些对比参照的详细介绍,本文后续内容都是自己通过各类子类整理之后的结果,可能存在理解不到位的地方,请各位大牛多多指正。

二、各系统调用介绍

大部分内容来自百度百科。

1、fsync

调用 fsync 可以保证文件的修改时间也被更新。fsync 系统调用可以使您精确的强制每次写入都被更新到磁盘中。您也可以使用同步(synchronous)I/O 操作打开一个文件,这将引起所有写数据都立刻被提交到磁盘中。通过在 open 中指定 O_SYNC 标志启用同步I/O。

2、fwrite

fwrite() 是 C 语言标准库中的一个文件处理函数,功能是向指定的文件中写入若干数据块,如成功执行则返回实际写入的数据块数目。该函数以二进制形式对文件进行操作,不局限于文本文件。

3、fflush

fflush是一个在C语言标准输入输出库中的函数,功能是冲洗流中的信息,该函数通常用于处理磁盘文件。fflush()会强迫将缓冲区内的数据写回参数stream 指定的文件中。

4、mmap

mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。mmap在用户空间映射调用系统中作用很大。

三、各系统调用之间的区别

其实熟悉的朋友都知道每个调用的作用是什么,但是他们在底层到底是一个什么关系,估计很多同学还是无法说清楚。为了搞清楚这些问题,我参考网上大牛的资料,画了下面这张图:

 通过上图,我们可以清楚的了解到,在整个文件写入的过程中,其需要经过很多个buffer缓存。如IO Buffer、Page Cache、驱动缓存、Disk cache。所有这些缓存存在的意义都是为了提升我们的文件读写速度。但是在我们需要确保数据百分之百安全的场景下(如WAL),这些buffer就成了一个一个的障碍。为了让数据从应用层直接万无一失的写入磁盘,我们需要合理的利用上面提到的各类方法调用。根据不同的业务需求我们可以采用不同的方法组合。

1、允许应用奔溃的写操作

通过上图可知只有数据写入内核的Page Cache中之后,应用崩溃才不会导致数据丢失。通常我们有两个种方式可以将数据写入到内核中。

A、普通写入(write/flush/close)

咱们平时调用write(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(fdatasync)即可。此时就算机器宕机了,咱们的数据还是安全的。这也就是很多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不会被重排序,且能够正确落盘。

四、疑问解答

到此我们就对整个OS IO操作流程有了全面的了解。现在我们再回过头来看看咱们开始提出的问题,是不是这些问题都能够很好的解答了呢?

1、fsync和fwrite/fflush组合的区别是啥?

fwrite和fllush组合是将数据从应用层写入C标准库Buffer,然后刷新到内核的Page Cache中。

fsync是将内核Page Cache中的数据写入磁盘(并不一定会落盘到介质中)。

2、mmap和fsync有什么关系?

mmap:在application和内核的Page Cache之前建立映射。使得应用程序在应用层可以直接操作内核Page Cache中的数据。

fsync:将内核Page Cache中的数据刷入磁盘。

3、为什么都说fsync之后数据就不会丢失了,真的不会丢失么?

我们知道fsync的功能,是将内核中的数据直接刷盘。但是其刷盘之后数据并不一定就安全了。首先如果文件系统没有Write Barriers,或者没有开启Write Barriers。且磁盘也没有备用电源。那么如果系统宕机(掉电)之后,还在磁盘缓存中的数据就会丢失。所以fsync并不一定能确保数据不丢失。

4、数据写入磁盘就安全了么?

这具体要看数据写入到磁盘的哪个位置。如果只是在磁盘缓存中,则可能存在风险。如果已经落盘或者成功写日志和Commit record则是安全的。

其实就算数据真正落盘到介质了,也不一定是安全的。因为磁盘可能会损坏什么的。但这已经超出了本文的讨论范围,我们就不深入讨论了。

5、为什么不能直接close文件,而需要先flush

因为通过write写入的数据实际还在应用的缓存中,此时如果flcose文件。则可能由于应用崩溃导致数据丢失。所以在close之前,需要通过flush将数据刷新到内核的page cache中。

参考:

Linux IO流程:blog.csdn.net/caogenwangb…

Linux OS: Write Barriers:www.rosoo.net/a/201211/16…

Barriers and journaling filesystems:lwn.net/Articles/28…

五、惯例

如果你对本文有任何疑问或者高见,欢迎添加公众号lifeofcoder共同交流探讨。