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)原語來保證:app

1 #include <unistd.h>
2 int fsync(int fd);

fsync的功能是確保文件fd全部已修改的內容已經正確同步到硬盤上,該調用會阻塞等待直到設備報告IO完成。less

 

 

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只會同步文件的修改內容。async

 

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."性能

 

多餘的一次IO操做,有多麼昂貴呢?根據Wikipedia的數據,當前硬盤驅動的平均尋道時間(Average seek time)大約是3~15ms,7200RPM硬盤的平均旋轉延遲(Average rotational latency)大約爲4ms,所以一次IO操做的耗時大約爲10ms左右。這個數字意味着什麼?下文還會提到。

 

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的開銷

相關文章
相關標籤/搜索