漫談linux檔案IO
在Linux 開發中,有幾個關係到效能的東西,技術人員非常關注:程序,CPU,MEM,網路IO,磁碟IO。本篇檔案打算詳細全面,深入淺出。剖析檔案IO的細節。從多個角度探索如何提高IO效能。本文儘量用通俗易懂的視角去闡述。不copy核心程式碼。
闡述之前,要先有個大視角,讓我們站在萬米高空,鳥瞰我們的檔案IO,它們設計是分層的,分層有2個好處,一是架構清晰,二是解耦。讓我們看一下下面這張圖。
圖一
1. 穿越各層寫檔案方式
程式的最終目的是要把資料寫到磁碟上, 但是系統從通用性和效能角度,儘量提供一個折中的方案來保證這些。讓我們來看一個最常用的寫檔案典型example,也是路徑最長的IO。
{
char *buf = malloc(MAX_BUF_SIZE);
strncpy(buf, src, , MAX_BUF_SIZE);
fwrite(buf, MAX_BUF_SIZE, 1, fp);
fclose(fp);
}
這裡malloc的buf對於圖層中的application buffer,即應用程式的buffer;呼叫fwrite後,把資料從application buffer 拷貝到了 CLib buffer,即C庫標準IObuffer。fwrite返回後,資料還在CLib buffer,如果這時候程序core掉。這些資料會丟失。沒有寫到磁碟介質上。當呼叫fclose的時候,fclose呼叫會把這些資料重新整理到磁碟介質上。除了fclose方法外,還有一個主動重新整理操作fflush 函式,不過
從上面類子看到,一個常用的fwrite函式過程,基本上歷經千辛萬苦,資料經過多次copy,才到達目的地。有人心生疑問,這樣會提高效能嗎,反而會降低效能吧。這個問題先放一放。
有人說,我不想通過fwrite+fflush這樣組合,我想直接寫到page cache。這就是我們常見的檔案IO呼叫read/write函式。這些函式基本上是一個函式對應著一個系統呼叫,如sys_read/sys_write. 呼叫write函式,是直接通過系統呼叫把資料從應用層拷貝到核心層,從application buffer 拷貝到 page cache 中。
系統呼叫,write會觸發使用者態/核心態切換?是的。那有沒有辦法避免這些消耗。這時候該mmap出場了,mmap把page cache 地址空間對映到使用者空間,應用程式像操作應用層記憶體一樣,寫檔案。省去了系統呼叫開銷。
那如果繼續刨根問底,如果想繞過page cache,直接把資料送到磁碟裝置上怎麼辦。通過open檔案帶上O_DIRECT引數,這是write該檔案。就是直接寫到裝置上。
如果繼續較勁,直接寫扇區有沒有辦法。這就是所謂的RAW裝置寫,繞開了檔案系統,直接寫扇區,想fdsik,dd,cpio之類的工具就是這一類操作。
2. IO呼叫鏈
列舉了上述各種穿透各種cache 層寫操作,可以看到系統提供的介面相當豐富,滿足你各種寫要求。下面通過講解圖一,瞭解一下檔案IO的呼叫鏈。
fwrite是系統提供的最上層介面,也是最常用的介面。它在使用者程序空間開闢一個buffer,將多次小資料量相鄰寫操作先快取起來,合併,最終呼叫write函式一次性寫入(或者將大塊資料分解多次write呼叫)。
Write函式通過呼叫系統呼叫介面,將資料從應用層copy到核心層,所以write會觸發核心態/使用者態切換。當資料到達page cache後,核心並不會立即把資料往下傳遞。而是返回使用者空間。資料什麼時候寫入硬碟,有核心IO排程決定,所以write是一個非同步呼叫。這一點和read不同,read呼叫是先檢查page cache裡面是否有資料,如果有,就取出來返回使用者,如果沒有,就同步傳遞下去並等待有資料,再返回使用者,所以read是一個同步過程。當然你也可以把write的非同步過程改成同步過程,就是在open檔案的時候帶上O_SYNC標記。
資料到了page cache後,核心有pdflush執行緒在不停的檢測髒頁,判斷是否要寫回到磁碟中。把需要寫回的頁提交到IO佇列——即IO排程佇列。又IO排程佇列排程策略排程何時寫回。
提到IO排程佇列,不得不提一下磁碟結構。這裡要講一下,磁頭和電梯一樣,儘量走到頭再回來,避免來回搶佔是跑,磁碟也是單向旋轉,不會反覆逆時針順時針轉的。從網上copy一個圖下來,具體這裡就不介紹。
IO佇列有2個主要任務。一是合併相鄰扇區的,而是排序。合併相信很容易理解,排序就是儘量按照磁碟選擇方向和磁頭前進方向排序。因為磁頭尋道時間是和昂貴的。
這裡IO佇列和我們常用的分析工具IOStat關係密切。IOStat中rrqm/s wrqm/s表示讀寫合併個數。avgqu-sz表示平均佇列長度。
核心中有多種IO排程演算法。當硬碟是SSD時候,沒有什麼磁軌磁頭,人家是隨機讀寫的,加上這些排程演算法反而畫蛇添足。OK,剛好有個排程演算法叫noop排程演算法,就是什麼都不錯(合併是做了)。剛好可以用來配置SSD硬碟的系統。
從IO隊列出來後,就到了驅動層(當然核心中有更多的細分層,這裡忽略掉),驅動層通過DMA,將資料寫入磁碟cache。
至於磁碟cache時候寫入磁碟介質,那是磁碟控制器自己的事情。如果想要睡個安慰覺,確認要寫到磁碟介質上。就呼叫fsync函式吧。可以確定寫到磁碟上了。
3. 一致性和安全性
談完呼叫細節,再將一下一致性問題和安全問題。既然資料沒有到到磁碟介質前,可能處在不同的實體記憶體cache中,那麼如果出現程序宕機,核心死,掉電這樣事件發生。資料會丟失嗎。
當程序宕機後:只有資料還處在application cache或CLib cache時候,資料會丟失。資料到了page cache。程序core掉,即使資料還沒有到硬碟。資料也不會丟失。
當核心core掉後,只要資料沒有到達disk cache,資料都會丟失。
掉電情況呢,哈哈,這時候神也救不了你,哭吧。
那麼一致性呢,如果兩個程序或執行緒同時寫,會寫亂嗎?或A程序寫,B程序讀,會寫髒嗎?
文章寫到這裡,寫得太長了,就舉出各種各樣的例子。講一下大概判斷原則吧。fwrite操作的buffer是在程序私有空間,兩個執行緒讀寫,肯定需要鎖保護的。如果程序,各有各的地址空間。是否要加鎖,看應用場景。
write操作如果寫大小小於PIPE_BUF(一般是4096),是原子操作,能保證兩個程序“AAA”,“BBB”寫操作,不會出現“ABAABB”這樣的資料交錯。O_APPEND 標誌能保證每次重新計算pos,寫到檔案尾的原子性。
資料到了核心層後,核心會加鎖,會保證一致性的。
4. 效能問題
效能從系統層面和裝置層面去分析;磁碟的物理特性從根本上決定了效能。IO的排程策略,系統呼叫也是致命殺手。
磁碟的尋道時間是相當的慢,平均尋道時間大概是在10ms,也就是是每秒只能100-200次尋道。
磁碟轉速也是影響效能的關鍵,目前最快15000rpm,大概就每秒500轉,滿打滿算,就讓磁頭不尋道,設想所有的資料連續存放在一個柱面上。大家可以算一下每秒最多可以讀多少資料。當然這個是理論值。一般情況下,碟片轉太快,磁頭感應跟不上,所以需要轉幾圈才能完全讀出磁軌內容。
另外裝置介面匯流排傳輸率是實際速率的上限。
另外有些等密度磁碟,磁碟外圍磁軌扇區多,線速度快,如果頻繁操作的資料放在外圍扇區,也能提高效能。
利用多磁碟併發操作,也不失為提高效能的手段。
這裡給個業界經驗值:機械硬碟順序寫~30MB,順序讀取速率一般~50MB好的可以達到100多M, SSD讀達到~400MB,SSD寫效能和機械硬碟差不多。
Ps:
O_DIRECT 和 RAW裝置最根本的區別是O_DIRECT是基於檔案系統的,也就是在應用層來看,其操作物件是檔案控制代碼,核心和檔案層來看,其操作是基於inode和資料塊,這些概念都是和ext2/3的檔案系統相關,寫到磁碟上最終是ext3檔案。而RAW裝置寫是沒有檔案系統概念,操作的是扇區號,操作物件是扇區,寫出來的東西不一定是ext3檔案(如果按照ext3規則寫就是ext3檔案)。一般基於O_DIRECT來設計優化自己的檔案模組,是不滿系統的cache和排程策略,自己在應用層實現這些,來制定自己特有的業務特色檔案讀寫。但是寫出來的東西是ext3檔案,該磁碟卸下來,mount到其他任何linux系統上,都可以檢視。而基於RAW裝置的設計系統,一般是不滿現有ext3的諸多缺陷,設計自己的檔案系統。自己設計檔案佈局和索引方式。舉個極端例子:把整個磁碟做一個檔案來寫,不要索引。這樣沒有inode限制,沒有檔案大小限制,磁碟有多大,檔案就能多大。這樣的磁碟卸下來,mount到其他linux系統上,是無法識別其資料的。兩者都要通過驅動層讀寫;在系統引導啟動,還處於真實模式的時候,可以通過bios介面讀寫raw裝置。