memblock代码分析
Table of Contents
在伙伴系统还未就绪前,通过memblock提供一个简单的内存分配功能,本文的分析对象就是memblock.
1. 结构体介绍
先引入几个结构体,这些结构体是由上到下的关系.
首先是memblock_type:
struct memblock { bool bottom_up; /* is bottom up direction? */ phys_addr_t current_limit; struct memblock_type memory; struct memblock_type reserved; };
bottom_up控制着分配方向,地址是从小到大,还是从大到小.current_limit控制着可以分配的最大地址.而memblock_type是下一级结构体:
struct memblock_type { unsigned long cnt; unsigned long max; phys_addr_t total_size; struct memblock_region *regions; char *name; };
各成员解释如下:
- cnt:针对regions数组,当前里面有多少个memblock_region.
- max:针对regions数组,其最大的容量.
- total_size:对于regions里的所有memblock_region,其size的和大小.
- name:memblock_tye的字符串名字,比如memory或者reserved.
最后一层的结构体就是memblock_region:
struct memblock_region { phys_addr_t base; phys_addr_t size; enum memblock_flags flags; #ifdef CONFIG_NUMA int nid; #endif };
各成员意义如下:
- base:该region所管理地址范围的起始地址.
- size:该region所管理地址范围的大小.
- flags:该region的属性,比如没有特别属性,热插拔等.
- nid:该region的内存所属哪个NUMA结点.
在memblock.c文件中,全局的定义了memblock类型的实例,其也叫memblock:
struct memblock memblock __initdata_memblock = { .memory.regions = memblock_memory_init_regions, .memory.cnt = 1, /* empty dummy entry */ .memory.max = INIT_MEMBLOCK_MEMORY_REGIONS, .memory.name = "memory", .reserved.regions = memblock_reserved_init_regions, .reserved.cnt = 1, /* empty dummy entry */ .reserved.max = INIT_MEMBLOCK_RESERVED_REGIONS, .reserved.name = "reserved", .bottom_up = false, .current_limit = MEMBLOCK_ALLOC_ANYWHERE, };
可以看到,memory和reserved里的regions分别为memblock_memory_init_regions和memblock_reserved_init_regions,这两个数组是提前定义好的:
static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_MEMORY_REGIONS] __initdata_memblock; static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_RESERVED_REGIONS] __initdata_memblock;
而INIT_MEMBLOCK_MEMORY_REGIONS和INIT_MEMBLOCK_RESERVED_REGIONS的大小都为128. __initdata_memblock修饰符号根据是否定义了CONFIG_ARCH_KEEP_MEMBLOCK,来决定是否将这些数据结构放入.meminit.data节中,从而在memblock将控制权转交给伙伴系统后释放这些结构体.
后续的添加region和分配内存主要就是操作regions.
2. 添加region
以memblock_add函数为例,来分析一个region添加到memblock.memory.regions的过程:
int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size) { phys_addr_t end = base + size - 1; memblock_dbg("%s: [%pa-%pa] %pS\n", __func__, &base, &end, (void *)_RET_IP_); return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0); }
这个函数比较简单,就是计算了end位置在哪,然后根据配置,memblock_dbg会决定是否打印调试信息,关于这点在这篇文章有详细介绍.随后以&memblock.memory参数调用了memblock_add_range函数,就是说这段region要添加到memory这个memblock_type的regions中去,下面继续分析memblock_add_range,以下分几种情况来说明添加过程:
regions数组为空:
这种情况没有什么特别的处理,直接往regions里的第一个成员赋值就行:
static int __init_memblock memblock_add_range(struct memblock_type *type, phys_addr_t base, phys_addr_t size, int nid, enum memblock_flags flags) { bool insert = false; phys_addr_t obase = base; phys_addr_t end = base + memblock_cap_size(base, &size); int idx, nr_new, start_rgn = -1, end_rgn; struct memblock_region *rgn; if (!size) return 0; /* special case for empty array */ if (type->regions[0].size == 0) { WARN_ON(type->cnt != 1 || type->total_size); type->regions[0].base = base; type->regions[0].size = size; type->regions[0].flags = flags; memblock_set_region_node(&type->regions[0], nid); type->total_size = size; return 0; } }
可以看到,判断regions的第一个region的size为0,就代表还没有任何region被添加到regions里,这时直接添加就行.用0可以判断是因为前面定义memblock_region时都是static类型的.
该类情况就是regions里先前已经有region了,这下面又分为三类,分别分析.首先按下图将这三种情况区分出来:
base end base end base end |--case1--| |--case2--| |--case3--| --------- --------- --------- ----------------------------------------------------- | | rbase rend
解释下此图的意义,首先base-end之间是一个要插入的region,叫做new-rgn,而rbase-rend是原来regions中的一个region的范围,叫做old-rgn.那么new-rgn和old-rgn如图所示有三种位置关系,case1是new-rgn完全小于old-rgn,也就是rbase >= end.case3是new-rgn完全大于old-rgn,也就是rend <= base.case2就是new-rgn卡在rbase之间.下面分析代码:
for_each_memblock_type(idx, type, rgn) { phys_addr_t rbase = rgn->base; phys_addr_t rend = rbase + rgn->size; if (rbase >= end) break; if (rend <= base) continue; /* * @rgn overlaps. If it separates the lower part of new * area, insert that portion. */ if (rbase > base) { #ifdef CONFIG_NUMA WARN_ON(nid != memblock_get_region_node(rgn)); #endif WARN_ON(flags != rgn->flags); nr_new++; if (insert) { if (start_rgn == -1) start_rgn = idx; end_rgn = idx + 1; memblock_insert_region(type, idx++, base, rbase - base, nid, flags); } } /* area below @rend is dealt with, forget about it */ base = min(rend, end); } /* insert the remaining portion */ if (base < end) { nr_new++; if (insert) { if (start_rgn == -1) start_rgn = idx; end_rgn = idx + 1; memblock_insert_region(type, idx, base, end - base, nid, flags); } }
对于case1,直接跳出循环了,跳出循环后的代码会调用memblock_insert_region插入region,这里遇到了memblock_insert_region,就先分析下这个函数然后再继续分析剩余的两个case,因为不论哪种case,都是通过memblock_insert_region来真正的插入region.
static void __init_memblock memblock_insert_region(struct memblock_type *type, int idx, phys_addr_t base, phys_addr_t size, int nid, enum memblock_flags flags) { struct memblock_region *rgn = &type->regions[idx]; BUG_ON(type->cnt >= type->max); memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn)); rgn->base = base; rgn->size = size; rgn->flags = flags; memblock_set_region_node(rgn, nid); type->cnt++; type->total_size += size; }
参数idx就是这个新region要插入的位置,从regions数组里取出这个memblock_region为rgn.首先使用memove将自rgn起始的memblock_region都往后挪一个位置,以给新的要插入的rgn在idx处腾开位置.后面的几条语句就是执行插入动作了,作用都简单明了.
回过头来继续分析剩下的两种case,先看case 3,当要插入的region完全大于当前循环遍历的region,也即rend <= base为case 3时,则继续循环遍历下一个rgn,查看for_each_memblock_type的定义知道:
#define for_each_memblock_type(i, memblock_type, rgn) \ for (i = 0, rgn = &memblock_type->regions[0]; \ i < memblock_type->cnt; \ i++, rgn = &memblock_type->regions[i])
会更新rgn为下一个,并且idx作为新region的插入位置,也会自增,如果所有已有的region都比新的要插入的region小,那么就意味着在regions数组的末尾插入这个新的region.
最后就是rbase > base的case 2,这种情况稍微复杂,通过调用memblock_insert_region的参数知道,该情况下,也会新插入一个region,但是它和传进来的base到base+size范围略有不同,而是从base到rbase.而若end在rbase到rend之间的话,那么rbase到end之间的范围就依旧含在当前遍历的old-rgn里,但是还有另外一种情况,那就是end跨过了rend,也就是end >= rend,如下图:
base end |------------case2-------------| ------------------------------ ----------------------------------------------------- | | rbase rend
该情况除了base到rbase之间新插入一个region,而在rend到end之间还会诞生一个region,这就是注释insert the remaining portion所表达的情况,只是注意下,这时的base已经在for_each_memblock_type的最后被取为rend和end的最小者.
除了通过memblock_add向memblock.memory添加region,另有一个接口memblock_reserve,会向memblock.reserved添加region,与memblock_add唯一的不同就是其第一个参数是memblock.reserved,后续分配内存以及伙伴系统里都会避开添加到memblock.reserved中的内存范围.
最后,何时memblock_add会被调用呢?以x86架构为例,在e820__memblock_setup函数中会遍历e820_table,然后逐个对里面的e820_entry调用memblock_add以添加region.关于这部分的细节请参考文档物理内存信息获取及初始化,里面分x86和arm64分别作了分析.
3. 分配内存
前面介绍了memblock相关的初始化,主要是添加region,这些region归根结底就是一个内存范围,这些范围划定了起始地址,大小以及哪些范围不能用(memblock.reversed).本节将介绍使用memblock分配内存的一个主要接口memblock_alloc.
memblock_alloc本身较为简单,就是调用了memblock_alloc_try_nid:
static __always_inline void *memblock_alloc(phys_addr_t size, phys_addr_t align) { return memblock_alloc_try_nid(size, align, MEMBLOCK_LOW_LIMIT, MEMBLOCK_ALLOC_ACCESSIBLE, NUMA_NO_NODE); }
可以看到提供给用户的接口只有大小和对齐的位置,而memblock_alloc_try_nid本身也很简单,主要是通过memblock_alloc_internal获取起始的地址并且返回这个地址前通过memset将这段空间清零.memblock_alloc_internal自身逻辑也比较简单,一是检查下slab准备好了的话,直接通过kzalloc_node接口获取虚拟地址,如果slab并没有初始化好,就调用memblock_alloc_range_nid获取虚拟地址,也就是通过memblock机制而不是slab的机制,通过memblock_alloc_range_nid获得的地址,还要通过memblock_reserve接口添加到memblock.reserved中,代表该段内存已经分配出去了,后面的分配不能再使用这段内存了,最后通过phys_to_virt转换为虚拟地址并返回.
memblock_alloc_range_nid会调用memblock_find_in_range_node获得一个可用的物理地址,后者函数如下:
static phys_addr_t __init_memblock memblock_find_in_range_node(phys_addr_t size, phys_addr_t align, phys_addr_t start, phys_addr_t end, int nid, enum memblock_flags flags) { /* pump up @end */ if (end == MEMBLOCK_ALLOC_ACCESSIBLE || end == MEMBLOCK_ALLOC_NOLEAKTRACE) end = memblock.current_limit; /* avoid allocating the first page */ start = max_t(phys_addr_t, start, PAGE_SIZE); end = max(start, end); if (memblock_bottom_up()) return __memblock_find_range_bottom_up(start, end, size, align, nid, flags); else return __memblock_find_range_top_down(start, end, size, align, nid, flags); }
就是确定了分配的上限地址end,下限地址start,然后根据增长方向调用不同的函数,以__memblock_find_range_bottom_up为例继续分析:
static phys_addr_t __init_memblock __memblock_find_range_bottom_up(phys_addr_t start, phys_addr_t end, phys_addr_t size, phys_addr_t align, int nid, enum memblock_flags flags) { phys_addr_t this_start, this_end, cand; u64 i; for_each_free_mem_range(i, nid, flags, &this_start, &this_end, NULL) { this_start = clamp(this_start, start, end); this_end = clamp(this_end, start, end); cand = round_up(this_start, align); if (cand < this_end && this_end - cand >= size) return cand; } return 0; }
可以看到,for循环里会确定当前遍历时得到的起始地址this_start和结束地址this_end,并且this_sart和this_end要钳入到start到end之间.最后的起始地址还要通过round_up以参数align对齐得到cand,cand才是真正作为候选的可以返回的物理地址,这个物理地址要小于当前循环的this_end,并且this_end到cand之间有足够大小的size,满足这些条件最后才能返回这个cand.那么for_each_free_mem_range就很关键了:
#define for_each_free_mem_range(i, nid, flags, p_start, p_end, p_nid) \ __for_each_mem_range(i, &memblock.memory, &memblock.reserved, \ nid, flags, p_start, p_end, p_nid)
这里的memblock.memory是指要从这里面的region分配内存,并且排除在memblock.reserved里出现的内存范围,p_start是分配出来的起始地址,而p_end是可以用的截至到结束的位置.继续看__for_each_mem_range:
#define __for_each_mem_range(i, type_a, type_b, nid, flags, \ p_start, p_end, p_nid) \ for (i = 0, __next_mem_range(&i, nid, flags, type_a, type_b, \ p_start, p_end, p_nid); \ i != (u64)ULLONG_MAX; \ __next_mem_range(&i, nid, flags, type_a, type_b, \ p_start, p_end, p_nid))
从这里可以看到,p_start和p_end随每次调用__next_mem_range而更新,i会一直增大到ULLONG_MAX.那么__next_mem_range又比较关键了:
void __next_mem_range(u64 *idx, int nid, enum memblock_flags flags, struct memblock_type *type_a, struct memblock_type *type_b, phys_addr_t *out_start, phys_addr_t *out_end, int *out_nid) { int idx_a = *idx & 0xffffffff; int idx_b = *idx >> 32; if (WARN_ONCE(nid == MAX_NUMNODES, "Usage of MAX_NUMNODES is deprecated. Use NUMA_NO_NODE instead\n")) nid = NUMA_NO_NODE; for (; idx_a < type_a->cnt; idx_a++) { struct memblock_region *m = &type_a->regions[idx_a]; phys_addr_t m_start = m->base; phys_addr_t m_end = m->base + m->size; int m_nid = memblock_get_region_node(m); if (should_skip_region(type_a, m, nid, flags)) continue; if (!type_b) { if (out_start) *out_start = m_start; if (out_end) *out_end = m_end; if (out_nid) *out_nid = m_nid; idx_a++; *idx = (u32)idx_a | (u64)idx_b << 32; return; } /* scan areas before each reservation */ for (; idx_b < type_b->cnt + 1; idx_b++) { struct memblock_region *r; phys_addr_t r_start; phys_addr_t r_end; r = &type_b->regions[idx_b]; r_start = idx_b ? r[-1].base + r[-1].size : 0; r_end = idx_b < type_b->cnt ? r->base : PHYS_ADDR_MAX; /* * if idx_b advanced past idx_a, * break out to advance idx_a */ if (r_start >= m_end) break; /* if the two regions intersect, we're done */ if (m_start < r_end) { if (out_start) *out_start = max(m_start, r_start); if (out_end) *out_end = min(m_end, r_end); if (out_nid) *out_nid = m_nid; /* * The region which ends first is * advanced for the next iteration. */ if (m_end <= r_end) idx_a++; else idx_b++; *idx = (u32)idx_a | (u64)idx_b << 32; return; } } } /* signal end of iteration */ *idx = ULLONG_MAX; }
分析这段代码,idx是64位的,高32位idx_b用于索引type_b里regions的补集,这个补集和数学上的补集有点不一样,举例说明,假设原始的regions分布如下:
0:[0-16), 1:[32-48), 2:[128-130)
那么所谓补集就是:
0:[0-0), 1:[16-32), 2:[48-128), 3:[130-MAX)
idx的低32位idx_a用于索引type_a里的regions.
最后,像memblock_phys_alloc这样带phys的接口,返回的是物理地址,而memblock_alloc这样不带phys的接口返回的是虚拟地址,但这点区别微不足道,它们最后都会调用memblock_alloc_range_nid.
函数首先从循环变量i中取出idx_a和idx_b,有了这两个索引就可以去type_a和type_b里取region了,如果没有给出type_b,就是type_b为NULL的话就很简单了,把从type_a里取出的region的base和(base+end)的值放到out_start和out_end输出并返回即可,当然在返回前还得更新idx_a,进行自增,至于返回的out_start和out_end到底合适不能用不,在返回后的逻辑里有体现,这个之前分析过了.这种没有给出type_b的是最简单的情况.
下面分析给出type_b的情况,也就是type_b不为NULL.这意味着从type_a的region里拿到的范围还要在里面排除有type_b里的region的范围.这里的做法是先求type_b里region的补集,然后用这个补集去和type_a里的region进行相交,得出的范围再交给out_start和out_end进行返回.那么如何先求type_b里region的补集呢?这里有个c语言的技巧,某数组元素的指针为r,那么r[-1]就是r前一个元素,但是除开第一个元素不适用这条技巧.利用这个技巧,求解指向type_b里的一个region的指针r,其补集可以写为代码:
r_start = idx_b ? r[-1].base + r[-1].size : 0; r_end = idx_b < type_b->cnt ? r->base : PHYS_ADDR_MAX;
这样就可以循环遍历type_b里的region了,采用上面的代码对每个region都求出补集,然后求这个补集和type_a里region的交集.求交集就是out_start采用m_start和r_start的较大者,而out_end取m_end和r_end的较小者,就是两个集合都有的部分.最后会更新idx的值,通过比较type_a里region的结束位置m_end和补集的结束位置r_end,看哪个较小,就需要增加一个索引值,最后通过移位和相或组成到idx.当然,如果m_end都小于r_start,也就是二者没有交集,意味着当前来自type_a里的region太小,需要自增idx_a索引去找到下一个来自type_a里的region,以使这个新的region更有可能和补集交上,也就是r_start >= m_end的情况,直接break出循环了.
4. 交接给伙伴系统
在本文一开始提到过,memblock主要是提供给启动早期伙伴系统没有准备就绪时,内核的一些流程需要内存.其初始化以x86 e820为例,主要是
start_kernel->setup_arch->e820__memblock_setup->memblock_add
进行初始化,而释放memblock的内存到伙伴系统主要是通过
start_kernel->mm_core_init->mem_init->memblock_free_all
可以很清楚的看到,memblock主要就是使用在start_kernel里调用的setup_arch之后到start_kernel里调用的mm_core_init之前.本节主要简单介绍memblock.c提供的函数memblock_free_all释放内存到伙伴系统.
memblock_free_all主要是调用free_low_memory_core_early,后者主要的逻辑是通过for_each_free_mem_range去循环每个在memblock.memory里而又不在memblock.reserved的region地址范围,并给到start和end两个值中,这个for宏之前介绍过,这里就不再重复了.针对每个start-end的范围,均调用_free_memory_core函数,_free_memory_core里主要的逻辑是调用函数__free_pages_memory,__free_pages_memory函数如下:
static void __init __free_pages_memory(unsigned long start, unsigned long end) { int order; while (start < end) { /* * Free the pages in the largest chunks alignment allows. * * __ffs() behaviour is undefined for 0. start == 0 is * MAX_ORDER-aligned, set order to MAX_ORDER for the case. */ if (start) order = min_t(int, MAX_ORDER, __ffs(start)); else order = MAX_ORDER; while (start + (1UL << order) > end) order--; memblock_free_pages(pfn_to_page(start), start, order); start += (1UL << order); } }
可以看到逻辑比较简单,主要是确定order,然后作为参数调用memblock_free_pages,order的确定分两种情况,主要是__ffs无法处理参数为0的情况,__ffs的作用是找寻参数第一个设置为1的bit位置.后面的while循环,就是针对order较大时,要减小order的值,2的order次方代表多少个页面,所以发现自start起始加上2的order次方大于本次end位置时,证明order太大,需要减小.memblock_free_pages里会调用__free_pages_core释放到伙伴系统,关于这个函数的分析留到伙伴系统了,参见这篇文章.