Linux內核虛擬內存管理之匿名映射缺頁異常分析

今天咱們就來討論下這種缺頁異常,讓你們完全理解它。注:本文使用linux-5.0內核源代碼。文章分爲如下幾節內容:linux

  1. 匿名映射缺頁異常的觸發狀況數組

  2. 0頁是什麼?爲何使用0頁?緩存

  3. 源代碼分析

    3.1 觸發條件架構

    3.2 第一次讀匿名頁app

    3.3 第一次寫匿名頁ide

    3.4 讀以後寫匿名頁函數

  4. 應用層實驗this

  5. 總結spa

在講解匿名映射缺頁異常以前咱們先要了解如下什麼是匿名頁?與匿名頁相對應的是文件頁,文件頁咱們應該很好理解,就是映射文件的頁,如:經過mmap映射文件到虛擬內存而後讀文件數據,進程的代碼數據段等,這些頁有後備緩存也就是塊設備上的文件,而匿名頁就是沒有關聯到文件的頁,如:進程的堆、棧等。還有一點須要注意:下面討論的都是私有的匿名頁的狀況,共享匿名頁在內核演變爲文件映射缺頁異常(僞文件系統),後面有機會咱們會講解,感興趣的小夥伴能夠看一看mmap的代碼實現對共享匿名頁的處理。code

一,匿名映射缺頁異常的觸發狀況

前面咱們講解了什麼是匿名頁,那麼思考一下什麼狀況下會觸發匿名映射缺頁異常呢?這種異常對於咱們來講很是常見:

1.當咱們應用程序使用malloc來申請一塊內存(堆分配),在沒有使用這塊內存以前,僅僅是分配了虛擬內存,並無分配物理內存,第一次去訪問的時候纔會經過觸發缺頁異常來分配物理頁創建和虛擬頁的映射關係。

2.當咱們應用程序使用mmap來建立匿名的內存映射的時候,頁一樣只是分配了虛擬內存,並無分配物理內存,第一次去訪問的時候纔會經過觸發缺頁異常來分配物理頁創建和虛擬頁的映射關係。

3.當函數的局部變量比較大,或者是函數調用的層次比較深,致使了當前的棧不夠用了,這個時候須要擴大棧。固然了上面的這幾種場景對應應用程序來講是透明的,內核爲用戶程序作了大量的處理工做,下面幾節會看到如何處理。

二,0頁是什麼?爲何使用0頁?

這裏爲何會說到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從新分配頁來寫。

三,源代碼分析

3.1 觸發條件

當第一節中的觸發狀況發生的時候,處理器就會發生缺頁異常,從處理器架構相關部分過渡處處理器無關部分,最終到達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行 如何頁表不存在則分配頁表(有可能缺頁地址的頁表項所在的直接頁表不存在)。

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)
   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相關文章,再此不在贅述。下面用圖說話:

image

3.3 第一次寫匿名頁的狀況

接着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 將設置好的頁表項值填充到頁表項中。

下面用圖說話:

image

3.4 讀以後寫匿名頁

讀以後寫匿名頁,其實已經很簡單了,那就是發生COW寫時複製缺頁。下面依然看圖說話:

image

四,應用層實驗

實驗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填充,而後映射到虛擬頁上去。而若是是先讀訪問一頁而後寫訪問這一頁,則會發生兩次缺頁異常:第一次是匿名頁缺頁異常的讀的處理,第二次是寫時複製缺頁異常處理。

相關文章
相關標籤/搜索