本文為原創,轉載請註明: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; /* 如果此vma用於對映檔案,那麼儲存的是在對映檔案中的偏移量。如果是匿名線性區,它等於0或者vma開始地址對應的虛擬頁框號(vm_start >> PAGE_SIZE),這個虛擬頁框號用於vma向下增長時反向對映的計算(棧) */
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中的虛擬頁框號(此頁的開始線性地址 >> PAGE_SIZE)。共享匿名頁的產生應該只有在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 + ;

  anon_vma分配時要2位元組對齊,也就是所有分配的anon_vma其最低位都為0,經過上面的操作,mapping最低位就為1了,然後通過mapping獲取anon_vma地址時,進行如下操作:

struct anon_vma * anon_vma = (struct anon_vma *)(page->mapping - );

  這裡需要著重說說虛擬頁框號,我們知道在實體記憶體中,每個地址區間(0~4K,4K~8K,8K~12K...)都有它們的頁框號,則在每個程序地址空間中,每個線性地址區間(0~4K,4K~8K,8K~12K...)也應該有它們對應的虛擬頁框號,這個虛擬頁框號的作用之後會說到,這裡只需要記住對於匿名對映區,它的vma->vm_pgoff = vma開始地址對應的虛擬頁框號。

  這幾個結構都看完了,後面我們具體分析核心是怎麼把這幾個結構組織起來,又怎麼通過一個頁的頁描述符獲得所有映射了此頁的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的vm_start和vm_pgoff,vm_start儲存的是此vma開始的線性地址,而vm_pgoff儲存的是vma的開始線性地址對應的虛擬頁框號,比如vma的開始線性地址是10K,那麼這個vm_pgoff就等於3(起始線性地址屬於第3個頁框的範圍)。之後會說到這個有什麼用作。

  這時我們假設程序訪問了此新建的線性區的線性地址區間,由於此線性區是新建的,它的線性地址區間對應的頁表項並不會在建立的時候進行對映,所以會產生了缺頁異常,在缺頁異常中首先會判斷此線性地址是否所屬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) < )
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,如果不能夠合併,則申請一個空閒的anon_vma
* 新建一個anon_vma_chain
* 將avc->anon_vma指向獲得的vma(這個vma可能是新申請的空閒的anon_vma,也可能是獲取到的可以合併的vma的anon_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 ;
/* 以下是錯誤處理 */
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;
}

  這裡面流程很簡單:

  1. 呼叫anon_vma_prepare()獲取一個anon_vma結構,這個結構可能屬於此vma,也可能屬於此vma能夠合併的前後一個vma
  2. 通過夥伴系統分配一個頁(在32位上,會優先從高階記憶體分配)
  3. 根據vma預設頁表項引數vm_page_prot建立一個頁表項,這個頁表項用於加入到address對應的頁表中
  4. 呼叫page_add_new_anon_rmap()給此page新增一個反向對映
  5. 將頁表項和頁表還有此頁進行關聯,由於頁表已經在呼叫前分配好頁了,只需要將頁表項與新匿名頁進行關聯,然後將設定好的頁表項寫入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, ); /* 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->index = ((address - vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;
*/
__page_set_anon_rmap(page, vma, address, );
}

  主要記住,如果頁為匿名頁並且是一個新頁(沒有進行過對映到程序),頁描述符的mapping會指向第一次訪問它的vma的anon_vma。並且page->index的計算公式需要記住,它等於第一次對映這個頁的vma的起始虛擬頁框號(vma->vm_pgoff)加上address相對於vma起始地址的頁框偏移量,其實也就等於page對映到的vma所在程序地址空間的虛擬頁框號。

  主要看anon_vma_prepare(),在這個函式中,首先會檢查此vma有沒有anon_vma,其次會檢查此vma能否與前後的vma進行合併,如果可以合併,則只建立一個anon_vma_chain做一定的關聯,否則會建立一個anon_vma和一個anon_vma_chain進行一定的關聯,先看程式碼:

/* 為vma準備反向對映條件
* 檢查此vma能與前後的vma進行合併嗎,如果可以,則使用能夠合併的那個vma的anon_vma,如果不能夠合併,則申請一個空閒的anon_vma
* 建立一個新的anon_vma_chain
* 將avc->anon_vma指向獲得的vma(此vma可能是新建的,也可能是可以合併的vma的anon_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,如果不可以,返回NULL
* 主要檢查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 ; 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的紅黑樹中。在這種情況中,匿名頁的反向對映就能夠找到新的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 = ;
/* 此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 = ;
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 ; /* 這裡開始先檢查父程序的此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 ; 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 ; 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()之後進行的處理,如下:

  可以看到,子程序的anon_vma的root指向了父程序的anon_vma,並且父程序的anon_vma的refcount++。這時候父程序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 ;
} /* 如果是使用了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 = ;
/* 獲取子程序對於開始地址對應的頁全域性目錄項 */
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中的虛擬頁框號),通過以下函式計算,即可獲得此匿名頁的起始線性地址:

