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

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

 [[397163]]

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

本文使用 Linux 内核 2.6.23 版本

HugePages分配器初始化

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

  1. static int __init hugetlb_init(void) 
  2.     unsigned long i; 
  3.  
  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.  
  10.     // 2. max_huge_pages 为系统能够使用的大页内存的数量, 
  11.     //    由系统启动项 hugepages 指定, 
  12.     //    这里主要申请大内存页, 并且保存到 hugepage_freelists 链表中. 
  13.     for (i = 0; i < max_huge_pages; ++i) { 
  14.         if (!alloc_fresh_huge_page()) 
  15.             break; 
  16.     } 
  17.  
  18.     max_huge_pages = free_huge_pages = nr_huge_pages = i; 
  19.  
  20.     return 0; 

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

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

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

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

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

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

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

  1. static void free_huge_page(struct page *page) 
  2.     ... 
  3.     enqueue_huge_page(page);     // 把大内存页放置到空闲大内存页链表中 
  4.     ... 

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

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

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

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

所以,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. const struct file_operations hugetlbfs_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. static int  
  2. hugetlbfs_file_mmap(struct file *file, struct vm_area_struct *vma) 
  3.     ... 
  4.     vma->vm_flags |= VM_HUGETLB | VM_RESERVED; // 为虚拟内存分区添加 VM_HUGETLB 标志位 
  5.     ... 
  6.     return ret; 

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

虚拟内存与物理内存映射

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

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

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

  1. asmlinkage void 
  2. __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code) 
  3.     ... 
  4.     struct mm_struct *mm; 
  5.     struct vm_area_struct *vma; 
  6.     unsigned long address; 
  7.     ... 
  8.  
  9.     mm = tsk->mm;         // 1. 获取当前进程对应的内存管理对象 
  10.     address = read_cr2(); // 2. 获取触发缺页异常的虚拟内存地址 
  11.  
  12.     ... 
  13.     vma = find_vma(mm, address); // 3. 通过虚拟内存地址获取对应的虚拟内存分区对象 
  14.     ... 
  15.  
  16.     // 4. 调用 handle_mm_fault 函数对异常进行修复 
  17.     fault = handle_mm_fault(mm, vma, address, write); 
  18.     ... 
  19.  
  20.     return

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

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

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

  1. int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma, 
  2.                     unsigned long address, int write_access) 
  3.     ... 
  4.     if (unlikely(is_vm_hugetlb_page(vma))) // 虚拟内存分区是否需要使用 HugePages 
  5.         return hugetlb_fault(mm, vma, address, write_access); // 如果使用 HugePages, 就调用 hugetlb_fault 进行处理 
  6.     ... 

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

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

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

  1. int hugetlb_fault(struct mm_struct *mm, struct vm_area_struct *vma, 
  2.                   unsigned long address, int write_access) 
  3.     pte_t *ptep; 
  4.     pte_t entry; 
  5.     int ret; 
  6.  
  7.     ptep = huge_pte_alloc(mm, address); // 1. 找到虚拟内存地址对应的页中间目录项 
  8.     ... 
  9.     entry = *ptep; 
  10.  
  11.     if (pte_none(entry)) { // 如果页中间目录项还没进行映射 
  12.         // 2. 那么调用 hugetlb_no_page 函数进行映射操作 
  13.         ret = hugetlb_no_page(mm, vma, address, ptep, write_access); 
  14.         ... 
  15.         return ret; 
  16.     } 
  17.     ... 

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

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

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

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

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

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

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

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

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

总结

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

责任编辑:武晓燕 来源: Linux内核那些事
相关推荐

2021-04-30 19:53:53

HugePages大内存页物理

2018-03-16 14:04:45

Linux大内存页虚拟内存管理

2010-09-25 09:56:46

JVM最大内存

2021-06-21 08:58:14

MySQL数据库Pages

2021-09-05 06:00:47

电脑内存Windows

2010-04-23 11:18:05

Ubuntu 10.0

2009-10-23 12:37:52

无线接入技术

2011-03-17 16:51:51

SQLServer数据加速剂

2021-11-05 16:33:32

手机内存技术

2013-06-04 17:23:55

Android开发移动开发Fragment

2020-04-07 11:25:24

JDK14内存API

2014-07-30 11:12:09

block

2024-08-02 16:25:10

2009-10-21 17:32:30

综合布线注意事项

2018-04-24 15:11:43

大内存SSD戴尔

2015-03-30 11:18:50

内存管理Android

2011-09-07 10:24:01

SQL Server镜像

2009-10-28 18:43:34

接入网技术

2009-11-02 17:59:51

2023-06-26 07:11:01

笔记本内存办公
点赞
收藏

51CTO技术栈公众号