内存管理基础学习笔记 - 2. 内核地址空间 - 伙伴系统

1. 前言

本专题我们开始学习内存管理部分,本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。
主要关注内核地址空间的管理部分,主要包括buddy管理,slab,以及非连续物理内存分配vmalloc。
本文开始主要记录伙伴系统分配/释放内存的过程,主要讲述buddy分配内存和释放内存的过程,也就是函数alloc_pages/free_pages的执行过程,此处主要以 GFP_KERNEL分配掩码为例分析分配过程

kernel版本:5.10
平台:arm64

2. 分配掩码

分配掩码是描述页面分配方法的标志,它影响页面分配的整个流程。修饰符在Linux 4.4被重新归类,大致分为如下:

  • zone modifier
  • mobility and placement modifier
  • watermark modifier
  • page reclaim modifier
  • action modifier
    由于以上标志繁多,使用困难,内核定义了一些常用标志的组合来方便使用:
    可参考:[https://blog.csdn.net/weixin_45264425/article/details/129327661]
/*调用者不能睡眠且保证分配成功,可访问系统预留内存*/
#define GFP_ATOMIC      (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
/*内核分配内存常用标志之一。分配过程可能睡眠*/
#define GFP_KERNEL      (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
/*分配中不允许睡眠等待*/
#define GFP_NOWAIT      (__GFP_KSWAPD_RECLAIM)
/*不需要启动任何IO操作*/
#define GFP_NOIO        (__GFP_RECLAIM)
/*不会访问任何文件系统的操作*/
#define GFP_NOFS        (__GFP_RECLAIM | __GFP_IO)
/*通常用户空间的进程分配内存。这些内存可被内核或硬件使用,如DMA缓存映射到用户空间*/
#define GFP_USER        (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA         __GFP_DMA
#define GFP_DMA32       __GFP_DMA32
/*
 * 用户空间进程分配内存,优先使用ZONE_HIGHMEM,这些内存可映射到用户空间,内核不会访问,
 * 这些内存不能迁移
 */
#define GFP_HIGHUSER    (GFP_USER | __GFP_HIGHMEM)
/*类似于GFP_HIGHUSER,但页面可以迁移*/
#define GFP_HIGHUSER_MOVABLE    (GFP_HIGHUSER | __GFP_MOVABLE)
#define GFP_TRANSHUGE_LIGHT     ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
__GFP_NOMEMALLOC | __GFP_NOWARN) & ~__GFP_RECLAIM)
/*用于透明页面分配*/
#define GFP_TRANSHUGE   (GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM)

3. alloc_pages

alloc_pages是伙伴系统核心的分配函数,由内核启动部分的分析可知,buddy的内存来源于memblock,它是连续的物理内存,映射到内核的线性映射区,因此伙伴系统分配出来的是连续的物理内存。
在alloc_pages基础上分别封装出如下函数:

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
分配内存,返回所分配内存的内核空间虚拟地址

unsigned long get_zeroed_page(gfp_t gfp_mask)
返回一个全填充为0的页面

#define __get_free_page(gfp_mask)  __get_free_pages((gfp_mask),0) 
分配一页内存,返回所分配内存的内核空间虚拟地址

#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
分配一页内存,返回所分配内存的内核空间虚拟地址

下面主要以GFP_KERNEL为例说明alloc_pages的执行过程

alloc_pages(gfp_t gfp_mask, unsigned int order)
    |--alloc_pages_current(gfp_mask, order)
           |--page = __alloc_pages_nodemask(gfp, order,...)
                  |--unsigned int alloc_flags = ALLOC_WMARK_LOW;
                  |--prepare_alloc_pages(gfp_mask, order, preferred_nid, 
                  |            nodemask, &ac, &alloc_mask, &alloc_flags)
                  |      \--初始化ac
                  |--alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp_mask)
                  |--page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac)
                  |--if (likely(page))
                  |      goto out;
                  |--page = __alloc_pages_slowpath(alloc_mask, order, &ac)     
  1. alloc_flags
    初始化为ALLOC_WMARK_LOW,即允许内存分配的条件为低水位,关于分配水位的宏定义如下:
    / The ALLOC_WMARK bits are used as an index to zone->watermark /
    #define ALLOC_WMARK_MIN WMARK_MIN
    #define ALLOC_WMARK_LOW WMARK_LOW
    #define ALLOC_WMARK_HIGH WMARK_HIGH

