linux的同步IO操做函數: sync、fsync與fdatasync

VFS(Virtual File System)的存在使得Linux能夠兼容不一樣的文件系統,例如ext三、ext四、xfs、ntfs等等,其不只具備爲全部的文件系統實現一個通用的 外接口的做用,還具備另外一個與系統性能相關的重要做用——緩存。VFS中引入了高速磁盤緩存的機制,這屬於一種軟件機制,容許內核將本來存在磁盤上的某些 信息保存在RAM中,以便對這些數據的進一步訪問能快速進行,而沒必要慢速訪問磁盤自己。高速磁盤緩存可大體分爲如下三種:html

  • 目錄項高速緩存——主要存放的是描述文件系統路徑名的目錄項對象node

  • 索引節點高速緩存——主要存放的是描述磁盤索引節點的索引節點對象linux

  • 頁高速緩存——主要存放的是完整的數據頁對象,每一個頁所包含的數據必定屬於某個文件,同時,全部的文件讀寫操做都依賴於頁高速緩存。其是Linux內核所使用的主要磁盤高速緩存。數據庫

正是因爲緩存的引入,因此VFS文件系統採用了文件數據延遲寫的技術,所以,若是在調用系統接口寫入數據時沒有使用同步寫模式,那麼大多數據將會先保存在緩存中,待等到知足某些條件時纔將數據刷入磁盤裏。緩存

內核是如何將數據刷入磁盤的呢?

什麼時候把髒頁寫入磁盤

內核不斷用包含塊設備數據的頁填充頁高速緩存。只要進程修改了數據,相應的頁就被標記爲髒頁,即把它的PG_dirty標誌位置。數據結構

    Unix系統容許把髒緩衝區寫入塊設備的操做延遲執行,由於這種策略能夠顯著地提升系統的性能。對高速緩存中的頁的幾回寫操做可能只需對相應的磁盤塊進行 一次緩慢的物理更新就能夠知足。此外,寫操做沒有讀操做那麼緊迫,由於進程一般是不會由於延遲寫而掛起,而大部分狀況都由於延遲讀而掛起。app

一個髒頁可能直到最後一刻(即直到系統關閉時)都一直逗留在主存中。然而,從延遲寫策略的侷限性來看,它有兩個主要的缺點:less

  1、若是發生了硬件錯誤或者電源掉電的狀況,那麼就沒法再得到RAM的內容,所以,從系統啓動以來對文件進行的不少修改就丟失了。異步

  2、頁高速緩存的大小(由此存放它所需的RAM的大小)就可要很大——至少要與所訪問塊設備的大小不一樣。async

所以,在下列條件下把髒頁刷新(寫入)到磁盤:

  • 頁高速緩存變得太滿,但還須要更多的頁,或者髒頁的數量已經太多。

  • 自從頁變成髒頁以來已過去太長時間。

  • 進程請求對塊設備或者特定文件任何待定的變化都進行刷新。經過調用sync()、fsync()或者fdatasync()系統調用來實現。

緩衝區頁的 引入是問題更加複雜。與每一個緩衝區頁相關的緩衝區首部使內核可以瞭解每一個獨立塊緩衝區的狀態。若是至少有一個緩衝區首部的PG_Dirty標誌被置位,就 應該設置相應緩衝區頁的PG_dirty標誌。當內核選擇要刷新的緩衝區時,它掃描相應的緩衝區首部,並只把髒塊的內容有效的寫到磁盤。一旦內核把緩衝區 的全部髒頁刷新到磁盤,就把頁的PG_dirty標誌清0。

誰來把髒頁寫入磁盤

由pdflush內核線程負責。早期版本的Linux使用bdfllush內核線程系統地掃描頁高速緩存以搜索要刷新的髒頁,而且使用另外一個內核線程kupdate來保證全部的頁不會「髒」太長時間。Linux 2.6用一組通用內核線程pdflush替代上述兩個線程。當系統沒有要刷新的髒頁時,pdflush線程會自動處於睡眠狀態,最後由pdflush_operation()函數來喚醒。

在下面幾種狀況下,系統會喚醒pdflush回寫髒頁:

1 、定時方式:
     定時機制定時喚醒pdflush內核線程,週期爲/proc/sys/vm/dirty_writeback_centisecs ,單位
