目录


Early_Res 内存分配器原理

Linux 内核在启动阶段从 BIOS/CMDLINE 中获得物理内存的相关信息,并将这些内存信息依托 E820 内存管理器进行管理。内核启动到 boot-time 阶段,E820 内存管理器将系统可用物理内存区域的信息传递给 Early_Res 分配器,Early_Res 分配器获得可用物理内存信息之后,构建并初始化 Early_Res 分配器,并在接下来的阶段为内核其他模块提供物理内存的分配、回收、以及物理内存区域的预留功能。当系统初始化到一定阶段之后,Early_Res 内存分配器将可用的物理内存转移给 Buddy 分配器,以便 Buddy 内存分配器的构建,当传递完毕之后,Early_Res 分配器的历史任务已经完成. 具体代码流程请参考:

Early_Res 架构逻辑分析

Early_Res 分配器的实现很简单,Early_Res 分配器一共维护两个数组,第一个数组名为 “early_node_map”, 这个数组由 struct node_active_region 数据结构构成。内核在初始化的时候,E820 内存管理器将当前系统可用的物理内存区域信息传递给 Early_Res 分配器,每个可用内存分配器使用 early_node_map[] 数组的一个成员,nr_nodemap_entries 变量用于描述 Early_Res 分配器中维护可用物理内存区域的数量; Early_Res 分配器维护的第二个数组就是 “early_res” 数组,这个数组由 struct early_res 数据结构构成。当 Early_Res 分配器新增一个预留区,那么 early_res 数组就会将预留区的信息填充到指定成员里,early_res_count 变量维护 early_res[] 数组已经使用成员的数量。Early_Res 分配器对物理内存的描述都使用区域进行描述,即通过起始物理地址、长度、结束地址等信息进行描述。

Early_Res 分配器基于上面的逻辑架构实现了物理内存的分配、回收和预留。当需要从 Early_Res 分配器中分配物理内存时,Early_Res 分配器首先从 early_node_map[] 数组中遍历所有的成员,直到找到一个成员,该成员包含了指定分配范围的物理内存区域,接着函数在这个物理内存区域查找一块符合要求的物理内存区域,且这块物理内存区域不在预留区内,那么系统直接将这段物理内存对应的虚拟地址返回给调用者; 如果期初查找到的物理内存区域已经在预留区内,那么 Early_Res 分配器继续在 early_node_map 数组中查看其他可用的物理内存区域。当使用完从 Early_Res 分配器中分配的物理内存,那么将这些物理内存归还给 Early_Res 分配器,Early_Res 分配器仅仅从 early_res[] 数组中将这段物理内存信息抹除即可. 最后当需要预留一段物理内存,仅需将预留的物理内存信息填充在 early_res[] 数组中即可。Early_Res 分配器分配、释放、预留具体使用方法请参考:

Early_Res 分配器的使用

Early_Res 分配器的存在与 Bootmem 是互斥的,因此两个分配器之间一定存在不可调和的差异,开发者可以根据自身的实际情况选择使用,两者的比较总结如下:

相同点

在 boot-time 阶段为系统其他功能提供物理内存的分配、回收和预留功能。都是从 E820 内存管理器处获得可用物理内存信息,并将这些信息用于构建本身分配器,当系统初始化到一定阶段,两个分配器都会将系统可用物理内存通过 struct page 的方式传递给 Buddy 内存分配器,而预留内存则不会传递给 Buddy 内存分配器。两个分配器对外接口可以兼容,其他内核子系统在 boot-time 阶段感知不到底层内存分配器是 Early_Res 提供还是 Bootmem 提供,但都可以使用统一接口进行内存分配 回收和预留。

不同点

Bootmem 分配器使用 bitmap 进行管理,其会为低端物理内存的每个 PAGE_SIZE 长度的物理内存区域分配一个 bit,当低端物理内存特别巨大的时候,且 boot-time 阶段物理内存分配获得不是很频繁,而且分配的物理内存也不多,这样大面积的 bitmap 势必造成物理内存的浪费。Early_Res 分配器则使用内存区域的概念管理物理内存区域,好处是在分配不是很频繁和分配的物理内存不是很多的情况下,Early_Res 分配器管理数据方面本省会占用很少的物理内存。最后 Early_Res 分配器的初始化早于 Bootmem 分配器.


Early_Res 内存分配器的使用

Early_Res 分配器和 Bootmem 分配器是互斥存在的,因此 Early_Res 分配器也要提供 Bootmem 分配器能提供的基础内存分配器行为:

> 分配一段物理内存

> 释放一段物理内存

> 预留一段物理内存


分配一段物理内存

Early_Res 内存分配器在 Boot Stage 阶段提供了接口用于不同场景的物理内存分配,由于 Early_Res 内存分配器互斥,因此一些 Bootmem 的分配接口也可以用于从 Early_Res 分配器中分配物理内存,不过 Early_Res 分配器也提供的独立的接口用于物理内存分配,具体接口如下:

