一、linux内存管理学习(1):物理内存探测

目录

1、内存探测

1.1、e820__memory_setup

1.2、parse_setup_data

[1.3 总结](#1.3 总结)

2、物理内存整理

2.1、parse_early_param

2.2、e820__reserve_setup_data

2.3、e820_add_kernel_range

[2.4、 e820__end_of_ram_pfn](#2.4、 e820__end_of_ram_pfn)


1、内存探测
1.1、e820__memory_setup
复制代码
e820__memory_setup 
	--> e820__memory_setup_default 
		--> append_e820_table                // 将BIOS提供的原始E820内存映射表复制到内核安全区域
		--> e820__update_table(e820_table);  //更新内核的E820内存映射表
	--> memcpy(e820_table_kexec, e820_table, sizeof(*e820_table_kexec));
		memcpy(e820_table_firmware, e820_table, sizeof(*e820_table_firmware));
	--> e820__print_table

该流程主要内容:

(1)从 BIOS 获取原始 E820 表

(2)构建三份内存表:

  • e820_table:主表,供内核内存管理使用。
  • e820_table_kexec:为 kexec 热重启保留,确保新内核看到一致的物理布局。
  • e820_table_firmware:备份原始 BIOS 数据,用于调试或回退。

(3)打印内存布局

复制代码
[    0.000000] BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[    0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000001ffdffff] usable
[    0.000000] BIOS-e820: [mem 0x000000001ffe0000-0x000000001fffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
1.2、parse_setup_data
复制代码
parse_setup_data 
	--> e820__memory_setup_extended
		--> early_memremap        // 映射物理地址到内核虚拟地址空间
		--> __append_e820_table   // 将扩展条目追加到内核 E820 表
		--> e820__update_table    // 合并/优化内存表(处理重叠或相邻区域)
		--> memcpy(e820_table_kexec, e820_table, sizeof(*e820_table_kexec));  // 备份更新后的内存表
    		memcpy(e820_table_firmware, e820_table, sizeof(*e820_table_firmware));
		--> early_memunmap        // 解除早期内存映射
		--> e820__print_table("extended");  // 打印扩展后的内存布局

主要内容:

(1)在早期启动阶段(页表未完全初始化时),将物理地址 phys_addr 临时映射到内核虚拟地址空间,以访问扩展的 E820 数据

(2)将扩展条目追加到内核主表 e820_table,不进行合并(保留原始信息)

(3)合并相邻/重叠区域,解决冲突(如高优先级类型覆盖低优先级类型)

(4)释放临时映射,避免内存泄漏

(5)打印扩展后的完整内存布局,前缀为 "extended",示例:

复制代码
[    0.000000] extended physical RAM map:
[    0.000000] extended: [mem 0x00000000-0x0009ffff] usable
[    0.000000] extended: [mem 0x00100000-0x3fffffff] reserved
1.3 总结

第一部分整体调用流程:

复制代码
start_kernel()
	--> setup_arch()
		--> e820__memory_setup()          // 处理前 128 条目
		--> parse_setup_data()            // 遍历 setup_data 链表
       	--> e820__memory_setup_extended()  // 处理扩展条目

目的:解决BIOS/引导加载程序struct boot_params​ 中 e820_table​ 仅能容纳 128 个内存条目的问题

解决方案:超出部分通过 SETUP_E820_EXT​ 节点(struct setup_data​ 链表)传递,传递后将数据合并到e820_table,经e820__update_table整理后更新到e820_table_kexec/firmware

2、物理内存整理
2.1、parse_early_param
复制代码
parse_early_param()
	--> parse_early_options(cmdline)
		--> parse_args(..., do_early_param)
			--> 对每个参数调用 parse_one()
				--> 若匹配 early 标记,调用 do_early_param()
					--> 执行 p->setup_func(val)(如 early_mem("512M"))

整个流程是解析早期启动参数(early param)的过程,涉及内核命令行参数(cmdline)的解析和相应处理函数的执行

(1)在 do_early_param()​ 中,内核通过以下步骤匹配并执行回调:

  • 遍历注册表:扫描 __setup_start 到 __setup_end 之间的所有参数(由链接脚本生成)。
  • 匹配参数名:比较 cmdline 中的参数(如 mem=512M)与注册的 str 字段。
  • 执行回调:若匹配且标记为 early,则调用 setup_func(val),传入参数值(如 512M)。

(2)linux使用early_param​ 宏(定义于 include/linux/init.h​),将参数名与处理函数绑定,并标记为 EARLY​ 属性,例如:

复制代码
early_param("mem", early_mem); // 注册 "mem=" 参数,回调函数为 early_mem

展开后生成一个 struct obs_kernel_param​ 结构体,存入 .init.setup​ 段(通过链接脚本聚合),即__setup_start​ 到 __setup_end​ 之间的段

(3)因此,针对下面的代码:

复制代码
early_param("mem", parse_memopt);

若命令行中有 'mem= ' 字段,便会调用parse_memopt回调函数,进行内存大小重新调整

2.2、e820__reserve_setup_data
复制代码
e820__reserve_setup_data 
	--> e820__range_update
		--> e820__update_table
			--> e820__print_table

e820__finish_early_params
	--> e820__update_table 
		--> e820__print_table

保留bootloader传递的setup_data内存区域,并更新内存映射表

在完成早期启动参数(如 mem=xxx)解析后,对内存映射表进行最终规范化,并打印调试信息

2.3、e820_add_kernel_range
复制代码
e820_add_kernel_range 
	--> e820__mapped_all 
	--> e820__range_remove 
	--> e820__range_add

static void __init e820_add_kernel_range(void)
{
    // 计算内核的物理地址范围(从 _text 到 _end)
    u64 start = __pa_symbol(_text);       // 内核代码起始物理地址
    u64 size = __pa_symbol(_end) - start; // 内核占用的总大小

    // 检查是否已被正确标记为 RAM
    if (e820__mapped_all(start, start + size, E820_TYPE_RAM))
        return; // 如果已经是 RAM,直接返回

    // 报警:内核区域未被标记为 RAM
    pr_warn(".text .data .bss are not marked as E820_TYPE_RAM!\n");

    // 修复步骤:
    // 1. 移除该范围内的所有现有内存条目(无论类型)
    e820__range_remove(start, size, E820_TYPE_RAM, 0);
    // 2. 重新添加为 E820_TYPE_RAM
    e820__range_add(start, size, E820_TYPE_RAM);
}
  • 背景:BIOS 或 bootloader 提供的 E820 内存映射表可能不准确,有时会错误地将内核代码和数据所在的内存区域标记为"保留"或"不可用"(如 E820_TYPE_RESERVED)。例如:用户通过 memmap=exactmap 或 memmap=xxM$yyM 手动排除内核所在的内存区域。
  • 目的:强制将内核的代码段(.text)、数据段(.data)和未初始化数据段(.bss)标记为 E820_TYPE_RAM,避免内核因运行在"非 RAM"区域而崩溃。
2.4、 e820__end_of_ram_pfn

这段代码主要用于 确定系统物理内存的边界(以页帧号 pfn​ 表示)

复制代码
e820__end_of_ram_pfn    // 计算系统中可用物理内存(E820_TYPE_RAM)的最后一页的页帧号(last_pfn)。
	--> e820_end_pfn

函数主要功能:

(1)确定物理内存边界:通过遍历 E820 表,计算可用 RAM 的最大页帧号(last_pfn​)。

(2)区分低端/高端内存(32/64 位):

  • 32 位:直接调用 find_low_pfn_range()。

  • 64 位:根据物理内存是否超过 4GB 动态设置 max_low_pfn,last_pfn 和 max_low_pfn 是内核内存管理的关键参数,用于初始化页表、伙伴系统等。

    复制代码
             MAX_ARCH_PFN (架构支持最大值)
                     │
                     ▼
             e820_end_pfn() → last_pfn
                     │
         ┌───────┴───────┐
         ▼               ▼

    max_low_pfn max_pfn
    (低端内存边界) (实际物理内存上限)
    │ │
    ▼ ▼
    32位: find_low_pfn_range() 64位: high_memory = __va(max_pfn)

    物理地址空间
    ┌───────────────────────┐
    │ Low Memory │ ◄─max_low_pfn (直接映射区)
    │ (0x00000000-3FFFFFFF)│
    ├───────────────────────┤
    │ High Memory │ ◄─动态映射
    │ (>=0x40000000) │
    └───────────────────────┘

    物理地址空间
    ┌───────────────────────┐
    │ Low Memory (<4GB) │ ◄─max_low_pfn
    ├───────────────────────┤
    │ High Memory (≥4GB) │ ◄─high_memory = __va(max_pfn)
    └───────────────────────┘