從Linux 2.6.32開始,Linux內(nèi)核臟頁回寫通過bdi_writeback機制實現(xiàn),bdi的全拼是backing device info(持久化存儲設(shè)備信息,如ssd、hdd)。用戶態(tài)調(diào)用write系統(tǒng)調(diào)用寫入數(shù)據(jù)后,文件系統(tǒng)只在頁緩存中寫入數(shù)據(jù)便返回了write系統(tǒng)調(diào)用,并沒有分配實際的物理磁盤塊,ext4稱為延遲分配技術(shù)(delay allocation)。本文將介紹內(nèi)核(kernel version 4.14)是在何時如何將寫入的數(shù)據(jù)回寫到磁盤。
核心數(shù)據(jù)結(jié)構(gòu)初始化
回寫機制借助了Linux中工作隊列來完成,在內(nèi)核啟動的時候,系統(tǒng)會使用alloc_workqueue函數(shù)申請一個用于回寫的工作隊列。具體實現(xiàn)在函數(shù)default_bdi_init中。
函數(shù)調(diào)用棧如下圖。
bdi_init()函數(shù)初始化bdi (struct backing_dev_info),該結(jié)構(gòu)體包含了塊設(shè)備信息,代表一個設(shè)備。
struct backing_dev_info:描述一個塊設(shè)備。
struct bdi_writeback:管理一個塊設(shè)備所有的回寫任務(wù)。
struct wb_writeback_work :描述需要回寫的任務(wù)。
還有管理回寫任務(wù)的結(jié)構(gòu)體bdi_writeback,描述任務(wù)的結(jié)構(gòu)體wb_writeback_work,其三者的關(guān)系如下圖所示。
backing_dev_info中維護了wb_list鏈表,管理bdi_writeback,同時每個bdi_writeback中維護了dwork和work_list,前者代表處理任務(wù)的函數(shù),后者則是任務(wù)列表。
在bdi_init中對bdi進行初始化后,會繼續(xù)調(diào)用倒wb_init(),該函數(shù)對bdi中的wb(struct bdi_writeback)進行初始化。
?
?
wb_init在初始化過程中,給wb->dwork字段賦值了函數(shù)wb_workfn,后面觸發(fā)回寫任務(wù)時,就會通過該函數(shù)進行執(zhí)行回寫。
至此bdi_writeback機制初始化完成。
觸發(fā)回寫任務(wù)的時機
由于寫入的數(shù)據(jù)都緩存在內(nèi)存中,猜想當(dāng)空閑內(nèi)存緊張的時候,內(nèi)核會執(zhí)行回寫任務(wù)。于是我們需要減少系統(tǒng)可用內(nèi)存,使用如下命令在內(nèi)存中創(chuàng)建文件系統(tǒng)然后往里面寫入文件。
使用 dd 命令在該目錄下創(chuàng)建文件。我們創(chuàng)建了一個79M的文件。
完成上述操作以后系統(tǒng)還剩余2M內(nèi)存,內(nèi)核并沒有立即觸發(fā)回寫,于是使用write系統(tǒng)調(diào)用繼續(xù)向磁盤寫入數(shù)據(jù)。
很快就觸發(fā)了內(nèi)核函數(shù)wakeup_flusher_threads(事先添加了斷點),函數(shù)調(diào)用棧如下:
從內(nèi)核函數(shù)調(diào)用棧來看是觸發(fā)了kswapd內(nèi)核線程的非活躍LRU鏈表回收。shrink_inactive_list函數(shù)掃描不活躍頁面鏈表并且回收頁面,調(diào)用了wakeup_flusher_threads函數(shù)進行回寫操作。
函數(shù)代碼如下,該函數(shù)遍歷所有bdi設(shè)備下的writeback,并通過函數(shù)wb_start_writeback執(zhí)行回寫操作:
GDB查看傳入wakeup_flusher_threads的參數(shù)值分別是nr_pages = 0和reason = WB_REASON_VMSCAN。
其中nr_pages等于0表示盡可能回寫所有的臟頁reason表示本次回寫觸發(fā)的原因。除了WB_REASON_VMSCAN,還定義了如下原因,如周期回寫:WB_REASON_PERIODIC,后臺回寫:WB_REASON_BACKGROUND。
我們繼續(xù)分析wb_start_writeback回寫函數(shù)。該函數(shù)創(chuàng)建并初始化了一個wb_writeback_work來描述本次回寫任務(wù),最后調(diào)用wb_queue_work。
wb_queue_work調(diào)用mod_delayed_work將該任務(wù)掛入工作隊列(workqueue),在等待delay時間后由工作隊列的工作線程(worker)執(zhí)行初始化時注冊的任務(wù)管理函數(shù)wb->dwork。Linux workqueue如何處理work的過程可以參考文章,本文跳過該過程,直接到回寫任務(wù)的處理函數(shù)wb_workfn繼續(xù)分析:
關(guān)于觸發(fā)內(nèi)核回寫的函數(shù)調(diào)用總結(jié)如下圖:
回寫任務(wù)的執(zhí)行
回寫的執(zhí)行在文件系統(tǒng)層的函數(shù)調(diào)用如下所示。
函數(shù)wb_workfn正常路徑為遍歷work_list,執(zhí)行wb_do_writeback函數(shù)。如果沒有足夠的worker則執(zhí)行writeback_inodes_wb函數(shù)回寫1024個臟頁。
wb_do_writeback函數(shù)在遍歷wb并調(diào)用wb_writeback回寫結(jié)束后會進行定時回寫和臟頁是否超過閾值的回寫檢查。
wb_writeback根據(jù)是否包含superblock,分別調(diào)用writeback_sb_inodes和__writeback_inodes_wb。
writeback_sb_inodes調(diào)用__writeback_single_inode。
?
__writeback_single_inode調(diào)用do_writepages。
do_writepages就出現(xiàn)了我們熟悉的頁緩存函數(shù)操作集struct address_space_operations *a_ops。其中writepages函數(shù)在ext4中的實現(xiàn)為ext4_writepages。
接下來會在ext4_writepages中打包bio結(jié)構(gòu)體,發(fā)送到通用塊層,繼續(xù)更底層的IO操作。
最后,bdi_writeback機制整體流程如下。
評論