find_early_area

find_early_area_size

alloc_bootmem

alloc_bootmem_low

alloc_bootmem_low_pages

alloc_bootmem_low_pages_node

alloc_bootmem_node

alloc_bootmem_pages

alloc_bootmem_pages_node

alloc_bootmem_pages_node_nopanic

alloc_bootmem_pages_nopanic

alloc_bootmem_nopanic

使用 Early_Res 分配物理内存,可以使用兼容 Bootmem 分配器提供的接口。同理 Bootmem 内存分配器相关的接口可以参考如下进行使用:

Bootmem 内存分配分配物理内存

在内核 Boot-time 阶段通过 Early_Res 分配器分配一段物理内存,可以参考如下代码 (这里 Boot Stage 阶段特指 start_kernel()->mm_init() 函数之前>的阶段):

#include <linux/bootmem.h>

static int BiscuitOS_Demo(void)
{
        char *buffer = NULL;
        int size = 0x20;

        /* Alloc memory from Early_Res */
        buffer = (char *)alloc_bootmem(size);
        if (!buffer) {
                printk("ERROR: Alloc memory failed.\n");
                return -ENOMEM;
        }

        /* Use */
        sprintf(buffer, "BiscuitOS-%s", "Buddy");
        printk("=> %s\n", buffer);

        return 0;
}

在上面例子中,调用 alloc_bootmem() 函数从 Bootmem 分配器中分配长度为 0x20 的物理内存,并返回物理内存对应的虚拟地址,然后使用这段内存存储一段字符串,并使用 printk 打印这段字符串。如果将这个函数放到 start_kernel() 函数里面 mm_init() 的前面进行调用,那么运行结果如下:


释放一段物理内存

当 Boot-time 阶段从 Early_Res 分配器中分配一段物理内存之后,当使用完毕之后,需要将这段物理内存归还给 Early_Res 分配器。Early_Res 提供了用于物理内存回收的函数,开发者可以参考如下代码进行使用 (这里 Boot Stage 阶段特指 start_kernel()->mm_init() 函数之前>的阶段):

#include <linux/bootmem.h>

static int BiscuitOS_Demo(void)
{
        char *buffer = NULL;
        int size = 0x20;

        /* Alloc memory from Early_Res */
        buffer = (char *)alloc_bootmem(size);
        if (!buffer) {
                printk("ERROR: Alloc memory failed.\n");
                return -ENOMEM;
        }

        /* Use */
        sprintf(buffer, "BiscuitOS-%s", "Buddy");
        printk("=> %s\n", buffer);

	/* Free */
	free_early(__pa(buffer), __pa(buffer) + size);

        return 0;
}

在上面例子中,调用 alloc_bootmem() 函数从 Bootmem 分配器中分配长度为 0x20 的物理内存,并返回物理内存对应的虚拟地址,然后使用这段内存存储一段字符串,并使用 printk 打印这段字符串。如果将这个函数放到 start_kernel() 函数里面 mm_init() 的前面进行调用,那么运行结果如下:


预留一段物理内存

当 Boot-time 阶段从 Early_Res 分配器中预留一段物理内存,这段物理内存就会被加入到 Early_Res 分配器的预留区内。Early_Res 分配器提供了独立的函数实现该功能,也可以使用 Bootmem 分配器提供的接口进行预留。Early_Res 提供了用于物理内存预留的函数,>开发者可以参考如下代码进行使用 (这里 Boot Stage 阶段特指 start_kernel()->mm_ini t() 函数之前>的阶段):

#include <linux/bootmem.h>

static int BiscuitOS_Demo(void)
{
	u64 start = 0x10000000;
	u64 size  = 0x100000;
	u64 end   = start + size;

	/* Reserve memory */
	reserve_early(start, end, "BiscuitOS");

        return 0;
}

在上面的例子中,调用 reserve_early() 函数将物理内存区域 0x10000000 - 0x10100000 进行了预留. 可以将这个函数放到 start_kernel() 函数里面 mm_init() 的前面进行调用。


Early_Res 分配器实践


实践准备

本实践只支持 i386 架构和 X86_64 架构,其只支持低版本内核,本文以 linux 2.6.36 i386 架构进行讲解,至于 x86 架构实践可以参考 i386 进行实践. 首先开发者需要搭建基于 i386 结构的 linux 2.6.36 实践环境,请参考如下问题:

> BiscuitOS Linux 2.x i386 Usermanual

> BiscuitOS Linux 2.x x86_64 Usermanual


实践部署

在部署完毕开发环境之后,开发者应该确保一下内核宏是打开的,参考如下:

