目录


E820 内存管理器原理

X86 架构机器在上电之后,BIOS 系统首先进行基础的硬件初始化,将机器初始化到 一个已知的状态,接着 BIOS 将 PC 指针跳转到特定位置以便引导内核的启动,最终 将执行权从 BIOS 移交给内核。以上便是一个最简单的内核引导过程,在这个过程中, BIOS 会初始化并探测系统可用和预留的内存信息,并将其存储在指定位置,内核在初始 话过程中,可以通过 BIOS 提供的一系列中断来获得内存相关的信息。

E820 内存管理器的由来与 BIOS 提供的中断有很深的联系。E820 内存管理器泛指从 BIOS 获得内存区域信息之后,对这些内存区域信息进行管理,以便于内存初始化时利用 这些信息对可用物理内存和预留内存进行规划和初始化等操作。如下图:

内核在初始化过程中,从 E820 内存管理器中获得了基础的内存区域布局信息,可以 从中看出每个区域的地址范围以及内存区域类型,内核根据这些信息,为之后的 Bootmem 内存管理器或者 MEMBLOCK 内存管理器的初始化提供了基础。

在 X86 架构 Linux 内核发展的过程中,物理内存支持的长度从 16MB 发展到 64MB、4GB 再到今天支持 64-bit 的地址总线,BIOS 在不同的历史阶段提供了不同的中断调用, 以满足内核从 BIOS 中获得内存区域相关的信息。具体 BIOS 中断可以参考链接:

E820 内存管理器是用于管理内存区域的信息,但不包括内存的分配和回收功能,E820 内存管理器采用 struct e820_entry 描述一段内存区域信息,包括了内存区域的起始 位置、长度以及内存区域类型,E820 内存管理器将多个内存区域维护在一张 E820 table 表里,该表使用 struct e820_table 进行描述。因此内核通过 E820 Table 表就可以 知道当前机器的内存布局信息,有了这些信息之后,内核根据 E820 Table 提供的内存 信息对 BootMEM 内存分配器或 MEMBLOCK 内核分配器进行初始化,因此在初始化接下来 的内存分配器之前,内核可以提供一些手段来修改 E820 Table 表来控制未来系统的内 存布局,这种修改比较常见的就是给系统 CMDLINE 机制提供选项,实现某段区域的预留, 或者控制系统可用物理内存的长度等功能,详细过程请参考如下:

E820 表的产生与 BIOS 有着千丝万缕的关系,因此通过研究 BIOS 的行为也可以从根源 上了解 E820 内存管理器的运作机理. BIOS POST 上点自检之后,会从 CMOS 中获得内存 相关的信息,这些信息用于构建 BIOS 的 BDA/EBDA 信息,以便 BIOS 构建自己的 E820 表,BIOS 构建的 E820 表构建完毕只有,会将 E820 表里的内存区域与 BIOS IVT 的 0x15/E820 中断例程进行绑定,当内核初始化时通过与 BIOS IVT 进行交互,BIOS 将 E820 表传递给了内核. BIOS 在初始化过程中也会预留一些内存区域,这些内存区域也会 一同传递给内核. 更多细节信息请查看:

QEMU/KVM 作为虚拟化核心组建,其可以用于模拟 X86 运行环境,因此其具备构建 X86 平台硬件信息的能力,QEMU/KVM 同样提供了创建 E820 的机制,用于为 seaBIOS 模拟 E820 相关的硬件信息,其中包括了 BIOS 的 CMOS 内存,hw_cfg 固件信息等,因此可以 通过研究 E820 在 QEMU/KVM 中行为更加深入的理解和实践 E820, 开发者可以参考:

内核在初始化过程中,实时模式中从 BIOS 中获得并构建 E820 表,然后结合 CMDLINE 参数和 boot_paras 参数构建内核自身的 E820 表,并基于该表构建系统的 IORESOURCE 信息和固件信息,最后基于 E820 表中可用内存区域信息构建 BootMEM 或者 MEMBLOCK 内存分配器,具体源码流程和涉及函数信息,开发者可以查看:

E820 内存管理器章节提供了完整的实践环节,不仅可以在内核中实践 E820,也可以在 seaBIOS 中实践 E820,更可以在 QEMU/KVM 中实践 E820. 开发者可以参考如下章节:


E820 内存管理器使用


使用 E820 固件

系统初始化完毕之后,用户可有时需要从 E820 table 中读取内存区域信息,因此 E820 内存管理器向用户空间提供了一系列接口可以获得相应的信息,如下:

/sys/firmware/memmap

通过 “/sys/firmware/memmap”, 用户可以获得 E820 Table 的内存区域信息,并按内存 区域的起始地址从低到高排序,没有内存区域提供了 “start”、”end” 和 “type” 三个 接口进行描述.


通过 CMDLINE 设置 E820 内存管理器

E820 内存管理器向 CMDLINE 机制提供了 “memmap=” 和 “mem=” 选项,用于用户添加或 修改 E820 内存区域。通过这些选项,用户可以简单的添加一块 “系统预留区域”、 “ACPI Data 区域”、”系统保护区域”,或者可以修改 e820_table 中某段内存区域的类型。 这些 CMDLINE 选项还可以修改系统最大可用物理内存或者系统可以使用的物理内存范围。 具体 CMDLINE 使用方式如下:

设置系统最大可用物理内存
mem=Size[KMG]

Size 参数用于指明系统最大可用物理内存的长度,即系统可用物理内存范围是 “0-Size”, 超过这一长度的物理内存系统将不可见. 一般 “mem=” 和 “memmap=” 配合使用,以此避 免物理空间冲突。如果设置了最大物理可用内存之后,如果没有使用 “memmap=” 进行 “ACPI” 区域的设置,那么 “ACPI” 区域将设置为不可用的 RAM 区域. 可以在 BiscuitOS 项目的 RunBiscuitOS.sh 中修改 CMDLINE 变量进行实践,如下:

CMDLINE="root=/dev/ram0 rw rootfstype=${FS_TYPE} console=ttyS0 init=/linuxrc loglevel=8 mem=256M"

参数包含在 RunBiscuitOS.sh 脚本的 CMDLINE 参数里。从上面的配置中,设置系统最大 的可用物理内存地址为 0x10000000,启动内核可以查看如下信息:

在内核启动过程中,已经看到 e820_table 已经将用户调整之后的内存区域打印出来了, e820_table 表中可用物理内存由原先的 “0x100000-0x3ffe0000” 调整为 “0x100000-0x10000000”.

内存正常运行过程中,可以使用 “cat /proc/iomem” 命令查看系统物理内存布局,其中 “System RAM” 的范围变成了 “0x100000-0x10000000”.


预留一段物理内存
memmap=Size[KMG]$Address[KMG]

Address 参数指明预留物理内存的起始地址,Size 参数指明预留内存的长度。可以在 BiscuitOS 项目的 RunBiscuitOS.sh 中修改 CMDLINE 变量进行实践,如下:

CMDLINE="root=/dev/ram0 rw rootfstype=${FS_TYPE} console=ttyS0 init=/linuxrc loglevel=8 memmap=1M\$0x20000000"

由于参数包含在 RunBiscuitOS.sh 脚本的 CMDLINE 变量里,因此需要使用 “\” 保留 “$” 符号. 从上面的配置中,可以得出将 “0x20000000-0x20100000” 区域进行预留,启动 内核可以查看如下信息:

在内核启动过程中,已经看到 e820_table 已经将用户调整之后的内存区域打印出来了, 其中包括了通过 CMDLINE 方式传递的内存区域 “0x20000000-0x20100000”。

内存正常运行过程中,可以使用 “cat /proc/iomem” 命令查看系统物理内存布局,其中 看到了 CMDLINE “memmap” 预留的内存区域,正常内核是无法使用这段预留内存.


预留一段 ACPI Data 区域
memmap=Size[KMG]#Address[KMG]

Address 参数指明 ACPI Data 区域的起始地址,Size 参数指 ACPI Data 区域的长度。 可以在 BiscuitOS 项目的 RunBiscuitOS.sh 中修改 CMDLINE 变量进行实践,如下:

CMDLINE="root=/dev/ram0 rw rootfstype=${FS_TYPE} console=ttyS0 init=/linuxrc loglevel=8 memmap=1M#0x20000000"

参数包含在 RunBiscuitOS.sh 脚本的 CMDLINE 变量里, 从上面的配置中,可以得出 将 “0x20000000-0x20100000” 区域设置为 ACPI Data 区域,启动内核可以查看如下信息:

在内核启动过程中,已经看到 e820_table 已经将用户调整之后的内存区域打印出来了, 其中包括了通过 CMDLINE 方式传递的内存区域 “0x20000000-0x20100000”, 该段内存 区域类型显示为 “ACPI Data”。

内存正常运行过程中,可以使用 “cat /proc/iomem” 命令查看系统物理内存布局,其中 看到了 CMDLINE “memmap” 设置的 ACPI Data 内存区域,正常内核是无法使用这段内存.


预留一段保护区域
memmap=Size[KMG]!Address[KMG]

Address 参数指明保护区域的起始地址,Size 参数指保护区域的长度。保护区域一般给 NVDIMM 和 ADR 内存使用. 可以在 BiscuitOS 项目的 RunBiscuitOS.sh 中修改 CMDLINE 变量进行实践,如下:

CMDLINE="root=/dev/ram0 rw rootfstype=${FS_TYPE} console=ttyS0 init=/linuxrc loglevel=8 memmap=1M!0x20000000"

参数包含在 RunBiscuitOS.sh 脚本的 CMDLINE 变量里, 从上面的配置中,可以得出 将 “0x20000000-0x20100000” 区域设置为保护区域,启动内核可以查看如下信息:

在内核启动过程中,已经看到 e820_table 已经将用户调整之后的内存区域打印出来了, 其中包括了通过 CMDLINE 方式传递的内存区域 “0x20000000-0x20100000”, 该段内存 区域类型显示为 “persistent (type 12)”。

内存正常运行过程中,可以使用 “cat /proc/iomem” 命令查看系统物理内存布局,其中 看到了 CMDLINE “memmap” 设置的 “Persistent Memory (legacy)” 内存区域,这段内存 预留给 NVDIMM 或者 ADR 内存使用.


修改一段内存区域的类型
memmap=<Size>%<Address>-<oldtype>+<newtype>

Address 参数指明修改内存区域的起始地址,Size 参数指修改内存区域的长度.oldtype 参数指明修改前的内存区域类型,newtype 参数指明修改后的内存区域类型, 对于 type 的值,1 代表 “RAM”、2 代表 “Reserved”、3 代表 “ACPI”、12 代表 “PRAM”。可以在 BiscuitOS 项目的 RunBiscuitOS.sh 中修改 CMDLINE 变量进行实践,如下:

CMDLINE="root=/dev/ram0 rw rootfstype=${FS_TYPE} console=ttyS0 init=/linuxrc loglevel=8 memmap=1M%0x20000000-1+2"

参数包含在 RunBiscuitOS.sh 脚本的 CMDLINE 变量里, 从上面的配置中,可以得出 将 “0x20000000-0x20100000” 区域有原先的 RAM 设置为预留类型,启动内核可以查 看如下信息:

在内核启动过程中,已经看到 e820_table 已经将用户调整之后的内存区域打印出来了, 其中包括了通过 CMDLINE 方式传递的内存区域 “0x20000000-0x20100000”, 该段内存 区域类型显示为 “reserved”。

内存正常运行过程中,可以使用 “cat /proc/iomem” 命令查看系统物理内存布局,其中 看到了 CMDLINE “memmap” 设置的 “Reserved” 内存区域.


打印 E820 内存区域

E820 内存管理器支持打印 E820 Table, 以方便开发者直观的查看 E820 Table 信息。 通过提供 “e820__print_table”、”e820__update_table_print” 以及 “e820_print_type” 等函数。本节用一个小例子介绍如何在内核中打印 E820_table. 源码如下:

void BiscuitOS_E820_print_table(void)
{
	u64 Region_base0 = 0x40000000;
	u64 Region_size0 = 0x100000;
	u64 Region_base1 = 0x40100000;
	u64 Region_size1 = 0x100000;

        /* Print e820_table table */
        e820__print_table("BiscuitOS");

	/* Add memory region */
	e820__range_add(Region_base0, Region_size0, E820_TYPE_RAM);
	e820__range_add(Region_base1, Region_size1, E820_TYPE_RAM);

	/* Update and print table */
	e820__update_table_print();
}

将上面的代码添加到 “arch/x86/kernel/e820.c” 文件指定位置. 然后通过调用函数 “e820__print_table” 打印待特定标记的 e820_table,然后将两个相邻的内存区域添 加到 e820_table 里面,直接调用 e820__update_table_print() 函数更新并打印 e820_table 表.

在 “start_kernel()->setup_arch()->e820__memory_setup()” 函数之后的位置调用该 函数,从图中可以看出,”e820__print_table” 已经将带有 “BiscuitOS” 标记的内存 区域信息全部打印出来。在添加两个类型相同且相邻的内存区域之后,调用 “e820__update_table_print” 函数将 e820_table 进行更新并打印新的内存区域信息。 可以看出新的内存区域已经将新加入的两个内存区域合并成一个内存区域.


更新 E820 Table

当向一个 E820 Table 表里添加或移除一段内存区域之后,可能会导致内存区域的重叠 和乱序,为了便于后期其他内存分配器的初始化,E820 Table 内的内存区域需要按照 起始地址从低到高的方式进行排序。因此 E820 内存管理器提供了 “e820__update_table” 函数更新一张 E820 Table,或者 “e820__update_table_kexec” 函数更新特定的表. 本节给出一个例子用于向 e820_table 中添加一个内存区域,然后更新 e820_tabla, 为了更好的展示结果,将更新前后的表打印出来.

void BiscuitOS_E820_update_table(void)
{
        u64 Region_base0 = 0x40000000;
        u64 Region_size0 = 0x100000;
        u64 Region_base1 = 0x40100000;
        u64 Region_size1 = 0x100000;

        /* Add memory region */
        e820__range_add(Region_base0, Region_size0, E820_TYPE_RAM);
        e820__range_add(Region_base1, Region_size1, E820_TYPE_RAM);
	/* Print privous table */
	e820__print_table("BiscuitOS_Before");
	/* Update table */
	e820__update_table(e820_table);
	/* Print modify table */
	e820__print_table("BiscuitOS_modify");
}

将上面的代码添加到 “arch/x86/kernel/e820.c” 文件指定位置. 然后通过调用函数 e820__range_add() 函数将 “0x40000000,0x40100000” 区域和 “0x40100000,0x40200000” 内存区域插入到 e820_table 表里。新增加的两个内存区域存在相连的区域。在调用 “e820__update_table” 之前打印 e820_table 表,并在更新之后再次打印 e820_table 表,以此确认 “e820__udpate_table” 函数有合并排序的功能.

在 “start_kernel()->setup_arch()->e820__memory_setup()” 函数之后的位置调用该 函数,从图中可以看出,第一次添加两个内存区域之后,在 e820_table 中占用两个内存 区域,而且内存区域的类型都一致。在使用 “e820__update_table” 函数之后,再将 e820_table 进行打印,打印之后可以看出两个类型相同且相邻的两个内存区域已经合并 为一个内存区域,因此 “e820__update_table” 的合并功能得到验证.


修改一个内存区域的类型

E820 内存管理器支持动态修改一个或者一段内存区域的类型,其通过调用函数 “__e820__range_update()” 或者 “e820__range_update()” 函数实现。通常情况下, E820 内存管理器使用这个功能将某段可用物理内存修改为预留内存,那么内核在 启动之后就无法使用预留内存,E820 内存管理器也可以通过设置可用物理内存的长度, 将多余的部分设置为预留内存,那么内核的可用长度就得到限制. 本节给出一个例子用于 实现设置系统最大可用物理内存,代码如下:

void BiscuitOS_E820_update(void)
{
        u64 Region_base = 0x10000000;
        u64 Region_size = 0x40000000;

        /* update memory region */
        e820__range_remove(Region_base, Region_size, E820_TYPE_RAM, E820_TYPE_RESERVED);
        /* Update and print table */
        e820__update_table_print();
}

将上面的代码添加到 “arch/x86/kernel/e820.c” 文件指定位置. 然后通过调用函数 e820__range_update() 函数将 “0x10000000,0x50000000” 区域的类型从原先的 E820_TYPE_RAM 修改为 E820_TYPE_RESERVED, 那么系统由原先可用物理内存区域从 “0x100000-0x3ffdffff” 变成了 “0x10000-0xfffffff”,然后调用 e820__update_table_print() 对 e820_table 表内的内存区域进行去重和排序,并将 表里的所有内存区域打印出来.

在 “start_kernel()->setup_arch()->e820__memory_setup()” 函数之后的位置调用该 函数,内核启动时打印信息如上图. 从上图可以看出原始 e820_table 的内存信息,以及 打印添加新区域之后 e820_table 表的信息. 可用物理内存 “0x100000-0x3ffdffff” 已 经只有 “0x100000-0xfffffff” 可用,那么系统可用内存如下:

从上图可以看出,系统可用物理内存为 256MB,但原始可用物理内存为 1024MB.


移除一个内存区域

E820 内存管理器支持从 E820 Table 里面移除一个内存区域,其通过调用函数 “e820__range_remove”, 在移除过程中,会遇到多种情况,例如移除一个完整的内存区域, 或者移除的内存区域与原始的内存区域重叠等问题,具体问题分析可以参考:

本小节只讨论简单的从 E820 Table 中移除一个内存区域方法。代码如下:

void BiscuitOS_E820_remove(void)
{
        u64 Region_base = 0x20000000;
        u64 Region_size = 0x200000;

        /* Remove memory region */
        e820__range_remove(Region_base, Region_size, E820_TYPE_RAM, 1);
        /* Update and print table */
        e820__update_table_print();
}

将上面的代码添加到 “arch/x86/kernel/e820.c” 文件指定位置. 然后通过调用函数 e820__range_remove() 函数将 “0x20000000,0x20200000” 从 e820_table 表里面移除, 然后调用 e820__update_table_print() 对 e820_table 表内的内存区域进行去重和 排序,并将表里的所有内存区域打印出来.

在 “start_kernel()->setup_arch()->e820__memory_setup()” 函数之后的位置调用该 函数,内核启动时打印信息如上图. 从上图可以看出原始 e820_table 的内存信息,以及 打印添加新区域之后 e820_table 表的信息. “0x100000-0x3ffdffff” 已经被拆成两个 内存区域, 分别是 “0x100000-0x1fffffff” 和 “0x20200000-0x3ffdffff”.


新增一个内存区域

E820 内存管理器支持向 E820 Table 里面新增一个内存区域,其通过调用函数 “__e820__range_add” 或者 “e820__range_add”, 在添加过程中,会遇到多种情况, 例如添加一个全新的内存区域,或者新添加的内存区域与原始的内存区域重叠等问题, 具体问题分析可以参考:

本小节只讨论简单的向 E820 Table 中添加一个新的内存区域方法。代码如下:

void BiscuitOS_E820_add(void)
{
	u64 Region_base = 0x70000000;
	u64 Region_size = 0x200000;

	/* Insert new memory region */
	e820__range_add(Region_base, Region_size, E820_TYPE_RAM);
	/* Update and print table */
	e820__update_table_print();
}

将上面的代码添加到 “arch/x86/kernel/e820.c” 文件指定位置. 然后通过调用函数 e820__range_add() 函数将 “0x70000000,0x70200000” 插入到 e820_table 表里面, 然后调用 e820__update_table_print() 对 e820_table 表内的内存区域进行去重和 排序,并将表里的所有内存区域打印出来.

在 “start_kernel()->setup_arch()->e820__memory_setup()” 函数之后的位置调用该 函数,内核启动时打印信息如上图. 从上图可以看出原始 e820_table 的内存信息,以及 打印添加新区域之后 e820_table 表的信息.


创建一张 E820 Table

创建一张 E820 新表的方式有多种,可以新创建一张表并添加内存区域信息。也可以 拷贝原有的表,然后再修改特定的内存区域信息,便生成新的表。E820 表的存活周期 可以只在内存初始化阶段,也可以随系统一直存在。无论采用那种建表方式,新表创立 阶段位于 “start_kernel()->setup_arch()” 函数内部.

创建一张新 E820 表
static struct e820_table BiscuitOS_e820 __initdata;
static u64 Region0_start = 0x60000000;
static u64 Region0_size  = 0x2000000;
static u64 Region1_start = 0x70000000;
static u64 Region1_size  = 0x2000000;

