Linux内存管理:大页内存原理

背景
没啥背景,就是想清个库存…emm 真说要说背景的话,就是云计算时代来了,大页内存在服务器上的应用越来越多了。

Tips:由于内核中大页相关的feature仍有一些在开发中,所以可能随着时间的推移,文中描述将会有不对的地方(虽然本来也可能有不对的地方),本文基于linux 5.12的内核。

大页的优点:
降低TLB miss的概率:拿普通的4KB页面和2MB的大页相比,都是使用一条页表项,能cover的内存大小却差了511倍,所以更多的使用大页能大大减少系统中页表项的数量,再加上TLB cache大小固定且有限,再再加上程序访问的地址的局部性原理,TLB miss的概率就下来了。

降低walk page table的长度:由于大页的页表级数(PGD PUD PMD)比普通页面级数(PGD PUD PMD PTE)小1,所以在走表时会高效一些。(以普通页面是四级页表为例)

综上,外在的体现就是访问内存的带宽会有提升。

推荐一篇关于大页性能测评的文章:HERE

大页的种类:

  1. 静态大页(persistent hugepage),通过用户自行控制它的分配、释放、使用。 关于释放:早期的内核在启动阶段预分配大页是不支持释放的,当前内核(5.12)已经支持了。

  2. 透明大页(transparent hugepage),由系统自己控制透明大页的分配、释放、使用。若用户开启透明大页功能,系统会在后台运行一个khugepaged的内核线程扫描系统内存,将合适的内存合并成为大页,用户无感。

本文主要介绍静态大页:后面有时间的话会单独写一篇透明大页的 (待填坑)。

静态大页的使用:
方式一:通过在bootargs传参在系统启动过程中预留大页。

bootargs参数:预分配大页数量hugepages= 和 预分配大页的大小hugepagesz= ,更详细的使用可参看内核文档kernel-parameters.txt

方法一相对于方法二的优点是开机时就通过bootmem分配大页,不存在因为内存碎片导致分不出大页的情况,从而保证预留的成功性。

方式二:通过sysfs下的文件节点申请和释放大页

echo 5 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

含义:保持系统中2MB的大页有5个。若已经存在5个大页则什么都不做;若少于5个则分配够5个;若多于5个则释放多余的大页(前提是未被使用)。

echo 5 > /sys/devices/system/node/node[0-9]*/hugepages/hugepages-2048kB/nr_hugepages

含义:在指定的numa node上分配5个指定大小(2MB)的大页。

cat /sys/kernel/mm/hugepages/hugepages-2048kB/free_hugepages

含义:查看系统中空闲的的2MB大页的数量

编程中使用静态大页:
通过mmap接口申请内存时在flag中添加MAP_HUGETLB,并在参数len中设置页面的大小。

透明大页的使用:
通过/sys/kernel/mm/transparent_hugepage下的节点控制THP的开关、积极程度等行为。

详见官方文档:HERE

相关概念澄清:
本文首先假设读者对内存管理源码有一定的认识,接下来我们澄清几个概念:

大页内存池:在内核中有一个全局的大页内存池,它管理着系统上所有节点上的、各种大小的、已使用的、未使用的大页。当池中的页面不够时,则会通过系统分配大页填充池子,当池中的页面盈余(盈余的概念见我下面对surplus_huge_pages的注释)时,则会从池子中释放一部分的空闲页面。这个大页内存池结构如下:

struct hstate {
        int next_nid_to_alloc;
        int next_nid_to_free;
        unsigned int order;     
        unsigned long mask;
        unsigned long max_huge_pages;   
        //系统中全部的大页数量,包含surplus页面,sysfs下同名节点就是控制它的
        unsigned long nr_huge_pages;   
        //系统中未被使用的大页数量,也包含resv_huge_pages的页面
        unsigned long free_huge_pages;
        //应用程序已经mmap的大页数量,未读写,即未分配实际物理页。但是这些pages也已经不可被别人用了。
        unsigned long resv_huge_pages;	
        /*系统中超过nr_huge_pages的大页数量,可以超分的数量被/proc/sys/vm/nr_overcommit_hugepages控制,
        若nr_huge_pages被改小了,但是页面仍in-use,结果就是nr_huge_pages不变,而增加surplus_huge_pages
        的数量,以至于shrink的时候会根据surplus_huge_pages释放已经不用的页面。*/
        unsigned long surplus_huge_pages;											
    	//它控制着surplus_huge_pages的最大值。
        unsigned long nr_overcommit_huge_pages;	
        struct list_head hugepage_activelist;
        struct list_head hugepage_freelists[MAX_NUMNODES];
        unsigned int nr_huge_pages_node[MAX_NUMNODES];
        unsigned int free_huge_pages_node[MAX_NUMNODES];
        unsigned int surplus_huge_pages_node[MAX_NUMNODES];
        char name[HSTATE_NAME_LEN];
};