cd BiscuitOS/output/linux-2.6.36-i386/linux/linux
make ARCH=i386 menuconfig

  Processor type and features  --->
    [*] Disable Bootmem code

开发者一定要确认上面的选项是开启的的,确认修改之后重新编译内核使用。


实践执行

环境部署完毕之后,开发者可以直接运行 BiscuitOS, 此时 boot-time 阶段使用的就是 Early_Res 分配器. 运行的情况,使用如下命令:

cd BiscuitOS/output/linux-5.0-i386/
./RunBiscuitOS.sh


Early_Res 分配器源码分析


Early_Res 分配器架构

Early_Res 分配器与 Bootmem 分配器是互斥存在的,当系统启用的 CONFIG_NO_BOOTMEM 宏之后,X86 架构的系统将在 Buddy 分配器初始化之前的最后一个物理内存分配器。与 Bootmem 分配器一样,Early_Res 分配器从 E820 内存管理器中获得可用物理内存信息之后,基于这些信息构建 Early_Res 分配器,并为系统其他模块提供物理内存的分配、回收、以及预留作用。内核初始化到 mem_init() 函数之后,Early_Res 分配器将所有可用的物理内存传递给 Buddy 分配器,至此 Early_Res 分配器的任务完成。

> initmem_init

> mem_init

> free_all_memory_core_early

> get_free_all_memory_range

> __free_pages_memory

X86 架构内核初始化到 initmem_init() 函数,无论支持高端物理内存,在 E820 内存管理器初始化完毕之后,函数通过调用 e820_register_active_regions() 函数将所有可用的物理内存区域加入到 Early_Res 分配器的 early_node_map[] 数组,该数组就是 Early_Res 分配器的核心,数组包含了多个可用的物理内存区域。函数执行完毕之后,Early_Res 分配器已经获得系统所有可用物理内存信息,接下来 Early_Res 分配器为系统其他模块提供物理内存的分配、回收和预留功能。

add_active_range

> find_early_area: 从 Early_Res 分配器中分配物理内存

> free_early: 释放物理内存到 Early_Res 分配器

> reserve_early: 在 Early_Res 分配器预留一块物理内存

内核初始化到一定阶段之后,函数在 mem_init() 函数内会将 Early_Res 分配器管理的可用物理内存转移给 Buddy 内存分配器. 如上图,函数调用 free_all_bootmem() 函数实现转换。

在 free_all_bootmem 函数里,Early_Res 分配器调用 free_all_memory_core_early() 函数将可用物理内存转移给 Buddy 分配器.

在 free_all_memory_core_early() 函数中,函数首先调用 get_free_all_memory_range() 函数将 Early_Res 内存分配器管理的物理内存转换为 struct range 进行维护,然后将 Early_Res 分配器中的预留区从 struct range 维护的区域中移除,最后在对所有 range 进行排序,最后返回 range 数组.

get_free_all_memory_range() 函数主要用于从 Early_Res 分配器中获得最终可以的物理内存。函数首先通过 find_fw_memmap_area() 函数通过 Early_Res 分配器分配一块物理内存用于存储系统 struct range 数组.

get_free_all_memory_range() 函数接着在 430 行调用 add_from_early_node_map() 函数将 Early_Res 分配器中所有可用物理内存区域填充到 struct range 数组里,接着调用 subtract_early_res() 函数将 Early_Res 分配器中预留区中的物理内存区域从 struct range 数组中对应的区域移除,移除完毕之后调用 clean_sort_range() 函数清零一下 struct range 数组和重新排序 struct range 数组。最后函数在 438-443 行将 Early_Res 分配器的预留区域全部清除,最后将 struct range 数组的地址存储到 rangep 参数,并返回 struct range 数组的个数。

__free_pages_memory() 函数的作用就是将 Early_Res 分配器中最后获得可以物理内存存储在 struct range 数组中,该数组的每个成员就是一段可用的物理内存区域,函数将每段可用物理内存区域最终通过 pfn_to_page() 函数获得对应的 struct page 结构,最后调用 __free_pages_bootmem() 函数将这些物理页传递给 Buddy 物理内存分配器。至此,Early_Res 分配器任务完成。


early_res_count

static int early_res_count __initdata;

early_res_count 变量用于描述 Early_Res 分配器的预留区域中预留区的数量.


early_res

static struct early_res early_res_x[MAX_EARLY_RES_X] __initdata;

static struct early_res *early_res __initdata = &early_res_x[0];

early_res 数组用于存储 Early_Res 分配器的所有预留内存区。


struct early_res

struct early_res 数据结构用于描述 Early_Res 分配器的预留区中的一个预留物理内存区域。start 和 end 成员用于描述预留区域的范围,成员 name 用于描述预留区的名字,成员 overlap_ok 用于描述预留区是否可以重叠,1 代表支持重叠,0 代表不支持重叠.


find_early_area_size