void BiscuitOS_E820_table()
{
	int i;

	/* Inseart new memory region */
	__e820__range_add(&BiscuitOS_e820, Region1_start, Region1_size, E820_TYPE_RAM);
	__e820__range_add(&BiscuitOS_e820, Region0_start, Region0_size, E820_TYPE_RAM);
	__e820__range_add(&BiscuitOS_e820, Region1_start, Region1_size, E820_TYPE_RAM);
	/* Update E820 new table */
	e820__update_table(&BiscuitOS_e820);
	/* print E820 table */
	for (i = 0; i < BiscuitOS_e820.nr_entries; i++) {
		pr_info("BiscuitOS: [mem %#018Lx-%#018Lx] ",
			BiscuitOS_e820.entries[i].addr,
			BiscuitOS_e820.entries[i].addr + BiscuitOS_e820.entries[i].size - 1);
		e820_print_type(BiscuitOS_e820.entries[i].type);
		pr_cont("\n");
	}
}

将上面的代码添加到 “arch/x86/kernel/e820.c” 文件指定位置. 使用 struct e820_table 创建一个新表,并且将该表插入到 .initdata section. 创建完毕之后,通过调用函数 __e820__range_add() 函数将 “0x60000000,0x62000000” 和 “0x70000000,0x72000000” 插入到新表里面,然后调用 e820__update_table() 对新表内的内存区域进行去重和排序, 最后使用 for 循环将表里的所有内存区域打印出来.

在 “start_kernel()->setup_arch()” 函数相应的位置调用该函数,内核启动时打印信 息如上图.

拷贝一张 E820 表
static struct e820_table BiscuitOS_e820 __initdata;
static u64 Region0_start = 0x60000000;
static u64 Region0_size  = 0x2000000;
static u64 Region1_start = 0x70000000;
static u64 Region1_size  = 0x2000000;

void BiscuitOS_E820_table()
{
        int i;

	/* Copy exist e820 table */
	memcpy(&BiscuitOS_e820, e820_table, sizeof(BiscuitOS_e820));

        /* Inseart new memory region */
        __e820__range_add(&BiscuitOS_e820, Region1_start, Region1_size, E820_TYPE_RAM);
        __e820__range_add(&BiscuitOS_e820, Region0_start, Region0_size, E820_TYPE_RAM);
        __e820__range_add(&BiscuitOS_e820, Region1_start, Region1_size, E820_TYPE_RAM);
        /* Update E820 new table */
        e820__update_table(&BiscuitOS_e820);
        /* print E820 table */
        for (i = 0; i < BiscuitOS_e820.nr_entries; i++) {
                pr_info("BiscuitOS: [mem %#018Lx-%#018Lx] ",
                        BiscuitOS_e820.entries[i].addr,
                        BiscuitOS_e820.entries[i].addr + BiscuitOS_e820.entries[i].size - 1);
                e820_print_type(BiscuitOS_e820.entries[i].type);
                pr_cont("\n");
        }
}

将上面的代码添加到 “arch/x86/kernel/e820.c” 文件指定位置. 使用 struct e820_table 创建一个新表,并且将该表插入到 .initdata section. 创建完毕之后,通过调用函数 memcpy() 复制 e820_table 里面的所有内存区域信息。然后通过调用函数 __e820__range_add() 函数将 “0x60000000,0x62000000” 和 “0x70000000,0x72000000” 插入到新表里面,然后调用 e820__update_table() 对新表内的内存区域进行去重和排序, 最后使用 for 循环将表里的所有内存区域打印出来.

在 “start_kernel()->setup_arch()->e820__memory_setup()” 函数之后的位置调用该 函数,内核启动时打印信息如上图. 从上图可以看出原始 e820_table 的内存信息,以及 新拷贝新表 BiscuitOS_e820 内存区域信息,在 BiscuitOS_table 中打印了新添加的内存 区域。具体函数实现可以参看如下链接:


E820 内存管理器实践


实践准备

本实践是基于 BiscuitOS Linux 5.0 i386/X86_64 环境进行搭建,因此开发者首先 准备实践环境,请查看如下任意一篇文档进行搭建 (本文以 i386 为例介绍):


实践部署

在部署完毕开发环境之后,实践过程中会修改大量的 CMDLINE 参数,因此 CMDLINE 位于:

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

如上图,RunBiscuitOS.sh 脚本中 CMDLINE 变量用于存储内核启动时候使用的 CMDLINE 信息,开发者可以将自定义的 CMDLINE 参数写入该变量里,内核启动自动加载作为 系统启动 CMDLINE. 值得注意的是脚本里面的 CMDLINE 变量通过字符串的方式存储 系统使用的 CMDLINE,因此特殊字符需要转换,例如 CMDLINE 参数中包含 “$” 符号的, 需要在特殊符号前面加 “\” 进行转译.


实践执行

环境部署完毕之后,开发者可以直接运行 BiscuitOS 并通过 dmesg 查看运行 E820 运行的情况,使用如下命令:

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

通过上面的实践,内核中 E820 实践完毕.


E820 内存管理器源码分析


E820 内存管理器架构

BIOS 启动完毕之后,引导内核的启动,并将系统的执行权移交给内核,内核开始进入实时 模式,并进行最早期的初始化,其中包括通过 BIOS IVT 获得物理内存的信息。内核进入 保护模式之后,将从 BIOS 获得的 E820 信息用于构建内核自己的 E820 Table。接着内核 会从 boot_params 里获得而外的内存区域,并构建 e820_table、e820_table_kexec 和 e820_table_firmware 三个表,并将 e820_table 按一定的顺序进行排序。处理完表之后, 内核计算出 max_pfn/max_low_pfn/high_memory 三个变量的值,接着内核将 e820_table 的可用内存区域添加到 MEMBLOCK 分配器,MEMBLOCK 分配器以这些信息为基础构建内核 第一个内存分配器。内核接着将 e820_table 中的预留区域插入到 iomem_resource 进行 管理,并将 e820_table_firmware 加入系统固件层进行管理。

内核进行实时模式之后,系统运行在 16bit 模式下,内核通过 BIOS 提供的 IVT 表分别 从 0x15/0xE820、0x15/0xE801 以及 0x15/0x88 中获得物理内存信息,并将信息存储在 boot_params 数据结构里面. 内核获得这些物理信息之后,内核进行相关早期初始化之后 进入保护模式.

内核进入保护模式之后,调用 e820__memory_setup() 函数进行内核自己的 E820 Table 构建,内核首先调用 e820__memory_setup_default() 将 boot_params 中基于实时模式 获得的 e820 信息构建内核的 e820_table, 并对 e820_table 进行一定的排序去重处理, 然后直接拷贝 e820_table, 形成两张新的表 e820_table_kexec 和 e820_table_firmware, 最后函数通过 e820__print_table 函数第一次将 e820_table 表中的所有内存信息输出到 dmesg 信息中,因此这部分信息是原始 BIOS E820 信息.

kernel 实时模式时通过 BIOS E820 中断例程获得 e820_table, 并存储在 boot_params.e820_table, 但由于其只能容纳固定的 128 个内存区域,如果 seaBIOS 传递的内存区域超过 128 个,那么就将多余的内存区域通过 boot_params.hdr.setup_data 进行传递,并在解析时使用 SETUP_E820_EXT 进行分辨。函数调用 e820__memory_setup_extended() 函数实现实际的添加操作,并更新了 e820_table、 e820_table_kexec 和 e820_table_firmware 三个表,并打印 “extended” 的表信息.

内核构建最原始的 e820_table 之后,内核从 CMDLINE 中解析关于 “mem=” 和 “memmap=” 的配置,”mem=” 参数指明了系统最大物理地址,因此 e820_table 中可用物理内存区域 的范围只要超过这个值的,那么内存区域的类型将会被修改为 “E820_TYPE_RESERVED”。 同理,”memmap=” 参数用于修改一个或多个内存区域的信息,内核根据该修改动态调整 e820_table 表里相关内存区域的信息.

内核初始化到 e820__reserve_setup_data() 函数,会将之前从 boot_params.hdr.setup_data 中解析出来的区域,全部在 e820_table 和 e820_table_kexec 表中标记为预留, 并更新两个表,最后通过 e820__print_table() 函数和 “reserve setup_data” 打印 所有的内存信息.

内核执行到 e820__finish_early_params() 阶段,内核已经从 CMDLINE 获得其他路径 获得很多内存区域,并将这些内存区域插入到 e820_table, 但此时 e820_table 内的 内存区域可能存在重叠,而且没有按内存区域起始地址从低到高进行排序. 于是内核在 这个函数调用 e820__update_table() 函数对 e820_table 进行了重新排序,并将排序 后的 e820_table 使用字符串 “user” 进行输出.

内核初始化到 e820_add_kernel_range() 函数,内核会检测内核本身占用的区域是否 已经属于 E820_TYPE_RAM。由于 BIOS 复杂性或是 CMDLINE 中包含了 “memmap=exactmap” 或 “memmap=xxM$yyM” 导致内核本身占用的内存区域不是 E820_TYPE_RAM, 那么内核会 调用 e820__range_remove() 和 e820__range_add() 函数将内核占用的内存区域标记 为 E820_TYPE_RAM。

当 e820_table 已经加入系统所有的内存区域之后,内核通过 e820__end_of_ram_pfn() 函数计算出最大的物理页帧号. 并将最大物理页帧号存储在 max_pfn. 在 64bit 系统中, 如果 max_pfn 超过 4 Gig 物理内存的最大页帧号,那么 max_low_pfn 用于表示最大物理 页帧号,而 high_memory 则是最大物理页帧对应的虚拟地址.

内核初始化到 e820__memblock_setup() 函数,那么内核将基于 e820_table 的数据构建 MEMBLOCK 内存分配器。内核将 e820_table 中 E820_TYPE_RAM 的区域通过 memblock_add 函数添加到 MEMBLOCK 内存分配器中进行维护. 那么后续的物理内存管理权限就移交给了 MEMBLOCK 内存分配器.

内核初始化到 e820__reserve_resource() 函数,内核将 e820_table 中预留区域内存 通过 insert_resource() 函数插入到 iomem_resource 资源树下,以便系统维护。函数 还将 e820_table_firmware 表中的内存区域通过 firmware_map_add_early() 函数添加 到系统的固件层, 以便用户可以通过 “/sys/firmware.memmap” 接口获得 e820_table 相关的信息.


e820__update_table_print

e820__update_table_print() 函数用于更新 e820_table 表,并打印更新之后的表. 函数首先在 574 行调用 e820__update_table() 函数将 e820_table 进行排序合并等 更新操作,更新完毕之后判断是否更新新内容,如果有,则在 578-579 行调用函数 e820__print_table() 函数将更新之后的 e820_table 表的内存区域信息打印出来.


e820__range_update

e820__range_update() 函数的作用是更新 e820_table 内指定范围的内存区域类型。 参数 start 和 size 指定了更新的范围,参数 old_type 用于指明更新前内存区域的 类型,参数 new_type 用于指明需要更新的类型. 函数在 503 行通过调用函数 __e820__range_update() 函数进行实际的更新操作.