注意:对照sysfs下大页相关的文件节点,你会发现许多与大页内存池数据结构成员同名。

复合页面(compound page):复合页面是由两个及以上的连续普通页面组成,与伙伴系统分配出的order-N的连续页面相比,复合页面的meta data不太一样。复合页面的第一个页面称之为head page,剩下的pages称为tail page。tail page中的第一个page的struct page又和剩下的tail page中的元数据不太一样,但是所有的tail page的struct page中的mapping指针都会指向head page的struct page,这样就可以通过任何一个tail page找到head page了。具体细节读者可以查看内核源码中struct page里的compound page部分的定义(这块新内核中也不时会有扩充和改动)。

huge page 与 gigantic page的区别:页面大小小于MAX_ORDER的大页称之为huge page,大于等于MAX_ORDER的大页称之为gigantic page,拿常用的举例,2MB是huge page,1GB是gigantic page。huge page和gigantic page走的是不同的分配路径。huge page相对较小,所以通过伙伴系统来分配(注意:虽然走伙伴系统,但最终它的元数据设置和普通伙伴系统页面的设置是分开处理的),而gigantic page页面较大,伙伴系统无法满足分配要求,所以通过连续内存分配接口alloc_contig_range()(没错,CMA内存也走这里)

大页内存的状态: head page的page.private中存放了当前大页的状态。共四种状态,如下:

HPG_restore_reserve //当大页被用户用mmap申请了,但并未实际使用前,会处于此状态
HPG_migratable      //若此大页支持迁移,分配出来时会被设置此标志
HPG_temporary       //临时从伙伴系统中分配出的大页,典型的应用是作为migration的target page。
                    //后文释放大页的流程中会对它进行判断。
HPG_freed           //当页面为空闲是会被挂载free list上,同时设置此flag

大页内存的分配:
前面我们说到,静态大页的分配方式有两种,本文我们只介绍通过sysfs分配大页的流程,因为笔者个人觉得以sysfs的方式现在的灵活性完全可以取代bootargs的方式。至于sysfs可能由于内存碎片导致大页分配失败的问题,可以通过在系统启动后立马通过脚本去预留,那时是不存在内存碎片问题的。

比方现在系统中有4个2MB的大页,通过下面的命令再去增加1个2MB的大页
命令:echo 5 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

内核中的执行则是:nr_hugepages_store ->__nr_hugepages_store_common ->set_max_huge_pages

接下来看一下核心实现set_max_huge_pages():

set_max_huge_pages
  ->alloc_pool_huge_page      //若大页内存池中大页不够,则再分配一个新的大页放入大页内存池中。
    ->alloc_fresh_huge_page   //在指定的node上分配一个新的大页
      ->alloc_gigantic_page   //1GB大页走这里。基于CONTIG_ALLOC或CMA内存分配
      ->alloc_buddy_huge_page //2MB大页走这里。基于伙伴系统分配
      ->prep_compound_gigantic_page //元数据初始化。若是gigantic页,设置first tail_page的order,并设置所有tail_page指向head_page.
      ->prep_new_huge_page          //元数据初始化。1.将page[0]的lru指向自己 2.设置hugetlb的析构函数类型 3.设置page[2]和page[3]的相关cgroup成员private变量
      				    //3.拿大页内存池锁hugetlb_lock,并更新hstate中页面numbers 以及 设置page[0]的private的状态标志HPG_freed 

下面是具体huge page和gigantic page分配细节:

alloc_gigantic_page   //1G巨页
  ->cma_alloc                  //新加进来的feature,通过预留hugetlb_cma内存用以分配巨页。
    ->__alloc_contig_pages     //形参MIGRATE_CMA.
  ->alloc_contig_pages         //若未开启CONFIG_CMA或hugetlb_cma中的内存用完了则走这里。
    ->1.根据zonelist逐个在zone上逐个取1GB的pfn range.(注意两点:1先在当前zonelist上找,找不到再根据nodemask换zonelist继续找;2 migrate_movable的fallback不包含migrate_cma)
    ->2.找contig page需要在zone上1GB对齐,然后通过pfn_range_valid_contig()检查pfn range中的page是否可用,不可用换下1GB pageblock。
    ->3.__alloc_contig_pages   //形参MIGRATE_MOVABLE. 原理和memory compaction一样。详见下文大图
  
alloc_buddy_huge_page  //2MB巨页
  ->__alloc_pages_nodemask
    ->get_page_from_freelist
      ->rmqueue
      ->prep_new_page             
        ->post_alloc_hook    //页面基本信息的初始化。第一个页面的refcount初始化为1,private初始化为0。若设置了DEBUG_PAGEALLOC,则检查page的poison并unpoison
        ->prep_compound_page //元数据初始化。第一个page设置PG_head,refcount保持1. tail page的compound_head指向head并bit0置1,refcount重置为0. 
        		     //设置first tail的compound_dtor,设置first tail的compound_order,设置first tail的compound_nr的页面总数
        