static inline unsigned long
__vma_address(struct page *page, struct vm_area_struct *vma)
{
/* 獲取此頁在vma所屬程序地址空間的線性地址
* 如果是匿名線性區,page->index中儲存的是此頁對映到了匿名線性區中的虛擬頁框號
*/
pgoff_t pgoff = page_to_pgoff(page);
/* vma->vm_pgoff儲存vma起始地址所在的虛擬頁框號
* pgoff儲存的是page在此vma的程序地址空間的虛擬頁框號
* vma->vm_start儲存的是vma的起始地址
*/
return vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT);
}

  這樣就獲得了page在vma所屬的程序空間的線性地址,然後通過此線性地址找到程序頁表中對應的頁表項,再通過檢查頁表項中對映的物理頁框號是否與此反向對映的物理頁框號相同,如果是,則說明此頁表項映射了此物理頁,否則說明此頁表項並不是映射了此物理頁,具體程式碼在page_check_address(),這裡就不列出來了。

  回過頭,我們再看看程序建立一個新的vma,然後這個vma能夠與前/後的vma進行合併的情況,在這種情況中,新的vma會與能夠合併的vma共用一個anon_vma,如果這個新的vma訪問了一個頁的時候,這個頁的mapping會指向這個能夠合併的vma的anon_vma,然後通過上面反向對映的步驟,通過anon_vma的紅黑樹找遍歷連結到此的vma,發現此頁並不屬於能夠合併的vma(因為page->index肯定不在能夠合併的vma的地址區間對應的虛擬頁框號區間中),但是遍歷到新的vma時就能夠判斷出此page是對映到了新的vma中。注意,以上所說情景的全都是基於堆和棧以及私有匿名mmap的匿名頁反向對映,而共享匿名mmap共享記憶體(其實就是shmem)實際並不是基於匿名頁反向對映的,可以說是基於檔案頁的反向對映的。

關於vma是向下增長的型別(棧)

  如上,先考慮一種理想情況,在系統中所有程序的棧大小都是足夠使用的,,那麼所有程序的棧的vma的開始線性地址和結束線性地址都會相同(見dup_mm()),而當某個程序的棧不夠用了,需要向下擴大時,會呼叫expand_downwards():

/* 擴大程序棧大小,程序棧向下增長的情況
* vma: 當前程序棧的vma
* address: 產生缺頁異常的address,並且 address < vma->vm_start
*/
int expand_downwards(struct vm_area_struct *vma,
unsigned long address)
{
int error; /* 為vma準備反向對映條件
* 檢查此vma能與前後的vma進行合併嗎,如果可以,則使用能夠合併的那個vma的anon_vma,如果不能夠合併,則申請一個空閒的anon_vma
* 建立一個新的anon_vma_chain
* 將avc->anon_vma指向獲得的vma(此vma可能是新建的,也可能是可以合併的vma的anon_vma),avc->vma指向vma,並把avc加入到vma的anon_vma_chain中
*/
/* 如果棧已經在使用中,是棧空間不足導致需要向下增長的,那麼這裡基本不做任何工作,因為棧的vma已經有了anon_vma */
if (unlikely(anon_vma_prepare(vma)))
return -ENOMEM; /* 獲取目標address線性地址對應的頁的起始地址 */
address &= PAGE_MASK;
/* 檢查此地址是否合理 */
error = security_mmap_addr(address);
if (error)
return error; /* 對anon_vma->root->rwsem上鎖 */
vma_lock_anon_vma(vma); /* 因為是向下擴充套件,address一定會小於vma->vm_start,因為外面一層函式判斷過了才會呼叫到此函式 */
if (address < vma->vm_start) {
unsigned long size, grow; /* address到程序棧結束的大小範圍,也就是棧擴大後最小的大小 */
size = vma->vm_end - address;
/* address到程序棧開始地址中間空出的大小(以頁為單位) */
grow = (vma->vm_start - address) >> PAGE_SHIFT; error = -ENOMEM;
if (grow <= vma->vm_pgoff) { /* 做檢查 */
error = acct_stack_growth(vma, size, grow);
if (!error) {
/* 對該程序頁表上鎖 */
spin_lock(&vma->vm_mm->page_table_lock);
/* 將此vma的anon_vma_chain連結串列上的結點從它們加入的紅黑樹中移除 */
anon_vma_interval_tree_pre_update_vma(vma);
/* 將此vma的開始線性地址設定為address */
vma->vm_start = address;
/* 將此vma的開始線性地址對應的虛擬頁框號更新 */
vma->vm_pgoff -= grow;
/* 將此vma的anon_vma_chain連結串列上的結點重新加入到它們原本所屬的紅黑樹中(通過avc->anon_vma->rb_root) */
anon_vma_interval_tree_post_update_vma(vma);
vma_gap_update(vma);
/* 解鎖 */
spin_unlock(&vma->vm_mm->page_table_lock); perf_event_mmap(vma);
}
}
}
vma_unlock_anon_vma(vma);
khugepaged_enter_vma_merge(vma, vma->vm_flags);
/* 更新vma所屬mm_struct的vma紅黑樹 */
validate_mm(vma->vm_mm);
return error;
}

  可以看到這裡更新了vma->vm_start和vma->vm_pgoff。這樣對反向對映就沒有什麼影響了,即使棧的vm_start改變了,一樣可以通過vma_address()函式獲取物理頁框對應的線性地址。

總結

  整篇文章寫得並不好,這段內容實際上我也不知道該怎麼寫,涉及到太多東西,寫得過於凌亂,如有不足歡迎指正,謝謝。

待研究解決的問題:

  1. 對於匿名線性區vma,它的vm_file指標是否會指向一個struct file結構,是否是swap file。
  2. 如果在父程序建立子程序完成後,父程序此vma中的對映的頁全部取消掉對映,是否會把vma的anon_vma的紅黑樹中的anon_vma_chain都清除。
  3. 部落格園有沒有點選檢視大圖的功能?看不清楚圖片的可以右鍵從新視窗開啟圖片。