最近遇到Document UI和FileManager同時copy文件到U盤,且同樣正常使用reject選項安全移除U盤後,後者大概率出現所傳輸文件變爲0KB的情況。
使用FileManager複製時,調用adb shell sync命令後安全移除,同樣也能複製到問題。
經過實驗發現,Document UI有多調用如下代碼:
code:
// Need to invoke Os#fsync to ensure the file is written to the storage device.
try {
Os.fsync(dstFile.getFileDescriptor());
} catch (ErrnoException error) {
// fsync will fail with fd of pipes and return EROFS or EINVAL.
if (error.errno != OsConstants.EROFS && error.errno != OsConstants.EINVAL) {
throw new SyncFailedException(
"Failed to sync bytes after copying a file.");
}
}
FileManager中加入同樣代碼,問題同樣再也無法複製。
搜到一篇文章講sync fsync fdatasync的差,才恍然大悟。
最後,我們還有個重大發現,不同的U盤,讀到的cache mode居然不同,例如
兩個u盤讀出的分別爲write-back和write-whrough兩種mode,其中只有write back mode的u盤才能複製到前面的問題。
另外,write-through mode的u盤傳輸性能確實會差不少(實驗u盤差了一倍)
Write-through- Write is done synchronously both to the cache and to the backing store.
Write-back (or Write-behind) - Writing is done only to the cache. A modified cache block is written back to the store, just before it is replaced.
Write-through(直寫模式)在數據更新時,同時寫入緩存Cache和後端存儲。此模式的優點是操作簡單;缺點是因爲數據修改需要同時寫入存儲,數據寫入速度較慢。
Write-back(回寫模式)在數據更新時只寫入緩存Cache。只在數據被替換出緩存時,被修改的緩存數據纔會被寫到後端存儲。此模式的優點是數據寫入速度快,因爲不需要寫存儲;缺點是一旦更新後的數據未被寫入存儲時出現系統掉電的情況,數據將無法找回
鏈接:https://read01.com/Poo42z.html#.W1gbLGiWZPY
怕日後網頁失效,這裏摘抄如下:
傳統的UNIX實現在內核中設有緩衝區高速緩存或頁面高速緩存,大多數磁盤I/O都通過緩衝進行。當將數據寫入文件時,內核通常先將該數據複製到其中一個緩衝區中,如果該緩衝區尚未寫滿,則並不將其排入輸出隊列,而是等待其寫滿或者當內核需要重用該緩衝區以便存放其他磁盤塊數據時,再將該緩衝排入輸出隊列,然後待其到達隊首時,才進行實際的I/O操作。這種輸出方式被稱爲延遲寫(delayed write)(Bach [1986]第3章詳細討論了緩衝區高速緩存)。
延遲寫減少了磁盤讀寫次數,但是卻降低了文件內容的更新速度,使得欲寫到文件中的數據在一段時間內並沒有寫到磁盤上。當系統發生故障時,這種延遲可能造成文件更新內容的丟失。爲了保證磁盤上實際文件系統與緩衝區高速緩存中內容的一致性,UNIX系統提供了sync、fsync和fdatasync三個函數。
sync函數或者命令:只是將所有修改過的塊緩衝區排入寫隊列,然後就返回,它並不等待實際寫磁盤操作結束。通常稱爲update的系統守護進程會週期性地(一般每隔30秒)調用sync函數。這就保證了定期沖洗內核的塊緩衝區。命令sync(1)也調用sync函數。linux默認情況下幾乎所有應用場合都是不會將文件立馬保存入磁盤的,在嵌入式上也是同樣的表現,如果mount時候加上sync選項就可以保證數據立馬寫入,但是這種會造成系統較多的寫入磁盤,磁盤的壽命就不會太長了,如果是需要保存數據,可以顯式調用命令sync來寫入所有文件,或者fsync來保存某個文件。unix系統運行經驗表明,爲確保可靠起見,應執行兩遍sync命令,這是因爲sync命令完成時,並不保證信息實際寫到了磁盤上,雖然已經執行了一遍這個命令,cp 完之後要執行 sync 命令將緩衝區的內容寫到磁盤中。
fsync函數只對由文件描述符filedes指定的單一文件起作用,並且等待寫磁盤操作結束,然後返回。fsync可用於數據庫這樣的應用程序,這種應用程序需要確保將修改過的塊立即寫到磁盤上。
fdatasync函數類似於fsync,但它隻影響文件的數據部分。而除數據外,fsync還會同步更新文件的屬性。
對於提供事務支持的數據庫,在事務提交時,都要確保事務日誌(包含該事務所有的修改操作以及一個提交記錄)完全寫到硬盤上,才認定事務提交成功並返回給應用層。
一個簡單的問題:在*nix操作系統上,怎樣保證對文件的更新內容成功持久化到硬盤?
write不夠,需要fsync
一般情況下,對硬盤(或者其他持久存儲設備)文件的write操作,更新的只是內存中的頁緩存(page cache),而髒頁面不會立即更新到硬盤中,而是由操作系統統一調度,如由專門的flusher內核線程在滿足一定條件時(如一定時間間隔、內存中的髒頁達到一定比例)內將髒頁面同步到硬盤上(放入設備的IO請求隊列)。
因爲write調用不會等到硬盤IO完成之後才返回,因此如果OS在write調用之後、硬盤同步之前崩潰,則數據可能丟失。雖然這樣的時間窗口很小,但是對於需要保證事務的持久化(durability)和一致性(consistency)的數據庫程序來說,write所提供的“鬆散的異步語義”是不夠的,通常需要OS提供的同步IO(synchronized-IO)原語來保證:
1#include <unistd.h>2intfsync(intfd);
fsync的功能是確保文件fd所有已修改的內容已經正確同步到硬盤上,該調用會阻塞等待直到設備報告IO完成。
PS:如果採用內存映射文件的方式進行文件IO(使用mmap,將文件的page cache直接映射到進程的地址空間,通過寫內存的方式修改文件),也有類似的系統調用來確保修改的內容完全同步到硬盤之上:
1#incude <sys/mman.h>2intmsync(void*addr, size_t length,intflags)
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,放寬了同步的語義以提高性能:
1#include <unistd.h>2intfdatasync(intfd);
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的開銷
另外兩個命令:async(IO異步),rsync(遠程同步)。