当前位置: 首页 > 科技观察

Linux下访问匿名页面时发生的神奇“化学反应”

时间:2023-03-17 20:13:46 科技观察

备份文件支持的Linux页面称为文件页面,比如属于进程的代码段和数据段的页面。只有当内存被回收时,这些页面才需要变脏。页面同步就可以了(干净的页面可以直接丢弃)。反之就是匿名页面,比如进程的栈使用的页面。这些页面在内存回收时不能简单的丢弃,需要交换到交换分区或交换文件中。在本文中,我们主要分析访问匿名页面时会发生的可能颠覆我们认知的“化学反应”。1、示例代码先用一个简单的示例代码进行说明:#include#include#include#include#include#defineMAP_SIZE(100*1024*1024)intmain(intargc,char*argv[]){char*p;charval;inti;puts("beforemmapok,pleaceexec'free-m'!");睡眠(5);//mmapp=mmap(NULL,MAP_SIZE,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);if(p==NULL){perror("failtomalloc");return-1;}puts("aftermmapok,pleaceexec'免费-m'!”);sleep(5);//readfor(i=0;ihandle_mm_fault->__handle_mm_fault->handle_pte_fault->if(!vmf->pte){-------------------1if(vma_is_anonymous(vmf->vma))----------------2returndo_anonymous_page(vmf);------------------3pagefault异常进入handle_pte_fault后,在1标签代码处,判断访问的虚拟内存页的页表项是否为空。如果为空,说明这个虚拟页面还没有和物理页面建立映射关系。然后在2标签代码处判断是否是匿名页面错误异常(其实就是判断是否是私有匿名页面,当前示例代码场景申请的是私有匿名页面)。在3-label代码处,进行了真正的私有匿名页面错误异常处理。下面主要看下一次读北京名字页的处理:do_anonymous_page->pte_alloc(vma->vm_mm,vmf->pmd)--------------------1->/*Usethezero-pageforreads*/if(!(vmf->flags&FAULT_FLAG_WRITE)&&----------------2!mm_forbids_zeropage(vma->vm_mm)){-----------------3entry=pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),vma->vm_page_prot));------------------4vmf->pte=pte_offset_map_lock(vma->vm_mm,vmf->pmd,vmf->address,&vmf->ptl);-------------------5...gotosetpte;}->page=alloc_zeroed_user_highpage_movable(vma,vmf->address);--------------------6->entry=mk_pte(page,vma->vm_page_prot);----------------7entry=pte_sw_mkyoung(entry);-------------------8if(vma->vm_flags&VM_WRITE)entry=pte_mkwrite(pte_mkdirty(entry));----------------9vmf->pte=pte_offset_map_lock(vma->vm_mm,vmf->pmd,vmf->地址,&vmf->ptl);--------------------10->set_pte_at(vma->vm_mm,vmf->地址,vmf->pte,条目);--------------------111标签:判断虚拟地址对应的pmd表项是否为空,如果为空则分配直接页表并设置为pmd表项。2Label:判断是否为读权限。3label:判断第0页是否不被禁止。标号4:设置不禁止page0的匿名页读访问的页表。这里页表项的值由page0的页框号和mmap映射时指定的访问权限组合而成。5label:通过pagefault的虚拟地址计算出页表项的地址,保存在vmf->pte中。在第11个标签处:将第4个标签开头合并的页表项的值写入到第5个标签开头计算的页表项中。上面的分析表明:对于私有匿名页面,在第一次读访问时会出现页面错误,然后通过页表映射0页。这个page0没有什么特别的,只是在系统启动的时候初始化的。一个内容全为0的好页可以为进程分配内存以供只读访问节省大量物理内存。2.3第一次写匿名页面的内存消耗可以在示例代码中屏蔽读访问,只进行写访问,观察内存消耗情况。此时发生页错误时,不是在2345处运行代码,而是在6处分配一个物理页,在789处合并页表项的值,页表项的地址在10处计算,最后将合并后的值设置到页表项中。注意在第9点,如果是写访问,页表项的可写标志位会被置位。上面的分析表明:对于私有匿名页面,在第一次写访问的时候会出现pagefault,实际会分配一个物理page,然后通过page将虚拟page映射到物理page,所以我们可以观察到它发生在写入大内存消耗之后。2.4第一次读写匿名页面的内存消耗该场景是示例代码中所做的实验。可以看到读的时候基本没有内存消耗,写的时候出现大量的内存消耗。关于第一次读取,上面已经说明了,下面主要看读取后对页面的写访问情况。2.4.1从mmap说起其实对于私有内存映射来说,在mmap准备页表映射的访问权限时,并没有给所有的权限,而是去掉了writable属性。我们可以从源码中找到答案:"mm/mmap.c"do_mmap->mmap_region->vma_set_page_prot(vma)->vm_page_prot=vm_pgprot_modify(vma->vm_page_prot,vm_flags);--------1->pgprot_modify(oldprot,vm_get_page_prot(vm_flags))->WRITE_ONCE(vma->vm_page_prot,vm_page_prot);------------2/*descriptionofeffectsofmappingtypeandprotincurrentimplementation.*thisisduetothelimitedx86pageprotectionhardware.Theexpected*behaviorisinpar*map_typeprot*PROT_NONEPROT_READPROT_WRITEPROT_EXEC*MAP_SHAREDr:(no)nor:(yes)yesr:(no)yesr:(no)yes*w:(no)now:(no)now:(yes)yesw:(no)no*x:(no)nox:(no)yesx:(no)yesx:(yes)yes**MAP_PRIVATEr:(no)nor:(yes)yesr:(no)yesr:(no)yes*w:(no)now:(no)now:(copy)copyw:(no)no*x:(no)nox:(no)yesx:(no)yesx:(yes)yes*/->vm_get_page_protpgprot_tprotection_map[16]__ro_after_init={__P000,__P001,__P010,__P011,__P100,__P101,__P110,__P111,__S000,__S001,__S010,__S011,__S100,__S101,__S110,__S111};对于__Pxxx,最后一个x表示vma属性是否可读,倒数第二个x表示vma属性是否可写,P后面的x表示是否可执行。1label根据mmap传递的访问权限构造最终的访问权限标识。2.将构造的访问权限标识记录到标签处的vma->vm_page_prot中,用于pagefault设置页表。评论里已经做了详细的解释。具体的页表属性如何表达由各自处理器架构相关的代码来完成(eg:对于x86架构#define__P111PAGE_COPY_EXEC),我们只需要知道:无论我们希望vma有什么属性组合都会阻止写入属性。详情请参考相关处理器架构的实现。于是,又回到缺页异常处理代码。在2.2节标签4处,使用mmap设置的页表访问权限设置页表属性。在当前场景下,我们知道mmap被指定为私有可读写属性,而页表只被设置为只读属性。2.4.2写时复制触发器读取访问以只读方式将虚拟页面映射到页面0。当再次发生写操作时,会再次触发数据访问异常,最后进入缺页异常处理程序。调用链如下:"mm/memory.c"handle_pte_fault->if(vmf->flags&FAULT_FLAG_WRITE){------------1if(!pte_write(entry))-----------2returndo_wp_page(vmf);------------3可以看到最后在handle_pte_fault:判断是否是1标号处的写访问。在2标签判断页表项的属性是否只读。实际的写时复制处理发生在3标记处。上面的分析表明:当发生写访问操作时,如果vma是可写的,但是页表属性标识是不可写的(只读),就会发生copy-on-writepagefault。当前场景中第0页的写访问就是这种情况。在do_wp_page中会重新分配物理页并映射到虚拟页,然后将页表设置为可写属性,页错误处理就完成了。3、总结1)mmap分配私有匿名内存时,会设置vma的vm_page_prot成员,并去掉页表的写访问标志。2)第一次读取匿名页时,对于可读可写的vma,虚拟页会以只读方式映射到page0。3)第一次写匿名页时,对于一个可读可写的vma,会申请一个物理页,虚拟页以可读可写的方式映射到这个物理页上。4)第一次读完匿名页,然后写匿名页,先以只读方式映射到0页,然后发生copy-on-write,分配物理页,映射虚拟页以可读可写的方式添加到这个物理页面。可以发现,访问匿名页面时发生的“化学反应”并不是那么简单,这涉及到mmap的映射原理,0页的映射,匿名页面的处理,copy-on-write的处理,等,读写顺序不同。同样,结果也会不同。大家可以结合内核源码进行分析,希望能帮助大家理解匿名页错误异常。