以上我们可以看到huge page的分配走的是伙伴系统(参考Linux内存管理:伙伴系统实现一文)。gigantic page走的是连续内存分配__alloc_contig_pages()。

__alloc_contig_pages()很重要,涉及的知识点也较多,这里画了一幅图来说明它的大体流程:

在这里插入图片描述
步骤1:将目标内存块(内存块的大小是pageblock整数倍,你懂的,因为内存的迁移属性是按pageblock划分的)的迁移属性标记为MIGRATE_ISOLATED,这样被标记的内存将不会被伙伴系统分配出去,方便我们后续的迁移工作。

步骤2:将PCP(per-cpu pages)的内存池清空,即将页面归还给伙伴系统,并临时关闭了pcp功能。(mm, page_alloc: disable pcplists during memory offline)

步骤3:将准备添加到LRU链表上,却还未加入LRU的页面(即还待在pagevec中的页面)添加到LRU上(后面补篇LRU的文章,待填坑)。这是为了后面migrate_page()迁移做准备,因为migrate_page()需要从LRU链表上摘取待迁移的页面,并isolate(此isolate非迁移属性的isolate)

步骤4:调用migrate_pages(),将已经有内容的页面(in-use)迁移出去。(参考之前的文章Linux内存管理:页面迁移)

步骤5:跟步骤1反着来,恢复内存块的迁移属性。至此,我们获得了一块干净且连续的内存。

大页内存的释放:
接口:put_page()

此接口既可以用来释放order-0的页面,也可以用来释放各种复合页面,包括普通复合页面、大页、透明大页。

原理:回看分配大页的流程中,在设置大页元数据时prep_new_huge_page()->set_compound_page_dtor()会去初始化大页的析构函数。而在put_page()中,若大页的refcount==0时,则会去调用它的析构函数。

实现:对大页中的任何一个页面执行put_page()最后都是对page[0](即head page)的refcount减1,当refcount为0时会去调用大页的析构函数去释放大页。

put_page
  __put_page  		    //若此次减1后refcount为0,则接着下面的步骤
    __put_single_page	    //普通的order-0页面走这里
    __put_compound_page	    //复合页面走这里
      __page_cache_release  //若是THP的话先做处理下它的page[0].lru,普通大页不需要
      destroy_compound_page //调用复合页面的对应的析构函数page[1].compound_dtor。内存回收时也可能调析构函数用来回收compound page
        free_compound_page  //普通复合页的释放
        free_huge_page      //静态大页的释放,接下来详细介绍它
        free_transhuge_page //透明大页的释放

核心实现:

由于put_page()可以在中断中调用,我们知道中断中是不允许特别耗时的操作的,而释放一个可能达到若干GB的大页来说,当然不会很迅速。于是free_huge_page()中就添加了在中断里释放复合页面的额外处理,defer到延时工作队列里去释放。判断如下:

free_huge_page
  if !in_task
	schedule_work(&free_hpage_work)  //中断中走workqueue推迟释放
           __free_huge_page              //workqueue最终调__free_huge_page
  else
    __free_huge_page                     //进程上下文的话,直接调__free_huge_page

__free_huge_page()的流程 :

__free_huge_page
  hugetlb_set_page_subpool
  page->mapping = NULL
  ClearHPageRestoreReserve
  hugepage_subpool_put_pages
  ClearHPageMigratable
  hugetlb_cgroup_uncharge_page
  hugetlb_cgroup_uncharge_page_rsvd
  if 情况1:HPageTemporary      //该hugepage是temporary状态
    list_del(&page->lru)       //将该hugepage从freelist或activelist上直接移除
    update_and_free_page       //释放该hugepage,详见下文
  else if 情况2:surplus_huge_pages_node //当前node上有surplus不为零
    list_del(&page->lru)                //将该hugepage从freelist或activelist上直接移除
    update_and_free_page                //释放该hugepage,详见下文
    h->surplus_huge_pages--		//减少hstate中surplus的页面和对应node的surplus的页面数量
    h->surplus_huge_pages_node[nid]--
  else 情况3:hstate池中的page总数无需改变 
    enqueue_huge_page         //将页面加入大页内存池的free list上,并设置页面状态为HPG_freed
  
  
  update_and_free_page          //注意:此时已经持有hugetlb_lock锁
    hstate                      //更新页面数量
    set_compound_page_dtor      //清空析构函数
    set_page_refcounted		//要被释放head page的refcount为0,此时置1用来后续释放页面使用
    if gigantic_page
      destroy_compound_gigantic_page //设置所有的tail page的refcount为1,释放时使用;清空head page和tail page的成员
      free_gigantic_page             //逐页的释放掉所有的page
    else buddy_page
  	  __free_pages   //huge page的话直接走buddy释放。Tips:普通的order>0的伙伴系统页面也走这里释放:free_pages->__free_pages



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值