1. 程式人生 > 實用技巧 >Linux核心虛擬記憶體管理之匿名對映缺頁異常分析

Linux核心虛擬記憶體管理之匿名對映缺頁異常分析

今天我們就來討論下這種缺頁異常,讓大家徹底理解它。注:本文使用linux-5.0核心原始碼。文章分為以下幾節內容:

  1. 匿名對映缺頁異常的觸發情況

  2. 0頁是什麼?為什麼使用0頁?

  3. 原始碼分析

    3.1 觸發條件

    3.2 第一次讀匿名頁

    3.3 第一次寫匿名頁

    3.4 讀之後寫匿名頁

  4. 應用層實驗

  5. 總結

在講解匿名對映缺頁異常之前我們先要了解以下什麼是匿名頁?與匿名頁相對應的是檔案頁,檔案頁我們應該很好理解,就是對映檔案的頁,如:通過mmap對映檔案到虛擬記憶體然後讀檔案資料,程序的程式碼資料段等,這些頁有後備快取也就是塊裝置上的檔案,而匿名頁就是沒有關聯到檔案的頁,如:程序的堆、棧等。還有一點需要注意:下面討論的都是私有的匿名頁的情況,共享匿名頁在核心演變為檔案對映缺頁異常(偽檔案系統),後面有機會我們會講解,感興趣的小夥伴可以看一看mmap的程式碼實現對共享匿名頁的處理。

一,匿名對映缺頁異常的觸發情況

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

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

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

下面用圖說話:

3.4 讀之後寫匿名頁

讀之後寫匿名頁,其實已經很簡單了,那就是發生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填充,然後對映到虛擬頁上去。而如果是先讀訪問一頁然後寫訪問這一頁,則會發生兩次缺頁異常:第一次是匿名頁缺頁異常的讀的處理,第二次是寫時複製缺頁異常處理。