在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器.
1 前景回顾
1.1 Linux内存管理的层次结构
Linux把物理内存划分为三个层次来管理
层次 | 描述 |
存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 |
管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 |
页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | |
为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点
- 首先, 内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为
pg_data_t
的实例. 系统中每个节点被链接到一个以NULL结尾的pgdat_list
链表中<而其中的每个节点利用pg_data_tnode_next
字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. - 接着各个节点又被划分为内存管理区域, 一个管理区域通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射.
- 最后页帧(page frame)代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号.
1.2 启动过程中的内存初始化
在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉.
系统启动
首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在init/main.c?v=4.7, line 479
其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示
asmlinkage __visible void __init start_kernel(void)
{
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();
}
函数 | 功能 |
是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 | |
初始化CPU屏蔽字 | |
函数(查看定义)给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间. 在SMP系统中, setup_per_cpu_areas初始化源代码中(使用per_cpu宏)定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本. 此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本 在非SMP系统中这是一个空操作 | |
建立并初始化结点和内存域的数据结构 | |
建立了内核的内存分配器, 其中通过mem_init停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统) 然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 | |
在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器slab, slob, slub都会定义此函数 | |
Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 /sys/kernel/debug/kmemleak中, Kmemcheck能够帮助定位大多数内存错误的上下文 | |
初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配 由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. |
1.3 今日内容(非bootmem下的memblock内存管理)
在引导内核的过程中, 需要使用内存, 而这个时候内核的内存管理并没有被创建, 因此也就需要一种精简的内存管理系统先接受这个工作, 而在初始化完成后, 再将旧的接口废弃, 转而使用强大的buddy系统来进行内存管理.
前面我们讲解了引导内存管理bootmem机制, 它基于最先适配算法, 早期的Linux内核在引导阶段都是通过bootmem来完成初期的内存管理的. 但是后来的版本(笔者分析的是3.19)开始把bootmem弃用了,__alloc_memory_core_aarly()
取代了bootmem的__alloc_memory_core()
来完成内存分配, 而后者其实就是调用的memblock来分配内的.
memblock算法是linux内核初始化阶段的一个内存分配器,本质上是取代了原来的bootmem算法. memblock实现比较简单,而它的作用就是在page allocator初始化之前来管理内存,完成分配和释放请求.
为了保证系统的兼容性, 内核为bootmem和memblock提供了相同的API接口.
这样在编译Kernel的时候可以选择nobootmem或者bootmem 来在buddy system起来之前管理memory.
这两种机制对提供的API是一致的,因此对用户是透明的
ifdef CONFIG_NO_BOOTMEM
obj-y += nobootmem.o
else
obj-y += bootmem.o
endif
由于接口是一致的, 那么他们共同使用一份
头文件 | bootmem接口 | nobootmem接口 |
Memblock是在早期引导过程中管理内存的方法之一,此时内核内存分配器还没运行. Memblock以前被定义为Logical Memory Block( 逻辑内存块), 但根据Yinghai Lu的补丁, 它被重命名为memblock.
2 memblock的数据结构
2.1 struct 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)
2.2 struct memblock_type
我们现在又接触到了一个数据结构memblock_type, 它的定义在include/linux/memblock.h?v=4.7, line 40
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结构体的数组指针
2.3 内存区域memblock_region
memblock_region结构体描述了内存区域,它的定义在它的定义在include/linux/memblock.h?v=4.7, line 31
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号 |
2.4 内存区域标识
memblock_region的flags字段存储了当期那内存域的标识信息, 标识用enum变量来定义, 参见include/linux/memblock.h?v=4.7, line 23
/* 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 */
};
2.5 结构总体布局
图示法可以用来展示以上结构体之间的关系:
+---------------------------+ +---------------------------+
| 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的初始化过程。
2.6 初始化memblock静态变量
在编译时,会分配好memblock结构所需要的内存空间, 文件是mm/memblock.c
结构体memblock的初始化变量名和结构体名相同memblock
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,
};
__initdata_memblock宏指定存储位置
我们可以注意到初始化使用了__initdata_memblock宏,它的定义在include/linux/memblock.h?v=4.7, line 64, 如下所示
#ifdef CONFIG_ARCH_DISCARD_MEMBLOCK
#define __init_memblock __meminit
#define __initdata_memblock __meminitdata
#else
#define __init_memblock
#define __initdata_memblock
#endif
如果启用CONFIG_ARCH_DISCARD_MEMBLOCK
宏配置选项,memblock代码会被放到.init代码段, 在内核启动完成后 memblock代码会从.init代码段释放。
3个memblock_type的初始化
接下来的是memblock结构体中3个memblock_type类型数据 memory, reserved和physmem的初始化
它们的memblock_typecnt域(当前集合中区域个数)被初始化为1.
memblock_typemax域(当前集合中最大区域个数)被初始化为INIT_MEMBLOCK_REGIONS
和INIT_PHYSMEM_REGIONS
其中INIT_MEMBLOCK_REGIONS
为128, 参见include/linux/memblock.h?v=4.7, line 20
#define INIT_MEMBLOCK_REGIONS 128
#define INIT_PHYSMEM_REGIONS 4
而memblock_type.regions域都是通过memblock_region数组初始化的, 所有的数组定义都带有__initdata_memblock宏
memblock结构体中最后两个域bottom_up内存分配模式被禁用(bottom_up = false, 因此内存分配方式为top-down.), 当前 Memblock的大小限制是MEMBLOCK_ALLOC_ANYWHERE
为~(phys_addr_t)0即为0xffffffff.
/* Flags for memblock_alloc_base() amd __memblock_alloc_base() */
#define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0)
#define MEMBLOCK_ALLOC_ACCESSIBLE 0
3 Memblock-API函数接口
3.1 Memblock-API函数接口
既然内核静态创建并初始化了__initdata_memblock这个变量, 那么memblock又是怎么运作的呢?
在上文中我提到过所有关于memblock的实现都在mm/memblock.c源文件中
抛开其他的先不谈,如果要使用memblock,最上层函数一共就4个
/
// 基本接口
/
// 向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)
/
// 查找 & 遍历
/
// 在给定的范围内找到未使用的内存
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)
/
// 获取信息
/
// 获取内存区域信息
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);
/
// 获取信息
/
#define memblock_dbg(fmt, ...) \
if (memblock_debug) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
大致翻看了一下内核代码, 发现很少使用memblock_free(),因为很多地方都是申请了内存做永久使用的. 再者,其实在内核中通过memblock_alloc来分配内存其实比较少,一般都是在调用memblock底层的一些函数来简单粗暴的分配的.
3.2 memblock_add将内存区域加入到memblock中
3.2.1 memblock_add函数
memblock_add函数负责向memory区中添加内存区域, 有两个参数:物理基址和内存区域大小,并且把该内存区域添加到memblock。
memblock_add函数本身并没有什么, 它只是调用了memblock_add_range函数来完成工作, 定义在mm/memblock.c?v=4.7, line 609
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)和标志
3.2.2 memblock_add_range函数代码
memblock_add_range函数添加新的内存区域到内存块中, 定义在mm/memblock.c?v=4.7, line 504
- 首先,该函数检查给定的内存区域大小, 如果是0就返回.
- 在这之后, memblock_add_range用给定的memblock_type检查memblock结构体中是否存在内存区域
- 如果没有,我们就用给定的值填充新的memory_region然后返回
- 如果memblock_type不为空,我们就把新的内存区域添加到memblock_type类型的memblock中。
/**
* memblock_add_range - add new memblock region
* @type: memblock type to add new region into
* @base: base address of the new region
* @size: size of the new region
* @nid: nid of the new region
* @flags: flags of the new region
*
* Add new memblock region [@base,@base+@size) into @type. The new region
* is allowed to overlap with existing ones - overlaps don't affect already
* existing regions. @type is guaranteed to be minimal (all neighbouring
* compatible regions are merged) after the addition.
*
* RETURNS:
* 0 on success, -errno on failure.
*/
int __init_memblock memblock_add_range(struct memblock_type *type,
phys_addr_t base, phys_addr_t size,
int nid, unsigned long flags)
{
bool insert = false;
phys_addr_t obase = base;
/* 获取内存区域的结束位置,
* memblock_cap_size函数会设置size大小确保base + size不会溢出 */
phys_addr_t end = base + memblock_cap_size(base, &size);
int idx, nr_new;
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;
}
repeat:
/*
* The following is executed twice. Once with %false @insert and
* then with %true. The first counts the number of regions needed
* to accomodate the new area. The second actually inserts them.
*/
base = obase;
nr_new = 0;
for_each_memblock_type(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_HAVE_MEMBLOCK_NODE_MAP
WARN_ON(nid != memblock_get_region_node(rgn));
#endif
WARN_ON(flags != rgn->flags);
nr_new++;
if (insert)
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)
memblock_insert_region(type, idx, base, end - base,
nid, flags);
}
/*
* If this was the first round, resize array and repeat for actual
* insertions; otherwise, merge and return.
*/
if (!insert) {
while (type->cnt + nr_new > type->max)
if (memblock_double_array(type, obase, size) < 0)
return -ENOMEM;
insert = true;
goto repeat;
} else {
memblock_merge_regions(type);
return 0;
}
}
3.2.3 memblock_add_range函数流程解析
首先,我们用如下代码获得内存区域的结束位置:
phys_addr_t end = base + memblock_cap_size(base, &size);
memblock_cap_size函数会设置size大小确保base + size不会溢出。该函数实现相当简单, 参见mm/memblock.c?v=4.7, line 79
/* adjust *@size so that (@base + *@size) doesn't overflow, return new size */
static inline phys_addr_t memblock_cap_size(phys_addr_t base, phys_addr_t *size)
{
return *size = min(*size, (phys_addr_t)ULLONG_MAX - base);
}
memblock_cap_size返回size和ULLONG_MAX - base中的最小值
在那之后我们得到了新的内存区域的结束地址, 然后
- 查内存区域是否重叠
- 将新的添加到memblock, 并且看是否能和已经添加到memblock中的内存区域进行合并
首先遍历所有已经存储的内存区域并检查有没有和新的内存区域重叠
for_each_memblock_type(type, rgn) {
phys_addr_t rbase = rgn->base;
phys_addr_t rend = rbase + rgn->size;
if (rbase >= end)
break;
if (rend <= base)
continue;
/* ...... */
/* area below @rend is dealt with, forget about it */
base = min(rend, end);
}
如果新内存区域没有和已经存储在memblock的内存区域重叠, 把该新内存区域插入到memblock中. 如果有重叠通通过一个小巧的来完成冲突处理
base = min(rend, end);
重叠检查完毕后, 新的内存区域已经是一块干净的不包含重叠区域的内存, 把新的内存区域插入到memblock中包含两步:
- 把新的内存区域中非重叠的部分作为独立的区域加入到memblock
- 合并所有相邻的内存区域
这个过程分为两次循环来完成, 由一个标识变量insert和report代码跳转标签控制
- 第一次循环的时候, 检查新内存区域是否可以放入内存块中并调用memblock_double_array, 而由于insert = false, 则执行!insert条件语句标记的代码块, 并设置insert = true, 然后goto 跳转到report标签继续开始第二次循环
- 第二次循环中, insert = true, 则执行相应的insert == true的代码块, 并且执行memblock_insert_region将新内存区域插入, 最后执行memblock_merge_regions(type)合并内存区域
这是第一次循环, 我们需要检查新内存区域是否可以放入内存块中并调用memblock_double_array:
/*
* If this was the first round, resize array and repeat for actual
* insertions; otherwise, merge and return.
*/
if (!insert) { /* 第一次执行的的时候insert == false */
while (type->cnt + nr_new > type->max)
if (memblock_double_array(type, obase, size) < 0)
return -ENOMEM;
insert = true;
goto repeat;
} else {
/* ...... */
}
memblock_double_array函数加倍给定的内存区域大小,然后把insert设为true再转到repeat标签.
第二次循环,从repeat标签开始经过同样的循环然后用memblock_insert_region函数把当前内存区域插入到内存块:
/* insert the remaining portion */
if (base < end) {
nr_new++;
if (insert)
memblock_insert_region(type, idx, base, end - base,
nid, flags);
}
由于我们在第一次循环中把insert设为true, 现在memblock_insert_region函数将会被调用
memblock_insert_region函数几乎和把新内存区域插入到空的memblock_type代码块有同样的实现, 定义在mm/memblock.c?v=4.7, line 476该函数获得最后一个内存区域:
struct memblock_region *rgn = &type->regions[idx];
然后调用memmove函数移动该内存区域:
memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn));
紧接着填充新内存区域memblock_region的base域,size域等等, 然后增大memblock_type的大小。
最后memblock_add_range函数调用memblock_merge_regions合并所有相邻且兼容的内存区域, 定义在mm/memblock.c?v=4.7, line 444
/*
* If this was the first round, resize array and repeat for actual
* insertions; otherwise, merge and return.
*/
if (!insert) {
/* ...... */
} else {
memblock_merge_regions(type);
return 0;
}
3.3 memblock_remove删除内存区域
memblock_remove用来完成删除内存区域的工作, 该函数定义在mm/memblock.c?v=4.7, line 710
// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L710
int __init_memblock memblock_remove(phys_addr_t base, phys_addr_t size)
{
return memblock_remove_range(&memblock.memory, base, size);
}
3.4 memblock_alloc申请内存
而相比来说, 申请内存的函数memblock_alloc实现方式就比较麻烦了, 如下所示
emblock_alloc(phys_addr_t size, phys_addr_t align
)其实就是在当前NODE在内存范围0-MEMBLOCK_ALLOC_ACCESSIBLE(其实是current_limit)中分配一个大小为size的内存区域.
3.4.1 memblock_alloc函数代码
// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727
phys_addr_t __init memblock_alloc(phys_addr_t size, phys_addr_t align)
{
return memblock_alloc_base(size, align, MEMBLOCK_ALLOC_ACCESSIBLE);
}
// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1192
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 0x%llx bytes below 0x%llx.\n",
(unsigned long long) size, (unsigned long long) max_addr);
return alloc;
}
// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1186
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);
}
// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1163
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);
}
memblock_alloc()很粗暴的从能用的内存里分配, 而有些情况下需要从特定的内存范围内分配内存. 解决方法就是通过memblock_alloc_range_nid函数或者实现类似机制的函数
最终memblock_alloc的也是通过memblock_alloc_range_nid函数来完成内存分配的
3.4.2 memblock_alloc_range_nid函数
下面我们就来看看memblock_alloc_range_nid函数的实现, 该函数定义在mm/memblock.c?v=4.7, line 1133
// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L1133
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后将其标为已经分配
3.4.3 memblock_find_in_range_node函数
该函数定义在mm/memblock.c?v=4.7, lien 178
// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L178
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函数去查找内存:
3.4.4 __memblock_find_range_top_down查找内存区域
最后通过__memblock_find_range_top_down函数去查找内存
// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L140
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)返回回去
至此满足要求的内存块算是找到了。
3.4.5 memblock_reserve标记申请的内存
现在我们回到memblock_alloc_range_nid函数, 我们说该函数完成了两项工作
- 首先通过memblock_find_in_range_node指定内存区域和大小查找内存区域
- 找到内存区域后, 调用memblock_reserve后将其标为已经分配
现在我们已经找到了内存区域了, 那么我们继续看看memblock_reserve函数是如何堆内存进行标记的, 该函数定义在mm/memblock.c?v=4.7, line 727
// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L727
int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size)
{
memblock_dbg("memblock_reserve: [%#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.reserved, base, size, MAX_NUMNODES, 0);
}
// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L609
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_reserve函数也是通过memblock_add_range来实现的, 我们把memblock_add的实现贴出来进行对比, 我们会发现他们就第一个参数不一样
- memblock_reserve使用全局变量memblock的reserved域, 最终将分配到的内存块信息添加到reserved区域中
- emblock_add则使用了全局变量的memory域, 最终将内存块添加到了memory区域
memblock_add_range函数的流程我们前面已经将的很详细了, 这里只简单的叙述一下子
- 如果memblock算法管理内存为空的时候,则将当前空间添加进去
- 不为空的情况下,则先检查是否存在内存重叠的情况,如果有的话,则剔除重叠部分,然后将其余非重叠的部分添加进去
- 如果出现region[]数组空间不够的情况,则通过memblock_double_array()添加新的region[]空间
- 最后通过memblock_merge_regions()把紧挨着的内存合并了
2.5 memblock_free释放内存区域
// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L695
static int __init_memblock memblock_remove_range(struct memblock_type *type,
phys_addr_t base, phys_addr_t size)
{
int start_rgn, end_rgn;
int i, ret;
ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn);
if (ret)
return ret;
for (i = end_rgn - 1; i >= start_rgn; i--)
memblock_remove_region(type, i);
}
// http://lxr.free-electrons.com/source/mm/memblock.c?v=4.7#L716
int __init_memblock memblock_free(phys_addr_t base, phys_addr_t size)
{
memblock_dbg(" memblock_free: [%#016llx-%#016llx] %pF\n",
(unsigned long long)base,
(unsigned long long)base + size - 1,
(void *)_RET_IP_);
kmemleak_free_part(__va(base), size);
return memblock_remove_range(&memblock.reserved, base, size);
}
3 兼容bootmem的接口API
3.1 memblock与bootmem
我们了解到memblock是作为bootmem的替代品而引入内核的
在编译Kernel的时候可以选择nobootmem或者bootmem 来在buddy system起来之前管理memory.
这两种机制对提供的API是一致的,因此对用户是透明的
ifdef CONFIG_NO_BOOTMEM
obj-y += nobootmem.o
else
obj-y += bootmem.o
endif
为了保证系统的兼容性, 内核为bootmem和memblock提供了相同的API接口.
3.2 memblock与bootmem接口对比
由于接口是一致的, 那么他们共同使用一份
头文件 | bootmem接口 | nobootmem接口 |
我们知道memblock自己的接口都在自己的头文件和源文件中
memblock 头文件 | memblock接口 |
为了实现接口兼容, 内核用mm/memblock.c定义的memblock接口, 实现了一套bootmem的接口机制, 而bootmem的接口我们在上一篇引导内存管理bootmem机制中已经讲过了, 这些实现的bootmem函数接口API, 就定义在mm/nobootmem.c文件中, 然后内核把他们进行了封装, 然后提供了与bootmem相同功能和函数的接口, 这些接口都在include/linux/memblock.h.
在NUMA系统上, 基本的API是相同的, 但是函数增加了_node后缀, 与UMA系统的函数相比, 还需要一些额外的参数, 用于指定内存分配的结点.
函数 | bootmem定义 | nobootmem定义 |
ZONE_NORMAL的分配函数 | 按照指定大小在ZONE_NORMAL内存域分配函数. 数据是对齐的, 这使得内存或者从可适用于L1高速缓存的理想位置开始 | |
alloc_bootmem(size) | ||
alloc_bootmem_align(size) | alloc_bootmem_align基于__alloc_bootmem实现 | |
alloc_bootmem_pages(size)) | alloc_bootmem_pages基于__alloc_bootmem实现 | alloc_bootmem_pages基于__alloc_bootmem实现 |
alloc_bootmem_nopanic(size) | ||
ZONE_DMA区域的分配函数 | ||
alloc_bootmem_low(size) | alloc_bootmem_low__alloc_bootmem_low底层基于___alloc_bootmem | alloc_bootmem_low底层基于___alloc_bootmem |
alloc_bootmem_low_pages_nopanic(size) | alloc_bootmem_low_pages_nopanic底层基于__alloc_bootmem_low_nopanic | alloc_bootmem_low_pages_nopanic底层基于__alloc_bootmem_low_nopanic |
alloc_bootmem_low_pages(size) | ||
NUMA结构的分配函数 | ||
alloc_bootmem_node(pgdat, size) | ||
alloc_bootmem_node_nopanic(pgdat, size) | ||
alloc_bootmem_pages_node(pgdat, size) | ||
alloc_bootmem_pages_node_nopanic(pgdat, size) | alloc_bootmem_pages_node_nopanic__alloc_bootmem_node_nopanic | |
alloc_bootmem_low_pages_node(pgdat, size) | alloc_bootmem_low_pages_node__alloc_bootmem_low_node___alloc_bootmem_node | alloc_bootmem_low_pages_node__alloc_bootmem_low_node___alloc_bootmem_node |
我们可以看到最基本的实现思路都是一样的, 只是最底层的实现函数有细微的区别.
3.3 实现差异
- UMA结构下这些函数最终都是通过___alloc_bootmem_nopanic函数来实现的
- NUMA架构下, 最终这些函数都是简介的调用___alloc_bootmem_node_nopanic函数来实现的,
函数 | bootmem | memblock |
___alloc_bootmem_nopanic | ||
___alloc_bootmem_node_nopanic | mm/bootmem.c?v=4.7, line 708, 通过调用alloc_bootmem_core和alloc_bootmem_bdata来实现 | mm/nobootmem.c?v=4.7, line 317, 通过调用__alloc_memory_core_early来实现 |
bootmem的核心函数__alloc_memory_core()的实现机制我们前一篇博文引导分配器bootmem已经讲过了, 那么memblock下nobootmem的核心函数__alloc_memory_core_early是怎么实现的呢?
前面2.4.1节memblock_alloc函数代码我们分析memblock_alloc函数的时候提到, 该函数最终通过memblock_alloc_range_nid函数粗暴粗暴的进行内存分配, 而有些情况下需要从特定的内存范围内分配内存. 解决方法就是通过memblock_alloc_range_nid函数或者实现类似机制的函数, 这里的__alloc_memory_core_early函数就是基于memblock_alloc_range_nid同样的思路实现的函数
- 首先使用memblock_find_in_range_node指定内存区域和大小查找内存区域
- memblock_reserve后将其标为已经分配
我们列出 memblock_alloc_range_nid函数与__alloc_memory_core_early函数的实现对比
memblock_alloc_range_nid | __alloc_memory_core_early |
4 memblock初始化
如果从整个linux生命周期来讲,涉及到各种初始化等,这里来详细分析,因为还没有分析完内核,所以这里是分析到哪里就记录到哪里了.
4.1 x86架构下的memblock初始化
要理解memblock是如何工作和实现的, 我们首先看一下它的用法.
在Linux内核中有几处用到了memblock, 例如 arch/x86/kernel/e820.c中的函数memblock_x86_fill
. 该函数遍历由e820
提供的内存映射表并且通过memblock_add
函数把内核预留的内存区域添加到memblock。既然我们首先遇到了memblock_add
函数,那就从它开始吧。
在内核初始化初期,物理内存会通过Int 0x15
来被探测和整理, 存放到e820
中.而初始化就发生在这个以后. 参见arch/x86/kernel/setup.c?v=4.7, line 1096
void __init setup_arch(char **cmdline_p)
{
/*
* Need to conclude brk, before memblock_x86_fill()
* it could use memblock_find_in_range, could overlap with
* brk area.
*/
reserve_brk();
cleanup_highmap();
memblock_set_current_limit(ISA_END_ADDRESS);
memblock_x86_fill();
}
首先内核建立内核页表需要扩展__brk, 而扩展后的brk就立即被声明为已分配. 这项工作是由reserve_brk通过调用memblock_reserve完成的, 而其实并不是正真通过memblock分配的, 因为此时memblock还没有完成初始化
reserve_brk函数定义在arch/x86/kernel/setup.c?v=4.7, line 209, 此时memblock还没有初始化, 只能通过memblock_reserve来完成内存的分配
static void __init reserve_brk(void)
{
if (_brk_end > _brk_start)
memblock_reserve(__pa_symbol(_brk_start),
_brk_end - _brk_start);
/* Mark brk area as locked down and no longer taking any
new allocations */
_brk_start = 0;
}
设置完__brk后, 可以看到,setup_arch()函数通过memblock_x86_fill(),依据e820中的信息来初始化memblock.
void __init memblock_x86_fill(void)
{
int i;
u64 end;
/*
* EFI may have more than 128 entries
* We are safe to enable resizing, beause memblock_x86_fill()
* is rather later for x86
*/
memblock_allow_resize();
for (i = 0; i < e820.nr_map; i++) {
struct e820entry *ei = &e820.map[i];
end = ei->addr + ei->size;
if (end != (resource_size_t)end)
continue;
if (ei->type != E820_RAM && ei->type != E820_RESERVED_KERN)
continue;
memblock_add(ei->addr, ei->size);
}
/* throw away partial pages */
memblock_trim_memory(PAGE_SIZE);
memblock_dump_all();
}
比较简单,通过e820中的信息memblock_add(),将内存添加到memblock中的memory中,当做可分配内存.后两个函数主要是修剪内存使之对齐和输出信息.
至此, 我们的memblock就初始化好了, 简单而且粗暴
4.2 arm架构下的memblock初始化
arm下的memblock初始化也是从start_kernel()->setup_arch()开始的, 在setup_arch()中arm架构通过arm_memblock_init完成了memblock的初始化工作.
void __init setup_arch(char **cmdline_p)
{
arm_memblock_init(mdesc);
}
arm_memblock_init定义在arch/arm/mm/init.c, 如下所示
void __init arm_memblock_init(const struct machine_desc *mdesc)
{
/* Register the kernel text, kernel data and initrd with memblock. */
#ifdef CONFIG_XIP_KERNEL
memblock_reserve(__pa(_sdata), _end - _sdata);
#else
memblock_reserve(__pa(_stext), _end - _stext);
#endif
#ifdef CONFIG_BLK_DEV_INITRD
/* FDT scan will populate initrd_start */
if (initrd_start && !phys_initrd_size) {
phys_initrd_start = __virt_to_phys(initrd_start);
phys_initrd_size = initrd_end - initrd_start;
}
initrd_start = initrd_end = 0;
if (phys_initrd_size &&
!memblock_is_region_memory(phys_initrd_start, phys_initrd_size)) {
pr_err("INITRD: 0x%08llx+0x%08lx is not a memory region - disabling initrd\n",
(u64)phys_initrd_start, phys_initrd_size);
phys_initrd_start = phys_initrd_size = 0;
}
if (phys_initrd_size &&
memblock_is_region_reserved(phys_initrd_start, phys_initrd_size)) {
pr_err("INITRD: 0x%08llx+0x%08lx overlaps in-use memory region - disabling initrd\n",
(u64)phys_initrd_start, phys_initrd_size);
phys_initrd_start = phys_initrd_size = 0;
}
if (phys_initrd_size) {
memblock_reserve(phys_initrd_start, phys_initrd_size);
/* Now convert initrd to virtual addresses */
initrd_start = __phys_to_virt(phys_initrd_start);
initrd_end = initrd_start + phys_initrd_size;
}
#endif
arm_mm_memblock_reserve();
/* reserve any platform specific memblock areas */
if (mdesc->reserve)
mdesc->reserve();
early_init_fdt_reserve_self();
early_init_fdt_scan_reserved_mem();
/* reserve memory for DMA contiguous allocations */
dma_contiguous_reserve(arm_dma_limit);
arm_memblock_steal_permitted = false;
memblock_dump_all();
}
4.3 arm64下的memblock初始化
前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作.
void __init setup_arch(char **cmdline_p)
{
/* 初始化memblock */
arm64_memblock_init( );
/* 分页机制初始化 */
paging_init();
bootmem_init();
}
流程 | 描述 |
初始化memblock内存分配器 | |
初始化分页机制 | |
初始化内存管理 |
其中arm64_memblock_init就完成了arm64架构下的memblock的初始化
与arm架构类似, arm64的memblock初始化没有意外, 只是初始化函数成为arm64_memblock_init(), 该函数定义在arch/arm64/mm/init.c?v=4.7, line 192
5 总结
memblock内存管理是将所有的物理内存放到memblock.memory
中作为可用内存来管理, 分配过的内存只加入到memblock.reserved
中, 并不从memory
中移出.
同理释放内存也会加入到memory
中. 也就是说, memory
在fill
过后基本就是不动的了. 申请和分配内存仅仅修改reserved
就达到目的. 在初始化阶段没有那么多复杂的内存操作场景, 甚至很多地方都是申请了内存做永久使用的, 所以这样的内存管理方式已经足够凑合着用了, 毕竟内核也不指望用它一辈子. 在系统完成初始化之后所有的工作会移交给强大的buddy
系统来进行内存管理.