参考文档:https://blog.csdn.net/gatieme/article/details/52403148 感谢作者的无私分享
Linux 的 C 代码的开始主要关注 start_kernel 函数,这是整个系统开始初始化的入口。其实 start_kernel 函数的内容是非常的多的,这里先只关注内存管理部分的代码。
asmlinkage __visible void __init start_kernel(void)
{
.....
/* 设置特定架构的信息
* 同时初始化memblock */
setup_arch(&command_line);
mm_init_cpumask(&init_mm);
setup_per_cpu_areas();
/* 初始化内存结点和内段区域 */
build_all_zonelists(NULL, NULL);
page_alloc_init();
/*
* These use large bootmem allocations and must precede
* mem_init();
* kmem_cache_init();
*/
mm_init();
kmem_cache_init_late();
kmemleak_init();
setup_per_cpu_pageset();
rest_init();
.....
}
setup_arch —— 特定体系结构处理器相关的初始化
build_all_zonelists —— 建立并初始化结点和内存域的数据结构
mm_init —— 建立了内核的内存分配器,、其中通过 mem_init 停用最初使用的简易的内存管理(bootmem/memblock)并迁移到实际的内存管理器(比如伙伴系统)
内存管理是操作系统资源管理的重点, 但是在操作系统初始化的初期, 操作系统只是获取到了内存的基本信息, 但是内存管理的数据结构都没有建立, 而我们这些数据结构创建的过程本身就是一个内存分配的过程, 那么就出现一个问题
我们还没有一个内存管理器去负责分配和回收内存, 而我们又不可能将所有的内存信息都静态创建并初始化, 那么我们怎么分配内存管理器所需要的内存呢? 现在我们进入了一个先有鸡还是先有蛋的怪圈, 这种问题的一般解决方法是, 我们先实现一个满足要求的但是可能效率不高的笨家伙(内存管理器), 用它来负责系统初始化初期的内存管理, 最重要的, 用它来初始化我们内存的数据结构, 直到我们真正的内存管理器被初始化完成并能投入使用, 我们将旧的内存管理器丢掉
即因此在系统启动过程期间, 内核使用了一个额外的简化形式的内存管理模块早期的引导内存分配器(boot memory allocator–bootmem分配器)或者memblock, 用于在启动阶段早期分配内存, 而在系统初始化完成后, 该分配器被内核抛弃, 然后初始化了一套新的更加完善的内存分配器.
早期使用的是 bootmem,现在都是使用了 memblock,这里只看看 memblock 的一些东西
先看头文件 include/linux/memblock.h :
struct memblock {
bool bottom_up; /* is bottom up direction?
如果true, 则允许由下而上地分配内存*/
phys_addr_t current_limit; /*指出了内存块的大小限制*/
/* 接下来的三个域描述了内存块的类型,即预留型,内存型和物理内存*/
struct memblock_type memory;
struct memblock_type reserved;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
struct memblock_type physmem;
#endif
};
该结构体包含五个域。
字段 | 描述 |
---|---|
bottom_up | 表示分配器分配内存的方式 true:从低地址(内核映像的尾部)向高地址分配 false:也就是top-down,从高地址向地址分配内存. |
current_limit | 指出了内存块的大小限制, 用于限制通过memblock_alloc的内存申请 |
memory | 是可用内存的集合 |
reserved | 已分配内存的集合 |
physmem | 物理内存的集合(需要配置CONFIG_HAVE_MEMBLOCK_PHYS_MAP参数) |
接下来的三个域描述了内存块的类型
预留型
内存型
物理内存型(需要配置宏CONFIG_HAVE_MEMBLOCK_PHYS_MAP)
同样是定义在 include/linux/memblock.h
struct memblock_type
{
unsigned long cnt; /* number of regions */
unsigned long max; /* size of the allocated array */
phys_addr_t total_size; /* size of all regions */
struct memblock_region *regions;
};
该结构体存储的是内存类型信息
字段 | 描述 |
---|---|
cnt | 当前集合(memory或者reserved)中记录的内存区域个数 |
max | 当前集合(memory或者reserved)中可记录的内存区域的最大个数 |
total_size | 集合记录区域信息大小 |
regions | 内存区域结构指针 |
它包含的域分别描述了当前内存块含有的内存区域数量,
所有内存区域的总共大小,已经分配的内存区域大小和一个指向memblock_region结构体的数组指针
memblock_region结构体描述了内存区域,它的定义在它的定义在 include/linux/memblock.h
struct memblock_region
{
phys_addr_t base;
phys_addr_t size;
unsigned long flags;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
int nid;
#endif
};
字段 | 描述 |
---|---|
base | 内存区域起始地址 |
size | 内存区域大小 |
flags | 标记 |
nid | node号 |
memblock_region的flags字段存储了当期那内存域的标识信息, 标识用enum变量来定义:
/* Definition of memblock flags. */
enum {
MEMBLOCK_NONE = 0x0, /* No special request */
MEMBLOCK_HOTPLUG = 0x1, /* hotpluggable region */
MEMBLOCK_MIRROR = 0x2, /* mirrored region */
MEMBLOCK_NOMAP = 0x4, /* don't add to kernel direct mapping */
};
上述三个结构体的关系式:
+---------------------------+ +---------------------------+
| memblock | | |
| _______________________ | | |
| | memory | | | Array of the |
| | memblock_type |-|-->| membock_region |
| |_______________________| | | |
| | +---------------------------+
| _______________________ | +---------------------------+
| | reserved | | | |
| | memblock_type |-|-->| Array of the |
| |_______________________| | | memblock_region |
| | | |
+---------------------------+ +---------------------------+
Memblock主要包含三个结构体:memblock,,memblock_type,memblock_region。现在我们已了解了Memblock, 接下来我们将看到Memblock的初始化过程
在 mm/memblock.c 文件中:
/* 这几个宏在 memblock.h */
#define INIT_MEMBLOCK_REGIONS 128
#define INIT_PHYSMEM_REGIONS 4
#define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0)
#define MEMBLOCK_ALLOC_ACCESSIBLE 0
static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock;
#endif
struct memblock memblock __initdata_memblock = {
.memory.regions = memblock_memory_init_regions,
.memory.cnt = 1, /* empty dummy entry */
.memory.max = INIT_MEMBLOCK_REGIONS,
.reserved.regions = memblock_reserved_init_regions,
.reserved.cnt = 1, /* empty dummy entry */
.reserved.max = INIT_MEMBLOCK_REGIONS,
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
.physmem.regions = memblock_physmem_init_regions,
.physmem.cnt = 1, /* empty dummy entry */
.physmem.max = INIT_PHYSMEM_REGIONS,
#endif
.bottom_up = false,
.current_limit = MEMBLOCK_ALLOC_ANYWHERE,
};
几乎所有的 memblock 相关的 APIs 都在文件:mm/memblock.c 中进行了实现
最为常见的有:
/*
* 1. 基本接口
*/
// 向memory区中添加内存区域.
memblock_add(phys_addr_t base, phys_addr_t size)
// 向memory区中删除区域.
memblock_remove(phys_addr_t base, phys_addr_t size)
// 申请内存
memblock_alloc(phys_addr_t size, phys_addr_t align)
// 释放内存
memblock_free(phys_addr_t base, phys_addr_t size)
/*
* 2. 查找 & 遍历
*/
// 在给定的范围内找到未使用的内存
phys_addr_t memblock_find_in_range(phys_addr_t start, phys_addr_t end, phys_addr_t size, phys_addr_t align)
// 反复迭代 memblock
for_each_mem_range(i, type_a, type_b, nid, flags, p_start, p_end, p_nid)
/*
* 3. 获取信息
*/
// 获取内存区域信息
phys_addr_t get_allocated_memblock_memory_regions_info(phys_addr_t *addr);
// 获取预留内存区域信息
phys_addr_t get_allocated_memblock_reserved_regions_info(phys_addr_t *addr);
/*
* 4. 打印
*/
#define memblock_dbg(fmt, ...) \
if (memblock_debug) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
1. memblock_add :将内存区域加入到 memblock 中
memblock_add 函数负责向 memory 区中添加内存区域, 有两个参数:物理基址和内存区域大小,并且把该内存区域添加到memblock。
int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size)
{
memblock_dbg("memblock_add: [%#016llx-%#016llx] flags %#02lx %pF\n",
(unsigned long long)base,
(unsigned long long)base + size - 1,
0UL, (void *)_RET_IP_);
return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0);
}
memblock_add 传递的参数依次是 : 内存块类型 (memory), 物理基址, 内存区域大小, 最大节点数(0如果CONFIG_NODES_SHIFT没有在配置文件中设置,不然就是CONFIG_NODES_SHIFT)和标志。
memblock_add_range 函数添加新的内存区域到内存块中:
首先,该函数检查给定的内存区域大小, 如果是0就返回.
在这之后, memblock_add_range用给定的 memblock_type 检查memblock结构体中是否存在内存区域
如果没有,我们就用给定的值填充新的memory_region然后返回
如果memblock_type不为空,我们就把新的内存区域添加到memblock_type类型的memblock中。
当然在这个函数中对很多情况进行了处理:
查内存区域是否重叠
将新的添加到memblock, 并且看是否能和已经添加到memblock中的内存区域进行合并
如果新内存区域没有和已经存储在memblock的内存区域重叠, 把该新内存区域插入到memblock中. 如果有重叠通通过一个小巧的来完成冲突处理。
重叠检查完毕后, 新的内存区域已经是一块干净的不包含重叠区域的内存, 把新的内存区域插入到memblock中包含两步:
把新的内存区域中非重叠的部分作为独立的区域加入到memblock
合并所有相邻的内存区域
那么这个 memblock_add 在什么地方被调用呢?
arm下的memblock初始化也是从start_kernel()中,在setup_arch()中arm架构通过arm_memblock_init完成了memblock的初始化工作。在初始化的时候,从 dts 中解析完内存信息后进行调用。在调用 memblock_add 后,将指定的内存放入了 memblock.memory 的 region 中。
2. memblock_phys_alloc 申请内存
phys_addr_t __init memblock_phys_alloc(phys_addr_t size, phys_addr_t align)
传入的是期望申请的内存大小和对齐。
phys_addr_t __init memblock_phys_alloc(phys_addr_t size, phys_addr_t align)
{
return memblock_alloc_base(size, align, MEMBLOCK_ALLOC_ACCESSIBLE);
}
phys_addr_t __init __memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr)
{
return memblock_alloc_base_nid(size, align, max_addr, NUMA_NO_NODE,
MEMBLOCK_NONE);
}
phys_addr_t __init memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr)
{
phys_addr_t alloc;
alloc = __memblock_alloc_base(size, align, max_addr);
if (alloc == 0)
panic("ERROR: Failed to allocate %pa bytes below %pa.\n",
&size, &max_addr);
return alloc;
}
phys_addr_t __init memblock_alloc_base_nid(phys_addr_t size,
phys_addr_t align, phys_addr_t max_addr,
int nid, enum memblock_flags flags)
{
return memblock_alloc_range_nid(size, align, 0, max_addr, nid, flags);
}
然后调用 memblock_alloc_range_nid 函数:
static phys_addr_t __init memblock_alloc_range_nid(phys_addr_t size,
phys_addr_t align, phys_addr_t start,
phys_addr_t end, int nid, ulong flags)
{
phys_addr_t found;
if (!align)
align = SMP_CACHE_BYTES;
found = memblock_find_in_range_node(size, align, start, end, nid,
flags);
if (found && !memblock_reserve(found, size)) {
/*
* The min_count is set to 0 so that memblock allocations are
* never reported as leaks.
*/
kmemleak_alloc(__va(found), size, 0, 0);
return found;
}
return 0;
}
memblock_alloc_range_nid 函数的主要工作如下
首先使用memblock_find_in_range_node指定内存区域和大小查找内存区域
memblock_reserve后将其标为已经分配
然后继续看 memblock_find_in_range_node:
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, ulong flags)
{
phys_addr_t kernel_end, ret;
/* pump up @end */
if (end == MEMBLOCK_ALLOC_ACCESSIBLE)
end = memblock.current_limit;
/* avoid allocating the first page */
start = max_t(phys_addr_t, start, PAGE_SIZE);
end = max(start, end);
kernel_end = __pa_symbol(_end);
/*
* try bottom-up allocation only when bottom-up mode
* is set and @end is above the kernel image.
*/
if (memblock_bottom_up() && end > kernel_end) {
phys_addr_t bottom_up_start;
/* make sure we will allocate above the kernel */
bottom_up_start = max(start, kernel_end);
/* ok, try bottom-up allocation first */
ret = __memblock_find_range_bottom_up(bottom_up_start, end,
size, align, nid, flags);
if (ret)
return ret;
/*
* we always limit bottom-up allocation above the kernel,
* but top-down allocation doesn't have the limit, so
* retrying top-down allocation may succeed when bottom-up
* allocation failed.
*
* bottom-up allocation is expected to be fail very rarely,
* so we use WARN_ONCE() here to see the stack trace if
* fail happens.
*/
WARN_ONCE(1, "memblock: bottom-up allocation failed, memory hotunplug may be affected\n");
}
return __memblock_find_range_top_down(start, end, size, align, nid,
flags);
}
如果从memblock_alloc过来, end就是MEMBLOCK_ALLOC_ACCESSIBLE,这个时候会设置为current_limit.
如果不通过memblock_alloc分配, 内存范围就是指定的范围. 紧接着对start做调整,为的是避免申请到第一个页面
memblock_bottom_up返回的是memblock.bottom_up,前面初始化的时候也知道这个值是false(在numa初始化时会设置为true),所以初始化前期应该调用的是__memblock_find_range_top_down函数去查找内存:
再看最后一个调用 __memblock_find_range_top_down:
static phys_addr_t __init_memblock
__memblock_find_range_top_down(phys_addr_t start, phys_addr_t end,
phys_addr_t size, phys_addr_t align, int nid,
ulong flags)
{
phys_addr_t this_start, this_end, cand;
u64 i;
for_each_free_mem_range_reverse(i, nid, flags, &this_start, &this_end,
NULL) {
this_start = clamp(this_start, start, end);
this_end = clamp(this_end, start, end);
if (this_end < size)
continue;
cand = round_down(this_end - size, align);
if (cand >= this_start)
return cand;
}
return 0;
}
函数通过使用for_each_free_mem_range_reverse宏封装调用__next_free_mem_range_rev()函数,此函数逐一将memblock.memory里面的内存块信息提取出来与memblock.reserved的各项信息进行检验,确保返回的this_start和this_end不会是分配过的内存块。
然后通过clamp取中间值,判断大小是否满足,满足的情况下,将自末端向前(因为这是top-down申请方式)的size大小的空间的起始地址(前提该地址不会超出this_start)返回回去
至此满足要求的内存块算是找到了。
memblock 内存管理是将所有的物理内存放到 memblock.memory 中作为可用内存来管理, 分配过的内存只加入到memblock.reserved 中, 并不从 memory 中移出。
同理释放内存也会加入到memory中. 也就是说, memory 在 fill 过后基本就是不动的了. 申请和分配内存仅仅修改reserved 就达到目的. 在初始化阶段没有那么多复杂的内存操作场景, 甚至很多地方都是申请了内存做永久使用的, 所以这样的内存管理方式已经足够凑合着用了, 毕竟内核也不指望用它一辈子. 在系统完成初始化之后所有的工作会移交给强大的伙伴系统来进行内存管理。
参考文献:
https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/02-memory/03-initialize/03-memblock
http://www.maxwellxxx.com/linuxmemblock