find_early_area_size() 函数用于从 Early_Res 分配器中找到一块可用的物理内存。参数 ei_start 和 ei_last 表示一块可用的物理内存区域,start 和 sizep 参数用于指向需要查找的一块物理内存,align 参数用于对齐。

函数在 574-577 行用于重新定位查找区域的范围,函数 579 行用于重新计算 sizep 的长度,即一块存在物理内存的范围,接着函数调用 while 循环,并在循环中调用 bad_addr_size() 函数在指定的物理内存区域范围内查找一块可用的物理内存,如果这块物理内存已经预留,那么函数继续查找下一块可用的物理内存,知道遍历完所有可用物理内存为止。当遍历结束之后,函数在 582 行计算找到的一块区域的结束地址,如果 last 地址小于当前物理内存区域的结束地址,那么函数将返回找到的起始物理地址。反之返回错误。

bad_addr_size


bad_addr_size

bad_addr_size() 函数用于确认某个预留区的范围是否改变,并获得最新的范围。参数 addrp 和 sizep 参数用于指向一个预留区范围,align 参数用于对齐。函数首先在 510 和 511 行获得期望预留区的范围,然后在 514 行获得期望预留区的结束地址。函数使用 for 循环遍历 Early_Res 分配器预留区域内的所有预留区,每当遍历一个预留区,如果遍历到的预留区与期盼的预留区正好重合,那么函数将 sizep 的值加 1 并返回; 反之遍历到的预留区和期盼的预留区存在部分重合,那么函数重新计算 size 的值,并更新 addr 的值,然后将 changed 设置为 1,以此表示与期盼的预留区已经被修改,接着函数跳转到 again 处重新查找。当遍历完所有的预留区之后,如果 changed 的值为 1,那么表示期盼的预留区范围已经改变,因此此时将预留区新的地址范围更新到 addrp 和 sizep 指针上,最后返回 changed 的值。


reserve_early

reserve_early() 函数用于将一段物理内存进行预留。参数 start 和 end 用于指明一段需要预留的内存区域,name 参数用于指明预留区域的名字。

函数在 294 行调用 __check_and_double_early_res() 函数用于检测 Early_Res 分配器的预留区是否能容纳更多的预留区,如果不能就将原有的预留区数量扩大两倍。函数在 296 行调用 drop_overlaps_that_are_ok() 函数将 Early_Res 分配器预留区与 start 和 end 重叠的预留区拆分成重叠部分和不重叠部分,然后将原始预留区删除之后将不重叠部分重新插入到 Early_Res 分配器的预留区内。函数最后调用 __reserve_early() 函数将参数对应的预留区插入到 Early_Res 分配器的预留区内,并将新插入的预留区标记为不可重叠.

> __check_and_double_early_res

> drop_overlaps_that_are_ok

> __reserve_early


reserve_early_overlap_ok

reserve_early_overlap_ok() 函数用于确认一段内存区域是否重叠,如果重叠将原始区域进行切割,将新生成的区域重新插入到预留区内。参数 start 和 end 指向即将预留的内存区域。

函数首先在 128 行调用 for 循环遍历 Early_Res 分配器的所有预留区,如果遍历到的预留区与参数对应的区域不存在重叠部分,那么函数在 133 行进行返回; 反之遍历到的预留区与参数对应的区域存在重叠部分,那么函数继续检测遍历到的预留区是否支持重叠,如果不支持,那么函数在 141 行返回; 反之支持重叠,那么函数在 151 行拷贝预留区的名字到 name 遍历,并且在 153-162 行将遍历到的预留区和插入的预留区域重叠之后,被分成多个新的区域,那么函数将当前遍历到的预留区从 Early_Res 分配器的预留区内移除,然后将前部分不重叠和后部分不重叠区域通过 reserve_early_overlap_ok() 函数插入到 Early_Res 分配的预留区内。

reserve_early_overlap_ok


reserve_early_overlap_ok

reserve_early_overlap_ok() 函数用于将一段内存区域插入到 Early_Res 分配器的预留区内。参数 start 和 end 指向即将插入的预留区内,name 参数指明预留区的名字。

函数在 222 行调用 drop_overlap_that_are_ok() 函数确认 start 和 end 参数对应内存区域在 Early_Res 分配器的预留区内没有重叠,如果重叠,将重叠的部分独立成新的预留区; 如果没有重叠,那么将新的区域通过 __reserve_early() 函数将 start 到 end 的区域插入到 Early_Res 分配器的预留区内,并将新预留区标记为可重叠区域.

__reserve_early

drop_overlaps_that_are_ok


__reserve_early

__reserve_early() 函数用于将一段物理内存加入到 Early_Res 分配器的预留区域。参数 start 和 end 指明的预留区的范围,name 参数指明的预留区的名字,参数 overlap_ok 指明的预留区重叠与否.

