Linux内核6.6 内存管理(15)代码导读——关于物理内存完整布局的思考
arm64_memblock_init
https://elixir.bootlin.com/linux/v6.11/source/arch/arm64/kernel/setup.c#L329
1)s64 linear_region_size = PAGE_END - _PAGE_OFFSET(vabits_actual);计算内核能访问Physical memory的范围
2)适配KVM场景
3)memblock_remove(1ULL << PHYS_MASK_SHIFT, ULLONG_MAX);移除超PA支持范围的内存
4)memstart_addr = round_down(memblock_start_of_DRAM(), ARM64_MEMSTART_ALIGN);确定物理内存起始地址
5)
max(线性映射所能覆盖的最大物理地址,内核数据段结束的物理地址)
情况1:线性映射覆盖内核(A ≥ B)
┌──────────────┬──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐ │ 物理地址范围 │ 0 ~ S-1 │ S ~ B-1 │ B ~ A-1 │ A ~ D │ D+1 ~ ULLONG_MAX │ │ 标注 │ 低地址空闲区 │ 内核区 │ 线性映射内空闲区 │ 超线性映射高地址区 │ 无效高地址区 │ │ 内容 │ (未使用) │ 内核代码/数据 │ 可访问物理内存 │ 不可访问内存 │ (硬件不可达) │ │ 操作 │ 不处理 │ 保留(不可移除) │ 保留(可访问) │ 移除(memblock_remove)│ 移除(memblock_remove)│ ├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤ │ 关键参数标注 │ │ ↓ S=memstart_addr │ ↓ B=__pa_symbol(_end) │ ↓ A=S+linear_size │ ↓ D=memblock_end_DRAM │ └──────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘
情况2:线性映射未覆盖内核(A < B)
┌──────────────┬──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐ │ 物理地址范围 │ 0 ~ S-1 │ S ~ A-1 │ A ~ B-1 │ B ~ D │ D+1 ~ ULLONG_MAX │ │ 标注 │ 低地址空闲区 │ 线性映射内内核区 │ 超线性映射内核区 │ 超线性映射高地址区 │ 无效高地址区 │ │ 内容 │ (未使用) │ 内核代码/数据 │ 内核代码/数据 │ 不可访问内存 │ (硬件不可达) │ │ 操作 │ 不处理 │ 保留(不可移除) │ 保留(不可移除) │ 移除(memblock_remove)│ 移除(memblock_remove)│ ├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤ │ 关键参数标注 │ │ ↓ S=memstart_addr │ ↓ A=S+linear_size │ ↓ B=__pa_symbol(_end) │ ↓ D=memblock_end_DRAM │ └──────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘
我们最起码保证内核不会被裁剪,当内核完成早期初始化后,会建立完整的内核页表。内核会为自身所有区域(代码段 _text
、数据段 _data
、bss 段等,包括 A~B-1 区域)建立独立的虚拟地址映射,这些映射与线性映射无关;后续内核会通过 paging_init
等函数完成页表初始化,将内核所有物理区域的映射写入页表,彻底解决 “线性映射未覆盖” 的问题
问题1:为什么需要映射呢,我理解的是,可以直接让内核直接从内存起始地址为0的地方直接放
解答:
一.问题:直接将内核放物理地址0的硬件冲突
┌───────────────────────────────────────────── 物理地址空间 ──────────────────────────────────────────────┐ │ 0x00000000 ~ 0x0000FFFF │ 0x00010000 ~ 0x00FFFFFF │ 0x01000000 ~ ... │ ... │ 高地址物理内存 │ │ ┌─────────────────────┐ │ ┌─────────────────────┐ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ BIOS/ROM固件区域 │ │ │ 中断向量表/外设寄存器 │ │ │ 空闲内存 │ │ │ │ 空闲内存 │ │ │ │ (硬件必须占用) │ │ │ (硬件必须占用) │ │ │ │ │ │ │ │ │ │ └─────────────────────┘ │ └─────────────────────┘ │ └─────────────┘ │ │ └─────────────┘ │ │ │ │ │ │ │ │ ↓ 若强行放内核 → │ │ │ │ │ │ ┌─────────────────────┐ │ │ │ │ │ │ │ 内核代码/数据 │ │ │ │ │ │ │ │ (覆盖固件/硬件数据)│ │ │ │ │ │ │ └─────────────────────┘ │ │ │ │ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ → 结果:硬件固件被覆盖→启动失败;中断向量表被改→CPU无法响应中断;外设寄存器被写→硬件瘫痪
二.解决:通过“虚拟地址映射”隔离硬件与内核的示意图
1.物理地址空间(硬件真实地址)
┌───────────────────────────────────────────── 物理地址空间 ──────────────────────────────────────────────┐ │ 0x00000000 ~ 0x0000FFFF │ 0x00010000 ~ 0x00FFFFFF │ 0x01000000 ~ 0x02FFFFFF │ 0x03000000 ~ ... │ │ ┌─────────────────────┐ │ ┌─────────────────────┐ │ ┌─────────────────────┐ │ ┌─────────────┐ │ │ │ BIOS/ROM固件区域 │ │ │ 中断向量表/外设寄存器│ │ │ 内核实际存放区域 │ │ │ 其他内存 │ │ │ │ (硬件保留,不可写)│ │ │ (硬件保留,不可写)│ │ │ (空闲物理内存) │ │ │ │ │ │ └─────────────────────┘ │ └─────────────────────┘ │ └─────────────────────┘ │ └─────────────┘ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘
2.内核虚拟地址空间(内核看到的地址)
┌───────────────────────────────────────────── 内核虚拟地址空间 ────────────────────────────────────────────┐ │ 0xFFFF000000000000 ~ 0xFFFF000001FFFFFF │ 0xFFFF000002000000 ~ ... │ ... │ 其他虚拟地址区域 │ │ ┌─────────────────────────────────────┐ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ 内核虚拟地址区域(线性映射) │ │ │ 其他虚拟 │ │ │ │ 动态映射 │ │ │ │ → 映射到物理地址 0x01000000 ~ ... │ │ │ 内存区域 │ │ │ │ 区域 │ │ │ └─────────────────────────────────────┘ │ └─────────────┘ │ │ └─────────────┘ │ └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ → 核心:内核代码在虚拟地址空间中“逻辑连续”,但实际映射到物理地址中“远离硬件保留区”的空闲内存,既不冲突又安全
3.映射关系表(MMU硬件维护)
内核虚拟地址范围 | 对应的物理地址范围 | 权限设置 |
0xFFFF000000000000 ~ ... | 0x01000000 ~ ... | 内核可读写,用户不可访问 |
... | 0x00000000 ~ 0x00FFFFFF | 只读(仅内核可访问硬件区域) |
... | 其他物理内存 | 按需分配权限 |
6)
通过上移线性映射的物理起点(memstart_addr
) 和裁剪低地址内存,让最终管理的物理内存大小刚好等于线性映射范围,确保内核能通过线性映射访问所有可用内存。
问题2:把内核地址上移动到物理内存地址上限,然后裁剪低地址部分,那么低地址裁剪的部分用来干嘛?那为什么不能让内核用低地址,非要让内核用高地址呢?
1.硬件/固件的专属区域(bios/bootloader/cpu终端向量表)优先占用低地址的极小固定区域,剩余地址交给用户态使用
2.地址空间划分:如 64 位系统通常将 0xFFFF000000000000
以上作为内核空间),低地址留给用户程序(如应用、进程)使用。内核用高地址,能避免与用户程序的地址冲突,简化内存管理逻辑。
┌─────────────────────────────────────────────────────────────────┐ │ 物理地址空间(真实硬件地址) │ │ ┌───────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ 硬件专属区 │ │ 用户可用物理区 │ │ 内核专属物理区 │ │ │ │ (低地址) │ │ (中低地址) │ │ (高地址) │ │ │ └───────────┘ └───────────────┘ └───────────────┘ │ ↑ ↑ ↑ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 虚拟地址空间(内核+用户) │ │ ┌───────────────┐ ┌───────────────────────────┐ │ │ │ 用户虚拟空间 │ │ 内核虚拟空间 │ │ │ │ (映射用户区) │ │ (映射硬件+用户+内核区) │ │ │ └───────────────┘ └───────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘
7)
这是给initrd页面对齐的代码
问题1.此时尚未开启页面管理,为什么需要页面对齐?
1. 为后续 MMU 页表映射做准备(MMU 只认整页);
2. 避免与后续 memblock / 页分配器的内存块冲突;
3. 简化后续 initrd 解压 / 读取的跨页处理,提升效率。
问题2.如何使得页面对齐?
以PAGE_SIZE=4KB为例
phys_initrd_start = 0x80001234
(未对齐起始地址)phys_initrd_size = 0x5678
(未对齐大小)mask = 0xFFF
(PAGE_SIZE-1
),~mask = 0xFFFFF000
(PAGE_MASK
)
- PAGE_SIZE:4KB = 0x1000(十六进制),即页的最小单位;
- PAGE_MASK:4KB 对应的页掩码为
~(PAGE_SIZE - 1)
=0xFFFFF000
(十六进制),作用是 “屏蔽低 12 位地址”,实现向下对齐
PAGE_MASK:https://elixir.bootlin.com/linux/v6.11/source/arch/arm64/include/asm/page-def.h#L16
PAGE_ALIGH的本质: https://elixir.bootlin.com/linux/v6.11/source/include/uapi/linux/const.h#L32
步骤 1:计算 base(向下对齐基地址) base = phys_initrd_start & PAGE_MASK = 0x80001234 & 0xFFFFF000 // 按位与运算,保留高地址,清除低12位 = 0x80001000 // 结果为4KB对齐的基地址 步骤 2:计算 phys_initrd_start + phys_initrd_size(原始结束地址) phys_initrd_start + phys_initrd_size = 0x80001234 + 0x5678 = 0x800068AC // 未对齐的结束地址 步骤 3:用 __ALIGN_KERNEL_MASK 对齐结束地址 PAGE_ALIGN(0x800068AC) = ((0x800068AC) + 0xFFF) & 0xFFFFF000 // x + mask 后与 ~mask 按位与 = (0x800068AC + 0xFFF) & 0xFFFFF000 = 0x800078AB & 0xFFFFF000 = 0x80007000 // 对齐后的结束地址(4KB对齐) 计算 size(对齐后的总大小) size = 0x80007000(对齐后结束地址) - 0x80001000(base) = 0x6000 // 24KB,正好是6个4KB页(4KB×6=24KB)
问题3.initrd放在哪?initrd 是 “内核专属物理区的临时子区域”
┌─────────────────────────────────────────────────────────────────────────────────────────┐ │ 地址范围 │ 归属 │ 子区域/用途说明 │ 访问权限 │ ├─────────────────────────┼───────────────┼───────────────────────────────────────────┼────────────────--------┤ │ 0x00000000 ~ 0x0000FFFF │ 硬件专属区 │ BIOS/UEFI 启动代码、中断向量表 │ 只读(内核可访问,用户不可)│ │ 0x00010000 ~ 0x000FFFFF │ 硬件专属区 │ 外设寄存器(UART/GPIO/定时器等) │ 读写(仅内核可操作) │ │ 0x00100000 ~ 0x7FFFFFFFF │ 用户可用物理区 │ 用户程序未来的物理内存(堆/栈/代码) │ 读写(内核管控,用户可访问)│ │ 0x80000000 ~ 0x8FFFFFFF │ 内核专属物理区 │ ① 内核代码段(_text)、数据段(_data) │ 读写(仅内核可访问) │ │ │ │ ② initrd 物理区域(base ~ base+size) │ 读写(仅内核可访问,保留)│ │ 0x90000000 ~ 0xFFFFFFFF │ 内核专属物理区 │ 线性映射覆盖的其他高地址内存(如内核堆) │ 读写(仅内核可访问) │ └─────────────────────────────────────────────────────────────────────────────────────────┘#C++##嵌入式##嵌入式软开##通信硬件人笔面经互助##牛客创作赏金赛#