韓傳華,就職于南京大魚半導體有限公司,主要從事linux相關系統(tǒng)軟件開發(fā)工作,負責Soc芯片BringUp及系統(tǒng)軟件開發(fā),樂于分享喜歡學習,喜歡專研Linux內核源代碼。
前面講到過寫時復制缺頁異常(COW),一般用于父子進程之間共享頁,而我們會常見一種缺頁異常是匿名映射缺頁異常,今天我們就來討論下這種缺頁異常,讓大家徹底理解它。注:本文使用linux-5.0內核源代碼。文章分為以下幾節(jié)內容:
1.匿名映射缺頁異常的觸發(fā)情況 2.0頁是什么?為什么使用0頁?
3.源代碼分析
3.1 觸發(fā)條件
3.2 第一次讀匿名頁
3.3 第一次寫匿名頁
3.4 讀之后寫匿名頁
4.應用層實驗
5.總結
在講解匿名映射缺頁異常之前我們先要了解以下什么是匿名頁?與匿名頁相對應的是文件頁,文件頁我們應該很好理解,就是映射文件的頁,如:通過mmap映射文件到虛擬內存然后讀文件數(shù)據,進程的代碼數(shù)據段等,這些頁有后備緩存也就是塊設備上的文件,而匿名頁就是沒有關聯(lián)到文件的頁,如:進程的堆、棧等。還有一點需要注意:下面討論的都是私有的匿名頁的情況,共享匿名頁在內核演變?yōu)槲募成淙表摦惓#▊挝募到y(tǒng)),后面有機會我們會講解,感興趣的小伙伴可以看一看mmap的代碼實現(xiàn)對共享匿名頁的處理。
一,匿名映射缺頁異常的觸發(fā)情況
前面我們講解了什么是匿名頁,那么思考一下什么情況下會觸發(fā)匿名映射缺頁異常呢?這種異常對于我們來說非常常見:
1.當我們應用程序使用malloc來申請一塊內存(堆分配),在沒有使用這塊內存之前,僅僅是分配了虛擬內存,并沒有分配物理內存,第一次去訪問的時候才會通過觸發(fā)缺頁異常來分配物理頁建立和虛擬頁的映射關系。
2.當我們應用程序使用mmap來創(chuàng)建匿名的內存映射的時候,頁同樣只是分配了虛擬內存,并沒有分配物理內存,第一次去訪問的時候才會通過觸發(fā)缺頁異常來分配物理頁建立和虛擬頁的映射關系。
3.當函數(shù)的局部變量比較大,或者是函數(shù)調用的層次比較深,導致了當前的棧不夠用了,這個時候需要擴大棧。當然了上面的這幾種場景對應應用程序來說是透明的,內核為用戶程序做了大量的處理工作,下面幾節(jié)會看到如何處理。
二,0頁是什么?為什么使用0頁?
這里為什么會說到0頁呢?什么是0頁呢?是地址為0的頁嗎?答案是:系統(tǒng)初始化過程中分配了一頁的內存,這段內存全部被填充0。下面我們來看下0頁如何分配的:在arch/arm64/mm/mmu.c中:
61 /*
62 * Empty_zero_page is a special page that is used for zero-initialized data
63 * and COW.
64 */
65 unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)] __page_aligned_bss;
66 EXPORT_SYMBOL(empty_zero_page);
可以看到定義了一個全局變量,大小為一頁,頁對齊到bss段,所有這段數(shù)據內核初始化的時候會被清零,所有稱之為0頁。
那么為什么使用0頁呢?一個是它的數(shù)據都是被0填充,讀的時候數(shù)據都是0,二是節(jié)約內存,匿名頁面第一次讀的時候數(shù)據都是0都會映射到這頁中從而節(jié)約內存(共享0頁),那么如果有進程要去寫這個這個頁會怎樣呢?答案是發(fā)生COW重新分配頁來寫。
三,源代碼分析
3.1 觸發(fā)條件
當?shù)谝还?jié)中的觸發(fā)情況發(fā)生的時候,處理器就會發(fā)生缺頁異常,從處理器架構相關部分過渡到處理器無關部分,最終到達handle_pte_fault函數(shù):
3742 static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
3743 {
3744 pte_t entry;
...
3782 if (!vmf->pte) {
3783 if (vma_is_anonymous(vmf->vma))
3784 return do_anonymous_page(vmf);
3785 else
3786 return do_fault(vmf);
3787 }
3782和3783行是匿名映射缺頁異常的觸發(fā)條件:
1.發(fā)生缺頁的地址所在頁表項不存在。
2.是匿名頁發(fā)生的,即是vma->vm_ops為空。
當滿足這兩個條件的時候就會調用do_anonymous_page函數(shù)來處理匿名映射缺頁異常。
2871 /*
2872 * We enter with non-exclusive mmap_sem (to exclude vma changes,
2873 * but allow concurrent faults), and pte mApped but not yet locked.
2874 * We return with mmap_sem still held, but pte unmapped and unlocked.
2875 */
2876 static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
2877 {
2878 struct vm_area_struct *vma = vmf->vma;
2879 struct mem_cgroup *memcg;
2880 struct page *page;
2881 vm_fault_t ret = 0;
2882 pte_t entry;
2883
2884 /* File mapping without ->vm_ops ? */
2885 if (vma->vm_flags & VM_SHARED)
2886 return VM_FAULT_SIGBUS;
2887
2888 /*
2889 ¦* Use pte_alloc() instead of pte_alloc_map(). We can't run
2890 ¦* pte_offset_map() on pmds where a huge pmd might be created
2891 ¦* from a different thread.
2892 ¦*
2893 ¦* pte_alloc_map() is safe to use under down_write(mmap_sem) or when
2894 ¦* parallel threads are excluded by other means.
2895 ¦*
2896 ¦* Here we only have down_read(mmap_sem).
2897 ¦*/
2898 if (pte_alloc(vma->vm_mm, vmf->pmd))
2899 return VM_FAULT_OOM;
2904
...
2885行判斷:發(fā)生缺頁的vma是否為私有映射,這個函數(shù)處理的是私有的匿名映射。
2898行 如何頁表不存在則分配頁表(有可能缺頁地址的頁表項所在的直接頁表不存在)。
3.2 第一次讀匿名頁情況
...
2905 /* Use the zero-page for reads */
2906 if (!(vmf->flags & FAULT_FLAG_WRITE) &&
2907 !mm_forbids_zeropage(vma->vm_mm)) {
2908 entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
2909 vma->vm_page_prot));
2910 vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
2911 vmf->address, &vmf->ptl);
2912 if (!pte_none(*vmf->pte))
2913 goto unlock;
2914 ret = check_stable_address_space(vma->vm_mm);
2915 if (ret)
2916 goto unlock;
2917 /* Deliver the page fault to userland, check inside PT lock */
2918 if (userfaultfd_missing(vma)) {
2919 pte_unmap_unlock(vmf->pte, vmf->ptl);
2920 return handle_userfault(vmf, VM_UFFD_MISSING);
2921 }
2922 goto setpte;
2923 }
... 2968 setpte:
2969 set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
2906到2923行是處理的是私有匿名頁讀的情況:這里就會用到我們上面將的0頁了。
2906和 2907行判斷是否是由于讀操作導致的缺頁而且沒有禁止0頁。
2908-2909行是核心部分:設置頁表項的值映射到0頁。
我們主要研究這個語句:pfn_pte用來將頁幀號和頁表屬性拼接為頁表項值:
arch/arm64/include/asm/pgtable.h:
77 #define pfn_pte(pfn,prot)
78 __pte(__phys_to_pte_val((phys_addr_t)(pfn) << PAGE_SHIFT) | pgprot_val(prot))
是將pfn左移PAGE_SHIFT位(一般為12bit),或上pgprot_val(prot)
先看my_zero_pfn:
include/asm-generic/pgtable.h:
875 static inline unsigned long my_zero_pfn(unsigned long addr)
876 {
877 extern unsigned long zero_pfn;
878 return zero_pfn;
879 }
mm/memory.c:
126 unsigned long zero_pfn __read_mostly;
127 EXPORT_SYMBOL(zero_pfn);
128
129 unsigned long highest_memmap_pfn __read_mostly;
130
131 /*
132 * CONFIG_MMU architectures set up ZERO_PAGE in their paging_init()
133 */
134 static int __init init_zero_pfn(void) 1
35 { 136 zero_pfn = page_to_pfn(ZERO_PAGE(0)); 137 return 0; 138 } 139 core_initcall(init_zero_pfn);
||
/
arch/arm64/include/asm/pgtable.h:
54 /*
55 * ZERO_PAGE is a global shared page that is always zero: used
56 * for zero-mapped memory areas etc..
57 */
58 extern unsigned long empty_zero_page[PAGE_SIZE / sizeof(unsigned long)];
59 #define ZERO_PAGE(vaddr) phys_to_page(__pa_symbol(empty_zero_page))
最終我們看到使用的就是內核初始化設置的empty_zero_page這個0頁得到頁幀號。再看看pfn_pte的第二個參數(shù)vma->vm_pageprot,這是vma的訪問權限,在做內存映射mmap的時候會被設置。
那么我們想知道的時候是什么時候0頁被設置為了只讀屬性的(也就是頁表項何時被設置為只讀)?
我們帶著這個問題去在內核代碼中尋找答案。其實代碼看到這里一般看不到頭緒,但是我們要知道何時vma的vm_page_prot成員被設置的,如何被設置的,有可能就能找到答案。
我們到mm/mmap.c中去尋找答案:我們以do_brk_flags函數(shù)為例,這是設置堆的函數(shù)我們關注到3040行設置了vm_page_prot:
3040 vma->vm_page_prot = vm_get_page_prot(flags);
||
/
110 pgprot_t vm_get_page_prot(unsigned long vm_flags)
111 {
112 pgprot_t ret = __pgprot(pgprot_val(protection_map[vm_flags &
113 (VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)]) |
114 pgprot_val(arch_vm_get_page_prot(vm_flags)));
115
116 return arch_filter_pgprot(ret);
117 }
118 EXPORT_SYMBOL(vm_get_page_prot);
vm_get_page_prot函數(shù)會根據傳遞來的vmflags是否為VMREAD|VMWRITE|VMEXEC|VMSHARED來轉換為保護位組合,繼續(xù)往下看
||
/
78 /* description of effects of mapping type and prot in current implementation.
79 * this is due to the limited x86 page protection hardware. The expected
80 * behavior is in parens:
81 *
82 * map_type prot
83 * PROT_NONE PROT_READ PROT_WRITE PROT_EXEC
84 * MAP_SHARED r: (no) no r: (yes) yes r: (no) yes r: (no) yes
85 * w: (no) no w: (no) no w: (yes) yes w: (no) no
86 * x: (no) no x: (no) yes x: (no) yes x: (yes) yes
87 *
88 * MAP_PRIVATE r: (no) no r: (yes) yes r: (no) yes r: (no) yes
89 * w: (no) no w: (no) no w: (copy) copy w: (no) no
90 * x: (no) no x: (no) yes x: (no) yes x: (yes) yes
91 *
92 * On arm64, PROT_EXEC has the following behaviour for both MAP_SHARED and
93 * MAP_PRIVATE:
94 * r: (no) no
95 * w: (no) no
96 * x: (yes) yes
97 */
98 pgprot_t protection_map[16] __ro_after_init = {
99 __P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111,
100 __S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111
101 };
protection_map數(shù)組定義了從P000到S111一共16種組合,P表示私有(Private),S表示共享(Share),后面三個數(shù)字依次為可讀、可寫、可執(zhí)行,如:_S010表示共享、不可讀、可寫、不可執(zhí)行。
||
/
arch/arm64/include/asm/pgtable-prot.h:
93 #define PAGE_NONE __pgprot(((_PAGE_DEFAULT) & ~PTE_VALID) | PTE_PROT_NONE | PTE_RDONLY | PTE_NG | PTE_PXN | PTE_UXN)
94 #define PAGE_SHARED __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_NG | PTE_PXN | PTE_UXN | PTE_WRITE)
95 #define PAGE_SHARED_EXEC __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_NG | PTE_PXN | PTE_WRITE)
96 #define PAGE_READONLY __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PTE_NG | PTE_PXN | PTE_UXN)
97 #define PAGE_READONLY_EXEC __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PTE_NG | PTE_PXN)
98 #define PAGE_EXECONLY __pgprot(_PAGE_DEFAULT | PTE_RDONLY | PTE_NG | PTE_PXN)
99
100 #define __P000 PAGE_NONE
101 #define __P001 PAGE_READONLY
102 #define __P010 PAGE_READONLY
103 #define __P011 PAGE_READONLY
104 #define __P100 PAGE_EXECONLY
105 #define __P101 PAGE_READONLY_EXEC
106 #define __P110 PAGE_READONLY_EXEC
107 #define __P111 PAGE_READONLY_EXEC
108
109 #define __S000 PAGE_NONE
110 #define __S001 PAGE_READONLY
111 #define __S010 PAGE_SHARED
112 #define __S011 PAGE_SHARED
113 #define __S100 PAGE_EXECONLY
114 #define __S101 PAGE_READONLY_EXEC
115 #define __S110 PAGE_SHARED_EXEC
116 #define __S111 PAGE_SHARED_EXEC
可以發(fā)現(xiàn)對于私有的映射只有只讀(PTE_RDONLY)沒有可寫屬性(PTE_WRITE)105-107行 ,雖然之前設置的時候是設置了可寫(VM_WRITE)!而對應共享映射則會有可寫屬性。
而這個被設置的保護位組合最終會在缺頁異常中被設置到頁表中:上面說到的do_anonymous_page函數(shù):
2908 entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
2909 vma->vm_page_prot));
對于私有匿名映射的頁,假設設置的vmflags為VMREAD|VMWRITE則對應的保護位組合為:P110即為PAGE_READONLY_EXEC=pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PT_ENG | PTE_PXN)不會設置為可寫。
所以就將其頁表設置為了只讀!!!
2922行 跳轉到setpte去將設置好的頁表項值填寫到頁表項中。
當匿名頁讀之后再次去寫時候會由于頁表屬性為只讀導致COW缺頁異常,詳將COW相關文章,再此不在贅述。下面用圖說話:
3.3 第一次寫匿名頁的情況
接著do_anonymous_page函數(shù)繼續(xù)往下分析:
2876 static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
2877 {
...
2924
2925 /* Allocate our own private page. */
2926 if (unlikely(anon_vma_prepare(vma)))
2927 goto oom;
2928 page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
2929 if (!page)
2930 goto oom;
2931
2932 if (mem_cgroup_try_charge_delay(page, vma->vm_mm, GFP_KERNEL, &memcg,
2933 false))
2934 goto oom_free_page;
2935
2936 /*
2937 ¦* The memory barrier inside __SetPageUptodate makes sure that
2938 ¦* preceeding stores to the page contents become visible before
2939 ¦* the set_pte_at() write.
2940 ¦*/
2941 __SetPageUptodate(page);
2942
2943 entry = mk_pte(page, vma->vm_page_prot);
2944 if (vma->vm_flags & VM_WRITE)
2945 entry = pte_mkwrite(pte_mkdirty(entry));
2946
2947 vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
2948 &vmf->ptl);
2949 if (!pte_none(*vmf->pte))
2950 goto release;
2951
2952 ret = check_stable_address_space(vma->vm_mm);
2953 if (ret)
2954 goto release;
2955
2956 /* Deliver the page fault to userland, check inside PT lock */
2957 if (userfaultfd_missing(vma)) {
2958 pte_unmap_unlock(vmf->pte, vmf->ptl);
2959 mem_cgroup_cancel_charge(page, memcg, false);
2960 put_page(page);
2961 return handle_userfault(vmf, VM_UFFD_MISSING);
2962 }
2963
2964 inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
2965 page_add_new_anon_rmap(page, vma, vmf->address, false);
2966 mem_cgroup_commit_charge(page, memcg, false, false);
2967 lru_cache_add_active_or_unevictable(page, vma);
2968 setpte:
2969 set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
2970
2971 /* No need to invalidate - it was non-present before */
2972 update_mmu_cache(vma, vmf->address, vmf->pte);
2973 unlock:
2974 pte_unmap_unlock(vmf->pte, vmf->ptl);
2975 return ret;
2976 release:
2977 mem_cgroup_cancel_charge(page, memcg, false);
2978 put_page(page);
2979 goto unlock;
2980 oom_free_page:
2981 put_page(page);
2982 oom:
2983 return VM_FAULT_OOM;
2984 }
當判斷不是讀操作導致的缺頁的時候,則是寫操作造成,處理寫私有的匿名頁情況,請記住這依然是第一次訪問這個匿名頁只不過是寫訪問而已。
2928 行會分配一個高端 可遷移的 被0填充的物理頁。2941 設置頁中數(shù)據有效 2943 使用頁幀號和vma的訪問權限設置頁表項值(注意:這個時候頁表項屬性依然為只讀)。
2944-2945行 如果vma可寫,則設置頁表項值為臟且*可寫*(這個時候才設置為可寫)。
2964行 匿名頁計數(shù)統(tǒng)計 2965行 添加到匿名頁的反向映射中 2967行 添加到lru鏈表 2969 將設置好的頁表項值填充到頁表項中。
下面用圖說話:
3.4 讀之后寫匿名頁
讀之后寫匿名頁,其實已經很簡單了,那就是發(fā)生COW寫時復制缺頁。下面依然看圖說話:
四,應用層實驗
實驗1:主要體驗下內核的按需分配頁策略!實驗代碼:mmap映射10 * 4096 * 4096/1M=160M內存空間,映射和寫頁前后獲得內存使用情況:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/mman.h>
4 #include <unistd.h>
5
6
7 #define MAP_LEN (10 * 4096 * 4096)
8
9 int main(int argc, char **argv)
10 {
11 char *p;
12 int i;
13
14
15 puts("before mmap ->please exec: free -mn");
16 sleep(10);
17 p = (char *)mmap(0, MAP_LEN, PROT_READ |PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
18
19 puts("after mmap ->please exec: free -mn");
20 puts("before write....n");
21 sleep(10);
22
23 for(i=0;i <4096 *10; i++)
24 p[4096 * i] = 0x55;
25
26
27 puts("after write ->please exec: free -mn");
28
29 pause();
30
31 return 0;
32 }
執(zhí)行結果:
出現(xiàn)“before mmap ->please exec: free -m”打印后執(zhí)行:
$ free -m
總計 已用 空閑 共享 緩沖/緩存 可用
內存:15921 6561 462 796 8897 8214
交換:16290 702 15588
出現(xiàn)“after mmap ->please exec: free -m”打印后執(zhí)行:
$ free -m
總計 已用 空閑 共享 緩沖/緩存 可用
內存:15921 6565 483 771 8872 8236
交換:16290 702 15588
出現(xiàn)“after write ->please exec: free -m”后執(zhí)行:
$:~/study/user_test/page-fault$ free -m
總計 已用 空閑 共享 緩沖/緩存 可用
內存:15921 6727 322 770 8871 8076
交換:16290 702 15588
我們只關注已用內存,可以發(fā)現(xiàn)映射前后基本上已用內存沒有變化(考慮到其他內存申請情況存在,也會有內存變化)是6561M和6565M,說明mmap的時候并沒有分配物理內存,寫之后發(fā)現(xiàn)內存使用為6727M, 6727-6565=162M與我們mmap的大小基本一致,說明了匿名頁實際寫的時候才會分配等量的物理內存。
實驗2:主要體驗下匿名頁讀之后寫內存頁申請情況 實驗代碼:mmap映射10 * 4096 * 4096/1M=160M內存空間,映射、讀然后寫頁前后獲得內存使用情況:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/mman.h>
4 #include <unistd.h>
5
6
7 #define MAP_LEN (10 * 4096 * 4096)
8
9 int main(int argc, char **argv)
10 {
11 char *p;
12 int i;
13
14
15 puts("before mmap...pls show free:.n");
16 sleep(10);
? 17 p = (char *)mmap(0, MAP_LEN, PROT_READ |PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
18
19 puts("after mmap....n");
20
21 puts("before read...pls show free:.n");
22 sleep(10);
23
24 puts("start read....n");
25
26
27 for(i=0;i <4096 *10; i++)
28 printf("%d ", p[4096 * i]);
29 printf("n");
30
31 puts("after read....pls show free:n");
32
33 sleep(10);
34
35 puts("start write....n");
36
37 for(i=0;i <4096 *10; i++)
38 p[4096 * i] = 0x55;
39
40
41 puts("after write...pls show free:.n");
42
43 pause();
44
45 return 0;
46 }
執(zhí)行結果:出現(xiàn)"before mmap ->please exec: free -m" 后執(zhí)行:
$ free -m
總計 已用 空閑 共享 緩沖/緩存 可用
內存:15921 6590 631 780 8700 8164
交換:16290 702 15588
出現(xiàn)"before read ->please exec: free -m"后執(zhí)行:
$ free -m
總計 已用 空閑 共享 緩沖/緩存 可用
內存:15921 6586 644 770 8690 8178
交換:16290 702 15588
出現(xiàn)"after read ->please exec: free -m"后執(zhí)行:
$ free -m
總計 已用 空閑 共享 緩沖/緩存 可用
內存:15921 6587 624 789 8709 8158
交換:16290 702 15588
出現(xiàn)"after write ->please exec: free -m"后執(zhí)行:
$ free -m
總計 已用 空閑 共享 緩沖/緩存 可用
內存:15921 6749 462 789 8709 7996
交換:16290 702 15588
可以發(fā)現(xiàn):讀之后和之前基本上內存使用沒有變化(實際上映射到了0頁,這是內核初始化時候分配好的),知道寫之后6749-6587=162M符合預期,而且打印可以發(fā)現(xiàn)數(shù)據全為0。
分析:實際上,mmap的時候只是申請了一塊vma,讀的時候發(fā)生一次缺頁異常,映射到0頁,所有內存沒有分配,當再次寫這個頁面的時候,發(fā)生了COW分配新頁(cow中分配新頁的時候會判斷原來的頁是否為0頁,如果為0頁就直接分配頁然后用0填充)。
五,總結
匿名映射缺頁異常是我們遇到的一種很常用的一種異常,對于匿名映射,映射完成之后,只是獲得了一塊虛擬內存,并沒有分配物理內存,當?shù)谝淮卧L問的時候:如果是讀訪問,會將虛擬頁映射到0頁,以減少不必要的內存分配;如果是寫訪問,則會分配新的物理頁,并用0填充,然后映射到虛擬頁上去。而如果是先讀訪問一頁然后寫訪問這一頁,則會發(fā)生兩次缺頁異常:第一次是匿名頁缺頁異常的讀的處理,第二次是寫時復制缺頁異常處理。