函数在 181 行调用 find_overlapped_early() 函数在 Early_res 分配器的预留区查看参数对应的区域重叠的区域对应的索引,如果索引大于等于 max_early_res, 那么 Early_res 分配器不能在容纳更多的预留区,于是触发 panic(). 如果索引没有查出最大预留区个数,那么函数获得重叠区域,如果此时 r->end 值存在,那么表示这个预留区在 Early_Res 分配器预留区内已经存在,那么函数不能将这个区域加入到预留区内,直接触发 panic(). 通过上面的检测之后,函数可以将这个新的预留区加入到 Early_Res 预留区内,并将预留区的信息填入到索引对应的预留区内,并将 overlap_ok 的值写入到预留区的 overlap_ok 成员上,如果 name 参数存在,那么将 name 拷贝到 name 成员里,最终增加 early_res_count 的值,以此增加 Early_res 分配器预留区的个数.

find_overlapped_early


free_early_partial

free_early_partial() 函数用于从 Early_Res 分配器的预留区内移除部分区域。参数 start 和 end 指明部分移除预留区范围。

函数在 342 行进行检测,如果 start 等于 end,那么函数无法进行移除直接返回。函数首先在 349 行调用 find_overlapped_early() 函数在 Early_Res 分配器的预留区内查找与参数重合的区域对应的索引,如果 i 大于等于 max_early_res, 那么表示 Early_Res 分配器的预留区内没有重合的区域,因此不能进行移除,直接返回; 反之函数在 Early_Res 分配器的预留区内找到了一个预留区。函数在 355 行进行检测,如果需要移除的区域只是找到预留区的部分,那么函数直接调用 drop_range_partial() 函数进行移除,并返回; 反之如果移除的区域是 Early_Res 分配器内多个区域,那么函数调用 drop_range_partial() 函数移除当前区域之后,并调转到 try_next 处继续移除下一个区域.

> find_overlapped_early

> drop_range


drop_range_partial

drop_range_partial() 函数用于从 Early_Res 分配器的预留区内释放部分区域. 参数 i 指明释放的物理内存区域在 Eaarly_Res 分配器预留区数组上的索引,参数 start 和 end 指明从预留区内移除的区域范围。

函数首先在 71-72 行获得原始预留区的起始地址和终止地址,接着在 73-74 行一个最小的移除集合。获得集合之后,如果 common_start 大于 common_end, 那么获得预留区存在溢出,不符合移除条件,直接返回; 函数继续在 80 行进行检测,如果原始预留区的起始地址小于新集合的起始地址,那么说明函数需要将原始预留区头部进行拆分,那么函数在 82 行进行调整,将原始预留区的结束地址设置为 common_start。函数如果此时检测到原始预留区的的结束地址大于集合的结束地址,那么函数需要将原始预留区的尾部新增加一个预留区。那么函数在 84-96 行用于在 Early_Res 分配器中新增一个预留区,该预留区的起始地址就是集合的结束地址; 如果函数一开始检测到原始预留区的起始地址大于集合的起始地址,那么函数只需缩减原始预留区的范围,如果此时函数检测到原始预留区的结束地址大于集合的结束地址,那么 Early_Res 分配器仅需调整原始的预留区的起始地址为集合的结束地址; 反之原始预留区被集合包围,那么函数直接调用 drop_range() 函数将该预留区从 Early_Res 分配器预留区数组中移除.

drop_range


free_bootmem

free_bootmem() 函数用于将一段物理内存设置为可用。参数 addr 和 size 指向一段物理内存区域。函数 free_bootmem() 函数可以由 Bootmem 分配器提供,也可以由 Early_Res 分配器提供,这里研究后者。

函数在 462 行调用 free_early() 函数完成实际的释放动作,Early_Res 分配器会将这段物理内存区域从 Early_Res 分配器的预留区 early_res[] 数组中移除,以此让这段物理内存区域变得可用。

free_early


free_early

free_early() 函数用于从 Early_res 分配器的预留内存区域中移除一块预留内存区域。参数 start 和 end 指明了移除预留区域的范围。

函数在 326 行调用 find_overlapped_early() 函数在 Early_Res 分配器的预留区中找到参数对应的预留区在 early_res[] 数组中的索引。函数在 327 行获得索引对应的预留区。函数在 328 行进行检测,如果 i 大于 max_early_res 的值或者检测查找到的预留区与参数对应的预留区不重合,那么 Early_Res 分配器将放弃移除预留区的操作; 反之如果检测通过,那么函数调用 drop_range() 函数将参数对应的预留区从 Early_Res 分配器的预留区中移除,那么移除的区域将变得可用.

drop_range


drop_range

drop_range() 函数用于在 Early_res 分配器中,将指定的区域从预留区中移除. 参数 i 指向预留区的索引。

