內存回收(匿名頁反向映射)
本文為原創,轉載請注明:http://www.cnblogs.com/tolimit/
概述
看完了內存壓縮,最近在看內存回收這塊的代碼,發現內容有些多,需要分幾塊去詳細說明,首先先說說匿名頁的反向映射,匿名頁主要用於進程地址空間的堆、棧、還有私有匿名共享內存(用於有親屬關系的進程),這些匿名頁所屬的線性區叫做匿名線性區,這些線性區只映射內存,不映射具體磁盤上的文件。匿名頁的反向映射對匿名頁的回收起到了很大的作用。為了進行內存回收,內核為每個zone管理區的內存頁維護了5個LRU鏈表(最近最少使用鏈表),它們分別是:LRU_INACTIVE_ANON、LRU_ACTIVE_ANON、LRU_INACTIVE_FILE、LRU_ACTIVE_FILE、LRU_UNEVICTABLE。
- LRU_INACTIVE_ANON:保存所屬zone中的非活動匿名頁,每次會從鏈表頭部加入,這里的匿名頁都是從LRU_ACTIVE_ANON鏈表中移動過來的。這個鏈表長度一般為所屬zone的匿名頁數量的25%
- LRU_ACTIVE_ANON:保存所屬zone中的活動匿名頁,每次會從鏈表頭部加入,當LRU_INACTIVE_ANON的數量不足所屬zone的25%時,會從LRU_ACTIVE_ANON鏈表末尾移動一些頁到LRU_INACTIVE_ANON鏈表頭部。
- LRU_INACTIVE_FILE:保存所屬zone中的非活動文件頁,同LRU_INACTIVE_ANON類似。
- LRU_ACTIVE_FILE:保存所屬zone中的活動文件頁,同LRU_ACTIVE_ANON類似。
- LRU_UNEVICTABLE:保存所屬zone中的禁止回收的頁,一般這些頁通過mlock被鎖在內存中。
這篇文章先不詳細描述這幾個LRU鏈表,主要先說匿名頁的反向映射,在LRU_INACTIVE_ANON和LRU_ACTIVE_ANON這兩個鏈表中,鏈入的是物理頁框對應的頁描述符。當要進行內存回收時,內存回收函數會掃描LRU_INACTIVE_ANON鏈表中的頁,將一部分頁放入swap,然后釋放掉這個物理頁框,這時候會有個問題,有些進程已經將這個頁映射到了它們的頁表中,如果要講頁換出就需要對映射了此頁的進程頁表進行處理,並且映射了此頁的進程很多時候並不是只有一個。匿名頁反向映射就是作用在這種場景,它能夠通過物理頁框的頁描述符,找到所有映射了此頁的匿名線性區vma和所屬的進程,然后通過修改這些進程的頁表,標記此頁已被換出內存,之后這些進程訪問到此頁時,就能夠進行相應的處理。
數據結構
關於反向映射,需要稍微說幾個數據結構,分別是內存描述符struct mm_struct,線性區描述符struct vm_area_struct,頁描述符struct page,匿名線性區描述符struct anon_vma,和匿名線性區結點描述符struct anon_vma_chain。
每個進程都有自己的內存描述符struct mm_struct,除了內核線程(使用前一個進程的mm_struct)、輕量級進程(使用父進程的mm_struct)。在這個mm_struct中,在反向映射中,我們比較關心的參數如下:
/* 內存描述符,每個進程都會有一個,除了內核線程(使用被調度出去的進程的mm_struct)和輕量級進程(使用父進程的mm_struct) */
/* 所有的內存描述符存放在一個雙向鏈表中,鏈表中第一個元素是init_mm,它是初始化階段進程0的內存描述符 */
struct mm_struct {
/* 指向線性區對象的鏈表頭,鏈表是經過排序的,按線性地址升序排列,里面包括了匿名映射線性區和文件映射線性區 */
struct vm_area_struct *mmap; /* list of VMAs */
/* 指向線性區對象的紅黑樹的根,一個內存描述符的線性區會用兩種方法組織,鏈表和紅黑樹,紅黑樹適合內存描述符有非常多線性區的情況 */
struct rb_root mm_rb;
u32 vmacache_seqnum; /* per-thread vmacache */
#ifdef CONFIG_MMU
/* 在進程地址空間中找一個可以使用的線性地址空間,查找一個空閑的地址區間
* len: 指定區間的長度
* 返回新區間的起始地址
*/
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
#endif
/* 標識第一個分配的匿名線性區或文件內存映射的線性地址 */
unsigned long mmap_base; /* base of mmap area */
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
unsigned long task_size; /* size of task vm space */
/* 所有vma中最大的結束地址 */
unsigned long highest_vm_end; /* highest vma end address */
/* 指向頁全局目錄 */
pgd_t * pgd;
/* 次使用計數器,存放了共享此mm_struct的輕量級進程的個數,但所有的mm_users在mm_count的計算中只算作1 */
atomic_t mm_users; /* 初始為1 */
/* 主使用計數器,當mm_count遞減時,系統會檢查是否為0,為0則解除這個mm_struct */
atomic_t mm_count; /* 初始為1 */
/* 頁表數 */
atomic_long_t nr_ptes; /* Page table pages */
/* 線性區的個數,默認最多是65535個,系統管理員可以通過寫/proc/sys/vm/max_map_count文件修改這個值 */
int map_count; /* number of VMAs */
/* 線性區的自旋鎖和頁表的自旋鎖 */
spinlock_t page_table_lock; /* Protects page tables and some counters */
/* 線性區的讀寫信號量,當需要對某個線性區進行操作時,會獲取 */
struct rw_semaphore mmap_sem;
/* 用於鏈入雙向鏈表中 */
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/
/* 進程所擁有的最大頁框數 */
unsigned long hiwater_rss; /* High-watermark of RSS usage */
/* 進程線性區中的最大頁數 */
unsigned long hiwater_vm; /* High-water virtual memory usage */
/* 進程地址空間的大小(頁框數) */
unsigned long total_vm; /* Total pages mapped */
/* 鎖住而不能換出的頁的數量 */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
unsigned long pinned_vm; /* Refcount permanently increased */
/* 共享文件內存映射中的頁數量 */
unsigned long shared_vm; /* Shared pages (files) */
/* 可執行內存映射中的頁數量 */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE */
/* 用戶態堆棧的頁數量 */
unsigned long stack_vm; /* VM_GROWSUP/DOWN */
unsigned long def_flags;
/* start_code: 可執行代碼的起始位置
* end_code: 可執行代碼的最后位置
* start_data: 已初始化數據的起始位置
* end_data: 已初始化數據的最后位置
*/
unsigned long start_code, end_code, start_data, end_data;
/* start_brk: 堆的起始位置
* brk: 堆的當前最后地址
* start_stack: 用戶態棧的起始地址
*/
unsigned long start_brk, brk, start_stack;
/* arg_start: 命令行參數的起始位置
* arg_end: 命令行參數的最后位置
* env_start: 環境變量的起始位置
* env_end: 環境變量的最后位置
*/
unsigned long arg_start, arg_end, env_start, env_end;
#ifdef CONFIG_MEMCG
/* 所屬進程 */
struct task_struct __rcu *owner;
#endif
/* 代碼段中映射的可執行文件的file */
struct file *exe_file;
......
};
這里面需要注意的就是mmap鏈表和mm_rb這個紅黑樹,一個進程的所有線性區vma都會被鏈入此進程的mm_struct中的mmap鏈表和mm_rb紅黑樹,這兩個都是為了查找線性區vma方便。
再來看看線性區vma描述符,線性區分為匿名映射線性區和文件映射線性區,如下:
/* 描述線性區結構
* 內核盡力把新分配的線性區與緊鄰的現有線性區進程合並。如果兩個相鄰的線性區訪問權限相匹配,就能把它們合並在一起。
* 每個線性區都有一組連續號碼的頁(非頁框)所組成,而頁只有在被訪問的時候系統會產生缺頁異常,在異常中分配頁框
*/
struct vm_area_struct {
/* 線性區內的第一個線性地址 */
unsigned long vm_start;
/* 線性區之外的第一個線性地址 */
unsigned long vm_end;
/* 整個鏈表會按地址大小遞增排序 */
/* vm_next: 線性區鏈表中的下一個線性區 */
/* vm_prev: 線性區鏈表中的上一個線性區 */
struct vm_area_struct *vm_next, *vm_prev;
/* 用於組織當前內存描述符的線性區的紅黑樹的結點 */
struct rb_node vm_rb;
/* 此vma的子樹中最大的空閑內存塊大小(bytes) */
unsigned long rb_subtree_gap;
/* 指向所屬的內存描述符 */
struct mm_struct *vm_mm;
/* 頁表項標志的初值,當增加一個頁時,內核根據這個字段的值設置相應頁表項中的標志 */
/* 頁表中的User/Supervisor標志應當總被置1 */
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
/* 線性區標志
* 讀寫可執行權限會復制到頁表項中,由分頁單元去檢查這幾個權限
*/
unsigned long vm_flags; /* Flags, see mm.h. */
/* 鏈接到反向映射所使用的數據結構,用於文件映射的線性區,主要用於文件頁的反向映射 */
union {
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} linear;
struct list_head nonlinear;
} shared;
/*
* 指向匿名線性區鏈表頭的指針,這個鏈表會將此mm_struct中的所有匿名線性區鏈接起來
* 匿名的MAP_PRIVATE、堆和棧的vma都會存在於這個anon_vma_chain鏈表中
* 如果mm_struct的anon_vma為空,那么其anon_vma_chain也一定為空
*/
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
/* 指向anon_vma數據結構的指針,對於匿名線性區,此為重要結構 */
struct anon_vma *anon_vma;
/* 指向線性區操作的方法,特殊的線性區會設置,默認會為空 */
const struct vm_operations_struct *vm_ops;
/* 在映射文件中的偏移量。對匿名頁,它等於0或者vm_start/PAGE_SIZE */
unsigned long vm_pgoff;
/* 指向映射文件的文件對象,也可能指向建立shmem共享內存中返回的struct file,如果是匿名線性區,此值為NULL或者一個匿名文件(這個匿名文件跟swap有關?待看) */
struct file * vm_file;
/* 指向內存區的私有數據 */
void * vm_private_data; /* was vm_pte (shared mem) */
......
#ifndef CONFIG_MMU
struct vm_region *vm_region;
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy;
#endif
};
對我們匿名頁的反向映射來說,在vma中最重要的就是struct anon_vma * anon_vma和struct list_head anon_vma_chain。前者指向此一個匿名線性區的anon_vma結構,而anon_vma_chain用於整理一個所有屬於本vma的anon_vma_chain鏈表。具體后面會分析。
我們再看看struct anon_vma結構,這個結構是幾乎每個匿名線性區vma都會有的(除了兩個相鄰並且特征相同的匿名線性區會使用同一個anon_vma):
/* 匿名線性區描述符,每個匿名vma都會有一個這個結構 */
struct anon_vma {
/* 指向此anon_vma所屬的root */
struct anon_vma *root; /* Root of this anon_vma tree */
/* 讀寫信號量 */
struct rw_semaphore rwsem; /* W: modification, R: walking the list */
/* 紅黑樹中結點數量,初始化時為1,也就是只有本結點,當加入root的anon_vma的紅黑樹時,此值不變 */
atomic_t refcount;
/* 紅黑樹的根,用於存放引用了此anon_vma所屬線性區中的頁的其他線性區,用於匿名頁反向映射 */
struct rb_root rb_root; /* Interval tree of private "related" vmas */
};
這里面重要的是一個root指針,和一個rb_root紅黑樹,root指針指向此anon_vma的root(並不是指向其所屬的vma),然后紅黑樹時用於將不同進程的anon_vma_chain加入進來。單這樣看此結構現在看來比較難以理解,先不用管,之后慢慢分析。
再看看struct anon_vma_chain結構:
struct anon_vma_chain {
/* 此結構所屬的vma */
struct vm_area_struct *vma;
/* 此結構加入的紅黑樹所屬的anon_vma */
struct anon_vma *anon_vma;
/* 用於加入到所屬vma的anon_vma_chain鏈表中 */
struct list_head same_vma;
/* 用於加入到其他進程或者本進程vma的anon_vma的紅黑樹中 */
struct rb_node rb;
unsigned long rb_subtree_last;
#ifdef CONFIG_DEBUG_VM_RB
unsigned long cached_vma_start, cached_vma_last;
#endif
};
這個anon_vma_chain有兩個重要的結點,一個anon_vma_chain鏈表結點,一個紅黑樹結點,anon_vma_chain鏈表結點用於加入到其所屬的vma中,而rb紅黑樹結點加入到其他進程或者本進程的vma的anon_vma的紅黑樹中。
還有一個頁描述符struct page,我們主要關心的就是它的mapping變量,如果此頁是匿名頁,它的mapping變量會指向第一個訪問此頁的vma的anon_vma:
struct page {
/* First double word block */
/* 用於頁描述符,一組標志(如PG_locked、PG_error),同時頁框所在的管理區和node的編號也保存在當中 */
/* 在lru算法中主要用到兩個標志
* PG_active: 表示此頁當前是否活躍,當放到active_lru鏈表時,被置位
* PG_referenced: 表示此頁最近是否被訪問,每次頁面訪問都會被置位
*/
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
union {
/* 最低兩位用於判斷類型,其他位數用於保存指向的地址
* 如果為空,則該頁屬於交換高速緩存(swap cache,swap時會產生競爭條件,用swap cache解決)
* 不為空,如果最低位為1,該頁為匿名頁,指向對應的anon_vma(分配時需要對齊)
* 不為空,如果最低位為0,則該頁為文件頁,指向文件的address_space
*/
struct address_space *mapping; /* If low bit clear, points to
* inode address_space, or NULL.
* If page mapped as anonymous
* memory, low bit is set, and
* it points to anon_vma object:
* see PAGE_MAPPING_ANON below.
*/
/* 用於SLAB描述符,指向第一個對象的地址 */
void *s_mem; /* slab first object */
};
/* Second double word */
struct {
union {
/* 作為不同的含義被幾種內核成分使用。例如,它在頁磁盤映像或匿名區中標識存放在頁框中的數據的位置,或者它存放一個換出頁標識符
* 當此頁作為映射頁(文件映射)時,保存這塊頁的數據在整個文件數據中以頁為大小的偏移量
* 當此頁作為匿名頁時,保存此頁在線性區vma內的頁索引或者是頁的線性地址/PAGE_SIZE。
* 對於匿名頁的page->index表示的是page在vma中的偏移。共享匿名頁的產生應該只有在fork,clone等時候。
*/
pgoff_t index; /* Our offset within mapping. */
/* 用於SLAB和SLUB描述符,指向空閑對象鏈表 */
void *freelist;
/* 當管理區頁框分配器壓力過大時,設置這個標志就確保這個頁框專門用於釋放其他頁框時使用 */
bool pfmemalloc; /* If set by the page allocator,
* ALLOC_NO_WATERMARKS was set
* and the low watermark was not
* met implying that the system
* is under some pressure. The
* caller should try ensure
* this page is only used to
* free other pages.
*/
};
......
}
主要關注mapping和index,如果此頁被分配作為一個匿名頁,那么它的mapping會指向一個anon_vma,而index保存此匿名頁在vma中以頁的偏移量(比如vma的線性地址區間是12個頁的大小,此頁映射到了第8頁包含的線性地址上)。需要注意的是,mapping保存anon_vma變量地址時,會執行如下操作:
page->mapping = (void *)&anon_vma + 1;
anon_vma分配時要2字節對齊,也就是所有分配的anon_vma其最低位都為0,經過上面的操作,mapping最低位就為1了,然后通過mapping獲取anon_vma地址時,進行如下操作:
struct anon_vma * anon_vma = (struct anon_vma *)(page->mapping - 1);
這幾個結構都看完了,后面我們具體分析內核是怎么把這幾個結構組織起來,又怎么通過一個頁的頁描述符獲得所有映射了此頁的vma。我們將通過一條路徑進程分析,這條路徑是:一個空閑的匿名線性區訪問了其所屬線性地址區間的頁 -> 這個匿名線性區所屬的進程fork了一個子進程 -> 父子進程分別訪問了線性區中的頁。而這條路徑中涉及到幾個點:創建匿名頁與匿名線性區的關聯性、父子進程匿名線性區的關聯性、父子進程繼續訪問此匿名線性區的頁時的關聯性。
建立反向映射流程
我們將通過一條路徑進程分析,這條路徑是:一個空閑的匿名線性區訪問了其所屬線性地址區間的頁 -> 這個匿名線性區所屬的進程fork了一個子進程 -> 父子進程分別訪問了線性區中的頁。選擇這條路徑是方便說明內核是如何組織匿名頁反向映射的。
建立匿名線性區有兩種情況,一種是通過mmap建立私有匿名線性區,另一種是fork時子進程克隆了父進程的匿名線性區,這兩種方式有所區別,首先mmap建立私有匿名線性區時,應用層調用mmap時傳入的參數fd必須為-1,即不指定映射的文件,參數flags必須有MAP_ANONYMOUS和MAP_PRIVATE。如果是參數是MAP_ANONYMOUS和MAP_SHARED,創建的就不是匿名線性區了,而是使用shmem的共享內存線性區,這種shmem的共享內存線性區會使用打開/dev/zero的fd。而mmap使用MAP_ANONYMOUS和MAP_PRIVATE創建,可以得到一個空閑的匿名線性區,由於mmap代碼中更多的是涉及文件映射線性區的創建,這里就先不給代碼,當創建好一個匿名線性區后,結果如下:

創建后anon_vma和anon_vma_chain都為空,並且此線性區對應的線性地址區間的頁表項也都為空,但是此vma已經創建完成,之后進程訪問此vma的地址區間時合理的,我們知道,內核在創建vma時並不會馬上對整個vma的地址進行頁表的處理,只有在進程訪問此vma的某個地址時,會產生一個缺頁異常,在缺頁異常中判斷此地址屬於進程的vma並且合理,才會分配一個頁用於頁表映射,之后進程就可以順利讀寫這個地址所在的頁框。也就是說,我一個匿名線性區vma,開始地址是0,結束地址是8K,當我訪問6k這個地址時,內核會做好4K~8K地址的映射(正好是一個頁大小,四級頁表中一個頁表項映射的大小),而此匿名線性區0~4k的地址是沒有進行映射的。只有在第一次訪問的時候才會進行映射。
這時我們假設進程訪問了此新建的線性區的線性地址區間,由於此線性區是新建的,它的線性地址區間對應的頁表項並不會在創建的時候進行映射,所以會產生了缺頁異常,在缺頁異常中首先會判斷此線性地址是否所屬vma,如果此線性地址所屬的vma是匿名線性區,會通過此進程的頁表判斷發送異常的線性地址的頁表項,如果是第一次訪問此線性地址,此頁表項必定為空並且頁也肯定不在內存中(都沒有映射的頁),則說明此線性地址是第一次訪問到,會調用do_anonymous_page()函數進行處理,我們看看此函數是如何處理的:
/* 分配一個匿名頁,並為此頁建立映射和反向映射
* 進入此函數的條件,線性地址address對應的進程的頁表項為空,並且address所屬vma是匿名線性區
* 進入到此函數前,已經對address對應的頁全局目錄項、頁上級目錄項、頁中間目錄項和頁表進行分配和設置
*/
static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags)
{
struct mem_cgroup *memcg;
struct page *page;
spinlock_t *ptl;
pte_t entry;
/* X86_64下這里沒做任何事,而X86_32位下如果page_table之前用來建立了臨時內核映射,則釋放該映射 */
pte_unmap(page_table);
/* Check if we need to add a guard page to the stack */
/* 如果vma是向下增長的,並且address等於vma的起始地址,那么將vma起始地址處向下擴大一個頁用於保護頁
* 同樣,如果vma是向上增長的,address等於vma的結束地址,頁將vma在結束地址處向上擴大一個頁用於保護頁
*/
if (check_stack_guard_page(vma, address) < 0)
return VM_FAULT_SIGBUS;
/* Use the zero-page for reads */
/* vma中的頁是只讀的的情況,因為是匿名頁,又是只讀的,不會是代碼段,這里執行成功則直接設置頁表項pte,不會進行反向映射 */
if (!(flags & FAULT_FLAG_WRITE)) {
/* 創建pte頁表項,這個pte會指向內核中一個默認的全是0的頁框,並且會有vma->vm_page_prot中的標志,最后會加上_PAGE_SPECIAL標志 */
entry = pte_mkspecial(pfn_pte(my_zero_pfn(address),
vma->vm_page_prot));
/* 當(NR_CPUS >= CONFIG_SPLIT_PTLOCK_CPUS)並且配置了USE_SPLIT_PTE_PTLOCKS時,對pmd所在的頁上鎖(鎖是頁描述符的ptl)
* 否則對整個頁表上鎖,鎖是mm->page_table_lock
* 並再次獲取address對應的頁表項,有可能在其他核上被修改?
*/
page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
/* 如果頁表項不為空,則說明這頁曾經被該進程訪問過,可能其他核上更改了此頁表項 */
if (!pte_none(*page_table))
goto unlock;
goto setpte;
}
/* Allocate our own private page. */
/* 為vma准備反向映射條件
* 檢查此vma能與前后的vma進行合並嗎,如果可以,則使用能夠合並的那個vma的anon_vma,如果不能夠合並,則申請一個空閑的vma
* 新建一個anon_vma_chain,並且如果vma沒有anon_vma則新建一個
* 將avc->anon_vma指向獲得的vma,avc->vma指向vma,並把avc加入到vma的anon_vma_chain中
*/
if (unlikely(anon_vma_prepare(vma)))
goto oom;
/* 從高端內存區的伙伴系統中獲取一個頁,這個頁會清0 */
page = alloc_zeroed_user_highpage_movable(vma, address);
/* 分配不成功 */
if (!page)
goto oom;
/* 設置此頁的PG_uptodate標志,表示此頁是最新的 */
__SetPageUptodate(page);
/* 更新memcg中的計數,如果超過了memcg中的限制值,則會把這個頁釋放掉,並返回VM_FAULT_OOM */
if (mem_cgroup_try_charge(page, mm, GFP_KERNEL, &memcg))
goto oom_free_page;
/* 根據vma的頁參數,創建一個頁表項 */
entry = mk_pte(page, vma->vm_page_prot);
/* 如果vma區是可寫的,則給頁表項添加允許寫標志 */
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry));
/* 並再次獲取address對應的頁表項,並且上鎖,鎖可能在頁中間目錄對應的struct page的ptl中,也可能是mm_struct的page_table_lock
* 因為需要修改,所以要上鎖,而只讀的情況是不需要上鎖的
*/
page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
if (!pte_none(*page_table))
goto release;
/* 增加mm_struct中匿名頁的統計計數 */
inc_mm_counter_fast(mm, MM_ANONPAGES);
/* 對這個新頁進行反向映射
* 主要工作是:
* 設置此頁的_mapcount = 0,說明此頁正在使用,但是是非共享的(>0是共享)
* 統計
* 設置page->mapping最低位為1
* page->mapping指向此vma->anon_vma
* page->index存放此page在vma中的第幾頁
*/
page_add_new_anon_rmap(page, vma, address);
/* 提交memcg中的統計 */
mem_cgroup_commit_charge(page, memcg, false);
/* 通過判斷,將頁加入到活動lru緩存或者不能換出頁的lru鏈表 */
lru_cache_add_active_or_unevictable(page, vma);
setpte:
/* 將上面配置好的頁表項寫入頁表 */
set_pte_at(mm, address, page_table, entry);
/* No need to invalidate - it was non-present before */
/* 讓mmu更新頁表項,應該會清除tlb */
update_mmu_cache(vma, address, page_table);
unlock:
/* 解鎖 */
pte_unmap_unlock(page_table, ptl);
return 0;
/* 以下是錯誤處理 */
release:
/* 取消此page在memcg中的計數,這里處理會在mem_cgroup_commit_charge()之前 */
mem_cgroup_cancel_charge(page, memcg);
/* 將此頁釋放到每CPU頁高速緩存中 */
page_cache_release(page);
goto unlock;
oom_free_page:
page_cache_release(page);
oom:
return VM_FAULT_OOM;
}
這里面流程很簡單:
- 調用anon_vma_prepare()獲取一個anon_vma結構,這個結構可能屬於此vma,也可能屬於此vma能夠合並的前后一個vma
- 通過伙伴系統分配一個頁(在32位上,會優先從高端內存分配)
- 根據vma默認頁表項參數vm_page_prot創建一個頁表項,這個頁表項用於加入到address對應的頁表中
- 調用page_add_new_anon_rmap()給此page添加一個反向映射
- 將頁表項和頁表還有此頁進行關聯,由於頁表已經在調用前分配好頁了,只需要將頁表項與新匿名頁進行關聯,然后將設置好的頁表項寫入address在此頁表中的偏移地址即可。
着重看anon_vma_prepare()和page_add_new_anon_rmap()這兩個函數,我們先看page_add_new_anon_rmap()函數,這個函數比較固定和簡單:
/* 對一個新頁進行反向映射
* page: 目標頁
* vma: 訪問此頁的vma
* address: 線性地址
*/
void page_add_new_anon_rmap(struct page *page,
struct vm_area_struct *vma, unsigned long address)
{
/* 地址必須處於vma中 */
VM_BUG_ON_VMA(address < vma->vm_start || address >= vma->vm_end, vma);
SetPageSwapBacked(page);
/* 設置此頁的_mapcount = 0,說明此頁正在使用,但是是非共享的(>0是共享) */
atomic_set(&page->_mapcount, 0); /* increment count (starts at -1) */
/* 如果是透明大頁 */
if (PageTransHuge(page))
/* 統計 */
__inc_zone_page_state(page, NR_ANON_TRANSPARENT_HUGEPAGES);
__mod_zone_page_state(page_zone(page), NR_ANON_PAGES,
hpage_nr_pages(page));
/* 進行反向映射
* 設置page->mapping最低位為1
* page->mapping指向此vma->anon_vma
* page->index存放此page在vma中的第幾頁
*/
__page_set_anon_rmap(page, vma, address, 1);
}
這個函數沒什么好說的,主要記住,如果頁為匿名頁並且是一個新頁(沒有進行過映射到進程),頁描述符的mapping會指向第一次訪問它的vma的anon_vma。
主要看anon_vma_prepare(),在這個函數中,首先會檢查此vma有沒有anon_vma,其次會檢查此vma能否與前后的vma進行合並,如果可以合並,則只創建一個anon_vma_chain做一定的關聯,否則會創建一個anon_vma和一個anon_vma_chain進行一定的關聯,先看代碼:
/* 為vma准備反向映射條件
* 如果vma沒有anon_vma則新建一個,並且同時新建一個anon_vma_chain
* 檢查此vma能與前后的vma進行合並嗎,如果可以,則使用能夠合並的那個vma的anon_vma,如果不能夠合並,則申請一個空閑的vma
* 將avc->anon_vma指向獲得的vma,avc->vma指向vma,並把avc加入到vma的anon_vma_chain中
*/
int anon_vma_prepare(struct vm_area_struct *vma)
{
/* 獲取vma的反向映射的anon_vma結構 */
struct anon_vma *anon_vma = vma->anon_vma;
struct anon_vma_chain *avc;
/* 檢查是否需要睡眠 */
might_sleep();
/* 如果此vma的anon_vma為空,則進行以下處理 */
if (unlikely(!anon_vma)) {
/* 獲取vma所屬的mm */
struct mm_struct *mm = vma->vm_mm;
struct anon_vma *allocated;
/* 通過slab/slub分配一個struct anon_vma_chain */
avc = anon_vma_chain_alloc(GFP_KERNEL);
if (!avc)
goto out_enomem;
/* 檢查vma能否與其前后的vma進行合並,如果可以,則返回能夠合並的那個vma的anon_vma
* 先檢查能否與其后的vma合並,再檢查能否與前面的vma合並
*/
/* 檢查vma能否與其前/后vma進行合並,如果可以,則返回能夠合並的那個vma的anon_vma
* 主要檢查vma前后的vma是否連在一起(vma->vm_end == 前/后vma->vm_start)
* vma->vm_policy和前/后vma->vm_policy
* 是否都為文件映射,除了(VM_READ|VM_WRITE|VM_EXEC|VM_SOFTDIRTY)其他標志位是否相同,如果為文件映射,前/后vma映射的文件位置是否正好等於vma映射的文件 + vma的長度
* 這里有個疑問,為什么匿名線性區會有vm_file不為空的時候,我也沒找到原因
* 可以合並,則返回可合並的線性區的anon_vma
*/
anon_vma = find_mergeable_anon_vma(vma);
allocated = NULL;
/* anon_vma為空,也就是vma不能與前后的vma合並,則會分配一個 */
if (!anon_vma) {
/* 從anon_vma_cachep這個slab中分配一個anon_vma結構,將其refcount設為1,anon_vma->root指向本身 */
anon_vma = anon_vma_alloc();
if (unlikely(!anon_vma))
goto out_enomem_free_avc;
/* 剛分配好的anon_vma存放在allocated */
allocated = anon_vma;
}
/* 到這里,anon_vma有可能是可以合並的vma的anon_vma,也有可能是剛分配的anon_vma */
/* 對anon_vma->root->rwsem上寫鎖,如果是新分配的anon_vma則是其本身的rwsem */
anon_vma_lock_write(anon_vma);
/* page_table_lock to protect against threads */
/* 獲取當前進程的線性區鎖 */
spin_lock(&mm->page_table_lock);
/* 如果vma->anon_vma為空,這是很可能發生的,因為此函數開頭獲取的anon_vma為空才會走到這條代碼路徑上 */
if (likely(!vma->anon_vma)) {
/* 將vma->anon_vma設置為新分配的anon_vma,這個anon_vma也可能是前后能夠合並的vma的anon_vma */
vma->anon_vma = anon_vma;
/*
* avc->vma = vma
* avc->anon_vma = anon_vma(這個可能是當前vma的anon_vma,也可能是前后可合並vma的anon_vma)
* 將新的avc->same_vma加入到vma的anon_vma_chain鏈表中
* 將新的avc->rb加入到anon_vma的紅黑樹中
*/
anon_vma_chain_link(vma, avc, anon_vma);
/* 這兩個置空,后面就不會釋放掉 */
allocated = NULL;
avc = NULL;
}
/* 釋放mm的線性區鎖 */
spin_unlock(&mm->page_table_lock);
/* 釋放anon_vma的寫鎖 */
anon_vma_unlock_write(anon_vma);
if (unlikely(allocated))
put_anon_vma(allocated);
if (unlikely(avc))
anon_vma_chain_free(avc);
}
return 0;
out_enomem_free_avc:
anon_vma_chain_free(avc);
out_enomem:
return -ENOMEM;
}
我們畫圖描述兩種情況最后生成的結果:
這種是此vma沒有與其前后的vma進行合並:

可以看出,當一個新的vma不能與前后相似vma進行合並是,會為此新vma創建專屬的anon_vma和一個anon_vma_chain結構,然后將anon_vma_chain鏈入這個新的vma的anon_vma_chain鏈表中,並且加入到這個新的vma的anon_vma的紅黑樹中。而之后再次訪問此vma中不屬於已經映射好的頁的其他地址時,就不需要再次為此vma創建anon_vma和anon_vma_chain結構了。
而另一種情況,是此vma能與前后的vma進行合並,系統就不會為此vma創建anon_vma,而是這兩個vma共用一個anon_vma,但是會創建一個anon_vma_chain,如下:

這種情況,如果新的vma能夠與前后相似vma進行合並,則不會為這個新的vma創建anon_vma結構,而是將此新的vma的anon_vma指向能夠合並的那個vma的anon_vma。不過內核會為這個新的vma建立一個anon_vma_chain,鏈入這個新的vma中,並加入到新的vma所指的anon_vma的紅黑樹中。好的,到這里,建立一個新的匿名線性區並且訪問它的地址空間的邏輯已經理清,先別急着看懂這個圖,現在看很難理解,慢慢往后面看,就會明白anon_vma和anon_vma_chain為什么要這樣組織起來。
父子進程的匿名線性區關系
一些映射了相同匿名頁的匿名線性區,它們的關系是怎么樣的,首先,匿名線性區只有三種,堆、棧、mmap私有匿名映射,而我們知道,在fork過程中,子進程會拷貝父進程的vma(除了標記有VM_DONTCOPY的vma)和這些vma對應的頁表項,即使這些vma是非共享的(如堆、棧),fork也會這樣拷貝。這樣做的原因是為了效率和降低內存的使用率。而之后,內核會使用寫時復制技術,即使那些vma映射的頁是共享的,當父進程或子進程對這些中的某個頁進行寫入時,內核會拷貝一份這個頁的副本,並覆蓋掉原先這個頁所映射的頁表項,這樣就會隔離出來了。當然,父子進程對還沒映射的頁進行訪問時,都是映射各自的頁表,比如:父進程的匿名線性區vma的線性地址區間是0~8k,已經映射了4k~8k的區域,0~4k的區域沒有映射。此時父進程fork了一個子進程,此子進程的此vma也是映射了4k~8k的區域,0~4k區域沒有映射,當子進程訪問0~4k這段地址時,內核會分配一個頁,把這個頁映射子進程頁表中到0~4k這段地址對應的頁表項,並不會映射到父進程的頁表中。
所以,在fork完之后,內核需要能夠通過頁框,找到映射了此頁的vma和進程,這里就需要了解在fork時,怎么組織父子進程的匿名線性區反向映射的結構anon_vma和anon_vma_chain。,這些代碼主要都處於fork路徑中的dup_mm()函數里,在這個函數中,會以父進程的mm_struct為標准,初始化子進程的mm_struct。然后遍歷父進程的所有vma,在遍歷父進程的所有vma過程中,為每個父進程的匿名線性區建立一個子進程對應的匿名線性區vma、一個anon_vma和一個或多個anon_vma_chain。並將它們建立關系,然后子進程頁表對這些vma映射的頁建立關系。特別是對可寫vma的處理時,會將vma對應父子進程的頁表項都設置為只讀,具體詳細看代碼:
/*
* mm: 子進程的mm_struct
* oldmm: 父進程的mm_struct
*/
static int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
{
struct vm_area_struct *mpnt, *tmp, *prev, **pprev;
struct rb_node **rb_link, *rb_parent;
int retval;
unsigned long charge;
/* 獲取每CPU的dup_mmap_sem這個讀寫信號量的讀鎖 */
uprobe_start_dup_mmap();
/* 獲取父進程的mm->mmap_sem的寫鎖 */
down_write(&oldmm->mmap_sem);
/* 這里x86下為空 */
flush_cache_dup_mm(oldmm);
/* 如果父進程的mm_struct的flags設置了MMF_HAS_UPROBES,則子進程的mm_struct的flags設置MMF_HAS_UPROBES和MMF_RECALC_UPROBES */
uprobe_dup_mmap(oldmm, mm);
/*
* Not linked in yet - no deadlock potential:
*/
/* 獲取子進程的mmap_sem,后面SINGLE_DEPTH_NESTING意思需要查查 */
down_write_nested(&mm->mmap_sem, SINGLE_DEPTH_NESTING);
/* 復制父進程進程地址空間的大小(頁框數)到子進程的mm */
mm->total_vm = oldmm->total_vm;
/* 復制父進程共享文件內存映射中的頁數量到子進程mm */
mm->shared_vm = oldmm->shared_vm;
/* 復制父進程可執行內存映射中的頁數量到子進程的mm */
mm->exec_vm = oldmm->exec_vm;
/* 復制父進程用戶態堆棧的頁數量到子進程的mm */
mm->stack_vm = oldmm->stack_vm;
/* 子進程vma紅黑樹的根結點,保存到rb_link中 */
rb_link = &mm->mm_rb.rb_node;
rb_parent = NULL;
/* 獲取指向線性區對象的鏈表頭,鏈表是經過排序的,按線性地址升序排列,但是mmap並不是list_head結構,是一個struct vm_area_struct指針,這里由於子進程的mm是剛創建的,mm->map為空,而pprev是一個指向指針的指針 */
pprev = &mm->mmap;
/* 暫時不看,與ksm有關 */
retval = ksm_fork(mm, oldmm);
if (retval)
goto out;
/* 也暫時不看 */
retval = khugepaged_fork(mm, oldmm);
if (retval)
goto out;
prev = NULL;
/* 遍歷父進程所有vma,通過mm->mmap鏈表遍歷 */
for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next) {
/* mpnt指向父進程的一個vma */
struct file *file;
/* 父進程的此vma標記了不復制 */
if (mpnt->vm_flags & VM_DONTCOPY) {
/* 做統計,因為上面把父進程的total_vm、shared_vm、exec_vm、stack_vm都復制過來了,這些等於父進程所有vma的頁的總和,這里這個vma不復制,要相應減掉此vma的頁數量 */
vm_stat_account(mm, mpnt->vm_flags, mpnt->vm_file,
-vma_pages(mpnt));
continue;
}
charge = 0;
/* 此vma要求需要檢查是否有足夠的空閑內存用於映射 */
if (mpnt->vm_flags & VM_ACCOUNT) {
/* 此vma的頁數量, (mpnt->vm_end - mpnt->vm_start) >> PAGE_SHIFT */
unsigned long len = vma_pages(mpnt);
/* 安全檢查,是否有足夠的內存 */
if (security_vm_enough_memory_mm(oldmm, len)) /* sic */
goto fail_nomem;
charge = len;
}
/* 分配一個vma結構體用於子進程使用 */
tmp = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);
/* 分配失敗 */
if (!tmp)
goto fail_nomem;
/* 直接復制父進程的vma的數據到子進程的vma */
*tmp = *mpnt;
/* 初始化子進程新的vma的anon_vma_chain為空 */
INIT_LIST_HEAD(&tmp->anon_vma_chain);
/* 視情況復制父進程vma的權限,非vm_flags */
retval = vma_dup_policy(mpnt, tmp);
if (retval)
goto fail_nomem_policy;
/* 將子進程新的vma的vm_mm指向子進程的mm */
tmp->vm_mm = mm;
/* 對父子進程的anon_vma和anon_vma_chain進行處理
* 如果父進程的此vma沒有anon_vma,直接返回,vma用於映射文件應該會沒有anon_vma
*/
if (anon_vma_fork(tmp, mpnt))
goto fail_nomem_anon_vma_fork;
tmp->vm_flags &= ~VM_LOCKED;
tmp->vm_next = tmp->vm_prev = NULL;
/* 獲取vma所映射的文件,如果是匿名映射區,則為空 */
file = tmp->vm_file;
/* 如果此vma是映射文件使用 */
if (file) {
/* 文件對應的inode */
struct inode *inode = file_inode(file);
/* 文件inode對應的address_space */
struct address_space *mapping = file->f_mapping;
/* 增加file的引用計數 */
get_file(file);
/* 如果此vma區被禁止寫此文件,則減少文件對應inode的寫進程的引用次數 */
if (tmp->vm_flags & VM_DENYWRITE)
atomic_dec(&inode->i_writecount);
mutex_lock(&mapping->i_mmap_mutex);
if (tmp->vm_flags & VM_SHARED)
atomic_inc(&mapping->i_mmap_writable);
flush_dcache_mmap_lock(mapping);
/* insert tmp into the share list, just after mpnt */
if (unlikely(tmp->vm_flags & VM_NONLINEAR))
vma_nonlinear_insert(tmp,
&mapping->i_mmap_nonlinear);
else
vma_interval_tree_insert_after(tmp, mpnt,
&mapping->i_mmap);
flush_dcache_mmap_unlock(mapping);
mutex_unlock(&mapping->i_mmap_mutex);
}
/* 此vma用於映射hugetlbfs中的大頁的情況 */
if (is_vm_hugetlb_page(tmp))
reset_vma_resv_huge_pages(tmp);
/* pprev是指向子進程的mm->mmap(用於vma排序存放的鏈表)
* 第一次循環時將子進程的mm->mmap指向tmp
* 后面的循環將前一個vma的vm_next指向當前mva
*/
*pprev = tmp;
/* 雖然到這來tmp->vm_next,但是pprev指向tmp->vm_next,結合上面的*pprev = tmp,可以起到下次循環將tmp->vm_next指向tmp的作用 */
pprev = &tmp->vm_next;
/* tmp的前一個vma是prev */
tmp->vm_prev = prev;
/* 將tmp作為prev */
prev = tmp;
/* 將vma加入到mm的mm_rb這個vma紅黑樹中 */
__vma_link_rb(mm, tmp, rb_link, rb_parent);
rb_link = &tmp->vm_rb.rb_right;
rb_parent = &tmp->vm_rb;
/* 子進程mm的線性區個數++ */
mm->map_count++;
/* 做頁表的復制
* 將父進程的vma對應的開始地址到結束地址這段地址的頁表復制到子進程中
* 如果這段vma有可能會進行寫時復制(vma可寫,並且不是共享的VM_SHARED),那就會對子進程和父進程的頁表項都設置為映射的頁是只讀的(vma中權限是可寫),這樣寫時會發生缺頁異常,在缺頁異常中做寫時復制
*/
retval = copy_page_range(mm, oldmm, mpnt);
if (tmp->vm_ops && tmp->vm_ops->open)
tmp->vm_ops->open(tmp);
if (retval)
goto out;
}
/* a new mm has just been created */
/* 與體系架構相關的dup_mmap處理 */
arch_dup_mmap(oldmm, mm);
retval = 0;
out:
/* 釋放子進程mm->mmap_sem這個讀寫信號量的寫鎖 */
up_write(&mm->mmap_sem);
/* 刷新父進程的頁表tlb */
flush_tlb_mm(oldmm);
/* 釋放父進程mm->mmap_sem這個讀寫信號量的寫鎖 */
up_write(&oldmm->mmap_sem);
uprobe_end_dup_mmap();
return retval;
fail_nomem_anon_vma_fork:
mpol_put(vma_policy(tmp));
fail_nomem_policy:
kmem_cache_free(vm_area_cachep, tmp);
fail_nomem:
retval = -ENOMEM;
vm_unacct_memory(charge);
goto out;
}
在dup_mm()中會對父進程的所有vma進行復制到子進程的操作,由於我們只看匿名線性區,並且只需要分析一個,這里我們就主要看匿名線性區會做什么操作,可以看出來,對於匿名線性區的處理,dup_mm中一個最重要的函數就是anon_vma_fork(),在anon_vma_fork()中,首先判斷傳入的父進程是否有anon_vma,然后調用anon_vma_clone()處理,之后會為子進程的vma創建一個anon_vma和anon_vma_chain,之后再對這兩個結構進行處理:
/* vma為子進程的vma,pvma為父進程的vma,如果父進程的此vma沒有anon_vma,直接返回 */
int anon_vma_fork(struct vm_area_struct *vma, struct vm_area_struct *pvma)
{
struct anon_vma_chain *avc;
struct anon_vma *anon_vma;
int error;
/* 父進程的此vma沒有anon_vma,直接返回 */
if (!pvma->anon_vma)
return 0;
/* 這里開始先檢查父進程的此vma是否有anon_vma,有則繼續,而上面進行了判斷,只有父進程的此vma有anon_vma才會執行到這里
* 這里會遍歷父進程的vma的anon_vma_chain鏈表,對每個結點新建一個anon_vma_chain,然后
* 設置新的avc->vma指向子進程的vma
* 設置新的avc->anon_vma指向父進程anon_vma_chain結點指向的anon_vma(這個anon_vma不一定屬於父進程)
* 將新的avc->same_vma加入到子進程的anon_vma_chain鏈表中
* 將新的avc->rb加入到父進程anon_vma_chain結點指向的anon_vma
*/
error = anon_vma_clone(vma, pvma);
if (error)
return error;
/* 分配一個anon_vma結構用於子進程,將其refcount設為1,anon_vma->root指向本身
* 即使此vma是用於映射文件的,也會分配一個anon_vma
*/
anon_vma = anon_vma_alloc();
if (!anon_vma)
goto out_error;
/* 分配一個struct anon_vma_chain結構 */
avc = anon_vma_chain_alloc(GFP_KERNEL);
if (!avc)
goto out_error_free_anon_vma;
/* 將新的anon_vma的root指向父進程的anon_vma的root */
anon_vma->root = pvma->anon_vma->root;
/* 對父進程與子進程的anon_vma共同的root的refcount進行+1 */
get_anon_vma(anon_vma->root);
/* Mark this anon_vma as the one where our new (COWed) pages go. */
vma->anon_vma = anon_vma;
/* 對這個新的anon_vma上鎖 */
anon_vma_lock_write(anon_vma);
/* 新的avc的vma指向子進程的vma
* 新的avc的anon_vma指向子進程vma的anon_vma
* 新的avc的same_vma加入到子進程vma的anon_vma_chain鏈表的頭部
* 新的avc的rb加入到子進程vma的anon_vma的紅黑樹中
*/
anon_vma_chain_link(vma, avc, anon_vma);
/* 對這個anon_vma解鎖 */
anon_vma_unlock_write(anon_vma);
return 0;
out_error_free_anon_vma:
put_anon_vma(anon_vma);
out_error:
unlink_anon_vmas(vma);
return -ENOMEM;
}
我們再看看anon_vma_clone():
/* dst為子進程的vma,src為父進程的vma */
int anon_vma_clone(struct vm_area_struct *dst, struct vm_area_struct *src)
{
struct anon_vma_chain *avc, *pavc;
struct anon_vma *root = NULL;
/* 遍歷父進程的每個anon_vma_chain鏈表中的結點,保存在pavc中 */
list_for_each_entry_reverse(pavc, &src->anon_vma_chain, same_vma) {
struct anon_vma *anon_vma;
/* 分配一個新的avc結構 */
avc = anon_vma_chain_alloc(GFP_NOWAIT | __GFP_NOWARN);
/* 如果分配失敗 */
if (unlikely(!avc)) {
unlock_anon_vma_root(root);
root = NULL;
/* 再次分配,一定要分配成功 */
avc = anon_vma_chain_alloc(GFP_KERNEL);
if (!avc)
goto enomem_failure;
}
/* 獲取父結點的pavc指向的anon_vma */
anon_vma = pavc->anon_vma;
/* 對anon_vma的root上鎖
* 如果root != anon_vma->root,則對root上鎖,並返回anon_vma->root
* 第一次循環,root = NULL
*/
root = lock_anon_vma_root(root, anon_vma);
/*
* 設置新的avc->vma指向子進程的vma
* 設置新的avc->anon_vma指向父進程anon_vma_chain結點指向的anon_vma(這個anon_vma不一定屬於父進程)
* 將新的avc->same_vma加入到子進程的anon_vma_chain鏈表頭部
* 將新的avc->rb加入到父進程anon_vma_chain結點指向的anon_vma
*/
anon_vma_chain_link(dst, avc, anon_vma);
}
/* 釋放根的鎖 */
unlock_anon_vma_root(root);
return 0;
enomem_failure:
unlink_anon_vmas(dst);
return -ENOMEM;
}
在調用完anon_vma_clone()結束后,整個結構會如下圖:

這張圖是在anon_vma_fork()中調用anon_vma_clone()結束后父子進程匿名線性區的關系圖,可以看出anon_vma_clone()做的工作只是創建了一個anon_vma_chain結構用於鏈入到子進程(因為父進程只有一個anon_vma_chain),並且加入到父進程的anon_vma紅黑樹中。需要注意,這里是因為父進程只有一個anon_vma_chain,所以才為子進程創建一個anon_vma_chain,如果父進程有N個anon_vma_chain,這里也會為子進程創建N個anon_vma_chain。同一條鏈上有多個anon_vma_chain,那么它們所屬的vma是相同的,只是加入到的anon_vma的紅黑樹不同,之后會看到。
再看看anon_vma_clone()之后進行的處理,如下:

可以看到,這時候父進程p1有一個anon_vma_chain,而子進程p2有兩個anon_vma_chain,子進程這兩個anon_vma_chain分別加入了父進程anon_vma的紅黑樹和子進程anon_vma的紅黑樹,但是這兩個anon_vma_chain都是屬於子進程p2的。我們看看父進程的紅黑樹,里面現在有兩個anon_vma_chain,一個是父進程自己的,一個是子進程的,這樣已經映射好的頁就可以通過父進程的anon_vma的紅黑樹分別訪問到父進程和子進程。
整個過程完成之后,最終結構如此圖,但是即使到這里,子進程對這些已經映射的頁還是不能訪問的,原因是還沒有為這些vma映射好的頁建立頁表,這個工作在dup_mm()對父進程每個vma遍歷的最后的copy_page_range()函數,此函數如下:
int copy_page_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
struct vm_area_struct *vma)
{
pgd_t *src_pgd, *dst_pgd;
unsigned long next;
/* 開始地址 */
unsigned long addr = vma->vm_start;
/* 結束地址 */
unsigned long end = vma->vm_end;
unsigned long mmun_start; /* For mmu_notifiers */
unsigned long mmun_end; /* For mmu_notifiers */
bool is_cow;
int ret;
/* 這里只會處理匿名線性區或者包含有(VM_HUGETLB | VM_NONLINEAR | VM_PFNMAP | VM_MIXEDMAP)這幾種標志的vma */
if (!(vma->vm_flags & (VM_HUGETLB | VM_NONLINEAR |
VM_PFNMAP | VM_MIXEDMAP))) {
if (!vma->anon_vma)
return 0;
}
/* 如果是使用了hugetlbfs中的大頁的情況 vma->vm_flags & VM_HUGETLB */
if (is_vm_hugetlb_page(vma))
return copy_hugetlb_page_range(dst_mm, src_mm, vma);
if (unlikely(vma->vm_flags & VM_PFNMAP)) {
ret = track_pfn_copy(vma);
if (ret)
return ret;
}
/* 檢查是否可能會對此vma進行寫入(要求此vma是非共享vma,並且可能寫入) (flags & (VM_SHARED | VM_MAYWRITE)) == VM_MAYWRITE */
is_cow = is_cow_mapping(vma->vm_flags);
mmun_start = addr;
mmun_end = end;
/* 此vma可能會進行寫時復制的處理 */
if (is_cow)
/* 如果此vma使用了mm->mmu_notifier_mm這個通知鏈,則初始化這個通知鏈,通知的地址范圍是addr ~ end */
mmu_notifier_invalidate_range_start(src_mm, mmun_start,
mmun_end);
ret = 0;
/* 獲取子進程對於開始地址對應的頁全局目錄項 */
dst_pgd = pgd_offset(dst_mm, addr);
/* 獲取父進程對於開始地址對應的頁全局目錄項
* 實際這兩項在頁全局目錄的偏移是一樣的,只是項中的數據不同,子進程的頁全局目錄項是空的
*/
src_pgd = pgd_offset(src_mm, addr);
do {
/* 獲取從addr ~ end,一個頁全局目錄項從addr開始能夠映射到的結束地址,返回這個結束地址
* 循環后會將addr = next,這樣下次就會從 next ~ end,一步一步以pud能映射的地址范圍長度減小
*/
next = pgd_addr_end(addr, end);
/* 父進程的頁全局目錄項是空的的情況 */
if (pgd_none_or_clear_bad(src_pgd))
continue;
/* 對這個頁全局目錄項對應的頁上級目錄項進行操作
* 並會不停深入,直到頁表項
* 里面的處理與這個循環幾乎一致,不過會在里面判斷是否要對dst_pgd的各層申請頁用於頁表
*/
if (unlikely(copy_pud_range(dst_mm, src_mm, dst_pgd, src_pgd,
vma, addr, next))) {
ret = -ENOMEM;
break;
}
} while (dst_pgd++, src_pgd++, addr = next, addr != end);
if (is_cow)
mmu_notifier_invalidate_range_end(src_mm, mmun_start, mmun_end);
return ret;
}
當這個函數對vma的線性地址區間的頁表項映射完成后,子進程的vma已經可以正確進行訪問了。我們看看已經映射好的頁框怎么通過反向映射,找到父子進程映射了此頁的vma,如下:

可以很清楚看出來,一個映射了的匿名頁,主要通過其指向的anon_vma里的保存anon_vma_chain的紅黑樹進行訪問各個映射了此頁的vma。需要注意,這種情況只是發生在父進程映射好了這幾個頁之后,才創建的子進程。
接下來看看父進程創建子進程完畢后,父子進程映射沒有訪問過的頁時發生的情況,並看看反向映射的結果。
首先我們先看子進程此時訪問了此vma里沒有被映射的線性地址,可以根據之前分析的do_anonymous_page()函數,得出如下結果:

會將此新的頁的mapping指向子進程p2的anon_vma,並且會為子進程p2建立此頁的頁表映射,其他並沒有任何改變,我們再看看此時對新映射的頁進行反向映射:

好的,可以清楚看出來,子進程新映射的頁,通過反向映射,只能訪問到子進程,不能訪問的父進程,這就是為什么子進程會有兩個anon_vma_chain的原因。如果此時子進程p2創建一個子進程p3,那么子進程p3的此vma就會有3個anon_vma_chain,它們都屬於子進程p3的此vma,只是分別加入了祖父進程p1,父進程p2,和本身進程p3的vma的紅黑樹中。
我們繼續結合之前分析的do_anonymous_page(),再來看看如果是父進程映射了一個新頁的情況是怎么樣的:

可以看到父進程新訪問的匿名頁,它的mapping指向了父進程的anon_vma,再看看父進程訪問的新頁進行反向映射,結果如下:

發現沒,一個很奇怪的問題,父進程新映射的頁能夠通過反向映射訪問的子進程的vma,所以在反向映射時都要對遍歷到的vma所屬進程的頁表進行檢查,檢查是否有映射此頁,具體方法是:通過頁描述符中的index(記錄有此頁是是vma中的第幾頁),通過vma->vm_start + (index) << PAGE_SHIFT,即可獲得此匿名頁的起始線性地址,然后通過此線性地址找到進程頁表中對應的頁表項,再通過檢查頁表項中映射的物理頁框號是否與此反向映射的物理頁框號相同,如果是,則說明此頁表項映射了此物理頁,否則說明此頁表項並不是映射了此物理頁,具體代碼是在vma_address()和page_check_address(),這里就不列出來了。
總結
整篇文章寫得並不好,這段內容實際上我也不知道該怎么寫,涉及到太多東西,寫得過於凌亂,如有不足歡迎指正,謝謝。
待研究解決的問題:
- 在反向映射過程中,通過頁描述符中的index與vma中的vm_start可以獲取此頁映射的線性地址起始地址,如果vma的vm_start是不變的那沒問題,如果vma是可以上下增長的類型(如堆棧),這種情況是否會導致vma的vm_start變化,然后是怎么處理。
- 對於匿名線性區vma,它的vm_file指針是否會指向一個struct file結構,是否是swap file。
- 如果在父進程創建子進程完成后,父進程此vma中的映射的頁全部取消掉映射,是否會把vma的anon_vma的紅黑樹中的anon_vma_chain都清除。
- 博客園有沒有點擊查看大圖的功能?看不清楚圖片的可以右鍵從新窗口打開圖片。

