前段時間在開發一個使用SSD做緩存的系統,在高速寫入數據時會出現大量的磁盤緩存。太多的磁盤緩存如果沒有及時的寫入磁盤中,在機器出現問題時是非常危險的,這樣會導致很多的數據丟失,但是如果實時的將數據刷入磁盤中,這樣寫入效率有太低了。為了弄明白Linux系統的這種磁盤寫入特性,最近深入的學習了一下。 VFS(Virtual File System)的存在使得Linux可以兼容不同的文件系統,例如ext3、ext4、xfs、ntfs等等,其不僅具有為所有的文件系統實現一個通用的外接口的作用,還具有另一個與系統性能相關的重要作用——緩存。VFS中引入了高速磁盤緩存的機制,這屬於一種軟件機制,允許內核將原本存在磁盤上的某些信息保存在RAM中,以便對這些數據的進一步訪問能快速進行,而不必慢速訪問磁盤本身。高速磁盤緩存可大致分為以下三種:
目錄項高速緩存——主要存放的是描述文件系統路徑名的目錄項對象
索引節點高速緩存——主要存放的是描述磁盤索引節點的索引節點對象
頁高速緩存——主要存放的是完整的數據頁對象,每個頁所包含的數據一定屬於某個文件,同時,所有的文件讀寫操作都依賴於頁高速緩存。其是Linux內核所使用的主要磁盤高速緩存。 正是由於緩存的引入,所以VFS文件系統采用了文件數據延遲寫的技術,因此,如果在調用系統接口寫入數據時沒有使用同步寫模式,那麼大多數據將會先保存在緩存中,待等到滿足某些條件時才將數據刷入磁盤裡。
內核是如何將數據刷入磁盤的呢?在看完以下兩點後就能得到答案。
1. 把髒頁寫入磁盤 正如我們所了解的,內核不斷用包含塊設備數據的頁填充頁高速緩存。只要進程修改了數據,相應的頁就被標記為髒頁,即把它的PG_dirty標志位置。 Unix系統允許把髒緩沖區寫入塊設備的操作延遲執行,因為這種策略可以顯著地提高系統的性能。對高速緩存中的頁的幾次寫操作可能只需對相應的磁盤塊進行一次緩慢的物理更新就可以滿足。此外,寫操作沒有讀操作那麼緊迫,因為進程通常是不會因為延遲寫而掛起,而大部分情況都因為延遲讀而掛起。正是由於延遲寫,使得任一物理塊設備平均為讀請求提供服務將多於寫請求。一個髒頁可能直到最後一刻(即直到系統關閉時)都一直逗留在主存中。然而,從延遲寫策略的局限性來看,它有兩個主要的缺點: 一、如果發生了硬件錯誤或者電源掉電的情況,那麼就無法再獲得RAM的內容,因此,從系統啟動以來對文件進行的很多修改就丟失了。 二、頁高速緩存的大小(由此存放它所需的RAM的大小)就可要很大——至少要與所訪問塊設備的大小不同。因此,在下列條件下把髒頁刷新(寫入)到磁盤:
頁高速緩存變得太滿,但還需要更多的頁,或者髒頁的數量已經太多。
自從頁變成髒頁以來已過去太長時間。
進程請求對塊設備或者特定文件任何待定的變化都進行刷新。通過調用sync()、fsync()或者fdatasync()系統調用來實現。 緩沖區頁的引入是問題更加復雜。與每個緩沖區頁相關的緩沖區首部使內核能夠了解每個獨立塊緩沖區的狀態。如果至少有一個緩沖區首部的PG_Dirty標志被置位,就應該設置相應緩沖區頁的PG_dirty標志。當內核選擇要刷新的緩沖區時,它掃描相應的緩沖區首部,並只把髒塊的內容有效的寫到磁盤。一旦內核把緩沖區的所有髒頁刷新到磁盤,就把頁的PG_dirty標志清0。
2. pdflush內核線程 早期版本的Linux使用bdfllush內核線程系統地掃描頁高速緩存以搜索要刷新的髒頁,並且使用另一個內核線程kupdate來保證所有的頁不會“髒”太長時間。Linux 2.6用一組通用內核線程pdflush替代上述兩個線程。這些內核線程結構靈活,它們作用於兩個參數:一個指向線程要執行的函數的指針和一個函數要用的參數。系統中pdflush內核線程的數量是要動態調整的:pdflush線程太少時就創建,太多時就殺死。因為這些內核線程所執行的函數可以阻塞,所以創建多個而不是一個pdflush內核線程可以改善系統性能。根據下面的原則控制pdflush線程的產生和消亡:
必須有至少兩個,最多八個pdflush內核線程
如果到最近的1s期間沒有空閒pdflush,就應該創建新的pdflush線程
如果最近一次pdflush變為空閒的時間超過了1s,就應該刪除一個pdflush線程 所有的pdflush內核線程都有pdflush_work描述符,其數據結構如下:
類型字段說明
struct task_structwho指向內核線程描述符的指針
void (*) (unsigned long)fn內核線程所執行的回調函數
unsigned longarg0給回調函數的參數
struct list headlistpdflush_list鏈表的鏈接
unsigned longwhen_i_went_to_sleep當內核線程可用時的時間(以jiffies表示)
當系統沒有要刷新的髒頁時,pdflush線程會自動處於睡眠狀態,最後由pdflush_operation()函數來喚醒。那麼在這個過程中pdflush內核線程主要完成了哪些工作呢?其中一些工作與髒數據的刷新有關。尤其是pdflush通常執行下面的回調函數之一: 1. background_writeout(): 系統地掃描頁高速緩存以搜索要刷新的髒頁。
為了得到需要刷新的髒頁,就要徹底的搜索與在磁盤上有映像的索引節點相應的所有address_space對象(是一棵搜索樹)。由於頁高速緩存可能有大量的頁,如果用一個單獨的執行流來掃描整個高速緩存,會令CPU和磁盤長時間繁忙,因此,Linux使用一種復雜的機制把對頁高速緩存的掃描劃分為幾個執行流。當內存不足或者用戶顯式的(用戶態進程發出sync()系統調用等)調用請求刷新操作時會執行wakeup_bdflush()函數。wakeup_bdflush()函數會調用pdflush_operation()喚醒pdflush內核線程,並委托它執行回調函數background_writeout()。background_writeout()函數有效的從頁高速緩存中獲得指定數量的髒頁,並把它寫回磁盤。此外,執行background_writeout()函數的pdflush內核線程只有在滿足以下兩個條件下才能被喚醒:一是對頁高速緩存中的頁內容進行了修改,二是引起髒頁部分增加到超過某個髒背景阈值。背景阈值通常設置為系統中所有頁的10%,不過可以通過修改文件/proc/sys/vm/dirty_background_ratio來調整該值。
2. wb_kupdate():檢查頁高速緩存中是否有“髒”了很久時間的頁,避免當一些頁很久沒有被刷新時發生饑餓危險。
內核在初始化期間會建立wb_timer動態定時器,其的定時間距為dirty_writeback_centisecs文件中所規定的幾百分之一秒(通常是500分之一秒,不過可以通過修改/proc/sys/vm/dirty_writeback_centisecs文件調整該值)。定時器函數會調用pdflush_operation()函數,然後將wb_kupdate()函數的地址傳入。wb_kupdate()函數遍歷頁高速緩存搜索陳舊的髒索引節點,把已保持髒狀態時間超過30秒的頁都寫到磁盤,之後重置定時器。