Linux:sync、fsync與fdatasync函式使用介紹
三個函式的特點
sync、fsync與fdatasync都是磁碟同步函式,分別有以下特點。 sync函式只是將所有修改過的塊緩衝區排入寫佇列,然後就返回,它並不等待實際寫磁碟操作結束。通常稱為update的系統守護程序會週期性地(一般每隔30秒)呼叫sync函式。這就保證了定期沖洗核心的塊緩衝區。命令sync(1)也呼叫sync函式。
fsync函式只對由檔案描述符filedes指定的單一檔案起作用,並且等待寫磁碟操作結束,然後返回。fsync可用於資料庫這樣的應用程式,這種應用程式需要確保將修改過的塊立即寫到磁碟上。
fdatasync函式類似於fsync,但它隻影響檔案的資料部分。而除資料外,fsync還會同步更新檔案的屬性。
為什麼需要磁碟同步函式
傳統的UNIX實現在核心中設有緩衝區快取記憶體或頁面快取記憶體,大多數磁碟I/O都通過緩衝進行。當將資料寫入檔案時,核心通常先將該資料複製到其中一個緩衝區中,如果該緩衝區尚未寫滿,則並不將其排入輸出佇列,而是等待其寫滿或者當核心需要重用該緩衝區以便存放其他磁碟塊資料時,再將該緩衝排入(呼叫fflush命令)輸出佇列,然後待其到達隊首時,才進行實際的I/O操作。這種輸出方式被稱為延遲寫(delayed write)。
延遲寫減少了磁碟讀寫次數,但是卻降低了檔案內容的更新速度,使得欲寫到檔案中的資料在一段時間內並沒有寫到磁碟上。當系統發生故障時,這種延遲可能造成檔案更新內容的丟失。為了保證磁碟上實際檔案系統與緩衝區快取記憶體中內容的一致性,UNIX系統提供了sync、fsync和fdatasync三個函式。
髒頁與記憶體對映
髒頁:linux核心中的概念,因為硬碟的讀寫速度遠趕不上記憶體的速度,系統就把讀寫比較頻繁的資料事先放到記憶體中,以提高讀寫速度,這就叫快取記憶體,linux是以頁作為快取記憶體的單位,當程序修改了快取記憶體裡的資料時,該頁就被核心標記為髒頁,核心將會在合適的時間把髒頁的資料寫到磁碟中去,以保持快取記憶體中的資料和磁碟中的資料是一致的。
記憶體對映:記憶體對映檔案,是由一個檔案到一塊記憶體的對映。Win32提供了允許應用程式把檔案對映到一個程序的函式 (CreateFileMapping)。記憶體對映檔案與虛擬記憶體有些類似,通過記憶體對映檔案可以保留一個地址空間的區域,同時將物理儲存器提交給此區域,記憶體檔案對映的物理儲存器來自一個已經存在於磁碟上的檔案,而且在對該檔案進行操作之前必須首先對檔案進行對映。使用記憶體對映檔案處理儲存於磁碟上的檔案時,將不必再對檔案執行I/O操作,使得記憶體對映檔案在處理大資料量的檔案時能起到相當重要的作用。
在Unix作業系統上,怎樣保證對檔案的更新內容成功持久化到硬碟?
write不夠,需要fsync
一般情況下,對硬碟(或者其他持久儲存裝置)檔案的write操作,更新的只是記憶體中的頁快取(page cache),而髒頁面不會立即更新到硬碟中,而是由作業系統統一排程,如由專門的flusher核心執行緒在滿足一定條件時(如一定時間間隔、記憶體中的髒頁達到一定比例)內將髒頁面同步到硬碟上(放入裝置的IO請求佇列)。
因為write呼叫不會等到硬碟IO完成之後才返回,因此如果OS在write呼叫之後、硬碟同步之前崩潰,則資料可能丟失。雖然這樣的時間視窗很小,但是對於需要保證事務的持久化(durability)和一致性(consistency)的資料庫程式來說,write()所提供的“鬆散的非同步語義”是不夠的,通常需要OS提供的同步IO(synchronized-IO)原語來保證:
#include <unistd.h>
int fsync(int fd);
fsync的功能是確保檔案fd所有已修改的內容已經正確同步到硬碟上,該呼叫會阻塞等待直到裝置報告IO完成。
PS:如果採用記憶體對映檔案的方式進行檔案IO(使用mmap,將檔案的page cache直接對映到程序的地址空間,通過寫記憶體的方式修改檔案),也有類似的系統呼叫來確保修改的內容完全同步到硬碟之上:
#incude <sys/mman.h>2 int msync(void *addr, size_t length, int flags)
msync需要指定同步的地址區間,如此細粒度的控制似乎比fsync更加高效(因為應用程式通常知道自己的髒頁位置),但實際上(Linux)kernel中有著十分高效的資料結構,能夠很快地找出檔案的髒頁,使得fsync只會同步檔案的修改內容。
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,放寬了同步的語義以提高效能:
#include <unistd.h>
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,這樣的設定不夠靈活,應該很少使用。
使用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的開銷
總結
1、如果是對所有的緩衝區發出寫硬碟的命令,應該使用sync函式,但應該注意該函式僅僅只是把該命令放入佇列就返回了,在程式設計時需要注意。 2、如果是要把一個已經開啟的檔案所做的修改提交到硬碟,應呼叫fsync函式,該函式會在資料實際寫入硬碟後才返回,因此是最安全最可靠的方式。 3、如果是針對一個已經開啟的檔案流操作,則應該首先呼叫fsync函式把修改同步到核心緩衝區,然後再呼叫fsync把修改真正的同步到硬碟。 4、fflush:標準I/O函式(如:fread,fwrite)會在記憶體建立緩衝,該函式重新整理記憶體緩衝,將內容寫入核心緩衝,要想將其寫入磁碟,還需要呼叫fsync。(先呼叫fflush後呼叫fsync,否則不起作用)。