函数在 56 行调用 for 循环查找 early_res[] 数组中预留区最大索引,并将索引存储在 j 变量里。函数接着在 59 行调用 memmove() 函数将 early_res[] 数组第 i+1 个之后的成员全部向前覆盖一个位置,以此将 i 索引对应的预留区移除,函数最后更新 early_res[] 数组的长度和最后一个成员的结束地址.


__alloc_bootmem_nopanic

__alloc_bootmem_nopanic() 函数用于从 Early_res 分配器中分配指定长度的物理内存,如果分配失败不会触发 panic. 参数 size 指向分配内存的大小,align 参数表明对齐方式,goal 参数指明期盼从该地址开始进行分配.

函数首先将 limit 设置为最大物理地址,然后调用 ___alloc_bootmem_nopanic() 函数完成核心的分配。

___alloc_bootmem_nopanic


___alloc_bootmem_nopanic

___alloc_bootmem_nopanic() 函数用于从 Early_res 分配器中分配指定长度的物理内存, 如果分配失败不会触发 panic。size 参数指明所需内存的长度,参数 align 表示分配时候的对齐方式,goal 指明期盼从指定位置开始分配,limit 参数值限定最大分配物理地址.

函数首先在 689 行调用 slab_is_available() 函数检测 SLAB 分配器是否可用,如果可用则直接使用 kzalloc() 函数进行内存分配; 反之则从 Early_res 分配器中进行分配,函数在 694 行通过函数 __alloc_memory_core_early() 函数从 Early_res 分配器中分配指定长度的物理内存,并获得对应的虚拟地址,如果虚拟地址有效直接返回; 反之无效,则将 goal 设置为 0 并调转到 restart 从 0 地址开始查找分配.

__alloc_memory_core_early


for_each_active_range_index_in_nid

for_each_active_range_index_in_nid() 用于在 Early_res 分配器中遍历指定 NODE 上的所有可用物理内存区域. i 参数用于存储遍历索引,nid 参数用于指明 NUMA NODE.

函数使用 for 循环完成遍历,其中 first_active_region_index_in_nid() 函数用于查找指定 NUMA NODE 上的第一个可用物理内存区域,而 next_active_region_index_in_nid() 函数则指向索引之后的物理内存区域。通过遍历可以遍历指定 NUMA NODE 上可用的物理内存区域.

first_active_region_index_in_nid

next_active_region_index_in_nid


next_active_region_index_in_nid

next_active_region_index_in_nid() 函数用于查找 Eearly_res 分配器的下一个可用物理内存区域。参数 index 指向 Early_res 分配器当前的内存区域索引,nid 参数指明 NUMA NODE.

函数使用 for 循环,从 index 的下一个值开始遍历 Early_res 分配器的所有可用物理内存,如果 nid 等于 MAX_NUMNODES 或者下一个物理内存区域的 nid 与参数一致,那么函数返回对应的索引。


first_active_region_index_in_nid

first_active_region_index_in_nid() 函数用于获得 Early_res 分配器指定 NODE 上的第一个可用物理内存区域索引。参数 nid 指向 NUMA NODE. 函数在 3537 行使用 for 循环遍历 Early_res 分配器的所有物理内存区域,如果 nid 等于 MAX_NUMNODES 或者遍历到的内存区域的 nid 与参数一致,那么函数直接退出循环,返回索引. 如果没有找到则返回 -1.

early_node_map


__alloc_memory_core_early

__alloc_memory_core_early() 用于从 Early_res 分配器中分配指定长度的物理内存。参数 nid 指明 NUMA NODE,参数 size 指明分配物理内存的长度,参数 align 表示对齐方式,参数 goal 指明期盼从该地址开始分配,limit 参数指明了最大可分配物理地址.

函数首先在 3661 行调用 get_max_mapped() 函数确认 limit 参数没有超过最大映射物理地址,如果超过则将 limit 设置为当前系统最大映射物理地址。接着函数在 3665 行调用 for_each_active_range_index_in_nid() 函数遍历 Early_res 分配器所有可用物理内存区域,每当遍历一个可用物理区域,函数就调用 find_early_area() 函数在该区域内查找一个符合要求的内存区域,如果没找到则进入下 Early_res 分配器的下一个内存区域; 反之如果找到,那么函数获得查找到物理内存区域对应的虚拟地址,并将该区域清零,接着函数在 3688 行调用 reserve_early_without_check() 函数将该区域加入到 Early_res 分配器的预留区内进行管理,最后返回查找到区域的虚拟地址.

find_early_area

get_max_mapped

reserve_early_without_check

for_each_active_range_index_in_nid


reserve_early_without_check

