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

2025-05-27 0 85

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

在《一文读懂 HugePages的原理》一文中介绍了 HugePages(大内存页)的原理和使用,现在我们来分析一下 Linux 内核是怎么实现 HugePages 分配的。

本文使用 Linux 内核 2.6.23 版本

HugePages分配器初始化

在内核初始化时,会调用 hugetlb_init 函数对 HugePages 分配器进行初始化,其实现如下:

  1. staticint__inithugetlb_init(void)
  2. {
  3. unsignedlongi;
  4. //1.初始化空闲大内存页链表hugepage_freelists,
  5. //内核使用hugepage_freelists链表把空闲的大内存页连接起来,
  6. //为了分析简单,我们可以把MAX_NUMNODES当成1
  7. for(i=0;i<MAX_NUMNODES;++i)
  8. INIT_LIST_HEAD(&hugepage_freelists[i]);
  9. //2.max_huge_pages为系统能够使用的大页内存的数量,
  10. //由系统启动项hugepages指定,
  11. //这里主要申请大内存页,并且保存到hugepage_freelists链表中.
  12. for(i=0;i<max_huge_pages;++i){
  13. if(!alloc_fresh_huge_page())
  14. break;
  15. }
  16. max_huge_pages=free_huge_pages=nr_huge_pages=i;
  17. return0;
  18. }

hugetlb_init 函数主要完成两个工作:

  • 初始化空闲大内存页链表 hugepage_freelists,这个链表保存了系统中能够使用的大内存。
  • 系统申请空闲的大内存页,并且保存到 hugepage_freelists 链表中。

我们再来分析下 alloc_fresh_huge_page 函数是怎么申请大内存页的,其实现如下:

  1. staticintalloc_fresh_huge_page(void)
  2. {
  3. staticintprev_nid;
  4. structpage*page;
  5. intnid;
  6. //1.申请一个大的物理内存页…
  7. page=alloc_pages_node(nid,htlb_alloc_mask|__GFP_COMP|__GFP_NOWARN,
  8. HUGETLB_PAGE_ORDER);
  9. if(page){
  10. //2.设置释放大内存页的回调函数为free_huge_page
  11. set_compound_page_dtor(page,free_huge_page);
  12. //3.put_page函数将会调用上面设置的free_huge_page函数把内存页放入到缓存队列中
  13. put_page(page);
  14. return1;
  15. }
  16. return0;
  17. }

所以,alloc_fresh_huge_page 函数主要完成三个工作:

  • 调用 alloc_pages_node 函数申请一个大内存页(2MB)。
  • 设置大内存页的释放回调函数为 free_huge_page,当释放大内存页时,将会调用这个函数进行释放操作。
  • 调用 put_page 函数释放大内存页,其将会调用 free_huge_page 函数进行相关操作。

那么,我们来看看 free_huge_page 函数是怎么释放大内存页的,其实现如下:

  1. staticvoidfree_huge_page(structpage*page)
  2. {
  3. enqueue_huge_page(page);//把大内存页放置到空闲大内存页链表中
  4. }

free_huge_page 函数主要调用 enqueue_huge_page 函数把大内存页添加到空闲大内存页链表中,其实现如下:

  1. staticvoidenqueue_huge_page(structpage*page)
  2. {
  3. intnid=page_to_nid(page);//我们假设这里一定返回0
  4. //把大内存页添加到空闲链表hugepage_freelists中
  5. list_add(&page->lru,&hugepage_freelists[nid]);
  6. //增加计数器
  7. free_huge_pages++;
  8. free_huge_pages_node[nid]++;
  9. }

从上面的实现可知,enqueue_huge_page 函数只是简单的把大内存页添加到空闲链表 hugepage_freelists 中,并且增加计数器。

假如我们设置了系统能够使用的大内存页为 100 个,那么空闲大内存页链表 hugepage_freelists 的结构如下图所示:

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