是(1/100)秒,每次週期性喚醒的pdflush線程並非回寫全部的髒頁,而是隻回寫變髒時間超過
/proc/sys/vm/dirty_expire_centisecs(單位也是1/100秒)。
注意:變髒的時間是以文件的inode節點變髒的時間爲基準的,也就是說若是某個inode節點是10秒前變髒的,
pdflush就認爲這個inode對應的全部髒頁的變髒時間都是10秒前,即便可能部分頁面真正變髒的時間不到10秒,
細節能夠查看內核函數wb_kupdate()。

二、 內存不足的時候:
    這時並不將全部的dirty頁寫到磁盤,而是每次寫大概1024個頁面,直到空閒頁面知足需求爲止。

3 、寫操做時發現髒頁超過必定比例:
    當髒頁佔系統內存的比例超過/proc/sys/vm/dirty_background_ratio 的時候,write系統調用會喚醒
pdflush回寫dirty page,直到髒頁比例低於/proc/sys/vm/dirty_background_ratio,但write系統調
用不會被阻塞,當即返回。當髒頁佔系統內存的比例超過/proc/sys/vm/dirty_ratio的時候, write系
統調用會被被阻塞,主動回寫dirty page,直到髒頁比例低於/proc/sys/vm/dirty_ratio,這一點在
2.4內核中是沒有的。

4 、用戶調用sync系統調用:
    這是系統會喚醒pdflush直到全部的髒頁都已經寫到磁盤爲止。


linux系統在向存儲設備上寫數據的時候,其實,數據沒有被當即寫入到物理設備上,而通常處理過程是:

  1. 調用fwrite()將數據寫入文件緩衝區(用戶態進程的buffer)。

  2. 進程按期調用fflush()函數以後,把文件緩衝區中的文件數據寫到文件系統中,此時數據尚未被真正寫入到物理介質中。

  3. fsync(fileno(fp))。該函數返回後,才能保證寫入到了物理介質上。即先調用fileno得到文件描述符以後,再調用fsync函數返回後纔將文件寫入到物理介質上。


fflush和fsync的一些總結

1.提供者fflush是libc.a中提供的方法,fsync是linux系統內核提供的系統調用。
2.原形fflush接受一個參數FILE *.fflush(FILE *);fsync接受的時一個Int型的文件描述符。fsync(int fd);
3.功能fflush:是把C庫中的緩衝調用write函數寫到磁盤[實際上是寫到內核的緩衝區]。fsync:是把內核緩衝刷到磁盤上。
4.fsync 將文件相關的全部更改都發送到disk device。 這個調用是阻塞的,直到disk通知此函數傳輸完成。此函數也會將該文件的文件信息flush到disk。
5.fsync最終將緩衝的數據更新到文件裏。

因此能夠看出fflush和fsync的調用順序應該是:
c庫緩衝-----fflush---------〉內核頁高速緩存--------fsync-----〉磁盤


與文件讀寫相關的幾個重要概念

髒頁:linux內核中的概念,由於硬盤的讀寫速度遠趕不上內存的速度,系統就把讀寫比較頻繁的數據事先放到內存中,以提升讀寫速度,這就叫高速緩存,linux是以頁做爲高速緩存的單位,當進程修改了高速緩存裏的數據時,該頁就被內核標記爲髒頁,內核將會在合適的時間把髒頁的數據寫到磁盤中去,以保持高速緩存中的數據和磁盤中的數據是一致的

內存映射:內存映射文件,是由一個文件到一塊內存的映射。Win32提供了容許應用程序把文件映射到一個進程的函數 (CreateFileMapping)。內存映射文件與虛擬內存有些相似,經過內存映射文件能夠保留一個地址空間的區域,同時將物理存儲器提交給此區域,內存文件映射的物理存儲器來自一個已經存在於磁盤上的文件,並且在對該文件進行操做以前必須首先對文件進行映射。使用內存映射文件處理存儲於磁盤上的文件時,將沒必要再對文件執行I/O操做,使得內存映射文件在處理大數據量的文件時能起到至關重要的做用。

延遲寫(delayed write):傳統的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還會同步更新文件的屬性。

於提供事務支持的數據庫,在事務提交時,都要確保事務日誌(包含該事務全部的修改操做以及一個提交記錄)徹底寫到硬盤上,才認定事務提交成功並返回給應用層。

一個簡單的問題:在*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."

多餘的一次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的開銷

 

參考:http://blog.csdn.net/cywosp/article/details/8767327

http://blog.chinaunix.net/uid-1911213-id-3412851.html

http://blog.csdn.net/lwj103862095/article/details/38268647

相關文章
相關標籤/搜索