一文带你实现完全解析HugePages(大内存页)(赶快学习起来~)
我们来分析一下 Linux 内核是怎么实现 HugePages 分配的。
本文使用 Linux 内核 2.6.23 版本
HugePages分配器初始化
在内核初始化时,会调用 hugetlb_init 函数对 HugePages 分配器进行初始化,其实现如下:
hugetlb_init 函数主要完成两个工作:
初始化空闲大内存页链表 hugepage_freelists,这个链表保存了系统中能够使用的大内存。
为系统申请空闲的大内存页,并且保存到 hugepage_freelists 链表中。
我们再来分析下 alloc_fresh_huge_page 函数是怎么申请大内存页的,其实现如下:
所以,alloc_fresh_huge_page 函数主要完成三个工作:
调用 alloc_pages_node 函数申请一个大内存页(2MB)。
设置大内存页的释放回调函数为 free_huge_page,当释放大内存页时,将会调用这个函数进行释放操作。
调用 put_page 函数释放大内存页,其将会调用 free_huge_page 函数进行相关操作。
那么,我们来看看 free_huge_page 函数是怎么释放大内存页的,其实现如下:
free_huge_page 函数主要调用 enqueue_huge_page 函数把大内存页添加到空闲大内存页链表中,其实现如下:
从上面的实现可知,enqueue_huge_page 函数只是简单的把大内存页添加到空闲链表 hugepage_freelists 中,并且增加计数器。
假如我们设置了系统能够使用的大内存页为 100 个,那么空闲大内存页链表 hugepage_freelists 的结构如下图所示:

【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100名进群领取,额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)


所以,HugePages 分配器初始化的调用链为:
hugetlbfs 文件系统
为系统准备好空闲的大内存页后,现在来了解下怎样分配大内存页。在《一文读懂 HugePages的原理》一文中介绍过,要申请大内存页,必须使用 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 标志位的作用是:当对虚拟内存分区进行物理内存映射时,会进行特殊的处理,下面将会介绍。
虚拟内存与物理内存映射
使用 mmap 函数映射到 hugetlbfs 文件后,会返回一个虚拟内存地址。当对这个虚拟内存地址进行访问(读写)时,由于此虚拟内存地址还没有与物理内存地址进行映射,将会触发 缺页异常,内核会调用 do_page_fault 函数对 缺页异常 进行修复。
我们来看看整个流程,如下图所示:

所以,最终会调用 do_page_fault 函数对 缺页异常 进行修复操作,我们来看看 do_page_fault 做了什么工作,实现如下:
上面代码对 do_page_fault 进行了精简,精简后主要完成4个工作:
获取当前进程对应的内存管理对象。
调用 read_cr2 获取触发缺页异常的虚拟内存地址。
通过触发 缺页异常 的虚拟内存地址获取对应的虚拟内存分区对象。
调用 handle_mm_fault 函数对 缺页异常 进行修复。
我们继续来看看 handle_mm_fault 函数的实现,代码如下:
对 handle_mm_fault 函数进行精简后,逻辑就非常清晰。如果虚拟内存分区使用 HugePages,那么就调用 hugetlb_fault 函数进行处理(由于我们分析使用 HugePages 的情况,所以刚好进入这个分支)。
hugetlb_fault 函数主要对进程的页表进行填充,所以我们先来回顾一下 HugePages 对应的页表结构,如下图:

从上图可以看出,使用 HugePages 后,页中间目录 直接指向物理内存页。所以,hugetlb_fault 函数主要就是对 页中间目录项 进行填充。实现如下:
对 hugetlb_fault 函数进行精简后,主要完成两个工作:
通过触发 缺页异常 的虚拟内存地址找到其对应的 页中间目录项。
调用 hugetlb_no_page 函数对 页中间目录项 进行映射操作。
我们再来看看 hugetlb_no_page 函数怎么对 页中间目录项 进行填充:
通过对 hugetlb_no_page 函数进行精简后,主要完成3个工作:
调用 alloc_huge_page 函数从空闲大内存页链表 hugepage_freelists 中申请一个大内存页。
通过大内存页的物理地址生成页中间目录项的值。
设置页中间目录项的值为上面生成的值。
至此,HugePages 的映射过程已经完成。
还有个问题,就是 CPU 怎么知道 页中间表项 指向的是 页表 还是 大内存页 呢? 这是因为 页中间表项 有个 PSE 的标志位,如果将其设置为1,那么就表明其指向 大内存页 ,否则就指向 页表。
总结
本文介绍了 HugePages 实现的整个流程,当然本文也只是介绍了申请内存的流程,释放内存的流程并没有分析,如果有兴趣的话可以自己查阅源码。