注:关于各个节点中zone水位的设置是在postcore_initcall(init_per_zone_wmark_min)中完成的。zone水位包括high, low, min,当发现一旦内存达到low水位时就唤醒后台进程kswapd开始回收,直到回收到high水位kswapd才会结束,当触到min水位(可配置)时,应用程序的内存申请会被堵住。

  1. prepare_alloc_pages
    主要是初始化ac,这里的ac是alloc_context结构体,它记录了伙伴系统分配内存的参数。
    ac->highest_zoneidx = gfp_zone(gfp_mask);
    从掩码中计算出zone的zoneidx,放在ac.high_zoneidx中, high_zoneidx就是允许内存分配的最高zone
    ac->zonelist = node_zonelist(preferred_nid, gfp_mask);
    获取首选内存节点对应的zonelist,zonelist分为两类:ZONELIT_FALLBACK表示本地,ZONELIST_NOFALLBACK表示远端
    ac->migratetype = gfp_migratetype(gfp_mask)
    从掩码中获取MIGRATE_TYPES的类型,保存到ac.migratetype中,对于GFP_KERNEL类型为MIGRATE_UNMOVABLE
    ac->preferred_zoneref = first_zones_zonelist(ac->zonelist, ac->highest_zoneidx, ac->nodemask);
    获取最倾向于分配的zone的zoneref,初始化ac->preferred_zoneref

  2. get_page_from_freelist
    会通过for_each_zone_zonelist_nodemask(zone, z, zonelist, ac->high_zoneidx,ac->nodemask) 循环检查每个zone是否有足够的空闲空间,并将获取到的zone保存到ac.classzone_idx中

  3. __alloc_pages_slowpath
    如果get_page_from_freelist能够获取到内存则会退出,否则将进入慢速分配路径,期间会涉及到IO写回,内存回收等

|- -get_page_from_freelist

get_page_from_freelist(alloc_mask, order, alloc_flags, &ac)
    |--for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx,ac->nodemask)
          mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);
          if (!zone_watermark_fast(zone, order, mark,...)
              node_reclaim(zone->zone_pgdat, gfp_mask, order)
          page = rmqueue(ac->preferred_zoneref->zone, zone, order,
                                  gfp_mask, alloc_flags, ac->migratetype)
          prep_new_page(page, order, gfp_mask, alloc_flags)
              |--post_alloc_hook(page, order, gfp_flags) 
                     |--set_page_refcounted(page)     
  1. for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx,ac->nodemask)
    first_zones_zonelist(zlist, highidx, nodemask, &zone)从给定zoneidx开始查找,就是前面的ac->preferred_zoneref指向的zone,循环遍历每一个zone, 注意:
    (1)遍历的过程是从高端的zone开始到低端zone
    举例如下,对于只包含dma32 zone和normal zone,则遍历顺位为:
    ZONE_NORMAL _zonerefs[0]->zone_idx=1
    ZONE_DMA32 _zonerefs[1]->zone_idx=0
    从上面可以看出_zonerefs数组的下标与zone的id正好相反,高端zone位于低下标,下标的顺序代表了分配代价从低廉到昂贵。
    (2)另外遍历时不是遍历所有的zone,而是从ac->preferred_zoneref开始遍历

  2. zone_watermark_fast
    快速判断zone水位是否满足WMARK_LOW,根据order判断是否有足够大的空闲内存块

  3. node_reclaim
    对于zone内存不足的情况下,需要继续判断node_reclaim_mode的值,如果为0表示不能从本地zone中回收内存分配,只能尝试通过其它zone或其它节点分配内存;否则表示可以通过回收本地zone内存来进行分配

  4. rmqueue
    执行实际的分配动作,对于只申请一页的内存,则直接通过zone->pageset中分配,对于大于1页的内存,则从zone->free_area中分配,最后返回成功分配的pageblock的首页page.分配时将如下的分配策略:
    首先是通过__rmqueue_smallest在当前的分配阶的迁移类型中分配,如果不满足将从更大的分配阶分配,如果仍然不满足,则将选择其它的迁移类型进行分配,选择的顺序是从最大的分配阶,之所以从最大分配阶开始的理由:

from: ULA
如果无法避免分配迁移类型不同的内存块,那么就分配一个尽可能大的内存块。如果优先选择小的内存块,则会向其它列表引入碎片,因为不同迁移类型的内存块将会混合起来

  1. prep_new_page
    set_page_refcounted会设置page->_refcount为1

