Linux下訪問匿名頁發(fā)生的神奇“化學(xué)反應(yīng)”
Linux中有后備文件支持的頁稱為文件頁,如屬于進程的代碼段、數(shù)據(jù)段的頁,內(nèi)存回收的時候這些頁面只需要做臟頁的同步即可(干凈的頁面可以直接丟棄掉)。反之為匿名頁,如進程的堆棧使用的頁,內(nèi)存回收的時候這些頁面不能簡單的丟棄掉,需要交換到交換分區(qū)或交換文件。本文中,主要分析匿名頁的訪問將發(fā)生哪些可能顛覆我們認知的"化學(xué)反應(yīng)"。
1.實例代碼
首先以一個簡單的示例代碼來說明:
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <string.h>
- #include <sys/mman.h>
- #define MAP_SIZE (100 * 1024 * 1024)
- int main(int argc, char *argv[])
- {
- char *p;
- char val;
- int i;
- puts("before mmap ok, pleace exec 'free -m'!");
- sleep(5);
- //mmap
- p = mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
- if(p == NULL) {
- perror("fail to malloc");
- return -1;
- }
- puts("after mmap ok, pleace exec 'free -m'!");
- sleep(5);
- //read
- for (i = 0; i < MAP_SIZE; i++) {
- val = p[i];
- }
- puts("read ok, pleace exec 'free -m'!");
- sleep(5);
- #if 1
- //write
- memset(p, 0x55, MAP_SIZE);
- puts("write ok, pleace exec 'free -m'!");
- #endif
- //sleep
- pause();
- return 0;
- }
代碼非常簡單:首先通過mmap分配100M的私有可讀可寫匿名頁面,然后進行讀寫訪問,分別在提示的時候在另外一個窗口執(zhí)行free -m命令查看輸出結(jié)果。
程序執(zhí)行結(jié)果如下:
- $ ./anon_rw_demo
- before mmap ok, pleace exec 'free -m'!
- after mmap ok, pleace exec 'free -m'!
- read ok, pleace exec 'free -m'!
- write ok, pleace exec 'free -m'!
命令執(zhí)行結(jié)果如下:
- $ free -m
- 總計 已用 空閑 共享 緩沖/緩存 可用
- 內(nèi)存: 15729 8286 1945 895 5497 6220
- 交換: 16290 1599 14691
- $ free -m
- 總計 已用 空閑 共享 緩沖/緩存 可用
- 內(nèi)存: 15729 8286 1945 895 5497 6220
- 交換: 16290 1599 14691
- $ free -m
- 總計 已用 空閑 共享 緩沖/緩存 可用
- 內(nèi)存: 15729 8286 1945 895 5497 6220
- 交換: 16290 1599 14691
- $ free -m
- 總計 已用 空閑 共享 緩沖/緩存 可用
- 內(nèi)存: 15729 8383 1848 895 5497 6123
- 交換: 16290 1599 14691
可以看到:
第一次提示執(zhí)行free命令的時候,我們還沒有開始通過mmap分配內(nèi)存,此時free命令輸出作為參考。
第二次提示執(zhí)行free命令的時候,我們已經(jīng)通過mmap分配了100M的內(nèi)存,此時發(fā)現(xiàn)free命令輸出內(nèi)存消耗基本沒有變化。
第三次提示執(zhí)行free命令的時候,我們對于分配的匿名頁面進行了讀操作,此時發(fā)現(xiàn)free命令輸出內(nèi)存消耗頁基本沒有變化, 這基本上會顛覆我們的認知。
第四次提示執(zhí)行free命令的時候,我們對于分配的匿名頁面進行了寫操作,此時發(fā)現(xiàn)free命令輸出內(nèi)存消耗大概為100M。
2.內(nèi)核原理
下面我們從Linux內(nèi)核的層面來解析發(fā)生以上神奇現(xiàn)象的原理。
2.1 mmap的內(nèi)存消耗
mmap申請匿名頁的時候,只是申請了虛擬內(nèi)存(通過vm_area_struct結(jié)構(gòu)來描述,如描述虛擬內(nèi)存區(qū)域的地址范圍、訪問權(quán)限等,以下簡稱vma),實際的物理內(nèi)存并沒有申請(除了用于管理虛擬內(nèi)存區(qū)域的vma等結(jié)構(gòu)內(nèi)存的申請),當(dāng)前虛擬內(nèi)存和物理內(nèi)存并沒有建立頁表映射關(guān)系,而真正的申請的匿名頁所對應(yīng)的物理頁在實際訪問的時候按需分配獲得,所以此時我們看不到內(nèi)存的消耗情況。
2.2 第一次讀匿名頁的內(nèi)存消耗
通過mmap申請完虛擬內(nèi)存之后,進程就可以按照之前申請vma的訪問權(quán)限進行訪問,第一發(fā)生讀訪問,這個時候由于虛擬內(nèi)存和物理內(nèi)存并沒有建立頁表映射關(guān)系,通過虛擬地址并不能查找到物理內(nèi)存,所以會發(fā)生處理器的異常,最終分析是因為數(shù)據(jù)訪問異常導(dǎo)致,就由處理器架構(gòu)相關(guān)的代碼進入了我們通用的缺頁異常處理例程中。
缺頁異常調(diào)用鏈如下:
- "mm/memory.c"
- 處理器架構(gòu)相關(guān)異常處理代碼
- -> handle_mm_fault
- -> __handle_mm_fault
- -> handle_pte_fault
- -> if (!vmf->pte) { ------------------- 1
- if (vma_is_anonymous(vmf->vma)) ------------------- 2
- return do_anonymous_page(vmf); ------------------- 3
缺頁異常進入handle_pte_fault后,在1標簽代碼處,來判斷訪問的虛擬內(nèi)存頁的頁表項是否為空,為空說明這個這個虛擬頁沒有和物理頁建立映射關(guān)系。然后在2標簽代碼處判斷是否為匿名頁缺頁異常(實際上是判斷是否為私有的匿名頁,當(dāng)前當(dāng)前示例代碼場景申請的為私有匿名頁面)。在3標簽代碼處,進行真正的私有匿名頁缺頁異常處理。
下面主要看下第一次讀匿名頁的處理:
- do_anonymous_page
- ->pte_alloc(vma->vm_mm, vmf->pmd) ------------------- 1
- ->/* Use the zero-page for reads */
- if (!(vmf->flags & FAULT_FLAG_WRITE) && ------------------- 2
- !mm_forbids_zeropage(vma->vm_mm)) { ------------------- 3
- entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
- vma->vm_page_prot)); ------------------- 4
- vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
- vmf->address, &vmf->ptl); ------------------- 5
- ...
- goto setpte;
- }
- -> page = alloc_zeroed_user_highpage_movable(vma, vmf->address); ------------------- 6
- -> entry = mk_pte(page, vma->vm_page_prot); ------------------- 7
- entry = pte_sw_mkyoung(entry); ------------------- 8
- if (vma->vm_flags & VM_WRITE)
- entry = pte_mkwrite(pte_mkdirty(entry)); ------------------- 9
- vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
- &vmf->ptl); ------------------- 10
- ->set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry); ------------------- 11
1標簽處:判斷虛擬地址對應(yīng)的pmd表項是否為空,為空來分配直接頁表設(shè)置到pmd表項中。
2標簽處:判斷是否是進行讀訪問。
3標簽處:判斷是否沒有禁止0頁。
4標簽處:就是對于沒有禁止0頁的匿名頁讀訪問設(shè)置頁表,這里通過0頁的頁幀號和mmap映射時指定的訪問權(quán)限組合頁表項的值。
5標簽處:通過發(fā)生缺頁的虛擬地址來計算出頁表項的地址保存在 vmf->pte。
最11標簽處:將4標簽初組合出的頁表項的值寫入到5標簽初計算出的頁表項中。
以上分析可知:對于私有的匿名頁,第一次讀訪問的時候都會發(fā)生缺頁異常,然后通過頁表映射0頁,這個0頁沒有什么特殊之處,只不過它是在系統(tǒng)啟動過程中初始化好的一塊內(nèi)容全為0的頁面,這樣做可以為進程分配了內(nèi)存只進行讀訪問節(jié)省大量物理內(nèi)存。
2.3 第一次寫匿名頁的內(nèi)存消耗
大家可以將示例代碼中,讀訪問屏蔽掉只進行寫訪問,觀察內(nèi)存消耗。
這個時候發(fā)生缺頁異常時,不會在走2 3 4 5 便簽處代碼,而在6處分配了一個物理頁面,在7 8 9組合頁表項的值, 10處計算出頁表項的地址,最后把組合的值設(shè)置到頁表項中。
需要注意第9處,如果是寫訪問會設(shè)置頁表項的可寫標志位。
以上分析可知:對于私有的匿名頁,第一次寫訪問的時候都會發(fā)生缺頁異常,會真正分配一個物理頁面,然后將虛擬頁面通過頁面映射到物理頁面,所以我們能觀察到寫之后發(fā)生了大量內(nèi)存消耗。
2.4 第一次讀然后寫匿名頁的內(nèi)存消耗
這種場景就是示例代碼中所做的實驗,可以看到讀的時候基本上沒有內(nèi)存消耗,寫的時候發(fā)生了大量內(nèi)存消耗。
關(guān)于第一次讀,上面已經(jīng)做過解釋,下面主要看讀完之后的頁面發(fā)生寫訪問的情況。
2.4.1 從mmap說起
實際上,對于一個私有的內(nèi)存映射,在mmap的時候為頁表映射準備訪問權(quán)限的時候并不是給予所有的權(quán)限,而是把可寫屬性去掉了。
我們可以從源代碼找到答案:
- "mm/mmap.c"
- do_mmap
- ->mmap_region
- ->vma_set_page_prot(vma)
- ->vm_page_prot = vm_pgprot_modify(vma->vm_page_prot, vm_flags); ---------1
- ->pgprot_modify(oldprot, vm_get_page_prot(vm_flags))
- ->WRITE_ONCE(vma->vm_page_prot, vm_page_prot); ---------------2
- /* description of effects of mapping type and prot in current implementation.
- * this is due to the limited x86 page protection hardware. The expected
- * behavior is in parens:
- *
- * map_type prot
- * PROT_NONE PROT_READ PROT_WRITE PROT_EXEC
- * MAP_SHARED r: (no) no r: (yes) yes r: (no) yes r: (no) yes
- * w: (no) no w: (no) no w: (yes) yes w: (no) no
- * x: (no) no x: (no) yes x: (no) yes x: (yes) yes
- *
- * MAP_PRIVATE r: (no) no r: (yes) yes r: (no) yes r: (no) yes
- * w: (no) no w: (no) no w: (copy) copy w: (no) no
- * x: (no) no x: (no) yes x: (no) yes x: (yes) yes
- */
- ->vm_get_page_prot
- pgprot_t protection_map[16] __ro_after_init = {
- __P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111,
- __S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111
- };
對于__Pxxx, 最后一個x表示vma屬性是否可讀,倒數(shù)第二個x表示vma屬性是否可寫,P后面的x表示是否可執(zhí)行。
1標簽處根據(jù)mmap傳遞的訪問權(quán)限來構(gòu)造最終的訪問權(quán)限標識。
2標簽處將構(gòu)造好的訪問權(quán)限標識記錄到vma->vm_page_prot中,供缺頁異常設(shè)置頁表使用。
注釋中已經(jīng)做了詳細的解釋,具體頁表屬性如何表示由各自的處理器架構(gòu)相關(guān)代碼來做(eg: 對于x86架構(gòu) #define __P111 PAGE_COPY_EXEC),我們只需要知道:無論我們想讓vma具備那些屬性組合,都會屏蔽掉寫屬性,具體可以查看相關(guān)的處理器架構(gòu)實現(xiàn)。
所以,再次回到缺頁異常處理代碼中。在2.2小節(jié)的4標簽處,使用mmap設(shè)置好的頁表訪問權(quán)限設(shè)置頁表屬性,當(dāng)前場景我們知道,mmap中指定為私有的可讀可寫屬性,而頁表中只是設(shè)置為了只讀屬性。
2.4.2 寫時復(fù)制的觸發(fā)
讀訪問將虛擬頁以只讀的方式映射到了0頁,當(dāng)再次發(fā)生寫操作時,就會再次觸數(shù)據(jù)訪問異常,最終進入缺頁異常處理例程中。
下面給出調(diào)用鏈:
- "mm/memory.c"
- handle_pte_fault
- ->if (vmf->flags & FAULT_FLAG_WRITE) { -----------1
- if (!pte_write(entry)) -----------2
- return do_wp_page(vmf); -----------3
可以看到最終也是在handle_pte_fault中處理:在1標簽處判斷是否為寫訪問。在2標簽處判斷頁表項的屬性是否是只讀。在3標簽處進行實際的寫時復(fù)制處理。
以上分析可知:發(fā)生寫訪問操作時,如果vma可寫,但是頁表屬性標識不可寫(只讀),會發(fā)生寫時復(fù)制缺頁異常,對于當(dāng)前場景的0頁的寫訪問就是如此,在do_wp_page中會重新分配物理頁面映射到虛擬頁面,然后頁表設(shè)置為可寫屬性,就完成了缺頁處理。
3.總結(jié)
1)mmap分配私有匿名內(nèi)存時,會設(shè)置vma的vm_page_prot成員,去除掉頁表的寫訪問標識。
2)第一次讀匿名頁時,對于可讀可寫的vma,虛擬頁會以只讀的方式映射到0頁。
3)第一次寫匿名頁時,對于可讀可寫的vma,會申請物理頁面,虛擬頁以可讀可寫的方式映射到此物理頁。
4)第一次讀匿名頁后,然后寫匿名頁,先只讀方式映射到0頁,然后發(fā)生寫時復(fù)制,分配物理頁,虛擬頁以可讀可寫的方式映射到此物理頁。
可以發(fā)現(xiàn),訪問匿名頁面時發(fā)生的“化學(xué)反應(yīng)”并不是那么的簡單,其中會涉及mmap的映射原則,0頁的映射,匿名頁面的處理,寫時復(fù)制的處理等等,而且讀寫順序不一樣,產(chǎn)生的結(jié)果也會不一樣,大家可以結(jié)合內(nèi)核源代碼進行分析,希望對大家理解匿名頁缺頁異常有所幫助。