e820__get_entry_type

e820__get_entry_type() 函数用于查找一段内存在 e820_table 表内的内存区域类型。 参数 start 和 end 指定了查找范围。函数调用 __e820__mapped_all() 函数查找 指定范围内的内存区域,如果找到,那么直接返回内存区域的类型; 反之返回 NULL.


e820__mapped_any

e820__mapped_any() 函数用于检查某个内存区域是否已经存在于 e820_table 里面。 参数 start 和 end 指明了查找区域的范围,参数 type 指明了查找内存区域的类型。 函数在 79-87 行使用 for 循环遍历 e820_table 里面的所有内存区域,每当遍历到 一个内存区域,函数在 82 行首先检测内存区域的类型是否和参数 type 的一致,如果 一致,那么函数继续在 84 行检测内存区域是否与查找的范围有存在重叠的部分,只要 存在重叠部分,那么函数就返回 1.


do_mark_busy

do_mark_busy() 函数的作用是判断内存区域的类型是否属于 PCI mem,如果属于则返回 true. 函数首先将小于 1MB 的内存区域判定为 PCI mem,函数接着将 E820_TYPE_PRAM、 E820_TYPE_RESERVED 和 E820_TYPE_PMEM 之外的内存区域类型判定为 PCI mem.


e820__reserve_resources

e820__reserve_resources() 函数用于将 e820_table_firmware 表内的内存区域信息 添加到系统 firmware 里,系统可以在 /sys/firmware/memmap 里获得 E820 表信息.

内核在 1088 行出定义了一个 struct resource 的变量 e820_res, 用于将 E820 表 里的内存区域信息转换为系统 resource 机制能够维护的信息. 函数首先在 1096 行 调用 memblock_alloc() 函数为 res 分配所需的物理内存,然后从 1100-1124 行使用 for 循环遍历 e820_table 内所有的内存区域,在 1108-1113 行将内存区域的信息转换 为 Reserouce 机制能够维护的信息,并在 1119 行调用 do_mark_busy() 函数对内存 区域的内存进行检测,如果内存区域不是 E820_TYPE_RESERVED、E820_TYPE_PRAM 或者 E820_TYPE_PMEM 类型,那么函数将对应的资源 flags 设置为 IORESOURCE_BUSY, 表示 当前资源 busy 还不能使用,并调用 insert_resource() 函数将对应的资源插入都 全局 iomem_resource PCI 内存资源内进行维护.

函数遍历完毕所有的内存区域之后,在 1127-1131 行将 e820_table_firmware 表中的 所有内存区域全部插入到系统固件资源里面维护,每次遍历一个内存区域时候,函数在 1130 行调用 firmware_map_add_early() 函数将内存区域加入到系统固件资源里面, 系统启动完毕之后,可以在 /sys/firmware/memmap 各内存区域的信息.


e820_type_to_iores_desc

e820_type_to_iores_desc() 函数用于将 E820 内存区域类型转换为 I/O 资源描述类型。 内存区域对应类型如上.


e820_type_to_iomem_type

e820_type_to_iomem_type() 函数的作用是将 E820 内存区域的类型转换为 IO Memory 的类型。在函数转换过程中,E820_TYPE_RESERVED_KERN 和 E820_TYPE_RAM 属于 系统内存,其余均属于 IO 内存.


e820_type_to_string

e820_type_to_string() 函数的作用将内存区域的类型转换成字符串。参数 entry 用于指向一个内存区域的入口。函数根据内存区域的类型,匹配了一个特定字符串。


e820__memblock_alloc_reserved_mpc_new

e820__memblock_alloc_reserved_mpc_new() 函数用于给 mptable 从 MEMBLOCK 分配器 中分配物理内存,并将分配好的物理内存在 e820_table_kexec 表中做预留. CMDLINE 中可以使用 “alloc_mptable” 参数传递 MPTABLE 所需内存的大小,或者 CMEDLINE 中 包含了 “update_mptable” 选项来更新 MPTABLE 表。函数在 852 行判断 CMDLINE 中包 含了 “update_mptable” 且 “alloc_mptable” 已经被设置,那么函数在 853 行调用 e820__memblock_alloc_reserved() 函数为 MPTABLE 分配所需的物理内存.


e820__memblock_alloc_reserved

e820__memblock_alloc_reserved() 函数的作用是从 MEMBLOCK 内存分配器中分配一段 物理内存,并将这段物理内存在 e820_table_kexec 表中对应的内存区域类型,由原始 的 E820_TYPE_RAM 修改为 E820_TYPE_RESERVED. 参数 size 指明分配物理内存的大小, align 参数指明对齐方式.

函数在 781 行调用 __memblock_alloc_base() 函数从 MEMBLOCK 内存分配器中分配 所需的物理内存,分配成功后,函数在 783 行调用 e820__range_update_kexec() 函数 修改对应内存区域的类型,最后在 785 行调用 e820__update_table_kexec() 函数更新 e820_table_kexec 表. 最后返回申请到的内存地址.


e820__update_table_kexec

e820__update_table_kexec() 函数用于更新 e820_table_kexec 表。函数会将 e820_table_kexec 表内重复的内存区域进行合并,并对所有的内存区域按起始地址 从低到高排序呢. 函数通过将 e820_table_kexec 传入到 e820__update_table() 函数 实现其过程.


e820__range_update_kexec

e820__range_update_kexec() 函数用于更新 e820_table_kexec 表里一定范围内存区域 的类型. 参数 start 和 size 指明了修改区域的范围,参数 old_type 指明了修改之前 的类型,参数 new_type 指明了修改后的类型。函数通过调用 __e820__range_update() 函数实现.


__e820__range_update

__e820__range_update() 函数用于更新一张 E820 表内指定范围内内存区域的类型。 参数 table 指向 E820 表,参数 start 和 size 指明了更改类型的范围,参数 old_type 指明原始区域的类型,参数 new_type 指明更新的类型.

函数在 440 行首先检测类型是否有变动,如果没有直接使用 BUG_ON 进行报错。如果 更改的范围超过体系支持的最大范围,那么将 size 调整在合理的范围内。函数在 445 到 450 行做了一些打印信息.

函数在 452-497 行使用 for 循环遍历 E820 表内的所有内存区域,找到与参数 start 和 size 相关的区域,这些区域可能会被包含也可以能会嵌套,如下图情况:

当遍历到的内存区域嵌套在查找范围内,那么函数执行 464-466 行操作,将遍历到的 内存区域类型更新为新的类型,并将 read_update_size 更新增加上已修改的长度,最后 挑战到下一个区域.

当遍历到的内存区域包含了查找范围,那么此时会将一段内存区域拆分成三段,如上图, 其中 R0 和 R2 保持原始内存,但由于 R0 和 R2 的内存区域范围该表了,因此此时 需要调用 __e820__rnage_add() 函数将 R0 和 R2 重新插入到表中作为新的内存 区域,而 Region 则只需修改内存区域的类型,并更新长度而已,函数 470-476 行处理 的对应的逻辑,最后更新 real_update_size.

函数还要处理查找的范围一半与内存区域相交但不包含,如上图情况,面对上面情况, 函数在 478-480 行确认出相交部分的区域,确认区域之后检测区域是否溢出,如果 没有移除的话,调用 __e820__range_add() 函数将相交的区域的类型更改为新的 类型,在 491 行将相交区域的不相交部分区域的长度修改为新的值,最后更新 real_update_size 的值,最终返回该值以此表示此次更新内存区域的范围.


e820__memblock_setup

e820__memblock_setup() 函数用于将 e820_table 内的可用物理内存添加到 MEMBLOCK 内存分配器中进行管理. 该函数是 MEMBLOCK 内存分配器添加可用物理内存的起点。

函数在 1261 行调用 memblock_alloc_resize() 函数调整 MEMBLOCK 内存分配器的策略 为可以动态扩张,MEMBLOCK 内存分配器原始支持 128 region,初始化过程中可能超过 128 个 region,因此需要动态扩展支持 region 的个数. 函数 1263-1274 行使用 for 循环遍历 e820_table 里面的所有内存区域,每次遍历过程中,函数都会检测当前内存 区域结束地址是否超出 resource_size_t 规定的范围,如果超出直接跳转到下一个内存 区域; 反之函数继续检测内存区域的类型是否为 E820_TYPE_RAM 或 E820_TYPE_RESERVED_KERN,以此确保内存区域是可用的物理内存,检测通过之后调用 memblock_add() 函数将对应的内存区域加入到 MEMBLOCK 内存分配器里面。循环结束 之后,函数调用 memblock_trim_memory() 函数将 MEMBLOCK 内存分配器中可用的 内存区域进行对齐矫正. 最后函数在 1279 行调用 memblock_dump_all() 函数打印所有 的 MEMBLOCK 内存分配器信息.


e820_end_of_low_ram_pfn

e820_end_of_low_ram_pfn() 函数用于查找 4GB 内存内,e820_table 支持的最大 物理页帧号. “1UL « (32 - PAGE_SHIFT)” 代表 4GB 内存最大物理页帧号,并且 E820_TYPE_RAM 代表可用物理内存类型,将两个参数传递给 e820_end_pfn() 函数 进行查找.


e820__end_of_ram_pfn

e820__end_of_ram_pfn() 函数用于查找最大可用物理页帧。e820_end_pfn() 函数可以 查找指定范围内某种内存区域的最大物理页帧,MAX_ARCH_PFN 代表体系支持的最大物理 页帧号,在 I386-32 bit 系统上 (1 « 20); 在 i386-32bit PAE 模式下是 (1 « 24); 在 X64 上是 “MAXMEM » PAGE_SHIFT”. 最后将 E820_TYPE_RAM 传递给 e820_end_pfn() 以此查找 e820_table 中最大可用物理页帧.


e820_end_pfn

e820_end_pfn() 函数用于查找指定范围内,某类型内存区域最大的物理页帧。参数 limit_pfn 指明最大查找物理页帧范围,参数 type 指明查找内存区域类型。函数在 808 行定义了变量 max_arch_pfn 为 MAX_ARCH_PFN 用于指明当前体系最大的物理页帧。 函数在 810-829 行使用循环遍历 e820_table 里面的所有内存区域,在每次遍历中,如 果遍历内存区域的类型与参数 type 不一致,则跳转到下一个内存区域。函数在 821 行 首先检测遍历到的内存区域的起始物理页帧是否大于最大查找物理页帧,如果小于,那么 函数继续检测遍历到的内存区域的结束物理页帧是否大于最大查找物理页帧,如果大于, 那么将 last_pfn 设置为最大查找物理页帧,接着检测遍历到的物理页帧是否大于 last_pfn, 如果大于则将 last_pfn 设置为遍历到内存区域结束物理页帧。通过循环可以 获得 last_pfn 的范围,如果此时 last_pfn 大于 max_arch_pfn, 那么需要将 last_pfn 设置为 max_arch_pfn 以确认查找不超出范围. 最终返回 last_pfn 值.


