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

HugePages(大内存页)实现全解析

时间:2023-03-18 22:37:08 科技观察

文章《一文读懂 HugePages的原理》介绍了HugePages(大内存页)的原理和使用。下面我们来分析一下Linux内核是如何实现HugePages分配的。本文使用Linux内核2.6.23版本的HugePages分配器初始化。当内核初始化时,会调用hugetlb_init函数来初始化HugePages分配器。实现如下:staticint__initithugetlb_init(void){unsignedlongi;//1.初始化空闲大内存页链表hugepage_freelists,//内核使用hugepage_freelists链表连接空闲大内存页,//为了简单分析,我们可以把MAX_NUMNODES看成1for(i=0;ilru,&hugepage_freelists[nid]);//增加计数器free_huge_pages++;free_huge_pages_node[nid]++;}从上面的实现中,enqueue_huge_page的作用只是将内存页添加到空闲列表hugepage_freelists中,并使计数器递增。如果我们设置系统可以使用的大内存页数为100,那么空闲大页链表hugepage_freelists的结构如下图所示:因此,HugePages分配器初始化的调用链为:hugetlb_init()|+——>alloc_fresh_huge_page()||——>alloc_pages_node()|——>set_compound_page_dtor()+——>put_page()|+——>free_huge_page()|+——>enqueue_huge_page()hugetlbfs文件系统为系统页面准备大的空闲内存,现在让我们看看分配了多大的页面。一文介绍,申请大内存页,必须使用mmap系统调用,将虚拟内存映射到hugetlbfs文件系统中的文件。抛开繁琐的文件系统挂载过程,我们主要看看使用mmap系统调用将虚拟内存映射到hugetlbfs文件系统中的文件时会发生什么。每个文件描述符对象都有一个mmap方法,当调用mmap函数映射到文件时会触发该方法。我们来看看hugetlbfs文件的mmap方法对应的真实函数,如下:从上面的代码可以发现hugetlbfs文件的mmap方法设置为hugetlbfs_file_mmap函数。所以调用mmap函数映射hugetlbfs文件时,会调用hugetlbfs_file_mmap函数进行处理。hugetlbfs_file_mmap函数的主要工作是在虚拟内存分区对象的vm_flags字段中添加VM_HUGETLB标志,如下:;//为虚拟内存分区添加VM_HUGETLB标志位...returnret;}为虚拟内存分区对象设置VM_HUGETLB标志位的作用是:在对虚拟内存分区进行物理内存映射时,会进行特殊处理执行,下面将介绍。使用mmap函数将虚拟内存和物理内存映射到hugetlbfs文件后,会返回一个虚拟内存地址。当访问(读写)这个虚拟内存地址时,由于虚拟内存地址还没有和物理内存地址映射,会触发缺页异常,内核会调用do_page_fault函数修复缺页异常。我们来看看整个过程,如下图所示:因此,最终会调用do_page_fault函数来修复pagefault异常。我们来看看do_page_fault做了什么。实现如下:asmlinkagevoid__kprobesdo_page_fault(structpt_regs*regs,unsignedlongerror_code){。..structmm_struct*mm;structvm_area_struct*vma;unsignedlongaddress;...mm=tsk->mm;//1.获取当前进程对应的内存管理对象address=read_cr2();//2.获取触发缺页异常的地址Virtualmemoryaddress...vma=find_vma(mm,address);//3.通过虚拟内存地址获取对应的虚拟内存分区对象...//4.调用handle_mm_fault函数修复异常fault=handle_mm_fault(mm,vma,address,write);...return;}以上代码对do_page_fault进行了简化,简化后主要完成4个任务:获取对应的内存管理对象当前进程。调用read_cr2获取触发缺页异常的虚拟内存地址。通过触发缺页异常的虚拟内存地址获取对应的虚拟内存分区对象。调用handle_mm_fault函数修复缺页异常。我们继续看handle_mm_fault函数的实现,代码如下:virtualmemorypartitionneedtouseHugePagesreturnhugetlb_fault(mm,vma,address,write_access);//如果使用HugePages,则调用hugetlb_fault进行处理...}将handle_mm_fault函数简化后,逻辑就很清晰了。如果虚拟内存分区使用了HugePages,则调用hugetlb_fault函数进行处理(因为我们分析的是HugePages的使用,所以才进入这个分支)。hugetlb_fault函数主要是填充进程的页表,那么我们先回顾一下HugePages对应的页表结构,如下图所示:从上图可以看出,使用HugePages后,页面的中间目录直接指向物理内存页。所以hugetlb_fault函数主要是填充页面的中间目录入口。实现如下:inhugetlb_fault(structmm_struct*mm,structvm_area_struct*vma,unsignedlongaddress,intwrite_access){pte_t*ptep;pte_tentry;intret;ptep=huge_pte_alloc(mm,address);//1.找到虚拟内存地址对应的页中间目录项。..entry=*ptep;if(pte_none(entry)){//如果页面的中间目录项还没有被映射//2.然后调用hugetlb_no_page函数进行映射操作ret=hugetlb_no_page(mm,vma,address,ptep,write_access);...returnret;}...}hugetlb_fault函数简化后主要完成两个任务:找到对应的页面中间目录条目通过触发缺页异常的虚拟内存地址。调用hugetlb_no_page函数映射页面中间目录项。我们来看看hugetlb_no_page函数是如何填充页面的中间目录项的:(!page){...//1.从空闲大内存页链表hugepage_freelistspage=alloc_huge_page(vma,address);...}...//2.申请一个大内存页。通过大内存页的物理地址生成页中间目录项的值new_pte=make_huge_pte(vma,page,((vma->vm_flags&VM_WRITE)&&(vma->vm_flags&VM_SHARED)));//3.将page中间目录项的值设为上面生成的值set_huge_pte_at(mm,address,ptep,new_pte);...returnret;}hugetlb_no_page函数精简后主要完成三个任务:调用alloc_huge_page函数申请来自hugepage_freelists空闲大内存页列表的大内存页。从大页面的物理地址生成页面中间目录条目的值。将页面中间目录条目的值设置为上面生成的值。至此HugePages的映射过程就完成了。还有一个问题,就是CPU怎么知道页中间表入口指向的是页表还是大内存页呢?这是因为页中间表项有一个PSE标志位。如果设置为1,表示指向Hugepages,否则指向页表。总结本文介绍了HugePages实现的整个过程。当然,本文只介绍了申请内存的过程,并没有分析释放内存的过程。有兴趣的可以自己查看源码。