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

韩传华 , 就职于南京大鱼半导体有限公司 , 主要从事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_pageisaspecialpagethatisusedforzero-initializeddata63*andCOW.64*/65unsignedlongempty_zero_page[PAGE_SIZE/sizeof(unsignedlong)]__page_aligned_bss;66EXPORT_SYMBOL(empty_zero_page);可以看到定义了一个全局变量 , 大小为一页 , 页对齐到bss段 , 所有这段数据内核初始化的时候会被清零 , 所有称之为0页 。
那么为什么使用0页呢?一个是它的数据都是被0填充 , 读的时候数据都是0 , 二是节约内存 , 匿名页面第一次读的时候数据都是0都会映射到这页中从而节约内存(共享0页) , 那么如果有进程要去写这个这个页会怎样呢?答案是发生COW重新分配页来写 。
当第一节中的触发情况发生的时候 , 处理器就会发生缺页异常 , 从处理器架构相关部分过渡到处理器无关部分 , 最终到达handle_pte_fault函数:
3742staticvm_fault_thandle_pte_fault(structvm_fault*vmf)3743{3744pte_tentry;...3782if(!vmf->pte){3783if(vma_is_anonymous(vmf->vma))3784returndo_anonymous_page(vmf);3785else3786returndo_fault(vmf);3787}3782和3783行是匿名映射缺页异常的触发条件:
1.发生缺页的地址所在页表项不存在 。
2.是匿名页发生的 , 即是vma->vm_ops为空 。
当满足这两个条件的时候就会调用do_anonymous_page函数来处理匿名映射缺页异常 。
2871/*2872*Weenterwithnon-exclusivemmap_sem(toexcludevmachanges,2873*butallowconcurrentfaults),andptemappedbutnotyetlocked.2874*Wereturnwithmmap_semstillheld,butpteunmappedandunlocked.2875*/2876staticvm_fault_tdo_anonymous_page(structvm_fault*vmf)2877{2878structvm_area_struct*vma=vmf->vma;2879structmem_cgroup*memcg;2880structpage*page;2881vm_fault_tret=0;2882pte_tentry;28832884/*Filemappingwithout->vm_ops?*/2885if(vma->vm_flags&VM_SHARED)2886returnVM_FAULT_SIGBUS;28872888/*2889|*Usepte_alloc()insteadofpte_alloc_map().Wecan'trun2890|*pte_offset_map()onpmdswhereahugepmdmightbecreated2891|*fromadifferentthread.2892|*2893|*pte_alloc_map()issafetouseunderdown_write(mmap_sem)orwhen2894|*parallelthreadsareexcludedbyothermeans.2895|*2896|*Hereweonlyhavedown_read(mmap_sem).2897|*/2898if(pte_alloc(vma->vm_mm,vmf->pmd))2899returnVM_FAULT_OOM;2904...2885行判断:发生缺页的vma是否为私有映射 , 这个函数处理的是私有的匿名映射 。
2898行如何页表不存在则分配页表(有可能缺页地址的页表项所在的直接页表不存在) 。
...2905/*Usethezero-pageforreads*/2906if(!(vmf->flags&FAULT_FLAG_WRITE)&&2907!mm_forbids_zeropage(vma->vm_mm)){2908entry=pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),2909vma->vm_page_prot));2910vmf->pte=pte_offset_map_lock(vma->vm_mm,vmf->pmd,2911vmf->address,&vmf->ptl);2912if(!pte_none(*vmf->pte))2913gotounlock;2914ret=check_stable_address_space(vma->vm_mm);2915if(ret)2916gotounlock;2917/*Deliverthepagefaulttouserland,checkinsidePTlock*/2918if(userfaultfd_missing(vma)){2919pte_unmap_unlock(vmf->pte,vmf->ptl);2920returnhandle_userfault(vmf,VM_UFFD_MISSING);2921}2922gotosetpte;2923}...2968setpte:2969set_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#definepfn_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:875staticinlineunsignedlongmy_zero_pfn(unsignedlongaddr)876{877externunsignedlongzero_pfn;878returnzero_pfn;879}mm/memory.c:126unsignedlongzero_pfn__read_mostly;127EXPORT_SYMBOL(zero_pfn);128129unsignedlonghighest_memmap_pfn__read_mostly;130131/*132*CONFIG_MMUarchitecturessetupZERO_PAGEintheirpaging_init()133*/134staticint__initinit_zero_pfn(void)135{136zero_pfn=page_to_pfn(ZERO_PAGE(0));137return0;138}139core_initcall(init_zero_pfn);||
【Linux内核虚拟内存管理之匿名映射缺页异常分析】/


    推荐阅读