所以,HugePages 分配器初始化的调用链为:

  1. hugetlb_init()
  2. |
  3. +——>alloc_fresh_huge_page()
  4. |
  5. |——>alloc_pages_node()
  6. |——>set_compound_page_dtor()
  7. +——>put_page()
  8. |
  9. +——>free_huge_page()
  10. |
  11. +——>enqueue_huge_page()

hugetlbfs 文件系统

系统准备好空闲的大内存页后,现在来了解下怎样分配大内存页。在《一文读懂 HugePages的原理》一文中介绍过,要申请大内存页,必须使用 mmap 系统调用把虚拟内存映射到 hugetlbfs 文件系统中的文件中。

免去繁琐的文件系统挂载过程,我们主要来看看当使用 mmap 系统调用把虚拟内存映射到 hugetlbfs 文件系统的文件时会发生什么事情。

每个文件描述符对象都有个 mmap 的方法,此方法会在调用 mmap 函数映射到文件时被触发,我们来看看 hugetlbfs 文件的 mmap 方法所对应的真实函数,如下:

  1. conststructfile_operationshugetlbfs_file_operations={
  2. .mmap=hugetlbfs_file_mmap,
  3. .fsync=simple_sync_file,
  4. .get_unmapped_area=hugetlb_get_unmapped_area,
  5. };

从上面的代码可以发现,hugetlbfs 文件的 mmap 方法被设置为 hugetlbfs_file_mmap 函数。所以当调用 mmap 函数映射 hugetlbfs 文件时,将会调用 hugetlbfs_file_mmap 函数来处理。

而 hugetlbfs_file_mmap 函数最主要的工作就是把虚拟内存分区对象的 vm_flags 字段添加 VM_HUGETLB 标志位,如下代码:

  1. staticint
  2. hugetlbfs_file_mmap(structfile*file,structvm_area_struct*vma)
  3. {
  4. vma->vm_flags|=VM_HUGETLB|VM_RESERVED;//为虚拟内存分区添加VM_HUGETLB标志位
  5. returnret;
  6. }

为虚拟内存分区对象设置 VM_HUGETLB 标志位的作用是:当对虚拟内存分区进行物理内存映射时,会进行特殊的处理,下面将会介绍。

虚拟内存与物理内存映射

使用 mmap 函数映射到 hugetlbfs 文件后,会返回一个虚拟内存地址。当对这个虚拟内存地址进行访问(读写)时,由于此虚拟内存地址还没有与物理内存地址进行映射,将会触发 缺页异常,内核会调用 do_page_fault 函数对 缺页异常 进行修复。

我们来看看整个流程,如下图所示:

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

所以,最终会调用 do_page_fault 函数对 缺页异常 进行修复操作,我们来看看 do_page_fault 做了什么工作,实现如下:

  1. asmlinkagevoid
  2. __kprobesdo_page_fault(structpt_regs*regs,unsignedlongerror_code)
  3. {
  4. structmm_struct*mm;
  5. structvm_area_struct*vma;
  6. unsignedlongaddress;
  7. mm=tsk->mm;//1.获取当前进程对应的内存管理对象
  8. address=read_cr2();//2.获取触发缺页异常的虚拟内存地址
  9. vma=find_vma(mm,address);//3.通过虚拟内存地址获取对应的虚拟内存分区对象
  10. //4.调用handle_mm_fault函数对异常进行修复
  11. fault=handle_mm_fault(mm,vma,address,write);
  12. return;
  13. }

上面代码对 do_page_fault 进行了精简,精简后主要完成4个工作:

  • 获取当前进程对应的内存管理对象。
  • 调用 read_cr2 获取触发缺页异常的虚拟内存地址。
  • 通过触发 缺页异常 的虚拟内存地址获取对应的虚拟内存分区对象。
  • 调用 handle_mm_fault 函数对 缺页异常 进行修复。

