内存管理是一个复杂的过程,操作系统对内存的管理方式在启动阶段与运行阶段是不同的。
在启动阶段,系统程序已被加载到内存并开始执行,但是,此时并不存在能过管理内存的模型。
因此,操作系统需要根据当前情况对内存区域进行划分,避免系统程序再执行过程中发生冲突。
内存区域的划分主要围绕两方面,分别是:
内存区域的划分与管理,Linux 采用 mem_block 结构来完成。
mem_block 将内存划分为三种不同的类型,分别是:memroy,reserver,physmem。
具体的组织结构如下:
//整块内存可以看作多个块状内存的组合,内核创建 memblock_region 来描述块状内存。
struct memblock_region {
phys_addr_t base; //基地址
phys_addr_t size; //大小
enum memblock_flags flags;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
int nid; //所属节点号
#endif
};
//每个块状内存的作用是不同的,为了区分,内核创建 memblock_type 将作用相同的块状内存进行关联。
struct memblock_type {
unsigned long cnt; //区域个数
unsigned long max; //数组大小
phys_addr_t total_size; //所有区域的空间大小总和
struct memblock_region *regions;
char *name;
};
//内核创建 memblock 管理不同作用的 memblock_type,从而间接的管理memblock_region。
struct memblock {
bool bottom_up;
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
#ifdef CONFIG_HAVE_MEMBLOCK_PHY_MAP
struct memblock_type physmem;
#endif
};
从上述结构体中可看出内存目前的层级结构如图所示:
+----------------------------------------------------------------------------------------+
| memblock |
+----------------------------------------------------------------------------------------+
| memory | reserved | physmem |
+----------------------------------+--------------------------+--------------------------+
|
|
-----------------------------------------------------------------------
| | |
+--------------+ +--------------+ +-----------+
| region | | region | | region |
+--------------+ +--------------+ +-----------+
| | |
+---------------------------------------------------------------------------------------+
| real memory |
+---------------------------------------------------------------------------------------+
关于内存区域的划分,主要根据处理器架构而定。
以 mips 为例,内核对 mem_block 结构的定义如下:
static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
//memory:128
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_RESERVED_REGIONS] __initdata_memblock;
//reserved:128
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock;
//physmem:4
#endif
#define __initdata_memblock __meminitdata
#define __meminitdata __section(".meminit.data")
struct memblock memblock __initdata_memblock = {
.memory.regions = memblock_memory_init_regions,
.memory.cnt = 1,
.memory.max = INIT_MEMBLOCK_REGIONS,
.memory.name = "memory",
.reserved.regions = memblock_reserved_init_regions,
.reserved.cnt = 1,
.reserved.max = INIT_MEMBLOCK_RESERVED_REGIONS,
.reserved.name = "reserved",
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
.physmem.regions = memblock_physmem_init_regions,
.physmem.cnt = 1,
.physmem.max = INIT_PHYSMEM_REGIONS,
.physmem.name = "physmem",
#endif
.bottom_up = false,
.current_limit = MEMBLOCK_ALLOC_ANYWHERE,
};
mem_block 创建成功后,内核便可通过 memblock 对内存进行管理。
以向 memory 类型的 mem_block 添加对象为例,分析 mem_block 结构的应用。
//内核提供了相关的mem_block接口来对内存进行相关的操作,比如memblock_add()函数。
int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size)
{
return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0);
}
//该函数向类型为memory的mem_block中添加新的空间区域
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;
struct memblock_region *rgn;
if (!size)
return 0;
//当type->regions[0].size == 0时,意味着此时内存还没有进行区域划分,所以执行该分支代码
//直接将这段内存区域存入struct memblock_region memory结构体数组中的第一个元素。
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;
//该函数为该区域设置相对应的NUMA节点。type->regions[0].nid = 64。
memblock_set_region_node(&type->regions[0], nid);
type->total_size = size;
return 0;
}
repeat:
base = obase;
nr_new = 0;
//遍历struct memblock_regions memblock_memory_init_regions[]数组。
//遍历每一个区域的目的是为了避免区域之间的冲突。关于已添加区域与待添加区域,它们之间存在的情况有5种,如下:
// rbase----------------------------rend
// 1: base---end
// 2: base------------end
// 3: base--------end
// 4: base------------end
// 5: base-------end
for_each_memblock_type(idx, type, rgn) {
phys_addr_t rbase = rgn->base;
phys_addr_t rend = rbase + rgn->size;
//当前区域的基地址大于添加区域的结束地址时,第一种情况,直接退出循环,因为此时添加的区域不与任何其他区域相冲突。
//由此可知,struct memblock_region结构体的关联顺序是按照地址从小到大的顺序来连接。
if (rbase >= end)
break;
//当前区域的结束地址小于添加区域的基地址时,第五种情况,即当前区域不会与添加区域发生冲突,因此可以直接遍历下一个区域。
if (rend <= base)
continue;
//当前区域的基地址大于待添加区域的基地址时,第二种情况,如果insert为真,此时添加前半部分。
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);
}
//第三种和第四种情况,修改起始地址,第三种可认为其后半部分为0,不再做任何处理。
base = min(rend, end);
}
//第四种情况,base发生改变,只添加待添加区域的后半部分。
if (base < end) {
nr_new++;
if (insert)
memblock_insert_region(type, idx, base, end - base,
nid, flags);
}
if (!nr_new)
return 0;
//根据变量insert,可以将该过程看作两轮执行。第一轮是对memblock数组的插入与重新排序,第二轮是将首尾相接的memblock_region进行合并。
if (!insert) {
//当前类型的memblock个数与新增个数的和大于最大值时,执行该循环条件
while (type->cnt + nr_new > type->max)
if (memblock_double_array(type, obase, size) < 0)
return -ENOMEM;
insert = true;
goto repeat;
} else {
//合并该类型的memblock_region。即当同样类型的两个区域出现首尾相接的情况时,合并这两个region区域。
memblock_merge_regions(type);
return 0;
}
}
//memblock_insert_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);
//struct memblock_region 结构体按照地址大小来排序,因此当插入新区域时,需要将插入位置及后边的区域推后。
//memblock_region 是数组结构,所以 &type->regions[idx + 1] - &type->regions[idx] = rgn + 1 - rng。
//memmove 将 rgn 地址开始的 memblock_region 复制到 rgn+1 地址开始的内存空间。
memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn));
//对原有的 rgn 结构体以及 type 结构体重新进行赋值。
rgn->base = base;
rgn->size = size;
rgn->flags = flags;
memblock_set_region_node(rgn, nid);
type->cnt++;
type->total_size += size;
}
随着处理器的性能提升,多处理器被广泛的使用。为了解决多处理器访问内存时发生冲突的问题,Linux 提供了 NUMA(非一致性访问)机制。使每一个处理器对应一段独立的内存空间,这样的一个组织结构称为NUMA节点。因此,利用 mem_block 对内存区域的划分此时变为了对每个 numa 节点的区域划分,即从(CPU<>MEM)变为了(CPU1<>MEM1; CPU2<==>MEM2 …)的形式。
以 mips 架构的龙芯处理器为例:
static __init void prom_meminit(void)
{
unsigned int node, cpu, active_cpu = 0;
cpu_node_probe();
init_topplogy_matrix(); //初始NUMA节点拓扑矩阵
for (node = 0; node < loongson_sysconf.nr_nodes; node++) {
//遍历NUMA节点,并初始化NUMA节点
if (node_online(node)) {
szmem(node);
node_mem_init(node);
cpumask_clear(&__node_cpumask[node]);
}
}
memblocks_present();
max_low_pfn = PHYS_PFN();
for (cpu = 0; cpu < loongson_sysconf.nr_cpus; cpu++) {
node = cpu / loongson_sysconf.cores_per_node;
if (node >= num_online_nodes())
node = 0;
if (loongson_sysconf.reserved_cpus_mask & (1 << cpu))
continue;
cpumask_set_cpu(active_cpu, &__node_cpumask[node]);
pr_info("NUMA: set cpumask cpu %d on node %d\n", active_cpu, node);
active_cpu++;
}
}
static void __init szmem(unsigned int node)
{
u32 i, mem_type;
static unsigned long num_physpages;
u64 node_id, node_psize, start_pfn, end_pfn, mem_start, mem_size;
for (i = 0; i < loongson_memmap->nr_map; i++) {
node_id = loongson_memmap->map[i].node_id;
if (node_id != node)
continue;
mem_type = loongson_memmap->map[i].mem_type; //获取NUMA节点的内存类型
mem_size = loongson_memmap->map[i].mem_size; //获取NUMA节点的内存大小
mem_start = loongson_memmap->map[i].mem_start; //获取NUMA节点的内存起始地址
switch (mem_type) {
case SYSTEM_RAM_LOW: //当前内存如果为低端内存
start_pfn = ((node_id << 44) + mem_start) >> PAGE_SHIFT; // 起始页号
node_psize = (mem_size << 20) >> PAGE_SHIFT;
end_pfn = start_pfn + node_psize;
num_physpages += node_psize; //物理页个数
pr_info("Node%d: mem_type:%d, mem_start:0x%llx, mem_size:0x%llx MB\n", (u32)node_id, mem_type, mem_start, mem_size);
pr_info(" start_pfn:0x%llx, end_pfn:0x%llx, num_physpages:0x%lx\n", start_pfn, end_pfn, num_physpages);
memblock_add_node(PFN_PHYS(start_pfn), PFN_PHYS(end_pfn - start_pfn), node); //添加内存区域到mem_block中,并关联所对应的NUMA节点。
break;
case SYSTEM_RAM_HIGH:
start_pfn = ((node_id << 44) + mem_start) >> PAGE_SHIFT;
node_psize = (mem_size << 20) >> PAGE_SHIFT;
end_pfn = start_pfn + node_psize;
num_physpages += node_psize;
pr_info("Node%d: mem_type:%d, mem_start:0x%llx, mem_size:0x%llx MB\n", (u32)node_id, mem_type, mem_start, mem_size);
pr_info(" start_pfn:0x%llx, end_pfn:0x%llx, num_physpages:0x%lx\n", start_pfn, end_pfn, num_physpages);
memblock_add_node(PFN_PHYS(start_pfn), PFN_PHYS(end_pfn - start_pfn), node);
break;
case SYSTEM_RAM_RESERVED:
pr_info("Node%d: mem_type:%d, mem_start:0x%llx, mem_size:0x%llx MB\n", (u32)node_id, mem_type, mem_start, mem_size);
memblock_reserve(((node_id << 44) + mem_start), mem_size << 20);
break;
}
}
}
int __init_memblock memblock_add_node(phys_addr_t base, phys_addr_t size, int nid)
{
return memblock_add_range(&memblock.memory, base, size, nid, 0);
//添加的内存区域类型为memory,但绑定了 cpu 节点编号
}
static void __init node_mem_init(unsigned int node)
{
unsigned long node_addrspace offset;
unsigned long start_pfn, end_pfn;
node_addrspace_offset = nid_to_addroffset(node); //获取节点偏移量
pr_info("Node%d's addrspace_offset is 0x%lx\n", node, node_addrspace_offset);
get_pfn_range_for_nid(node, &start_pfn, &end_pfn); //获取起始页号,与末尾页号
pr_info("Node%d: start_pfn=0x%lx, end_pfn=0x%lx\n", node, start_pfn, end_pfn);
__node_data[node] = prealloc__node_data + node;
NODE_DATA(node)->node_start_pfn = start_pfn;
NODE_DATA(node)->node_spanned_pages = end_pfn - start_pfn; //页号个数
if (node == 0) {
unsigned long kernel_end_pfn = PFN_UP(__pa_symbol(&_end)); //内核代码的末尾页号
max_low_pfn = end_pfn; //节点的末尾页号
memblock_reserve(start_pfn << PAGE_SHIFT, ((kernel_end_pfn - start_pfn) << PAGE_SHIFT));
//创建预留空间,该部分空间主要用来存放内核代码
if (node_end_pfn(0) >= (0xffffffff >> PAGE_SHIFT))
memblock_reserve((node_addrspace_offset | 0xfe000000), 32 << 20);
}
}
当 numa 节点的内存区域划分,以及初始化完成后,便可以通过 mem_block 添加或申请内存区域。关于NUMA节点地址空间的起始与结尾,是由固件参数传递给内核的。
根据上边的分析,可推测内核在启动阶段会对 reserve 类型的内存进行保护,期间内存的申请与释放可能主要在 memory 类型的内存空间中完成。
至此,操作系统已大致了解了内存的使用情况以及区域的划分,接下来,操作系统将逐步的将内存管理递交给“页式管理”。
void __init setup_arch(char **cmdline_p)
{
arch_mem_init(cmdline_p);
//根据cpu的架构来对内存进行初始化。其内部调用函数plat_mem_setup(),该函数的主要目的是处理与平台相关的内存。
paging_init();
}
上述分析,如有错误请指出。