e820_add_kernel_range

e820_add_kernel_range() 函数用于确保将内核相关的 section 所有占用的内存区域 全部加入到 e820_table 里,并设置为 E820_TYPE_RAM 类型。内核在启动过程中,内核 所使用的 section 占用的内存可能会被标记为预留等,因此函数要确保将这些内存区域 全部设置为 RAM 类型。

函数在 742-743 行通过 __pa_symbol() 函数和 _text 以及 _end 变量确定内核 自己占用的物理内存区域。函数接着在 752 行调用 e820__mapped_all() 函数检测内核 自己占用的物理内存区域是否在 e820_table 表里面已经被映射为 E820_TYPE_RAM 类型, 如果已经映射,那么直接返回; 反之函数在 756-757 行调用 e820__range_remove() 函数 和 e820__range_add() 将内核自己占用的物理内存区域以 E820_TYPE_RAM 的类型插入到 e820_table 表里.


e820__mapped_all

e820__mapped_all() 函数的作用是检测某段内存区域是否已经存在与 e820_table 里面。 参数 start 指向查找的起始物理地址, 参数 end 指向查找的终止物理地址, 参数 type 指向查找的内存区域类型. 函数通过调用 __e820__mapped_all() 函数进行实际的查找。


__e820__mapped_all

__e820__mapped_all() 函数的作用就是通过参数从 e820 中找到符合要求的内存区域 信息. 参数 start 指向查找的起始物理地址, 参数 end 指向查找的终止物理地址, 参数 type 指向了查找内存区域的类型.

函数在 103 行使用 for 循环遍历 e820_table 内的所有内存区域,每次遍历过程中, 函数首先在 106 行对遍历到的内存区域类型和参数 type 进行比较,如果 type 参数 为真,且类型不相等,那么函数跳转到下一次内存区域。如果类型检测通过,那么函数 函数在 110 行检测遍历到的内存区域不包含查找的范围,那么函数直接跳转到下一个 内存区域。如果检测通过,那么函数继续在 117 行继续检测如果查找的起始物理地址 不小于遍历到内存区域的起始物理地址,那么将 start 的值指向该内存区域的结束物理 地址,如果此时函数在 124 行检测到 start 的值大于 end,那么函数找到了需求的内存 区域信息,并直接返回. 如果最终没有找到,那么函数直接返回 NULL.


e820__finish_early_params

e820__finish_early_params() 函数用于更新 “mem=” 设置过后的 e820_table. CMDLINE 提供了 “mem=” 参数用于设置最大可用物理内存地址,那么超过该值的物理内存将不可 用,系统将会从 e820_table 中将超出的部分移除。

函数在 1005 行处通过 userdef 变量检测系统是否已经解析过 “mem=” 参数,如果 userdef 为真,那么函数就调用 e820__update_table() 函数重新整理一下 e820_table. 整理完毕之后调用 e820__print_table() 重新打印 e820_table。如果在 BiscuitOS 的 CMDLINE 中添加 “mem=” 参数,可以从 dmesg 中获得如下信息:


parse_memopt

parse_memopt() 函数用于从 CMDLINE 中解析 “mem=” 参数,以此设置系统最大可用 物理内存,并更新系统 e820_table 以此设置最大可用内存区域. 参数 p 指向 “mem=” 字符串信息. CMDLINE 中,可以通过在 “mem=” 后面跟一个具体的长度,以此表示系统 最大的可用物理内存,如下:

mem=1024M

上面参数表示最大可用物理内存为 1024M,并且最大可用物理内存地址为 0x4000000。 系统在初始阶段解析 “mem=” 参数之后,接着更新 e820_table 表,将大于该地址的 可用内存区域从 e820_table 表中移除.

函数在 875 行将 userdef 设置为 1,以此标记 “mem=” 参数已经设置,函数接着在 876 行通过调用 memparse() 函数将 “mem=” 内的内存长度解析成整形并存储在 mem_size 内,如果函数检测到 mem_size 为 0,那么这是一个不可用的参数,直接返回错误。如果 参数有效,那么函数在 882 行调用 e820__range_remove() 函数从 mem_size 指向的物理 地址开始,将其后面全部物理内存区域信息从 e820_table 中移除,并且只移除内存区域 类型为 E820_TYPE_RAM 的。


e820__range_remove

e820__range_remove() 函数的作用是从 e820_table 中移除指定的内存区域。参数 start 指向将要移除内存区域的起始物理地址; 参数 size 指向将要移除内存区域的长度; 参 数 old_type 指向即将移除内存区域的类型; 参数 check_type 指明是否需要进行类型 检测。从 E820 表中移除一段内存区域可能会将一段原始内存区域拆分成多段区域,或 者会移除多段内存区域.

函数在 518 行检测移除的长度是否溢出,如果溢出,那么将 size 设置为最大可移除 长度。函数在 521 行确认了移除内存区域的结束物理地址。并在 522-525 打印相关的 移除区域信息. 函数在 527 行使用 for 循环遍历 e820_table 表里的所有内存区域,如 过 check_type 参数为真,那么函数会在 532 行检测即将移除的内存区域类型和遍历到 的内存区域类型是否一致,如果不一致则不移除该区域,直接跳过该内存区域。如果内存 区域一致,那么函数在 535 行计算出遍历到内存区域的结束物理地址。

函数在 538 行检测即将移除的内存区域是否包含遍历到的内存区域,如果确认包含, 那么函数在 539-541 行,将遍历到的内存区域长度累加到 real_removed_size 变量里, 以此统计以及移除的长度,接着将遍历到的内存区域信息清零,并跳转到下一次循环.

函数在 545 行处进行判断,如果即将移除的内存区域包含在遍历到内存区域的内部, 那么函数会将原始的内存区域拆分成两个内存区域,则后面一个内存区域就变成新的 内存区域

于是函数在 546 行调用 e820__range_add() 函数将新生成的内存区域插入 到 e820_table 里面,并更新了原始遍历到内存区域的长度, 最后更新了 real_removed_size 的值,以此记录已经移除的长度. 函数从 553-568 行统计了已经 移除长度,并进入下一次循环。函数最后在 570 行返回了最终移除内存区域的长度.


e820__memory_setup

e820__memory_setup() 函数用于将 BIOS 获得的 E820 信息转换为系统维护的 E820 信息,并打印 BIOS 提供的 E820 信息. 函数在 1236 行检查了 struct boot_e820_entry 数据结构的长度是否为指定的 20 个字节,如果不是,那么 BIOS E820 内存区域信息 和内核维护的 E820 内存区域信息存在差异,系统将通过 BUILD_BUG_ON() 进行报错. 函数在 1238 行调用 x86_init.resources.memory_setup() 函数从 BIOS E820 表中将 所有的内存区域信息转移到内核维护的 e820_table 表中,并将 BIOS 调用信息存储到 who 变量里,该函数实际调用了 e820__memory_setup_default() 函数。函数在 1240 行 将 e820_table 表内的信息复制一份到 e820_table_kexec 表,函数也在 1241 行将 e820_table 表内的信息又复制了一份到 e820_table_firmware 表里。函数最后在 1244 行通过调用 e820__print_table() 函数将 e820_table 表内的所有内存区域信息打印 出来,打印效果如下:


e820__print_table

e820__print_table() 用于打印内核 e820_table 表中的所有内存区域信息。参数 who 指明了 e820_table 来自 BIOS INT 调用信息. 函数 192-200 行通过 for 循环将遍历 所有表中的内存区域信息,并将其按一定的规律进行打印,其中包含了内存区域的起始 物理地址和终止物理地址,均以 64bit 的形式打印,最后还打印了内存区域的类型。 打印的效果如下:


e820_print_type

e820_print_type() 函数用于将内存区域的类型打印为字符串的形式。E820_TYPE_RAM 和 E820_TYPE_RESERVED_KERN 统一打印为 “usable”, E820_TYPE_RESERVED 打印为 “reserved” 表示预留; E820_TYPE_ACPI 打印为 ACPI 使用; E820_TYPE_NVS 打印为 “ACPI NVS” 使用; E820_TYPE_UNUSABLE 打印为 “unusable” 表示不可使用的类型; E820_TYPE_PMEM 和 E820_TYPE_PRAM 打印为 “persistent”。


e820__memory_setup_default

e820__memory_setup_default() 函数用于设置内核使用的 E820 表. 系统在初始化过程 中将从 BIOS 中获得 BIOS e820 表,并存储在 boot_params.e820_table 里,但随着 系统初始化进程,boot_params 变量会消失,因此需要将 boot_params.e820_table 表 里面的数据处理好之后存储在内核指定的数据结构里进行维护.

函数在 1195 定义了字符变量 who, 并指向字符串 “BIOS-e820”. 函数在 1203 行调用 append_e820_table() 函数将 BIOS e820 表中的内存区域信息转移到内核维护的全局 e820_table 表中。内核初始化中,从 BIOS 获得的内存区域信息存储在 boot_params.e820_table 中,但内核初始化完毕之后 boot_params 会被清除,因此 在接下来的初始化过程中需要将 boot_params.e820_table 表中的内存区域信息通过 函数 append_e820_table() 转移到内核维护的全局 e820_table 表中。如果转移失败, 函数检测 boot_params.alt_mem_k 的值是否小于 boot_params.screen_info.ext_mem_k 的值。boot_params.alt_mem_k 的值是内核位于启动早期的实施模式下,通过函数 detct_memory_e801() 获得大于 16M 的内存信息,而 boot_params.screen_info.ext_mem_k 则是通过函数 detect_memory_88() 函数获得小于 64MB 的内存信息,此处如果小于,那么代表系统物理内存小于 16MB,那么此时使用 “BIOS-88” 探测的信息作为系统内存的信息; 如果大于,那么系统物理内存大于 16MB 而小于 64MB,那么使用 “BIOS-e801” 探测的信息作为系统内存的信息。那么函数继续在 1215 行将全局的 e820_table 维护的内存区域数量设置为 0,以此表示不使用 BIOS e820 内的内存区域信息. 在这种情况下,使用 e820__range_add() 函数插入两段内存 区域信息到全局的 e820_table 里面,第一段的范围是从 0 到 LOWMEMSIZE(),第二段 从 HIGH_MEMORY 到 “mem_size « 10”, 两段内存类型都是 RAM. 在这种情况下, e820_table 值包含了两个内存区域信息,以通常的 e820_table 形成对比。函数在 1221 行调用 e820__update_table() 函数整理了 e820_table 表,并在 1223 行处进行返回.


