开启辅助访问 切换到窄版

打印 上一主题 下一主题

Linux内核虚拟内存管理之匿名映射缺页异常分析

[复制链接]
作者:海林淀粉厂 
版块:
嵌入式操作系统 linux 发布时间:2020-9-17 02:18:53
19690
楼主
跳转到指定楼层
| 只看该作者 回帖奖励 |倒序浏览 |阅读模式
韩传华,就职于南京大鱼半导体有限公司,主要从事linux相关系统软件开发工作,负责Soc芯片BringUp及系统软件开发,乐于分享喜欢学习,喜欢专研Linux内核源代码。
前面讲到过写时复制缺页异常(COW),一般用于父子进程之间共享页,而我们会常见一种缺页异常是匿名映射缺页异常,今天我们就来讨论下这种缺页异常,让大家彻底理解它。注:本文使用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页如何分配的:在arch/arm64/mm/mmu.c中:
61 /*62 * Empty_zero_page is a special page that is used for zero-initialized data63 * and COW.64 */65 unsigned long empty_zero_page __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 else3786 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 run2890 * pte_offset_map on pmds where a huge pmd might be created2891 * from a different thread.2892 *2893 * pte_alloc_map is safe to use under down_write(mmap_sem) or when2894 * 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) vm_pageprot,这是vma的访问权限,在做内存映射mmap的时候会被设置。</p>那么我们想知道的时候是什么时候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 expected80 * behavior is in parens:81 *82 * map_type prot83 * PROT_NONE PROT_READ PROT_WRITE PROT_EXEC84 * MAP_SHARED r: (no) no r: (yes) yes r: (no) yes r: (no) yes85 * w: (no) no w: (no) no w: (yes) yes w: (no) no86 * x: (no) no x: (no) yes x: (no) yes x: (yes) yes87 *88 * MAP_PRIVATE r: (no) no r: (yes) yes r: (no) yes r: (no) yes89 * w: (no) no w: (no) no w: (copy) copy w: (no) no90 * x: (no) no x: (no) yes x: (no) yes x: (yes) yes91 *92 * On arm64, PROT_EXEC has the following behaviour for both MAP_SHARED and93 * MAP_PRIVATE:94 * r: (no) no95 * w: (no) no96 * x: (yes) yes97 */98 pgprot_t protection_map __ro_after_init = {99 __P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111,100 __S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111101 };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_NONE101 #define __P001 PAGE_READONLY102 #define __P010 PAGE_READONLY103 #define __P011 PAGE_READONLY104 #define __P100 PAGE_EXECONLY105 #define __P101 PAGE_READONLY_EXEC106 #define __P110 PAGE_READONLY_EXEC107 #define __P111 PAGE_READONLY_EXEC108 109 #define __S000 PAGE_NONE110 #define __S001 PAGE_READONLY111 #define __S010 PAGE_SHARED112 #define __S011 PAGE_SHARED113 #define __S100 PAGE_EXECONLY114 #define __S101 PAGE_READONLY_EXEC115 #define __S110 PAGE_SHARED_EXEC116 #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 that2938 * preceeding stores to the page contents become visible before2939 * 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 2 #include 3 #include 4 #include 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 please exec: free -m”打印后执行:</p>$ 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 2 #include 3 #include 4 #include 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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表