reserve_early_without_check() 函数用于不检查待预留区域的有效性或与原先区域重叠的情况下,直接将指定区域添加到 early_res[] 数组内。参数 start 和 end 指明了一个新的预留区,name 指明预留区的名字。函数在 304 行进行检测,如果此时 start 大于 end,那么函数直接返回,无效参数。函数接着调用 __check_and_double_early_res() 函数,在必要的情况下将 Early_res 分配器的预留数组 early_res 的容量扩大一倍。函数接着在 209 行在 early_res[] 数组内获得一个预留区位置,并将参数指向的内存区域添加到这个预留区内,其中预留区的 overlap_ok 直接设置为 0,以此表示没有重叠。最后将 name 参数拷贝到预留区的名字上,并更新 early_res_count 的值.

__check_and_double_early_res


__check_and_double_early_res

__check_and_double_early_res() 函数用于检测 Early_res 内存分配器的预留数组 early_res 是否还能存储更多的预留区域。参数 ex_start 和 ex_end 用于指明一个新的预留区域。

函数在 232 行检测 max_early_res 与 early_res_count 的值是否大于 max_early_res/8 与 2 之间最大值,以此判断 early_res 预留数组是否还能存储更多的预留区域。max_early_res 指明了 Early_res 分配器中预留区域的最大个数,而 early_res_count 指明了当前预留区域的数量。如果函数检测到 early_res 数组内还可以存储预留区,那么函数直接返回; 反之如果没有可用的预留区,那么函数接下来要拓展预留区的数量. 函数在 237 行计算按 max_early_res 两倍拓展预留数组消耗的内存长度。接着函数检测 early_res 数组的起始地址是否与 early_res_x 数组一致,如果一致那么表示 res_early 数组还没有拓展过,那么将 start 设置为 0; 反之将 start 指向了 early_res[0].end。函数在 242 - 243 行定义了一个查找区域,并调用 find_fw_memmap_area() 函数在 E820 管理器的指定范围内找到一块可用的物理内存,如果没有找到,那么函数将 end 指向了系统最大物理地址,以此拓宽查找范围,如果此时还是没有找到,那么系统真没有可用物理内存,直接 panic().

反之找到一块可用物理内存之后,将这块物理内存区起始地址对应的虚拟地址存储在 new 变量里,然后设置 new 的第一个预留区,第一块预留区即自己本身。函数接下来将 old 的预留数组拷贝到新的内存区域上。在拷贝的时候,函数会检测这是第一次 early_res 数组拓展还是第 N 次,如果是第一次,那么函数在拷贝完毕之后会更新 early_res_count 的值,以此将自己也算入新的预留区内,如果是第 N 次拷贝,函数拷贝完毕之后不更新 early_res_count 的值,因此 res_early 的第一个成员就是 early_res 数组本身。函数最后将新的预留区域数据拷贝到 earlu_res 数组上,并将 max_early_res 扩大成 2 倍。

find_fw_memmap_area


get_max_mapped

get_max_mapped() 函数用于获得系统最大的映射物理地址。最大映射物理页帧存储在 max_pfn_mapped, 函数返回其对齐的物理地址。


find_fw_memmap_area

find_fw_memmap_area() 函数用于查看固件可以映射的区域。参数 start 和 end 指明的查找的范围,size 指明了查找的长度,align 指明了对齐的方式。函数通过调用 find_e820_area() 函数在 E820 内存管理器中找到一块可用的物理内存区域。

find_e820_area


find_e820_area

find_e820_area() 函数用于在指定范围内找到一块可用的物理内存区域。参数 start 和 end 指向查找的范围,size 指明了区域的长度,align 指明了对齐方式。

函数在 747 行使用 for 循环遍历 E820 分配器的所有可用物理内存区域,如果不是可用物理内存区域则直接跳过,每当遍历到一个物理内存区域的时候,函数将 ei_start 和 ei_last 指向了该物理内存区域,接着函数在 757 行调用 find_early_area() 函数在 ei_start 到 ei_last 的范围内,设置查找范围为 start 到 end,长度为 size 的可用物理内存区域,如果找到,则 addr 不为 -1,并直接返回。返回返回 -1.

find_early_area


find_early_eara

find_early_eara() 函数用于 Early_res 分配器在指定的范围内找到一块指定长度未使用的物理内存区域。参数 ei_start 和 ei_end 指明了一块物理区域,参数 start、end 和 size 指明了所需区域的范围,参数 align 用于对齐操作。

函数在 550 行将 addr 指向了 ei_start 按 align 对齐的地址上,如果此时 addr 小于 start,那么表示被查找物理内存区域的起始地址小于期盼值,因此将 addr 指向 start 对齐之后的地址。函数在 533 行进行检测,如果此时 addr 大于 ei_last, 那么被查找区域没有符合要求的地方。函数在 555 行使用 while 循环,在循环中调用 bad_addr() 函数在 addr 到 ei_end 之间查找一块长度为 size 并且没有预留的区域,如果找到并将该区域的起始地址存储在 addr 中; 如果函数遍历了该区域之后没有找到可用的物理内存区域,那么循环页也照样停止。接下来看函数需要确认是否真正找到可用的区域,函数在 558 行进行检测,如果找到,那么 last 一定小于 ei_last, 并且 last 也要小于 end 参数,最后返回 addr 的值; 反之没有找到直接返回 -1.

