linux 同步IO: sync、fsync与fdatasync

传统的UNIX实如今内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都经过缓冲进行。当将数据写入文件时,内核一般先将该数据复制到其中一个缓冲区中,若是该缓冲区还没有写满,则并不将其排入输出队列,而是等待其写满或者当内核须要重用该缓冲区以便存放其余磁盘块数据时,再将该缓冲排入输出队列,而后待其到达队首时,才进行实际的I/O操做。这种输出方式被称为延迟写(delayed write)(Bach [1986]第3章详细讨论了缓冲区高速缓存)。
延迟写减小了磁盘读写次数,可是却下降了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并无写到磁盘上。当系统发生故障时,这种延迟可能形成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。
sync函数只是将全部修改过的块缓冲区排入写队列,而后就返回,它并不等待实际写磁盘操做结束。
一般称为update的系统守护进程会周期性地(通常每隔30秒)调用sync函数。这就保证了按期冲洗内核的块缓冲区。命令sync(1)也调用sync函数。
fsync函数只对由文件描述符filedes指定的单一文件起做用,而且等待写磁盘操做结束,而后返回。fsync可用于数据库这样的应用程序,这种应用程序须要确保将修改过的块当即写到磁盘上。
fdatasync函数相似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
node

 

于提供事务支持的数据库,在事务提交时,都要确保事务日志(包含该事务全部的修改操做以及一个提交记录)彻底写到硬盘上,才认定事务提交成功并返回给应用层。数据库

一个简单的问题:在*nix操做系统上,怎样保证对文件的更新内容成功持久化到硬盘?缓存

 

1.  write不够,须要fsync

通常状况下,对硬盘(或者其余持久存储设备)文件的write操做,更新的只是内存中的页缓存(page cache),而脏页面不会当即更新到硬盘中,而是由操做系通通一调度,如由专门的flusher内核线程在知足必定条件时(如必定时间间隔、内存中的脏页达到必定比例)内将脏页面同步到硬盘上(放入设备的IO请求队列)。
由于write调用不会等到硬盘IO完成以后才返回,所以若是OS在write调用以后、硬盘同步以前崩溃,则数据可能丢失。虽然这样的时间窗口很小,可是对于须要保证事务的持久化(durability)和一致性(consistency)的数据库程序来讲,write()所提供的“松散的异步语义”是不够的,一般须要OS提供的同步IO(synchronized-IO)原语来保证:
1 #include <unistd.h> 2 int fsync(int fd);
fsync的功能是确保文件fd全部已修改的内容已经正确同步到硬盘上,该调用会阻塞等待直到设备报告IO完成。
 
 
PS:若是采用内存映射文件的方式进行文件IO(使用mmap,将文件的page cache直接映射到进程的地址空间,经过写内存的方式修改文件),也有相似的系统调用来确保修改的内容彻底同步到硬盘之上:
1 #incude <sys/mman.h>
2 int msync(void *addr, size_t length, int flags)

msync须要指定同步的地址区间,如此细粒度的控制彷佛比fsync更加高效(由于应用程序一般知道本身的脏页位置),但实际上(Linux)kernel中有着十分高效的数据结构,可以很快地找出文件的脏页,使得fsync只会同步文件的修改内容。数据结构

 

2. fsync的性能问题,与fdatasync

除了同步文件的修改内容(脏页),fsync还会同步文件的描述信息(metadata,包括size、访问时间st_atime & st_mtime等等),由于文件的数据和metadata一般存在硬盘的不一样地方,所以fsync至少须要两次IO写操做,fsync的man page这样说:

"Unfortunately fsync() will always initialize two write operations : one for the newly written data and another one in order to update the modification time stored in the inode. If the modification time is not a part of the transaction concept fdatasync() can be used to avoid unnecessary inode disk write operations."app

多余的一次IO操做,有多么昂贵呢?根据Wikipedia的数据,当前硬盘驱动的平均寻道时间(Average seek time)大约是3~15ms,7200RPM硬盘的平均旋转延迟(Average rotational latency)大约为4ms,所以一次IO操做的耗时大约为10ms左右。这个数字意味着什么?下文还会提到。less

 

Posix一样定义了fdatasync,放宽了同步的语义以提升性能:异步

1 #include <unistd.h> 2 int fdatasync(int fd);
fdatasync的功能与fsync相似,可是仅仅在必要的状况下才会同步metadata,所以能够减小一次IO写操做。那么,什么是“必要的状况”呢?根据man page中的解释:
"fdatasync does not flush modified metadata unless that metadata is needed in order to allow a subsequent data retrieval to be corretly handled."
举例来讲,文件的尺寸(st_size)若是变化,是须要当即同步的,不然OS一旦崩溃,即便文件的数据部分已同步,因为metadata没有同步,依然读不到修改的内容。而最后访问时间(atime)/修改时间(mtime)是不须要每次都同步的,只要应用程序对这两个时间戳没有苛刻的要求,基本无伤大雅。
 
 
PS:open时的参数O_SYNC/O_DSYNC有着和fsync/fdatasync相似的语义:使每次write都会阻塞等待硬盘IO完成。(实际上,Linux对O_SYNC/O_DSYNC作了相同处理,没有知足Posix的要求,而是都实现了fdatasync的语义)相对于fsync/fdatasync,这样的设置不够灵活,应该不多使用。
 
 

3. 使用fdatasync优化日志同步

文章开头时已提到,为了知足事务要求,数据库的日志文件是经常须要同步IO的。因为须要同步等待硬盘IO完成,因此事务的提交操做经常十分耗时,成为性能的瓶颈。
在Berkeley DB下,若是开启了AUTO_COMMIT(全部独立的写操做自动具备事务语义)并使用默认的同步级别(日志彻底同步到硬盘才返回),写一条记录的耗时大约为5~10ms级别,基本和一次IO操做(10ms)的耗时相同。
 咱们已经知道,在同步上fsync是低效的。可是若是须要使用fdatasync减小对metadata的更新,则须要确保文件的尺寸在write先后没有发生变化。日志文件天生是追加型(append-only)的,老是在不断增大,彷佛很难利用好fdatasync。
 
且看Berkeley DB是怎样处理日志文件的:
1.每一个log文件固定为10MB大小,从1开始编号,名称格式为“log.%010d"
2.每次log文件建立时,先写文件的最后1个page,将log文件扩展为10MB大小
3.向log文件中追加记录时,因为文件的尺寸不发生变化,使用fdatasync能够大大优化写log的效率
4.若是一个log文件写满了,则新建一个log文件,也只有一次同步metadata的开销
相关文章
相关标签/搜索