e820__update_table

e820__update_table() 函数用于整理一张 e820 表。参数 table 用于指向一张 e820 表。 当向一张 e820 表中插入内存区域信息之后,e820 表将新的内存区域信息插入到表的 末尾,因此会导致 e820 表中的内存区域并不是按起始物理地址进行排序,这会给内核 查找指定的内存区域带来不便以及效率上的印象,并且可能出现新插入的内存区域与表 内的内存区域存在相邻和重叠的情况,因此 e820__update_table() 函数用于整理 e820 表,首先将表中的内存区域信息进行排序,按内存区域信息的起始物理地址从低到高进行 排序,然后将存在重叠和相邻的同类型内存区域合并并只用一个内存区域信息进行描述。

函数在 295 行定义了一个 struct e820_entry 变量 entries 用于指向 e820 表中的 第一个内存区域信息。296 行定义了整形变量 max_nr_entries 用于描述 e820 表中 内存区域信息最大个数。函数在 303 行检测表中内存区域的个数是否小于 2 个,如果 小于则直接返回 -1,这里涉及一些历史的原因,在 X86/80286 架构中,前 16MB 的内 存会被分成两个部分,其中一个部分是给 RAM 使用,另外一部分地址预留给老实的 ISA 设备使用,因此内核在初始化过程中,e820_table 至少含有两个原始内存区域信息。 函数在 306 行检测 e820 表中内存区域的个数是否超过了 e820 表的最大内存区域个数, 如果超过则通过 BUG_ON() 进行报错。函数从 309-331 行代码通过将 e820 表扩充成 两部分,其中以起始物理地址的项包含了内存区域信息,而以结束物理地址的项不包含 内存区域信息,并在 334 行调用 sort() 函数进行排序操作.

343-382 行函数将拓展的 e820 表中重叠和相邻的同类型内存区域信息进行合并,并 将符合要求的内存区域信息存储到 new_entries 表中。最后函数在 387 行将处理完成 的表拷贝到 entries 指向的位置,并更新了原始表中内存区域的个数.


append_e820_table

append_e820_table() 函数用于将 BIOS E820 表存储到内核维护的指定位置。参数 entries 指向 BIOS E820 表,参数 nr_entries 指明 BIOS E820 表中原始内存区域 条目的数量. 内核在初始化过程中,首先通过 BIOS INT 调用的方式从 BIOS 获得原始 内存区域相关的信息存储在 boot_params.e820_table 表里,随着内核的寄存初始化, 由于 boot_params 只存在内核初始化的早期,因此需要将 boot_params.e820_table 表 存储到内核长期维护的地方。

函数首先在 427 行检测原始内存区域的数量是否小于 2,如果小于 2 那么直接返回 -1, 这里涉及一些历史的原因,在 X86/80286 架构中,前 16MB 的内存会被分成两个部分, 其中一个部分是给 RAM 使用,另外一部分地址预留给老实的 ISA 设备使用,因此内核 在初始化过程中,boot_params.e820_table 至少含有两个原始内存区域信息。检测通过 之后,函数在 430 行调用 __append_e820_table() 函数进行实际的操作.


__append_e820_table

__append_e820_table() 函数用于将 BIOS e820 表中的多个内存区域插入到内核 维护的 e820_table 表中。参数 entries 指向一个原始 e820 内存区域,参数 nr_entries 指明内存区域的数量。X86 内核在启动过程中,通过 BIOS INT 调用获得原始内存区域 相关的信息存储在 boot_params.e820_table,随着内核的初始化进程,内核将内存区域 信息从 boot_params.e820_table 中转移到内核维护的全局 e820_table 表中。

函数在 395 行定义了一个 struct boot_e820_entry 数据结构的变量 entry,entry 用于指向一个原始内存区域。函数在 397-411 行,通过 while 循环的方式,循环 nr_entries 次将原始内存区域插入到内核维护的全局 e820_table 表里. 在每次循环 中,函数通过 398-401 行获得每个原始内存区域的起始物理地址、长度、结束地址和 类型信息,并在 404 行对每个原始区域的内存信息进行检测,检测长度是否为零,且 原始内存区域的方位是否超过 64bit 的限制,如果不符合要求,那么直接退出。函数 在 407 行将原始内存区域的 start、size 和 type 信息传递给 e820__range_add() 函数,用于将该原始内存区域插入到 e820_table 中,函数执行完毕之后,409 行将 entry 变量指向了下一个原始内存区域,并在 410 行将 nr_entries 减一以此完成 一次循环. 循环完毕之后在 412 行进行返回 0 操作.


e820_table

X86/Linux 内核使用 e820_table 全局变量维护系统内存区域映射表. 系统在启动 过程中,通过 BIOS INT 调用从 BIOS 中获得硬件内存映射区域的信息,并将其存储 在 boot_params.e820_table 表里,并在后续的初始化中,系统将 boot_params.e820_table 中的内存区域信息重新添加到了 e820_table 全局变量里, 其定义如下:

struct e820_table *e820_table __refdata                 = &e820_table_init;

e820_table 全局变量定义为 struct e820_table, 初始化并指向 e820_table_init, e820_table_init 变量也是定义为 struct e820_table,但其被加入到了 “__initdata” section 内部,因此内核初始化完毕之后,e820_table_init 变量占用的空间将会被 释放.


e820_table_kexec

X86/Linux 内核使用 e820_table 全局变量维护系统内存区域映射表. 系统在启动 过程中,通过 BIOS INT 调用从 BIOS 中获得硬件内存映射区域的信息,并将其存储 在 boot_params.e820_table 表里,并在后续的初始化中,系统将 boot_params.e820_table 中的内存区域信息重新添加到了 e820_table 全局变量里, 其定义如下:

struct e820_table *e820_table_kexec __refdata           = &e820_table_kexec_init;

e820_table_kexec 拷贝自 e820_table 表,用于描述内核本身相关的内存区域信息.


e820_table_firmware

X86/Linux 内核使用 e820_table 全局变量维护系统内存区域映射表. 系统在启动 过程中,通过 BIOS INT 调用从 BIOS 中获得硬件内存映射区域的信息,并将其存储 在 boot_params.e820_table 表里,并在后续的初始化中,系统将 boot_params.e820_table 中的内存区域信息重新添加到了 e820_table 全局变量里, 其定义如下:

struct e820_table *e820_table_firmware __refdata        = &e820_table_firmware_init;

e820_table_table 拷贝自 e820_table 表,用于将 e820_table 内存区域的信息导出到 “/sys/firmware/memmap” 节点下供用户使用.


e820__range_add

e820__range_add() 函数用于向内核全局 e820_table 表中插入一段新的内存区域. 参数 start 指向新内存区域的起始物理地址; 参数 size 指明新内存区域的长度; 参数 type 指明新内存区域的类型. 函数通过调用 __e820__range_add() 函数进行实际的添加 动作,其中传入了 e820_table 变量,该变量指向全局的 e820 表.


enum e820_type

enum e820_type 用于描述 E820 内存区域的类型. E820_TYPE_RAM 表示内存区域是 一块 RAM; E820_TYPE_RESERVED 表示内存区域是预留做特定功能使用,内核无法使用 这类型的内存。


struct e820_entry

struct e820_entry 数据结构用于描述 E820 内存映射表中的一个内存区域。在 E820 内存管理器中,每个内存区域通过 struct e820_entry 进行描述。addr 成员指明了一 个内存区域的起始物理地址,其长度为 64 bit; size 成员用于描述内存区域的长度, 也是一个 64 bit 的变量; 成员 type 用于描述该内存区域的类型。struct e820_entry 数据结构通过 “attribute((packed))” 属性进行描述,因此在编译过程中,struct e820_entry 的内存布局不会被优化,其长度保持为 20 个字节。


struct e820_table

struct e820_table 数据结构用户维护一张 e820 内存区域表。nr_entries 成员指明 表中函数内存区域的数量. entries[] 数组用于存储所有的内存区域,每个成员是一个 struct e820_entry 数据结构,表示一个内存区域。一张 e820 内存区域表总共可以维护 E820_MAX_ENTRIES 个内存区域项.


__e820__range_add

__e820__range_add() 函数用于将一个新的内存区域加入到一个 e820 table 里面。 参数 table 指向一张 e820 内存映射表. 参数 start 指向新内存区域的起始物理地址; 参数 size 指明新内存区域的长度; 参数 type 指明了新内存区域的类型.

函数在 154 行定义了一个整形变量 x 用于存储参数 table 对应 e820 内存映射表中 含有内存区域的个数. 函数 156-160 行检测参数 table 对应的 e820 内存映射表是否 已经满了,如果 table 已经满了,那么该 table 不能再添加新的内存区域, 那么函数 抛出错误信息,并直接返回; 反之通过 162-164 行将新的内存区域加入到 table 里面, 并将 table 中内存区域的数量加 1.


struct boot_e820_entry

struct boot_e820_entry 用于描述 BIOS 探测到的一个内存区域。BIOS 上电启动之后 对内存进行探测,并将探测之后的信息存储在指定位置,BIOS 一般会探测到多个内存 区域,每个内存区域使用上图的数据结构方式进行存储,具体 BIOS 信息可以通过查看 下面文档进行理解:

BIOS 通过三个成员描述一个内存区域,第一个成员是 addr,用于描述内存区域的起始 物理地址。第二个成员是 size, 用于描述内存区域的长度. 第三个成员是 type, 用于 描述内存区域的类型. 前两个成员长度均为 64 位,type 的长度为 32 位. struct boot_e820_entry 数据结构定义的时候使用了 “__attribute__((packed))” 属性, 那么编译器是不能优化该数据结构的,因此保证了该数据结构的长度是 20 个字节.


struct biosregs

struct biosregs 数据结构用于维护一套寄存器组,以便用于 BIOS 调用时候交换寄存器。 struct biosregs 数据结构内部使用一个 union 联合体包含了三套寄存器.

第一套是保护模式下的寄存器组,包含了 EAX, EBX, ECX, EDX 等 32 位的寄存器.

第二组为实施模式下的寄存器组, 包含了 AX, BX, CX, DX 等 16 位寄存器.

