Linux 內核 File cache 機制(中篇)

本篇則接上篇,主要介紹 Linux mmap 讀文件流程涉及缺頁中斷。

一、mmap 及缺頁中斷讀文件流程分析

上面篇幅,“什麼是 Filecache” 篇幅介紹了文件緩存框架,走讀了 read() 流程中涉及文件緩存的操作流程。對於用戶來說,讀過程一般是同步讀的過程(當然也有異步讀的接口,其思想跟文件緩存異步預讀機制類同,不再闡述),其執行完接口調用後所需數據就已經在內存中了,而操作的時候就通過 write() 等接口進行讀寫。

除了以上的接口,文件緩存的讀取和使用是否還有其他方法?答案是肯定的,例如 mmap() 等接口可以實現將文件映射到用戶地址空間,進程可以像訪問普通內存一樣對文件進行訪問。mmap 接口從內核的歸屬上是內存接口,Linux 系統對於用戶內存分配總是苛刻的,當用戶分配內存時,內核優先只給虛擬內存(MAP_POPULATE 等模式本文不作討論),只有使用時才分配物理內存,通過 mmap 接口進行文件映射屬於內核內存的管理框架內,自然也符合這個管理思想。

那麼系統怎麼及時地給用戶分配物理內存呢?這裏就引出缺頁中斷的概念:

本篇幅通過分析 mmap/munmap 調用執行流程和文件緩存的缺頁中斷流程,瞭解該路徑下文件緩存的讀取過程。

1. mmap 函數生命週期

mmap 系統調用用於將用戶空間的一端內存區域映射到內核空間,根據傳遞參數的不同,其可以實現共享內存、單獨分配匿名內存以及映射文件頁面等。本文主要闡述映射文件緩存,普通文件被映射到進程地址空間後,進程可以像訪問普通內存一樣對文件進行訪問,而不需要調用 read()/write() 接口。

對於具體的參數使用可以通過 man mmap 進行查閱。對於文件映射,其 mmap 的生命週期如下:

mmap 系統調用最終調用到內核的 ksys_mmap_pgoff 接口,文件映射則通過文件描述符得到 file 結構體,然後最終調用到 do_mmap 函數,MAP_POPULATE 本文不闡述。

do_mmap 執行可以簡單歸納如下:

get_unmmaped_area 用於在進程虛擬地址中找到一塊合適的空閒地址,內核中是通過紅黑樹進行管理和查找,對於虛擬內存的管理框架,如果讀者有興趣,可以查閱文章《進程內存管理初探》。(虛擬內存框架是相對抽象的子系統,包括 vma、vm_struct 等抽象結構體以及操作函數集合、地址管理邏輯等,如果讀者有興趣,可以在評論區留言出專篇)

下面走讀一下 mmap_region 函數,一探 mmap 的核心邏輯,函數定義於 mm/mmap.c 中:

函數的邏輯主幹很清晰,就不在細述。最後調用 call_mmap 函數通過 mapping->a_ops->mmap 回調文件系統操作函數集,以 ext4 文件系統爲例,定義在 fs/ext4/file.c 中:

至此 mmap 函數調用就結束了,可能讀者會很疑惑:mmap 構建了一個 vma 結構體後什麼都沒做?其實非 MAP_POPULATE 分支的 mmap 函數就是構建 vma 結構體,該結構抽象管理一段虛擬地址。如本章節篇頭提及,Linux 內核對用戶內存分配是很苛刻的,從內核的角度:因爲物理內存是公共資源且比較緊缺,用戶申請分配物理內存可能不用,避免共用資源浪費,所以就只先給虛擬內存,因爲虛擬內存是屬於進程內部獨立的,隻影響進程內部,對系統的影響僅僅是增加了 vma 等管理結構體的內存佔用開銷。

2. 文件緩存的缺頁中斷處理流程

用戶經過 mmap 接口後,得到一段虛擬內存,其沒有物理載體,即沒有承載任何用戶需要的數據,那麼當用戶訪問該虛擬內存後,怎麼獲取數據?答案就是 Linux 的缺頁中斷機制,簡單概括:

Linux 根據內存用途分成多種頁面,缺頁中斷也根據頁面類型會走不同的分支。本文只討論文件缺頁中斷,其他類型可根據相同思路走讀代碼。以 arm 處理器,其處理流程如下:

其核心處理階段包括:

本文走讀前三部分,最後一部分放在 IO 和文件系統篇幅,後續更新。

(1)缺頁中斷

缺頁中斷跟正常的中斷處理流程相似,硬件觸發經過彙編代碼後跳轉到 do_DataAbort 函數處理,定義在 arch/arm/mm/fault.c,arm 平臺有兩個寄存器用於記錄中斷的信息:

內核使用 fsr_info 結構體抽象異常處理操作集合:

以二級頁表架構爲例子,定義於 arch/arm/mm/fsr-2level.h 中:

通過 do_page_fault() 調用到__do_page_fault 函數:通過當前進程的 mm_struct 和異常的地址得到 vma 結構體,然後調用 handle_mm_fault 函數。

handle_mm_fault 函數定義在 mm/memory.c 中,此時已經進入內存管理框架內,該函數主要分配頁表,然後通過 handle_pte_fault 函數,針對不同的頁面類型,進行分支處理:

處理流程也很清晰,根據不同的情況走不同的分支,do_fault 函數用於處理文件頁面的缺頁中斷,當然文件頁面的缺頁中斷也有不同的原因:

本文走讀 do_read_fault 流程,其他分支讀者可以自行分析,其邏輯大同小異。

__do_fault 函數通過 vma->vm_ops->fault() 回調到具體文件系統的操作函數集合,前文已知 vm_ops 函數操作集是在具體的文件系統賦值的,以 ext4 文件系統爲例,ext4_filemap_fault 通過 filemap_fault 函數進行文件頁面讀取。

(2)filemap_fault

filemap_fault 是文件缺頁中斷的核心處理函數,定義於 mm/memory.c 中,其邏輯作用:

其主要的邏輯還是集中在預讀窗口的構建。

函數主幹簡化後如上,邏輯很清晰就不再細述。關注一下此時的同步預讀和異步預讀策略跟 read 分析的預讀流程,是否存在不同。

首先是異步預讀 do_async_mmap_readahead 函數:

@1 中 PG_Readahead 標誌,表示該頁面上次是提前異步讀取的第一個頁面,在 “read 發起讀文件流程分析:4.1 章節” 的__do_page_cache_readahead 中有分析。當前如果訪問到這個文件表示,上次提前預讀的頁面被命中,那麼可能是順序讀,此時需繼續進行異步預讀,page_cache_async_readahead 已經在 “read 發起讀文件流程分析” 章節分析,這裏不再累述。

ra_submit 通過__do_page_cache_readahead 函數進行預讀,該函數在 “read 發起讀文件流程分析” 章節都已經分析。

3. munmap 函數生命週期

munmap 系統調用邏輯非常簡單:

這裏需要注意的是,調用 munmap 接口後,是否文件緩存就會一定馬上被回收?答案是否定的:

二、File cache 回收流程分析

Linux File cache 的回收涉及的知識點很多,包括內存管理 LRU 機制、workingset 機制、內存回收 shrink 機制和髒頁管理機制等,此部分內容放置在《Linux 內核 File cache 機制(下篇)》中,後續發佈,有興趣的可以關注。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Tf-J7vFrUsPW08Iy7hcsTA