翻译自:https://lwn.net/Articles/457667/
在一个完美的世界,没有操作系统崩溃,停电或者磁盘故障,程序员不需要为这些临界状况而担心。不幸的是,这些错误出现的可能性比我们期待的要常见的多。这个文档的目的是描述数据从应用到存储走过的路径,主要关注数据缓存,然后提供一个最佳的实践方式来确保数据可以保存到稳定的存储上面,从而保证在不良事件发生的时候数据不在路径上丢失。我们主要关心C语言,尽管所有提到的系统调用应该可以相当简单的翻译成其它语言。
I/O缓存
为了从数据完整性的角度进行编程,很重要的一点是理解整个系统的架构。如图,数据会通过几层,在最后进入稳定存储之前。为了方便讨论,我们把I/O分成三类:系统I/O,流I/O,内存映射(mmap)I/O。
系统I/O可以被定义为通过内核系统调用接口通过内核地址空间把数据写到存储层。以下程序(不是综合的,我们只关心写操作)是部分系统调用接口:
open | open(), creat() |
---|---|
write | write(), aio_write(), pwrite(), pwritev() |
sync | fsync(), sync() |
close | close() |
流I/O是通过C库流接口发起的I/O。用这些函数并不是一定会调用系统调用,也就是说在做了这样一个调用之后数据还是留在应用地址空间中的缓存当中。以下的库函数是部分的流接口:
Open | fopen(), fdopen(), freopen() |
Write | fwrite(), fputc(), fputs(), putc(), putchar(), puts() |
Sync | fflush(), followed by fsync() or sync() |
close | fclose() |
Memory mapped文件和上面系统I/O的例子类似。文件中的数据被映射到进程的地址空间,然后就像操作应用的buffer一样进行内存读写。
Open | open(), creat() |
Map | mmap() |
Write | memcpy(), memmove(), read(), 或者其它写到应用内存的程序 |
Sync | msync() |
Unmap | munmap() |
Close | close() |
在打开文件的时候有两个标志可以改变缓存行为,O_SYNC(以及相关的O_DSYNC)和O_DIRECT。在打开的时候用O_DIRECT标志的话,进行I/O操作会绕过内核页缓存直接写到存储上面。我们提到过村村自己本省可能把数据放到写回缓存上面,所以fsync()还是需要,如果用了O_DIRECT标志但是想把数据存到稳定存储上。O_DIRECT标志只和系统I/O API相关。
raw devices是O_DIRECT I/O的一个特例。这些设备不需要用O_DIRECT标志去打开,但是却还是有direct I/O的语义。同样的,所有的适用于raw device的规则也适用于用O_DIRECT打开的文件。
同步I/O(不管是否有O_DIRECT的系统I/O或者流IO)是任何通过O_SYNC或者O_DSYNC标志打开的文件描述符上面进行的I/O。这些同步模式由POSIX定义:
- O_SYNC: 文件数据和所有的文件元数据被同步写到硬盘
- O_DSYNC: 只有文件数据和访问文件数据需要的元数据被同步写到硬盘
- O_RSYNC: 没有定义
写到这样的文件描述符的数据和元数据会立即写到稳定存储。注意这里的用词。在检索文件的数据的时候并不必须的元数据可能不会被立即写到稳定存储。这些元数据可能包括文件访问时间、创建时间以及修改时间等。
还需要指出的是通过O_SYNC或者O_DSYNC打开的文件描述符然后关联它们到libc文件流的时候有一些微妙的地方。注意到fwrite()到文件指针会被C库进行缓存。如果没有调用fflush的话数据并不一定会被写到硬盘上。本质上说,关联一个同步文件描述符到一个文件流上面意味着不需要fsync,只需要fflush就可以了。
什么时候需要fsync
有一些简单的规则来确定是否需要顶用fsync。首先,你必须要回答一个问题:数据立即写到稳定存储是不是很重要。如果是临时数据,那么你可能并不需要fsync()。如果数据可以被重新生成,那么可能也不需要fsync。如果你要保存一个事务的结果,或者更行一个用户的配置,你很想要它成功。那么你就用fsyn()。
更加微妙的情况是新创建文件或者重写已经存在的文件。一个新创建的文件可能不仅需要给文件本身一个fsync(),还需要给文件所在目录一个fsync(),因为这是文件系统找到你的文件需要的。这个行为其实是文件系统相关的(以及挂载选项)。你可以针对特定的文件系统和挂载选项进行编码也可以直接用一个fsync()来确保代码的可移植性。
相似的,如果你在重写一个文件的时候遇到一个系统失败(比如power loss,ENOSPC或者一个I/O错误),这会导致已经存在数据的丢失。为了避免这个问题,常见的方法(建议的方法)是把新的数据写到一个临时的文件,确保数据已经在稳定存储之后重命名这个临时文件到原来的文件名字。这样做确保了文件的原子操作,而且其它读者也可以访问到这个数据。以下的步骤就可以帮助你完成这样的操作:
- 创建一个新的临时文件 (在同一个文件系统上)
- 写数据到这个临时文件里面
- fsync()则个临时文件
- 重命名这个临时文件到其它名字
- fsync()它所在的目录
错误检查
当做写I/O的时候,如果库或者buffer缓存了数据,错误可能就不会在调用write()或者fflush()的时候及时报出来,因为数据可能只写到了page cache。写错误往往需要在调用fsync()、msysc()或者close()的时候才报出来。因此非常重要的一点是检查这些调用的返回值。
写回缓存write-back caches
这一部分给出一些磁盘cache的通用信息,以及操作系统控制这些caches的信息。这一部分讨论的选项并不会影响程序的构建,主要就是介绍性的。
存储设备上的写回cache可能有很多不同的风格。有易失性的写回cache,这个是我们这个文档里面假设的。这样一个cache在出现power问题的时候会丢失。但是很多存储设备可以配置成运行在cache-less模式或者write-through模式。这样的话在数据没有写到稳定存储之前是不会返回的。外部存储阵列往往有一个非易失性的,电池供电的write-cache。这样的话,出现power问题的时候数据也不会丢。从一个应用程序员的角度看,我们看不到这些参数。所以最好假设是易失性的缓存,并且进行防御性编程。在那些数据已经被保存起来的情况下,操作系统会进行优化从而提高整体的性能。
有些文件系统提供挂载选项来控制缓存冲刷行为。对于ext3、ext4、xfs和btrfs,kernel 2.6.35,如果用“-o barriers”来打开(默认)或者“-o nobarrier”来关闭write-back cache flush。依赖于不同的文件系统,之前版本的内核可能需要不同的选项。同样,应用开发不需要考虑这个选项。当write barriers被关闭的时候,这意味着fsync调用不会冲刷disk caches。管理员应该要知道cache冲刷不需要的时候才用这个选项。
附录
下面部分提供代码示例给应用开发人员:
1. 同步IO到一个文件系统
2. 用文件描述符做同步IO(系统I/O)。实际上这是上面第一个例子的子集,跟有没有O_DIRECT没有关系(所以不管有没有O_DIRECT都可以)。
3. 替代已经存在的文件(重写)
4. sync-sample.h