前言
在linux内核中都有缓冲区或者页面高速缓存,大多数磁盘IO都是通过缓冲写的。当你想将数据write进文件时,内核通常会将该数据复制到其中一个缓冲区中,如果该缓冲没被写满的话,内核就不会把它放入到输出队列中。当这个缓冲区被写满或者内核想重用这个缓冲区时,才会将其排到输出队列中。等它到达等待队列首部时才会进行实际的IO操作。在进行数据库开发时,为了避免缓存中的数据还没有写入到磁盘就宕机导致的数据丢失,就需要使用fsync或这fdatasync来保证数据成功写入磁盘。
sync系统调用
函数定义:void sync(void);
我们知道write系统调用只是写入到PageCache,脏页不会立刻写入到磁盘,而是由内核的flusher线程在满足一定阈值(一定时间间隔、脏页达到一定比例),调用sync函数将脏页同步到磁盘上(放入设备的IO请求队列)。
POSIX语义要求sync系统调用只需将脏页提交到块设备IO队列就可以返回。所以我们看到sync函数返回值为void。同时sync函数返回后,并不等于写入磁盘结束,仍然会出现故障,此时sync函数是无法知晓的。对于可靠性要求比较高的应用,write提供的松散的异步语义是不够的,所以我们需要内核提供的同步IO来保证,常用fsync
以及fdatasync
。
sync函数是针对整个PageCache的,对所有的文件更新产生的脏页都会flush。
fsync系统调用
函数定义:int fsync(int fd);
fsync
将文件描述符 FD 所引用的文件的所有修改的核心数据(即修改后的缓冲区缓存页)传输(“刷新”)到该文件所在的磁盘设备(或其他永久存储设备)。调用会阻塞,直到设备报告传输已完成。它还会刷新与文件关联的元数据信息。
调用 fsync并不一定能确保包含该文件的目录中的条目也已到达磁盘。为此,还需要在目录的文件描述符上显式 调用fsync。
fsync会确保一直到写磁盘操作结束才会返回。所以fsync适合数据库这种程序。
通常文件的数据和元数据是存储在磁盘不同位置的,因此fsync至少需要两次IO操作,一次数据、一次元数据。根据Wikipedia的数据,当前硬盘驱动的平均寻道时间(Average
seek time)大约是3~15ms,7200RPM硬盘的平均旋转延迟(Average rotational
latency)大约为4ms,因此一次IO操作的耗时大约为10ms左右。所以多一次IO操作时昂贵的。
fdatasync系统调用
函数定义:int fdatasync(int fd);
将文件的所有数据缓冲区刷新到磁盘(在系统调用返回之前)。它类似于 fsync
,但不需要更新元数据,例如访问时间。
访问数据库或日志文件的应用程序通常会写入一个微小的数据片段(例如,日志文件中的一行),然后立即调用fsync
,以确保写入的数据以物理方式存储在硬盘上。不幸的是,fsync
总是会启动两个写入操作:一个用于新写入的数据,另一个用于更新存储在 inode 中的修改时间。
上文提到fsync会同步数据以及元数据,增大延迟,因此POSIX定义了fdatasync,放宽了同步的语义,以提高性能。
fdatasync的功能与fsync类似,但是仅仅在必要的情况下才会同步metadata,因此可以减少一次IO写操作。那么什么是“必要的情况”呢?
举例来说,文件的尺寸(st_size)如果变化,是需要立即同步的,否则OS一旦崩溃,即使文件的数据部分已同步,由于metadata没有同步,依然读不到修改的内容。而最后访问时间(atime)/修改时间(mtime)是不需要每次都同步的,只要应用程序对这两个时间戳没有苛刻的要求,基本无伤大雅。
open函数的O_SYNC
和O_DSYNC
open函数的O_SYNC
和O_DSYNC
参数有着和fsync
及fdatasync
类似的含义:使每次write都会阻塞到磁盘IO完成。
O_SYNC:使每次write操作阻塞等待磁盘IO完成,文件数据和文件属性都更新。
O_DSYNC:使每次write操作阻塞等待磁盘IO完成,但是如果该写操作并不影响读取刚写入的数据,则不需等待文件属性被更新。
O_DSYNC
和O_SYNC
标志有微妙的区别:
文件以O_DSYNC
标志打开时,仅当文件属性需要更新以反映文件数据变化(例如,更新文件大小以反映文件中包含了更多数据)时,标志才影响文件属性。在重写其现有的部分内容时,文件时间属性不会同步更新。
文件以O_SYNC
标志打开时,数据和属性总是同步更新。对于该文件的每一次write都将在write返回前更新文件时间,这与是否改写现有字节或追加文件无关。相对于fsync/fdatasync,这样的设置不够灵活,应该很少使用。
实际上:Linux对O_SYNC
、O_DSYNC
做了相同处理,没有满足POSIX的要求,而是都实现了fdatasync
的语义。
sync_file_range
IO密集型的程序如果频繁的刷盘,会有很大的性能问题。Linux在内核2.6.17之后支持了sync_file_range
,可以让我们在做多个更新后,一次性的刷数据,这样大大提高IO的性能。
sync_file_range
可以将文件的部分范围作为目标,将对应范围内的脏页刷回磁盘,而不是整个文件的范围。好处是,当我们对大文件进行了修改时,如果修改了大量的数据块,我们最后fsync的时候,可能会很慢。即使fdatasync,也是有问题的,例如这个大文件的长度在我们的修改过程中发生了变化,那么fdatasync将同时写metadata,而对于文件系统来说,单个文件系统的写metadata是串行的,这势必导致影响其他用户操作metadata(如创建文件)。
sync_file_range
是绝对不会写metadata的,所以用它非常合适,每次对文件做了小范围的修改时,立即调用sync_file_range
,把对应的脏数据刷到磁盘,那么在结束对文件的修改后,再调用fdatasync (flush dirty data page)、fsync(flush dirty data+metadata page)都是很快的。
sync_file_range
提供了几个flag:
SYNC_FILE_RANGE_WRIT
:是异步的,可以结合fsync、fdatasync使用。SYNC_FILE_RANGE_WAIT_BEFORE
:写前做一次全文件范围的sync_file_range
。从而保证在调用fdatasync或fsync前,该文件的dirty page已经全部刷到磁盘。SYNC_FILE_RANGE_WAIT_AFTER
:写后做一次全文件范围的sync_file_range
。从而保证在调用fdatasync或fsync前,该文件的dirty page已经全部刷到磁盘。