今天咱們就來討論下這種缺頁異常,讓你們完全理解它。注:本文使用linux-5.0內核源代碼。文章分爲如下幾節內容:linux
匿名映射缺頁異常的觸發狀況數組
0頁是什麼?爲何使用0頁?緩存
源代碼分析
3.1 觸發條件架構
3.2 第一次讀匿名頁app
3.3 第一次寫匿名頁ide
3.4 讀以後寫匿名頁函數
應用層實驗this
總結spa
在講解匿名映射缺頁異常以前咱們先要了解如下什麼是匿名頁?與匿名頁相對應的是文件頁,文件頁咱們應該很好理解,就是映射文件的頁,如:經過mmap映射文件到虛擬內存而後讀文件數據,進程的代碼數據段等,這些頁有後備緩存也就是塊設備上的文件,而匿名頁就是沒有關聯到文件的頁,如:進程的堆、棧等。還有一點須要注意:下面討論的都是私有的匿名頁的狀況,共享匿名頁在內核演變爲文件映射缺頁異常(僞文件系統),後面有機會咱們會講解,感興趣的小夥伴能夠看一看mmap的代碼實現對共享匿名頁的處理。code
前面咱們講解了什麼是匿名頁,那麼思考一下什麼狀況下會觸發匿名映射缺頁異常呢?這種異常對於咱們來講很是常見:
1.當咱們應用程序使用malloc來申請一塊內存(堆分配),在沒有使用這塊內存以前,僅僅是分配了虛擬內存,並無分配物理內存,第一次去訪問的時候纔會經過觸發缺頁異常來分配物理頁創建和虛擬頁的映射關係。
2.當咱們應用程序使用mmap來建立匿名的內存映射的時候,頁一樣只是分配了虛擬內存,並無分配物理內存,第一次去訪問的時候纔會經過觸發缺頁異常來分配物理頁創建和虛擬頁的映射關係。
3.當函數的局部變量比較大,或者是函數調用的層次比較深,致使了當前的棧不夠用了,這個時候須要擴大棧。固然了上面的這幾種場景對應應用程序來講是透明的,內核爲用戶程序作了大量的處理工做,下面幾節會看到如何處理。
這裏爲何會說到0頁呢?什麼是0頁呢?是地址爲0的頁嗎?答案是:系統初始化過程當中分配了一頁的內存,這段內存所有被填充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段,全部這段數據內核初始化的時候會被清零,全部稱之爲0頁。
那麼爲何使用0頁呢?一個是它的數據都是被0填充,讀的時候數據都是0,二是節約內存,匿名頁面第一次讀的時候數據都是0都會映射到這頁中從而節約內存(共享0頁),那麼若是有進程要去寫這個這個頁會怎樣呢?答案是發生COW從新分配頁來寫。
當第一節中的觸發狀況發生的時候,處理器就會發生缺頁異常,從處理器架構相關部分過渡處處理器無關部分,最終到達handle_pte_fault函數:
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行是匿名映射缺頁異常的觸發條件:
1.發生缺頁的地址所在頁表項不存在。
2.是匿名頁發生的,便是vma->vm_ops爲空。
當知足這兩個條件的時候就會調用do_anonymous_page函數來處理匿名映射缺頁異常。
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行判斷:發生缺頁的vma是否爲私有映射,這個函數處理的是私有的匿名映射。
2898行 如何頁表不存在則分配頁表(有可能缺頁地址的頁表項所在的直接頁表不存在)。
... 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) 135 { 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的第二個參數vma->vm_pageprot,這是vma的訪問權限,在作內存映射mmap的時候會被設置。
那麼咱們想知道的時候是何時0頁被設置爲了只讀屬性的(也就是頁表項什麼時候被設置爲只讀)?
咱們帶着這個問題去在內核代碼中尋找答案。其實代碼看到這裏通常看不到頭緒,可是咱們要知道什麼時候vma的vm_page_prot成員被設置的,如何被設置的,有可能就能找到答案。
咱們到mm/mmap.c中去尋找答案:咱們以do_brk_flags函數爲例,這是設置堆的函數咱們關注到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函數會根據傳遞來的vmflags是否爲VMREAD|VMWRITE|VMEXEC|VMSHARED來轉換爲保護位組合,繼續往下看
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數組定義了從P000到S111一共16種組合,P表示私有(Private),S表示共享(Share),後面三個數字依次爲可讀、可寫、可執行,如:_S010表示共享、不可讀、可寫、不可執行。
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
能夠發現對於私有的映射只有只讀(PTE_RDONLY)沒有可寫屬性(PTE_WRITE)105-107行 ,雖然以前設置的時候是設置了可寫(VM_WRITE)!而對應共享映射則會有可寫屬性。
而這個被設置的保護位組合最終會在缺頁異常中被設置到頁表中:上面說到的do_anonymous_page函數:
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相關文章,再此不在贅述。下面用圖說話:
接着do_anonymous_page函數繼續往下分析:
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 設置頁中數據有效
2943 使用頁幀號和vma的訪問權限設置頁表項值(注意:這個時候頁表項屬性依然爲只讀)。
2944-2945行 若是vma可寫,則設置頁表項值爲髒且可寫(這個時候才設置爲可寫)。
2964行 匿名頁計數統計
2965行 添加到匿名頁的反向映射中
2967行 添加到lru鏈表
2969 將設置好的頁表項值填充到頁表項中。
下面用圖說話:
讀以後寫匿名頁,其實已經很簡單了,那就是發生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 -m\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 ->please exec: free -m\n"); 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 -m\n"); 28 29 pause(); 30 31 return 0; 32 }
執行結果:
出現「before mmap ->please exec: free -m」打印後執行:
$ free -m 總計 已用 空閒 共享 緩衝/緩存 可用 內存:15921 6561 462 796 8897 8214 交換:16290 702 15588
出現「after mmap ->please exec: free -m」打印後執行:
$ free -m 總計 已用 空閒 共享 緩衝/緩存 可用 內存:15921 6565 483 771 8872 8236 交換:16290 702 15588
出現「after write ->please exec: free -m」後執行:
$:~/study/user_test/page-fault$ free -m 總計 已用 空閒 共享 緩衝/緩存 可用 內存:15921 6727 322 770 8871 8076 交換:16290 702 15588
咱們只關注已用內存,能夠發現映射先後基本上已用內存沒有變化(考慮到其餘內存申請狀況存在,也會有內存變化)是6561M和6565M,說明mmap的時候並無分配物理內存,寫以後發現內存使用爲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 }
執行結果:出現"before mmap ->please exec: free -m" 後執行:
$ free -m 總計 已用 空閒 共享 緩衝/緩存 可用 內存:15921 6590 631 780 8700 8164 交換:16290 702 15588
出現"before read ->please exec: free -m"後執行:
$ free -m 總計 已用 空閒 共享 緩衝/緩存 可用 內存:15921 6586 644 770 8690 8178 交換:16290 702 15588
出現"after read ->please exec: free -m"後執行:
$ free -m 總計 已用 空閒 共享 緩衝/緩存 可用 內存:15921 6587 624 789 8709 8158 交換:16290 702 15588
出現"after write ->please exec: free -m"後執行:
$ free -m 總計 已用 空閒 共享 緩衝/緩存 可用 內存:15921 6749 462 789 8709 7996 交換:16290 702 15588
能夠發現:讀以後和以前基本上內存使用沒有變化(實際上映射到了0頁,這是內核初始化時候分配好的),知道寫以後6749-6587=162M符合預期,並且打印能夠發現數據全爲0。
分析:實際上,mmap的時候只是申請了一塊vma,讀的時候發生一次缺頁異常,映射到0頁,全部內存沒有分配,當再次寫這個頁面的時候,發生了COW分配新頁(cow中分配新頁的時候會判斷原來的頁是否爲0頁,若是爲0頁就直接分配頁而後用0填充)。
匿名映射缺頁異常是咱們遇到的一種很經常使用的一種異常,對於匿名映射,映射完成以後,只是得到了一塊虛擬內存,並無分配物理內存,當第一次訪問的時候:若是是讀訪問,會將虛擬頁映射到0頁,以減小沒必要要的內存分配;若是是寫訪問,則會分配新的物理頁,並用0填充,而後映射到虛擬頁上去。而若是是先讀訪問一頁而後寫訪問這一頁,則會發生兩次缺頁異常:第一次是匿名頁缺頁異常的讀的處理,第二次是寫時複製缺頁異常處理。