目录
1.概述
linux系统中使用伙伴系统对物理页面进行分配管理,但是伙伴分配系统需要内核完成初始化以及建立相关内核数据结构后才能够正常工作。因此,我们不难看出在内核初始化相关数据结构时需要另一种内存分配器。早期Linux没有较为完善的引导内存分配器,但是随着硬件的发展和日趋复杂,处理不同体系的内存分配代码也渐渐复杂起来,随之就需要引导内存分配器来初始化系统主要内存分配器的数据结构以确保其正常工作。在内核2.3.23版本中bootmem引导内存分配器补丁被加入,使用位图来表示页面使用状况。然后在内核2.3.48版本时,linux内核移植到IA64时正式使用bootmem作为引导内存分配器。随着时间的流逝,内存检测已经从简单地向BIOS询问扩展内存块的大小演变为处理复杂的表,块,库和群集。这时开始使用memblock作为引导内存分配器。在bootmem向memblock过渡时,出现nobootmem作为兼容层,提供与bootmem类似api。在内核版本4.17时,在linux所支持的24种架构中,只有5种仍在使用bootmem作为唯一的早期内存分配器,14中将memblock与nobootmem一起使用,其余同时使用memblock和bootmem作为引导内存分配器。今天主要介绍bootmem引导内存分配器。
2.内核数据结构
首先查看bootmem_data数据结构,表示每个节点物理内存以及其页面使用情况。
typedef struct bootmem_data {
unsigned long node_min_pfn;
unsigned long node_low_pfn;
void *node_bootmem_map;
unsigned long last_end_off;
unsigned long hint_idx;
struct list_head list;
} bootmem_data_t;
node_min_pfn和node_low_pfn:表示该节点内存物理页面范围:node_min_pfn为起始页面,node_low_pfn则为结束页面;
node_bootmem_map:指向位图,每位表示内存页面使用情况,当页面可以被使用时,所对应的位图设为0,相反则设为1;
last_end_off:表示上次所分配内存的物理地址相对bootmem起始页面偏移(以字节计算);
hint_idx:记录上次设置位图的索引;
list:加入bootmem_data全局链表bdata_list;除了全局链表外,还存在bootmem_node_data所指向的bootmem_data全局数组,索引为内存对应节点号;
3.相关函数
3.1 初始化bootmem_data
static unsigned long __init init_bootmem_core(bootmem_data_t *bdata,
unsigned long mapstart, unsigned long start, unsigned long end)
{
unsigned long mapsize;
mminit_validate_memmodel_limits(&start, &end);
bdata->node_bootmem_map = phys_to_virt(PFN_PHYS(mapstart));
bdata->node_min_pfn = start;
bdata->node_low_pfn = end;
link_bootmem(bdata);
/*
* Initially all pages are reserved - setup_arch() has to
* register free RAM areas explicitly.
*/
mapsize = bootmap_bytes(end - start);
memset(bdata->node_bootmem_map, 0xff, mapsize);
bdebug("nid=%td start=%lx map=%lx end=%lx mapsize=%lx\n",
bdata - bootmem_node_data, start, mapstart, end, mapsize);
return mapsize;
}
首先调用mminit_validate_memmodel_limits()函数检查指定范围物理页面是否有效,然后初始化bootmem_data结构,其中mapstart指向所分配位图页面。link_bootmem()函数是将bootmem_data加入bdata_list链表中。最后将所有位图设为1,即页面目前不可用。
static void __init link_bootmem(bootmem_data_t *bdata)
{
bootmem_data_t *ent;
list_for_each_entry(ent, &bdata_list, list) {
if (bdata->node_min_pfn < ent->node_min_pfn) {
list_add_tail(&bdata->list, &ent->list);
return;
}
}
list_add_tail(&bdata->list, &bdata_list);
}
该函数将bootmem_data加入链表,bdata_list中节点顺序是按照起始页面从小到大排列。
3.2 释放bootmem所保留的页面
static void __init __free(bootmem_data_t *bdata,
unsigned long sidx, unsigned long eidx)
{
unsigned long idx;
bdebug("nid=%td start=%lx end=%lx\n", bdata - bootmem_node_data,
sidx + bdata->node_min_pfn,
eidx + bdata->node_min_pfn);
if (WARN_ON(bdata->node_bootmem_map == NULL))
return;
if (bdata->hint_idx > sidx)
bdata->hint_idx = sidx;
for (idx = sidx; idx < eidx; idx++)
if (!test_and_clear_bit(idx, bdata->node_bootmem_map))
BUG();
}
该函数中sidx表示所释放起始页面偏移,eidx表示结束页面偏移,循环遍历相应位图将其设为0,则该页面可用。
3.3 保留bootmem中页面
static int __init __reserve(bootmem_data_t *bdata, unsigned long sidx,
unsigned long eidx, int flags)
{
unsigned long idx;
int exclusive = flags & BOOTMEM_EXCLUSIVE;
bdebug("nid=%td start=%lx end=%lx flags=%x\n",
bdata - bootmem_node_data,
sidx + bdata->node_min_pfn,
eidx + bdata->node_min_pfn,
flags);
if (WARN_ON(bdata->node_bootmem_map == NULL))
return 0;
for (idx = sidx; idx < eidx; idx++)
if (test_and_set_bit(idx, bdata->node_bootmem_map)) {
if (exclusive) {
__free(bdata, sidx, idx);
return -EBUSY;
}
bdebug("silent double reserve of PFN %lx\n",
idx + bdata->node_min_pfn);
}
return 0;
}
该函数sidx和eidx表示锁保留页面的范围,如果flags设置BOOTMEM_EXCLUSIVE标志位则表示将指定范围内页面从bootmem中释放。
3.4 从bootmem中分配内存
static void * __init alloc_bootmem_bdata(struct bootmem_data *bdata,
unsigned long size, unsigned long align,
unsigned long goal, unsigned long limit)
{
unsigned long fallback = 0;
unsigned long min, max, start, sidx, midx, step;
bdebug("nid=%td size=%lx [%lu pages] align=%lx goal=%lx limit=%lx\n",
bdata - bootmem_node_data, size, PAGE_ALIGN(size) >> PAGE_SHIFT,
align, goal, limit);
BUG_ON(!size);
BUG_ON(align & (align - 1));
BUG_ON(limit && goal + size > limit);
if (!bdata->node_bootmem_map)
return NULL;
该函数size表示所需要分配多少字节的内存,align表示分配内存按多少字节对齐,goal表示分配内存最小物理地址,limit表示分配内存最大物理地址。首先对参数进行检验,size不能为空,align必须为2的指数,同时分配内存不能超出范围。
min = bdata->node_min_pfn;
max = bdata->node_low_pfn;
goal >>= PAGE_SHIFT;
limit >>= PAGE_SHIFT;
if (limit && max > limit)
max = limit;
if (max <= min)
return NULL;
step = max(align >> PAGE_SHIFT, 1UL);
if (goal && min < goal && goal < max)
start = ALIGN(goal, step);
else
start = ALIGN(min, step);
获取bootmem所保留页面范围并重新计算所分配内存起始物理页面。
sidx = start - bdata->node_min_pfn;
midx = max - bdata->node_min_pfn;
if (bdata->hint_idx > sidx) {
/*
* Handle the valid case of sidx being zero and still
* catch the fallback below.
*/
fallback = sidx + 1;
sidx = align_idx(bdata, bdata->hint_idx, step);
}
设置起始和结束索引,如果bootmem上次分配内存页面大于sidx则设置fallback。
while (1) {
int merge;
void *region;
unsigned long eidx, i, start_off, end_off;
find_block:
sidx = find_next_zero_bit(bdata->node_bootmem_map, midx, sidx);
sidx = align_idx(bdata, sidx, step);
eidx = sidx + PFN_UP(size);
if (sidx >= midx || eidx > midx)
break;
for (i = sidx; i < eidx; i++)
if (test_bit(i, bdata->node_bootmem_map)) {
sidx = align_idx(bdata, i, step);
if (sidx == i)
sidx += step;
goto find_block;
}
if (bdata->last_end_off & (PAGE_SIZE - 1) &&
PFN_DOWN(bdata->last_end_off) + 1 == sidx)
start_off = align_off(bdata, bdata->last_end_off, align);
else
start_off = PFN_PHYS(sidx);
merge = PFN_DOWN(start_off) < sidx;
end_off = start_off + size;
bdata->last_end_off = end_off;
bdata->hint_idx = PFN_UP(end_off);
/*
* Reserve the area now:
*/
if (__reserve(bdata, PFN_DOWN(start_off) + merge,
PFN_UP(end_off), BOOTMEM_EXCLUSIVE))
BUG();
region = phys_to_virt(PFN_PHYS(bdata->node_min_pfn) +
start_off);
memset(region, 0, size);
/*
* The min_count is set to 0 so that bootmem allocated blocks
* are never reported as leaks.
*/
kmemleak_alloc(region, size, 0, 0);
return region;
}
首先获取bootmem位图在sidx和eidx范围数个为0的索引,然后在进行对齐获取起始索引并重新计算结束索引,验证是否超出bootmem所拥有页面范围,超出则退出循环分配失败。否则进入for循环在sidx和eidx遍历查看是否存在页面被bootmem保留,如果存在则以此索引为新sidx进行设置,重新遍历,相反则继续进行分配。根据bootmem上次分配内存的偏移设置所分配内存起始物理地址以及结束地址,最后设置这些页面的位图为1,即保留页面。
if (fallback) {
sidx = align_idx(bdata, fallback - 1, step);
fallback = 0;
goto find_block;
}
检查fallback是否为0,如果不为0则返回继续进行查找。
3.5 释放bootmem中页面
static unsigned long __init free_all_bootmem_core(bootmem_data_t *bdata)
{
struct page *page;
unsigned long *map, start, end, pages, cur, count = 0;
if (!bdata->node_bootmem_map)
return 0;
map = bdata->node_bootmem_map;
start = bdata->node_min_pfn;
end = bdata->node_low_pfn;
bdebug("nid=%td start=%lx end=%lx\n",
bdata - bootmem_node_data, start, end);
获取bootmem页面范围以及位图。
while (start < end) {
unsigned long idx, vec;
unsigned shift;
idx = start - bdata->node_min_pfn;
shift = idx & (BITS_PER_LONG - 1);
/*
* vec holds at most BITS_PER_LONG map bits,
* bit 0 corresponds to start.
*/
vec = ~map[idx / BITS_PER_LONG];
if (shift) {
vec >>= shift;
if (end - start >= BITS_PER_LONG)
vec |= ~map[idx / BITS_PER_LONG + 1] <<
(BITS_PER_LONG - shift);
}
/*
* If we have a properly aligned and fully unreserved
* BITS_PER_LONG block of pages in front of us, free
* it in one go.
*/
if (IS_ALIGNED(start, BITS_PER_LONG) && vec == ~0UL) {
int order = ilog2(BITS_PER_LONG);
__free_pages_bootmem(pfn_to_page(start), start, order);
count += BITS_PER_LONG;
start += BITS_PER_LONG;
} else {
cur = start;
start = ALIGN(start + 1, BITS_PER_LONG);
while (vec && cur != start) {
if (vec & 1) {
page = pfn_to_page(cur);
__free_pages_bootmem(page, cur, 0);
count++;
}
vec >>= 1;
++cur;
}
}
}
循环遍历位图释放bootmem未进行保留的页面,即相应位图设为0的页面。
注:以上均为自己对linux内核4.15.1源码的分析。如果有不足之处,欢迎大家指出。
参考文献:
[1]早期引导内存分配器的快速历史.
[2]《深入理解linux虚拟内存管理》