目录
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)
- 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水位(可配置)时,应用程序的内存申请会被堵住。
-
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 -
get_page_from_freelist
会通过for_each_zone_zonelist_nodemask(zone, z, zonelist, ac->high_zoneidx,ac->nodemask) 循环检查每个zone是否有足够的空闲空间,并将获取到的zone保存到ac.classzone_idx中 -
__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)
-
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开始遍历 -
zone_watermark_fast
快速判断zone水位是否满足WMARK_LOW,根据order判断是否有足够大的空闲内存块 -
node_reclaim
对于zone内存不足的情况下,需要继续判断node_reclaim_mode的值,如果为0表示不能从本地zone中回收内存分配,只能尝试通过其它zone或其它节点分配内存;否则表示可以通过回收本地zone内存来进行分配 -
rmqueue
执行实际的分配动作,对于只申请一页的内存,则直接通过zone->pageset中分配,对于大于1页的内存,则从zone->free_area中分配,最后返回成功分配的pageblock的首页page.分配时将如下的分配策略:
首先是通过__rmqueue_smallest在当前的分配阶的迁移类型中分配,如果不满足将从更大的分配阶分配,如果仍然不满足,则将选择其它的迁移类型进行分配,选择的顺序是从最大的分配阶,之所以从最大分配阶开始的理由:
from: ULA
如果无法避免分配迁移类型不同的内存块,那么就分配一个尽可能大的内存块。如果优先选择小的内存块,则会向其它列表引入碎片,因为不同迁移类型的内存块将会混合起来
- 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