第三套是兼容 8086 模式下的寄存器组,包含了 AL, AH, BL, BH 等 8 位寄存器.


detect_memory_e820

detect_memory_e820() 函数用于在 X86 实施模式下,通过 BIOS INT 0x15/E820 调用 获得系统内存布局信息。BIOS 启动之后,会对所有的内存进行探测,并将探测到的内存 布局信息存储到 BIOS 的 INT 表里,X86/Linux 在启动过程中, 可以通过 INT 0x15/E820H 从 BIOS 中读取内存布局信息. detect_memory_e820() 函数就是通过 BIOS INT 0x15/E820 读取内存布局信息,并将内存信息存储到系统的 e820_table. 更多 E820 调用开发者请参考:

函数首先定义了两个 struct biosregs 变量,ireg 成员用于设置 E820 调用的寄存器 信息,而 oreg 用于存储 E820 调用返回的寄存器信息. 函数接着定义了两个 struct boot_e820_entry 变量 desc 和 buf,desc 用于指向 X86 内核维护的 E820 表,buf 用于暂时存储一个从 BIOS 中获得的内存区域.

函数在第 27 行到 31 行用于 BIOS INT 0x15/E820 调用前的准备工作,初始化相关的 寄存器,此时需要将 AX 设置为 0xe820, 将一个内存区域的长度存储到 cx 寄存器,然 后将 SMAP 信息存储到 edx 用于校验,最后将 DI 寄存器指向存储内存区域信息的位置。 准备完毕之后进行 BIOS 调用。由于 BIOS 一般维护多条内存区域信息,然而每次 BIOS 调用只能获得一条内存区域信息的值,因此需要使用循环,多次 BIOS 调用将所有的内存 区域信息从 BIOS 中读取出来。函数 47 行到 69 行循环从 BIOS 中读取所有的内存区域 信息。每次执行完该 BIOS 调用之后,BIOS 都会将下一条内存区域信息的索引存储在 EBX 寄存器中,当 BIOS 读到最后一条内存区域信息之后,BIOS 会将 EBX 的值设置为 0,因此 EBX 的值可以作为循环结束条件。其次 X86 内核将 e820 表维护在 “boot_params.e820_table” 里,其为一个 struct boot_e820_entry 数组,如果从 BIOS 中获得的内存区域信息条数超过 “boot_params.e820_table” 数组容量,那么函数也会 终止从 BIOS 中获得内存区域信息。因此 69 行明确了从 BIOS 中读取内存区域信息的 终止条件.

函数在 48 行调用 initcall() 函数进行了 BIOS INT 调用。第一次执行该循环时,由于 EBX 的值为 0,那么第一次读取了第一个内存区域信息,接着在 49 行,将 BIOS 返回 的 EBX 值写入到输入的 EBX 里面,以此指明下一次 BIOS INT 读取下一个内存区信息。 函数在 54 行检测 EFLAGS 寄存器的 CF 标志是否置位,如果置位表示此次 BIOS INT 调用是失败的,并在 55 行处返回; 反之 CF 标志清零,那么表示此次 BIOS INT 调用 成功。函数在 62 行继续检测 EAX 的返回值是否为 SMAP, 以此表明 BIOS INT 0x15/e820 是成功的,如果不是,那么函数在 63-64 行处将 count 设置为 0 并进行 break。以上 检测通过之后,在 67 行处,函数将获得的内存区域信息存储到内核的 e820_table 里, 并将 desc 指针指向 e820_table 的下一个位置. 函数在 68 行处更新 count 的值,以此 统计从 BIOS 中读取内存区域信息的个数. 函数通过循环不停的调用 BIOS INT 调用, 直到循环结束。结束之后,函数在 71 行处将读取内存区域的个数存储到内核 “boot_params.e820_entries”。

struct boot_e820_entry


detect_memory_e801

detect_memory_e801() 函数用于探测 X86 架构 4GB 以内的内存布局。 函数核心通过 BIOS INT 0x15/E810 调用获得 X86 架构 16MB 以内的内存布局以及大于 16MB 小于 4GB 的内存布局。详细 BIOS INT 0x15/E810 请参见:

函数在 76 行定义了两个 struct biosregs 变量,ireg 变量用于配置 BIOS 调用的 寄存器值,oreg 变量用于存储 BIOS 调用返回的寄存器值。函数在 78 行调用 initregs() 函数初始化了 ireg 变量,将相关的寄存器都设置为 0。函数在 79 行处 将 AX 寄存器的值设置为 0xe801, 然后调用 initcall() 函数执行 BIOS INT 操作。 函数在 82 行处检测执行 BIOS INT 之后的 EFLAGS 寄存器的 CF 标志,如果 CF 标志 置位,那么表示 BIOS INT 失败; 反之表示此次 BIOS INT 成功。函数继续检测 CX 和 DX 寄存器是否存在值,如果存在,那么将 CX 和 DX 的值分别赋值给 oreg 的 AX 和 BX,但这样做并无实际效果。BIOS INT 0x15/E801 调用成功之后,会将小于 16 MB 的 物理内存布局信息存储到 AX 寄存器,然后将大于 16MB 且小于 4GB 的信息存储到 BX 寄存器。函数在 91 行检测返回的 AX 寄存器是否等于 15*1024, 如果大于,那么这是 一个 BIOS bug,AX 最大的值只能是 15*1024. 如果此时 AX 的值等于 15*1024, 那么 表示系统的物理内存大于 16MB,那么函数在 93-94 行,将大于 16MB 内存数量存储到 内核的 boot_params.alt_mem_k 进行维护,因为大于 16MB 小于 4GB 的内存信息以 64KB 为单位存储到 BX 寄存器中,因此在计算内存的时候,需要转换一下; 如果检测到 AX 的值小于 15*1024, 那么表示系统的物理内存小于 16MB,那么此时将物理内存的 系统从 AX 寄存器中读取出来存储到内核的 “boot_params.alt_mem_k” 里面。 至于上面为什么 AX 的最大值只有 15MB,这是一个历史问题,请参考下文进行了解:


detect_memory_88

detect_memory_88() 函数用于从 BIOS 中获得小于 64MB 的物理内存布局。函数核心 通过 BIOS INT 0x15/0x88 调用获得小于 64MB 的物理内存布局。详细 BIOS INT 0x15/0x88 调用请参见:

函数在 109 行定义了两个 struct biosregs 变量,ireg 变量用于配置 BIOS 调用的 寄存器值,oreg 变量用于存储 BIOS 调用返回的寄存器值。函数在 111 行调用 initregs() 函数初始化了 ireg 变量,将相关的寄存器都设置为 0。函数在 112 行处 将 AH 寄存器的值设置为 0x88, 然后调用 initcall() 函数执行 BIOS INT 操作。函 数直接在 115 行将从 BIOS 中获得小于 64MB 内存布局信息存储到内核维护的 “boot_params.screen_info.ext_mem_k” 里面.


通过 BIOS IVT 探测物理内存

系统上电之后 BIOS 进行了一系列的初始化操作,包括了硬件的基础初始化、ACPI 表, E820 表、IVT 等创建,将系统带入一个已知的状态。接着 BIOS 将从指定位置引导内核 的启动,并将系统执行权限移交给内核,内核在进行早期的初始化阶段会通过 BIOS 提供 的 IVT (中断向量表) 获得物理内存的信息,其中包括了重要的 E820 Table 等信息,本 节用于研究 BIOS IVT 中与内存相关的中断例程:


BIOS C7

BIOS 通过向操作系统提供 “INT 0x15/AX=C7H” 中断, 可以获得从 0x00100000 开始