bad_addr


bad_addr

bad_addr() 函数的作用是确认一块物理内存是否已经预留。参数 addrp 用于指向一个物理地址,size 表示区域的长度,align 则表示对齐方式。函数在 496 行调用 find_overlapped_early() 函数在 Early_Res 分配器维护的 early_res[] 数组中查找与 start 和 addr 重叠的区域,并获得区域在 early_res[] 数组中的索引。函数在 498 行检测索引是否小于 max_early_res, 并且索引对应的区域的结束地址存在,这样可以说明 early_res[] 数组中确实存在一个重叠的区域,那么函数将重叠区域的结束地址存储在 addrp 和 addr 里面,然后将 changed 修改该为 1 之后,接着跳转到 again 继续查找可能重叠的部分,最后返回 changed 的值。因此当 changed 为 0 时表示参数对应的区域和 Early_Res 分配器维护的区域没有重叠的区域。

find_overlapped_early


find_overlapped_early

find_overlapped_early() 函数用于在 Early_Res 分配器维护的内存区内找到与参数重叠的区域。参数 start 和 end 指向了一个需要查找的区域.

函数在 38 行使用 for 循环遍历了 Early_Res 分配器 early_res[] 数组的所有成员,在没遍历到一个成员时,函数在 40 行检测该区域是否包含了参数指定的区域,如果包含那么函数直接停止循环,并返回该区域的位置。如果没有找到,那么函数返回 early_res[] 数组成员的个数.


max_early_res

static int max_early_res __initdata = MAX_EARLY_RES_X;

max_early_res 表示 Early_Res 分配器支持的最大 region 数量。


nr_nodemap_entries

static int __meminitdata nr_nodemap_entries;

nr_nodemap_entries 变量用于描述 early_node_map[] 数组成员的个数,即代表当前系统中维护物理内存区域的个数.


early_node_map

static struct node_active_region __meminitdata early_node_map[MAX_ACTIVE_REGIONS];

node_active_region[] 数组用于管理内核 boot-time 阶段的物理页集合. 其属于 struct node_active_region 数据结构构成的数组,数组中的成员表示一个可用的物理内存区域,并由 nr_nodemap_entries 变量指明数组的长度.

struct node_active_region


struct node_active_region

struct node_active_region 数据结构用于描述一段物理内存区域,其中 start_pfn 指明这段物理内存区域的起始物理页帧; end_pfn 成员指明了这段物理内存区域的结束物理页帧; nid 成员则指明了这段物理内存区域所在的 NUMA NODE。


add_active_range

add_active_range() 函数用于将注册一段物理页帧到 early_node_map[] 数组. 参数 nid 指向 NUMA NODE,start_pfn 和 end_pfn 参数指明物理页帧的信息.

在 boot-time 阶段,内核将所有的物理页帧信息全部存储在 early_node_map[] 数组中,该数组由 struct node_active_region 数据结构组成,用于描述一段物理内存区域。内核使用 nr_nodemap_entries 变量用于统计 early_node_map[] 数组中已经存在的成员数量,即已经维护的物理内存区域的数量. 函数在 4247 行使用 for 循环遍历 early_node_map[] 数组中的所有物理内存区域,函数首先在 4248 行检测变量的物理内存区域的 NODE 是否和参数 nid 一致,如果不一致,那么继续遍历下一个物理内存区域。函数接着在 4252 行检测新加入的物理内存区域是否包含在已经遍历的物理内存区域内,如果是,那么函数直接返回不再注册该物理内存区域; 反之继续检测, 函数在 4257 行检测,如果新插入的物理内存区域的起始物理页帧小于遍历到物理内存区域的结束物理页帧,以及插入的物理内存的结束物理页帧大于遍历到物理内存的结束物理页帧,那么新插入的物理内存区域的后半段重叠,那么函数修改遍历到物理内存区域的结束物理页帧。同理 4264 行则检测到前端重叠,那么修改遍历到物理内存区域的起始物理页帧。遍历完毕之后,如果 i 的值大于 MAX_ACTIVE_REGIONS, 那么函数进行报错并终止添加; 反之函数向 early_node_map[] 中添加一个新的物理内存区域, 并更新 nr_nodemap_entries 的值.

> struct node_active_region

> early_node_map

> nr_nodemap_entries


附录

BiscuitOS Home

BiscuitOS Driver

Linux Kernel

Bootlin: Elixir Cross Referencer

捐赠一下吧 🙂

MMU