我们继续来看看 handle_mm_fault 函数的实现,代码如下:

  1. inthandle_mm_fault(structmm_struct*mm,structvm_area_struct*vma,
  2. unsignedlongaddress,intwrite_access)
  3. {
  4. if(unlikely(is_vm_hugetlb_page(vma)))//虚拟内存分区是否需要使用HugePages
  5. returnhugetlb_fault(mm,vma,address,write_access);//如果使用HugePages,就调用hugetlb_fault进行处理
  6. }

对 handle_mm_fault 函数进行精简后,逻辑就非常清晰。如果虚拟内存分区使用 HugePages,那么就调用 hugetlb_fault 函数进行处理(由于我们分析使用 HugePages 的情况,所以刚好进入这个分支)。

hugetlb_fault 函数主要对进程的页表进行填充,所以我们先来回顾一下 HugePages 对应的页表结构,如下图:

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

从上图可以看出,使用 HugePages 后,页中间目录 直接指向物理内存页。所以,hugetlb_fault 函数主要就是对 页中间目录项 进行填充。实现如下:

  1. inthugetlb_fault(structmm_struct*mm,structvm_area_struct*vma,
  2. unsignedlongaddress,intwrite_access)
  3. {
  4. pte_t*ptep;
  5. pte_tentry;
  6. intret;
  7. ptep=huge_pte_alloc(mm,address);//1.找到虚拟内存地址对应的页中间目录项
  8. entry=*ptep;
  9. if(pte_none(entry)){//如果页中间目录项还没进行映射
  10. //2.那么调用hugetlb_no_page函数进行映射操作
  11. ret=hugetlb_no_page(mm,vma,address,ptep,write_access);
  12. returnret;
  13. }
  14. }

对 hugetlb_fault 函数进行精简后,主要完成两个工作:

  • 通过触发 缺页异常 的虚拟内存地址找到其对应的 页中间目录项。
  • 调用 hugetlb_no_page 函数对 页中间目录项 进行映射操作。

我们再来看看 hugetlb_no_page 函数怎么对 页中间目录项 进行填充:

  1. staticint
  2. hugetlb_no_page(structmm_struct*mm,structvm_area_struct*vma,
  3. unsignedlongaddress,pte_t*ptep,intwrite_access)
  4. {
  5. page=find_lock_page(mapping,idx);
  6. if(!page){
  7. //1.从空闲大内存页链表hugepage_freelists中申请一个大内存页
  8. page=alloc_huge_page(vma,address);
  9. }
  10. //2.通过大内存页的物理地址生成页中间目录项的值
  11. new_pte=make_huge_pte(vma,page,((vma->vm_flags&VM_WRITE)
  12. &&(vma->vm_flags&VM_SHARED)));
  13. //3.设置页中间目录项的值为上面生成的值
  14. set_huge_pte_at(mm,address,ptep,new_pte);
  15. returnret;
  16. }

通过对 hugetlb_no_page 函数进行精简后,主要完成3个工作:

  • 调用 alloc_huge_page 函数从空闲大内存页链表 hugepage_freelists 中申请一个大内存页
  • 通过大内存页的物理地址生成页中间目录项的值。
  • 设置页中间目录项的值为上面生成的值。

至此,HugePages 的映射过程已经完成。

还有个问题,就是 CPU 怎么知道 页中间表项 指向的是 页表 还是 大内存页 呢?

这是因为 页中间表项 有个 PSE 的标志位,如果将其设置为1,那么就表明其指向 大内存页 ,否则就指向 页表。

总结

本文介绍了 HugePages 实现的整个流程,当然本文也只是介绍了申请内存的流程,释放内存的流程并没有分析,如果有兴趣的话可以自己查阅源码。

原文地址:https://mp.weixin.qq.com/s/ys-sAVbe8bmv21FK2n9PlQ

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

快网idc优惠网 建站教程 HugePages(大内存页)实现完全解析 https://www.kuaiidc.com/63557.html

相关文章

发表评论
暂无评论