AH = C7h
DS:SI -> user supplied memory map table (see #00526)

Return:
CF set on error
CF clear if successful

虽然 BIOS C7 并没有称为主流获得内存的方法,但在 IBM 机器上提供了一个好的办法 获得内存映射信息。只需向 AH 传入 0xC7,并将 DS:SI 指向存储映射表的内存地址. 执行 0x15 中断例程后,如果 CF 标志没有置位,那么表示调用成功。如果调用成功之后, DS:SI 指向的内容如下:

Size   Offset  Description                                  

 2      00h     Number of significant bytes of returned data (excluding this uint16_t)
 4      02h     Amount of local memory between 1-16MB, in 1KB blocks
 4      06h     Amount of local memory between 16MB and 4GB, in 1KB blocks
 4      0Ah     Amount of system memory between 1-16MB, in 1KB blocks
 4      0Eh     Amount of system memory between 16MB and 4GB, in 1KB blocks
 4      12h     Amount of cacheable memory between 1-16MB, in 1KB blocks
 4      16h     Amount of cacheable memory between 16MB and 4GB, in 1KB blocks
 4      1Ah     Number of 1KB blocks before start of nonsystem memory between 1-16MB
 4      1Eh     Number of 1KB blocks before start of nonsystem memory between 16MB and 4GB
 2      22h     Starting segment of largest block of free memory in 0C000h and 0D000h segments
 2      24h     Amount of free memory in the block defined by offset 22h

 • Local Memory on the system board or memory that is not accessible from the channel. It can be system or nonsystem memory.
 • Channel Memory on adapters. It can be system or nonsystem memory.
 • System Memory that is managed and allocated by the primary operating system. This memory is cached if the cache is enabled.
 • Nonsystem Memory that is not managed or allocated by the primary operating system. This memory includes memory-mapped I/O devices; memory that is on an adapter and can be directly modified by the adapter; and memory that can be relocated within its address space, such as bank-switched and expanded-memory-specifications (EMS) memory. This memory is not cached. 

并不是所有的 X86 机器都支持这个中断例程,X86 内核支持在实施模式下使用 C 语言 调用该调用,使用如下:

detect_memory_C7() 函数实现逻辑很简单,直接通过调用 0x15/0x7C 进入 BIOS 的 IVT,调用完成之后, BIOS 将内存信息存储在 DS:SI 指向的内存空间寄存器,因此 可以使用 struct ibm_memory_map 数据结构将其读出,以此获得系统内存映射信息。 该段代码运行在 16 位实时模式下,因此开发者可以将上面的函数添加到 arch/x86/boot/memory.c 文件中运行, 更多 BIOS INT 0x15 8A 内容请参考下面文档:


BIOS 8A

BIOS 通过向操作系统提供 “INT 0x15/AX=8AH” 中断, 可以获得从 0x00100000 开始 之后的连续物理内存信息,其按 KiB 统计在 DX:BX 寄存器中。在有的平台可能不支持 该调用。如果 ISA 内存空洞存在于 0x00F00000 到 0x00FFFFFF 1 MiB 的空间,这段空间 用于映射 ISA 的 I/O 空间,那么该中断例程可能不会报告全部可以使用的 RAM 信息, 例如中断例程只会报告 0x00100000 到 0x00F00000 的内存大小,而不会报告 0x01000000 之后包含物理内存的大小. 另外假设当调用成功之后获得从 0x00100000 之后的物理内存 为 14 MiB,那么不能假设没有更多的物理内存在 0x00100000 之后.

AH = 8Ah

Return:
DX:AX = extended memory size in K

该 BIOS 调用执行之后, BIOS 会将从 0x00100000 之后的内存大小存储到 DX:AX 指向的 寄存器里,并按 KiB 进行统计。X86 内核支持在实施模式下使用 C 语言调用该调用,使 用如下:

detect_memory_8A() 函数实现逻辑很简单,直接通过调用 0x15/0x8A 进入 BIOS 的 IVT,调用完成之后, BIOS 将大于 0x00100000 的内存信息存储在 DX:AX 寄存器,因此 将 AX 寄存器和 DX 寄存器信息进行合并,并将结果乘与 1024,以此计算出物理内存的 大小。该段代码运行在 16 位实时模式下,因此开发者可以将上面的函数添加到 arch/x86/boot/memory.c 文件中运行, 更多 BIOS INT 0x15 8A 内容请参考下面文档:


BIOS DA88

BIOS 通过向操作系统提供 “INT 0x15/AX=DA88H” 中断, 可以获得从 0x00100000 开始 之后的物理内存信息,其按 KiB 统计在 CL:BX 寄存器中。在有的平台可能不支持该调用。 假设当调用成功之后获得从 0x00100000 之后的物理内存为 14 MiB,那么不能假设没有 更多的物理内存在 0x00100000 之后.

AH = DA88h

Return:
CF clear if successful
AX = 0000h
CL:BX = extended memory size in KiB

该 BIOS 调用执行之后,如果 EFLAGS 寄存器的 CF 标志置位,那么表示调用失败; 反之 如果 CF 标志位清零,那么调用成功。调用成功之后,BIOS 会将从 0x00100000 之后的 内存大小存储到 CL:DX 指向的寄存器里,并按 KiB 进行统计。X86 内核支持在实施模式 下使用 C 语言调用该调用,使用如下:

detect_memory_DA88() 函数实现逻辑很简单,直接通过调用 0x15/0xDA88 进入 BIOS 的 IVT,调用完成之后,如果 EFLAGS 的 CF 标志没有置位,那么表示调用成功。BIOS 将 大于 0x00100000 的内存信息存储在 CL:BX 寄存器,因此将 CL 寄存器和 BX 寄存器信息 进行合并,并将结果乘与 1024,以此计算出物理内存的大小。该段代码运行在 16 位实时 模式下,因此开发者可以将上面的函数添加到 arch/x86/boot/memory.c 文件中运行, 更 多 BIOS INT 0x15 DA88 内容请参考下面文档:


BIOS 88

BIOS 通过向操作系统提供 “INT 0x15/AX=88H” 中断, 可以获得小于 64M 的物理内存 布局,即使内存容量大于 64MB, 也只会识别显示 63MB. 因此该调用只会显示 1MB 之上 的内存,不包括 1MB 内存,因此在最后统计内存的时候需要手动加上 1MB. 该调用的使用 如下:

AH = 88h

Return:
CF clear if successful
AX = number of contiguous KB starting at absolute address 100000h
CF set on error
AH = status
80h invalid command (PC,PCjr)
86h unsupported function (XT,PS30)

该 BIOS 调用执行之后,如果 EFLAGS 寄存器的 CF 标志置位,那么表示调用失败; 反之 如果 CF 标志位清零,那么调用成功。调用成功之后,BIOS 会将小于 64 MB 的内存信息 存储到 AX 寄存器,并按 KB 进行描述。X86 内核支持在实施模式下使用 C 语言调用该 调用,使用如下:

这段代码来自 X86 架构 arch/x86/boot/memory.c, 该段代码运行在 16 位实施模式 下. 上面的代码逻辑就是通过 88 BIOS 调用获得小于 64 MB 的物理内存布局. 更多 BIOS INT 0x15 88 内容请参考下面文档:


BIOS E801

BIOS 通过向操作系统提供 “INT 0x15/AX=E801H” 中断, 可以获得小于 16M 的物理内存 布局信息,并且可以获得大于 16MB 的物理内存布局信息,但最大只能探测到 4GB 的物理 内存,因此大于 4GB 的物理内存无法使用该 BIOS 调用获得布局信息. 该调用的使用 如下:

AX = E801h

Return:
CF clear if successful
AX = extended memory between 1M and 16M, in K (max 3C00h = 15MB)
BX = extended memory above 16M, in 64K blocks
CX = configured memory 1M to 16M, in K
DX = configured memory above 16M, in 64K blocks
CF set on error

该调用通过向 AX 寄存器写入 E801 就可以进行 BIOS 调用。如果 BIOS 调用之后, EFLAGS 寄存器的 CF 表示清零,那么表示调用成功; 反之 CF 置位则表示调用失败。 调用成功之后,会将小于 16MB 物理内存的布局信息存储到 AX 寄存器,由于 AX 只能 描述小于 16MB 的内存信息,那么但系统物理内存大于 16M,那么 AX 最大值则为 3C00H, 表示低 16MB 内存空间全部布满。调用将大于 16MB 小于 4GB 的物理内存布局 信息存储在 BX 寄存里, BX 寄存器中的内存以 64K 为单位进行描述. CX 值同 AX 一致, DX 寄存器值同 BX 一致。

实际物理内存和检测到的内存总是差 1MB,言外之意是总有 1MB 内存不可用,很多问题 都是祖上传下来的,即著名的历史遗留问题。80286 拥有 24 位地址线,其寻址空间是 16MB。当时有一些 ISA 设备要用到地址 15MB 以上的内存作为缓存区,也就是此缓存区 为 1MB 大小,所以硬件系统就把这部分内存预留下来,操作系统不可以用此段内存空间。 保留的这部分内存区域就像不可以访问的黑洞,这就成了内存空洞 memory hole。现在 虽然很少很少能碰到这些老 ISA 设备了,但为了兼容,这部分空间还是保留下来了,只 不过通过 BIOS 选项的方式由用户自己选择是否开启。BIOS 厂商不同,一般的菜单选项 名称也不相同,不过大概思路都是差不多,比如开机进入 BIOS 界面之后,会有类似的 选项:

BIOS Menu:
  memory hole at address 15m-16m

将测选项设置为 enable 或 disable 变开启或关闭对这类扩展 ISA 设备的支持. 话说 起初定义这个 0xe801 子功能,就是为了支持扩展 ISA 服务。X86 内核支持在实施模式 下使用 C 语言调用该调用,使用如下:

这段代码来自 X86 架构 arch/x86/boot/memory.c, 该段代码运行在 16 位实施模式 下. 上面的代码逻辑就是通过 E801 BIOS 调用获得小于 16MB 和大于 16MB 小于 4GB 的物理内存布局. 更多 BIOS INT 0x15 E801 内容请参考下面文档:


BIOS E820

BIOS 通过向操作系统提供 “INT 0x15/AX=E820H” 中断,将物理内存映射地址描述符 信息传递给操作系统。物理内存在硬件上可能连续也可能不连续,连续的物理内存就是 一整块的区域,而不连续的两块内存中间存在的空间称为内存空洞,或者称为 Hole. 这些 Hole 要么是系统预留给某些特定的硬件设备使用,要么没有真实的物理内存。 由于物理内存的布局关系,物理内存会被分作很多区块,BIOS 在启动过程中探测到这些 物理内存区域之后,使用 Entry 为单位维护内存区块, Entry 的结构布局如下:

Offset  Size    Description
00h     QWORD   base address
08h     QWORD   length in bytes
10h     DWORD   type of address range

Entry 结构中,”Base address” 字段表示一个物理内存区域的起始物理地址,其长度 是一个 64 bit 的数据; “length” 字段表示一个物理内存区域的长度, 其长度是一个 64 bit 的数据; “type” 字段表示物理内存区域的类型, 其长度是一个 32 bit 的数据. BIOS 支持识别的物理内存类型如下表:

Values for System Memory Map address type:
01h    memory, available to OS
02h    reserved, not available (e.g. system ROM, memory-mapped device)
03h    ACPI Reclaim Memory (usable by OS after reading ACPI tables)
04h    ACPI NVS Memory (OS is required to save this memory between NVS sessions)
other  not defined yet -- treat as Reserved

在 BIOS 识别物理内存类型中,01 代表可用的物理内存; 02 代表预留空间,这些空间 可能为系统的 ROM/IOMEM 预留; 03 表示 ACPI 可回收内存. E820 系统调用如下:

AX  = E820h
INT 0x15
EAX = 0000E820h
EDX = 534D4150h ('SMAP')
EBX = continuation value or 00000000h to start at beginning of map
ECX = size of buffer for result, in bytes (should be >= 20 bytes)
ES:DI -> buffer for result

调用 E820 之前,需要将 0xE820 储存到 AX 寄存器,并将标识码 “SMAP” 存储到 EDX 寄存器里面,接着将存储内存区域信息的地址存储到 DI 寄存器,并且将存储内存区域 的长度存储到 CX 寄存器内。由于 BIOS 能够探测到多个内存区域,因此 EBX 用于指定 读取第几条内存区域信息。

准备好上面的寄存器之后,执行 BIOS 调用。调用完毕之后 EFLAGS 寄存器的 CF 标志位 用于指示本次调用的成功状态,如果 CF 标志位置位,那么此次调用失败; 反之如果 CF 标志位清零,那么此次调用成功。接着再检查 EAX 的值是否为 “SMAP”, 如果不是也代表 此次调用失败。以上两个检测都通过的话,那么 BIOS 会将一条内存信息存储在 ES:DI 指向的地址上,即之前设置缓存的位置。由于 BIOS 中存在多条内存区域的信息,因此 BIOS 会将下一条内存区域的信息存储在 EBX 寄存器里,因此可以使用循环将所有的内存 区域信息都读出来。X86 内核支持在实施模式下使用 C 语言调用该调用,使用如下:

这段代码来自 X86 架构 arch/x86/boot/memory.c, 该段代码运行在 16 位实施模式 下. 上面的代码逻辑就是通过 E820 BIOS 调用打印所有的内存区域. 更多 BIOS INT 0x15 E820 内容请参考下面文档:


附录

BiscuitOS Home

BiscuitOS Driver

Linux Kernel

Bootlin: Elixir Cross Referencer

捐赠一下吧 🙂

MMU