|- - -rmqueue

rmqueue
    |--do {
	        if (order > 0 && alloc_flags & ALLOC_HARDER) 
	            page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC)
	        if (!page)
	            page = __rmqueue(zone, order, migratetype, alloc_flags)
	              |--if(!__rmqueue_smallest(zone, order, migratetype))
	                    if (alloc_flags & ALLOC_CMA)
	                       if(!__rmqueue_cma_fallback(zone, order))
	                            __rmqueue_fallback(zone, order, migratetype,...)
	    } while (page && check_new_pages(page, order));

__rmqueue_smallest从相同迁移类型的更高阶分配;

__rmqueue_fallback从不同迁移类型的最高阶开始分配

check_new_pages检查新分配的page block的所有page,新分配page的_mapcount为0,_refcount为1

4. free_pages

free_pages
    |-- __free_pages(virt_to_page((void *)addr), order)
            |--if (put_page_testzero(page))
            |       free_the_page(page, order)
            |--else if (!PageHead(page))
                   while (order-- > 0)
                       free_the_page(page + (1 << order), order);            

|- -free_the_page

free_the_page
   |--if (order == 0)
   |      free_unref_page(page);//Free a 0-order page 
   |          |--unsigned long pfn = page_to_pfn(page)
   |--else
          __free_pages_ok(page, order, FPI_NONE) 
              |--unsigned long pfn = page_to_pfn(page)
              |--free_pages_prepare(page, order, true)
              |--migratetype = get_pfnblock_migratetype(page, pfn)
              |--free_one_page(page_zone(page), page, pfn, order, migratetype,fpi_flags)

|- - -free_unref_page

free_unref_page(page)
    |--unsigned long pfn = page_to_pfn(page)
    |--free_unref_page_prepare(page, pfn)
           |--free_pcp_prepare(page)
           |--migratetype = get_pfnblock_migratetype(page, pfn)
           |--set_pcppage_migratetype(page, migratetype)
    |--free_unref_page_commit(page, pfn)
           |--migratetype = get_pcppage_migratetype(page)
           |--list_add(&page->lru, &pcp->lists[migratetype]) //释放页面加入到pcp
           |--pcp->count++
           |--if (pcp->count >= pcp->high)//pcp页面数量超过high将释放到buddy
                  free_pcppages_bulk(zone, batch, pcp)

free_unref_page释放单个页面到pcp,如果pcp页面数量超过high将释放到buddy,一次释放batch个

|- - -__free_one_page

free_one_page
    |--__free_one_page(page, pfn, zone, order, migratetype, fpi_flags)
           |--max_order = min_t(unsigned int, MAX_ORDER, pageblock_order + 1)
           |--while (order < max_order - 1)
           |       buddy_pfn = __find_buddy_pfn(pfn, order)
           |       buddy = page + (buddy_pfn - pfn)
           |       if (!page_is_buddy(page, buddy, order))
           |           goto done_merging
           |       combined_pfn = buddy_pfn & pfn//combined_pfn为合并后的页帧号
           |       page = page + (combined_pfn - pfn)
           |       pfn = combined_pfn
           |       order++
done_merging:
                  set_buddy_order(page, order)//设置合并后的pageblock新的order
                  to_tail = buddy_merge_likely(pfn, buddy_pfn, page, order)//判断插入位置
                  if (to_tail)
                      add_to_free_list_tail(page, zone, order, migratetype); 
                  else
                      add_to_free_list(page, zone, order, migratetype)              
                  

__find_buddy_pfn计算与pfn邻近的阶数为order的pageblock的首页帧号保存到buddy_pfn,计算其对应的page地址保存到buddy,此处的pfn为要释放的pageblock的首页帧号

page_is_buddy用来判断邻近的pageblock是否与待释放的pageblock为伙伴关系(包括是否在同一个zone,是否order相同等),如果是伙伴关系,则记录合并后的首页帧号为combined_pfn(为pfn对应的页帧号),尝试与更高阶的pageblock合并,一旦page_is_buddy检查后不是伙伴关系,则跳转到done_merging,将之前检测到可以合并的pageblock执行合并,此时pfn保存了可合并的新的pageblock的首页帧,page为新的pageblock的首个page

buddy_merge_likely会判断新的pageblock的插入位置,插入位置如何计算(TODO)

参考文档

1.《奔跑吧,Linux内核》
2.ULA

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值