巨页
背景知识
Linux中把页面作为管理内存的基本单位,一个页面为4KB。如果一个程序在运行过程中会用到很多的内存,那么必然会导致很多的TLB未命中和缺页异常的情况,因为一个页最大只能是4KB所以也就造成了这种问题的发生。如果直接修改默认的页面大小,那么又容易造成内存浪费,显然是不可取的。Linux引入了巨页(Huge page)的概念。允许一个页面的大小超过4KB,默认为2MB。
说到这里其实你也应该知道为什么默认为2MB了。 Linux的采取4级页表管理虚拟地址到物理地址的转换,32位系统中页表项占4个字节,64位中占8个字节。4级页表从高到低,依次为pgd,pud,pmd,pte,pte指向真正的4kb划分的物理页。倒数第二级为pmd,包含512个pte页表项,指向的2MB的物理页。采用大页内存划分时,内核页表为三级,基本的分页单位为2MB,此时pmd直接指向一个2MB的大页面,作为最后一级页表。
页表项有标志位_PAGE_PSE,MMU以此来区分该页表项映射的是4KB的小页内存,还是2MB的大页内存。
#define _PAGE_PSE 0x080 /* 2MB page */
在系统启动期间,你能用“大内存页”为应用程序预留一部分内存。这部分内存,即被“大内存页”占用的这些存储器永远不会被交换出内存。它会一直保留其中,除非你修改了配置。这会极大地提高像 Oracle 数据库这样的需要海量内存的应用程序的性能。
特点
大页的优点
- 减少页表大小。每个Huge Page对应的是连续的2MB物理内存,这样12GB的物理内存只需要48KB的页表,与原来的24MB相比减少很多。
- Huge Page内存只能锁定在物理内存中,不能被交换到交换区,避免了交换引起的性能影响。
- 由于页表数量的减少,使得CPU中的TLB(可理解为CPU对页表的CACHE)命中率大大提高。
- Huge Page的页表在各进程之间可以共享,也降低了Page Table的大小。
大页的缺点
- 要预先分配
- 不够灵活,需要重启主机生效
- 如果分配过多,会造成浪费,不能被其他程序使用。
查看巨页信息
grep Huge /proc/meminfo
- HugePages_Total表示大页面池的大小
- HugePages_Free表示池中尚未分配的大页面数
- HugePages_Rsvd表示保留的大页面数,这些页面尚未分配
- HugePages_Surp是“盈余”页面,如果默认配置100,现在修改为80,则会显示20
巨页配置
配置有多种方式,可以修改启动项重新启动机器,也可以修改内核参数。这里以后者为例。
修改文件
HugePages_Total 对应内核参数 vm.nr_hugepages,也可以在运行中的系统上直接修改 /proc/sys/vm/nr_hugepages,修改的结果会立即影响空闲内存 MemFree的大小,因为HugePages在内核中独立管理,只要一经定义,无论是否被使用,都不再属于free memory。如下:
修改sysctl.conf
大页面缺页中断处理函数调用流程
vm_fault_t hugetlb_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
...
if (huge_pte_none(entry)) {
ret = hugetlb_no_page(mm, vma, mapping, idx, address, ptep, flags);
goto out_mutex;
}
...
}
static vm_fault_t hugetlb_no_page(struct mm_struct *mm,
struct vm_area_struct *vma,
struct address_space *mapping, pgoff_t idx,
unsigned long address, pte_t *ptep, unsigned int flags)
{
...
page = alloc_huge_page(vma, haddr, 0);
...
}
总体的流程其实是与普通的页分配类似的。在进程访问到尚未建立虚实映射的大页面内存区域时,就会产生缺页中断,缺页中断的处理函数是do_page_fault()函数。从do_page_fault()到函数handle_mm_fault()是缺页中断处理的公共流程,不是我们关注的重点,在此不作介绍。在函数handle_mm_fault()中首先会检查产生缺页中断的内存区域是否是大页面区域,即VM_HUGETLB标志是否设置,如果是,则调用函数hugetlb_fault()进行大页面的映射,这是大页面缺页中断处理的入口函数,其处理过程大致如下:
hugetlb_fault()
- 根据产生 Page Fault 的虚拟地址查找或分配相应的 PMD 表项;
- 调用hugetlb_no_page()以分配物理内存、建立虚实映射;
- 如果引发缺页中断的内存操作是写操作,且该大页面被设置为只读,则预先做一次 Copy on Write 操作,以避免因“违规操作”再次产生 Page Fault 而影响性能。
hugetlb_no_page()
- 在产生 Page Fault 的虚存区域所映射的 hugetlb 特殊文件的页面缓存(PageCache)中查找引发中断的虚拟地址所在的文件页面,如果找到则跳转到第三部;
- 分配大页面,这是通过函数alloc_huge_page()完成的。分配成功后,将该页面加入到该 hugetlb 文件对应的 Page Cache 中,以便可以与其它进程共享该大页面。
- 设置相应的 PMD 表项,需要强调的是,为了区分大页面与 4KB 页面需要设置页表项的_PAGE_PSE标志位,使得 MMU 在进行虚实地址转换时能将此 PMD 表项作为最后一级映射,得到大页面的物理地址。
alloc_huge_page()
在前面提到,系统初始化时为每个 NUMA node 都初始化了相应的空闲大页面链表——hugepage_freelists[],并分配了全部的大页面,因此,在系统运行过程中分配大页面的操作即为从该链表中获取空闲大页面的过程。至于大页面的解除映射以及释放,与分配与建立映射的过程相反,就不多说了。
总结
巨页的出现满足了很多对于大内存有需要的进程,对于不同的情况,操作系统会选择不同的处理方式,不仅仅是固定的处理方式,采取更灵活的方法也是性能优化的目的。
参考
http://blog.chinaunix.net/uid-31410005-id-5783976.html
https://blog.csdn.net/niu91/article/details/116429354