目录
六:关键函数解析:__create_pgd_mapping()
八:关键函数解析:alloc_init_cont_pmd()
九:关键函数解析:alloc_init_cont_pte() 和 init_pte()
总结:alloc_init_cont_pte 和 init_pte 是 ARM64
ARM64架构采用 四级页表 结构(L0-L3)来管理虚拟地址到物理地址的转换,支持 4KB、64KB、16KB 等页大小,并可扩展至 52位物理地址。以下以 4KB页 和 48位虚拟地址 的典型配置为例,详细解析页表映射流程,并附案例分析。
一、ARM64 页表结构
1. 虚拟地址划分
48位虚拟地址(VA)按四级页表分割如下(4KB页):
Level Bits Range 索引长度 功能
L0 (PGD) [47:39] 9 bits //顶级页目录(Page Global Directory)
L1 (PUD) [38:30] 9 bits //上级页目录(Page Upper Directory)
L2 (PMD) [29:21] 9 bits //中间页目录(Page Middle Directory)
L3 (PTE) [20:12] 9 bits //页表项(Page Table Entry)
Offset [11:0] 12 bits //页内偏移(4KB对齐)
2. 页表项(PTE)结构
每个页表项为 64位,关键字段包括:
物理页基地址(Bits[47:12]):指向下一级页表或物理页的基地址。
类型(Type)(Bits[1:0]):
0b01:指向下一级页表。
0b11:指向最终物理页(4KB页)。
属性位:
AP[2:0]:访问权限(读/写/用户权限)。
SH[1:0]:共享属性(Inner/Outer Shareable)。
AF(Access Flag):访问标记,用于缺页异常。
nG(Not Global):是否全局映射。
Contiguous:连续页表项标记(用于大页优化)。
二、页表映射流程
1. 确定页表基地址
TTBR0:用户空间虚拟地址(VA[63] = 0)的页表基地址。
TTBR1:内核空间虚拟地址(VA[63:48] = 0xffff)的页表基地址。
2. 逐级页表遍历
1.L0 (PGD) 查询
基地址:TTBR1(内核空间为例)。
计算索引:L0_Index = (VA >> 39) & 0x1FF。
页表项地址:L0_Entry_Addr = TTBR1 + L0_Index * 8。
若L0项无效(Type≠0b01),分配新页作为L1页表,更新L0项。
2.L1 (PUD) 查询
基地址:L0项指向的物理地址。
计算索引:L1_Index = (VA >> 30) & 0x1FF。
页表项地址:L1_Entry_Addr = L1_Base + L1_Index * 8。
若L1项无效,分配新页作为L2页表,更新L1项。
3.L2 (PMD) 查询
基地址:L1项指向的物理地址。
计算索引:L2_Index = (VA >> 21) & 0x1FF。
页表项地址:L2_Entry_Addr = L2_Base + L2_Index * 8。
若L2项无效,分配新页作为L3页表,更新L2项。
4.L3 (PTE) 查询
基地址:L2项指向的物理地址。
计算索引:L3_Index = (VA >> 12) & 0x1FF。
页表项地址:L3_Entry_Addr = L3_Base + L3_Index * 8。
填充L3项:设置物理页基地址(Bits[47:12])及属性(AP/SH/AF等)。
合成物理地址 物理地址 = (Physical_Page_Base << 12) | (VA & 0xFFF)。
三、案例分析:内核映射设备寄存器
场景描述
将 虚拟地址 0xFFFF800080000000 映射到 物理地址 0x80000000,属性为设备内存(不可缓存、不可执行)。
地址分解
虚拟地址:0xFFFF800080000000(内核空间,TTBR1)。
L0 Index:(0xFFFF800080000000 >> 39) & 0x1FF = 0x100。
L1 Index:(0xFFFF800080000000 >> 30) & 0x1FF = 0x000。
L2 Index:(0xFFFF800080000000 >> 21) & 0x1FF = 0x004。
L3 Index:(0xFFFF800080000000 >> 12) & 0x1FF = 0x000。
Offset:0x000。
映射步骤
1.L0查询
TTBR1基地址假设为 0x10000000。
L0项地址:0x10000000 + 0x100*8 = 0x10000800。
若L0项无效,分配物理页(如0x20000000)作为L1页表,设置L0项为 0x20000001(Type=0b01)。
2.L1查询
L1基地址:0x20000000。
L1项地址:0x20000000 + 0x000*8 = 0x20000000。
分配L2页表(如0x30000000),设置L1项为 0x30000001。
3.L2查询
L2基地址:0x30000000。
L2项地址:0x30000000 + 0x004*8 = 0x30000020。
分配L3页表(如0x40000000),设置L2项为 0x40000001。
4.L3查询
L3基地址:0x40000000。
L3项地址:0x40000000 + 0x000*8 = 0x40000000。
设置L3项:
物理页基地址:0x80000000 >> 12 = 0x80000。
属性:Type=0b11(4KB页),AP=0b011(EL1读/写),SH=0b00(Non-Shareable),AF=1,nG=1,MT_DEVICE(设备内存)。
最终PTE值:0x80000 | 0x4F3(具体属性位组合)。
5.物理地址生成
访问 0xFFFF800080000000 → 物理地址 0x80000000。
四、关键代码参考(Linux内核)
Linux内核中ARM64页表操作函数示例:
// 创建页表项(arch/arm64/mm/mmu.c)
static void __create_pgd_mapping(
pgd_t *pgdir, phys_addr_t phys,
unsigned long virt, phys_addr_t size,
pgprot_t prot, bool page_mapped_only) {
do {
pgd_t *pgd = pgd_offset_pgd(pgdir, virt);
pud_t *pud = pud_alloc_one(&init_mm, virt);
set_pgd(pgd, __pgd(__pa(pud) | PUD_TYPE_TABLE));
// 类似流程处理PUD、PMD、PTE
} while (virt += PAGE_SIZE, phys += PAGE_SIZE, size -= PAGE_SIZE);
}
五、总结
核心流程:四级页表逐级索引 → 分配下级页表(若缺失) → 填充最终PTE。
性能优化:支持大页(如1GB、2MB)减少页表层级,降低TLB Miss率。
安全机制:通过AP位控制访问权限,XN位防止代码执行(如设备内存)。
应用场景:内核启动映射、设备寄存器访问、用户进程内存分配。
ARM64通过灵活的四级页表设计,兼顾了内存管理的效率与安全性,支撑了从嵌入式设备到服务器的广泛应用。
六:关键函数解析:__create_pgd_mapping()
__create_pgd_mapping() 是 Linux 内核(ARM64 架构)中用于建立虚拟地址到物理地址页表映射的核心函数,通常在系统启动阶段或动态映射(如 ioremap)时调用。以下是对其作用的解析:
一、函数功能
1.核心作用
遍历指定的虚拟地址范围(virt 到 virt+size),逐级构建页表(PGD → PUD → PMD → PTE),最终建立虚拟地址与物理地址的映射关系。该过程包括:
页表分配:通过回调函数 pgtable_alloc 动态申请页表内存(如 PUD/PMD 页)。
地址对齐检查:确保物理地址(phys)与虚拟地址(virt)的页内偏移一致。
大页映射优化:若条件满足(如地址对齐、支持大页),直接创建 PUD/PMD 级大页,减少页表层级遍历。
2.典型调用场景
内核启动时映射 kernel image(如 _stext、__initdata 等段)。
初始化线性映射(direct mapping)区域,如 PAGE_OFFSET 开始的物理内存直接映射。
设备内存映射(如通过 ioremap 映射 MMIO 区域)。
二、代码逻辑解析
static void __create_pgd_mapping(pgd_t *pgdir, phys_addr_t phys,
unsigned long virt, phys_addr_t size,
pgprot_t prot,
phys_addr_t (*pgtable_alloc)(int),
int flags)
{
// 步骤1:检查物理地址与虚拟地址的页内偏移是否一致(否则无法正确映射)
if (WARN_ON((phys ^ virt) & ~PAGE_MASK))
return;
// 步骤2:地址对齐(按页大小)
phys &= PAGE_MASK;
addr = virt & PAGE_MASK;
end = PAGE_ALIGN(virt + size);
// 步骤3:逐级处理PGD条目,构建页表
do {
next = pgd_addr_end(addr, end); // 计算当前PGD条目覆盖的虚拟地址范围
alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc, flags); // 初始化PUD级页表
phys += next - addr; // 更新物理地址偏移
} while (pgdp++, addr = next, addr != end); // 处理下一个PGD条目
}
三、关键参数说明
参数 作用
pgdir 顶级页表(PGD)基地址,如 swapper_pg_dir(内核全局页表)或 early_pg_dir(临时页表)。
phys 映射的起始物理地址。
virt 映射的起始虚拟地址。
size 映射区域大小。
prot 页表属性(如 PAGE_KERNEL 可读写、PAGE_KERNEL_RO 只读)。
pgtable_alloc 页表内存分配回调函数(如 early_pgtable_alloc 使用 memblock 申请物理页)。
flags 控制标志(如 NO_BLOCK_MAPPINGS 禁用大页映射)。
四、技术细节
1.地址对齐检查
(phys ^ virt) & ~PAGE_MASK 验证物理地址与虚拟地址的页内偏移是否一致。若不一致,映射后访问会错位,触发警告并终止。
2.大页映射优化
在 alloc_init_pud() 和下级函数中,若满足以下条件,直接创建大页(如 1GB/2MB):
地址按大页大小对齐(如 1GB 对齐)。
硬件支持大页(如 pud_sect_supported() 返回 true)。
未设置 NO_BLOCK_MAPPINGS 标志。
3.动态页表分配
pgtable_alloc 回调函数负责申请下级页表内存。例如:
// 示例:通过 memblock 分配物理页,并通过 fixmap 临时映射以初始化页表内容
phys = memblock_phys_alloc(PAGE_SIZE, PAGE_SIZE);
ptr = pte_set_fixmap(phys); // 临时映射到 fixmap 区域
memset(ptr, 0, PAGE_SIZE); // 清空页表
pte_clear_fixmap();
五、典型调用链
1.内核启动阶段
paging_init() → map_kernel() → map_kernel_segment() → __create_pgd_mapping(),用于映射内核代码、数据段。
2.设备内存映射
ioremap() → ioremap_page_range() → ioremap_pud_range() → ... → __create_pgd_mapping(),用于映射 MMIO 区域。
六、关联场景
1.恒等映射(Identity Mapping)
在 ARM64 启动初期,需建立 virt=phys 的恒等映射,确保启用 MMU 后指令能连续执行(参考 idmap_pg_dir)。
2.线性映射(Linear Mapping)
通过 swapper_pg_dir 建立 PAGE_OFFSET 开始的线性映射,使内核可直接通过虚拟地址访问所有物理内存(如 __va(phys))。
总结:__create_pgd_mappin
g() 是内核页表构建的核心枢纽,通过逐级填充页表条目实现灵活的内存映射,兼顾效率(大页优化)与动态适应性(回调分配)。其设计体现了硬件特性与软件需求的深度结合。
七:关键函数解析:alloc_init_pud()
alloc_init_pud() 是 Linux 内核(ARM64 架构)中用于初始化 PUD(Page Upper Directory,页上层目录)级页表的核心函数,其核心作用是通过遍历虚拟地址范围,动态分配页表内存并建立物理地址到虚拟地址的映射(支持大页优化)。以下是对其功能、逻辑及技术细节的解析:
一、函数核心作用
1.PUD 页表初始化
从 PGD/P4D 级页表向下遍历,为指定的虚拟地址范围(addr 到 end)分配并初始化 PUD 级页表项,构建与物理地址(phys)的映射关系。
2.大页映射优化
若虚拟地址与物理地址按 1GB 对齐且硬件支持大页(如 ARM64_HW_AFDBM 标志),直接创建 1GB 的块映射(pud_set_huge()),减少页表层级遍历和内存占用。
3.递归处理下级页表
若无法使用大页,调用 alloc_init_cont_pmd() 继续初始化 PMD(Page Middle Directory)级页表,处理更细粒度的映射(如 2MB 或 4KB 页)。
4.页表内存动态分配
通过回调函数 pgtable_alloc 申请物理页(如 early_pgtable_alloc()),用于存储 PUD 或下级页表结构。
二、代码逻辑拆解
static void alloc_init_pud(pgd_t *pgdp, unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot,
phys_addr_t (*pgtable_alloc)(int),
int flags)
{
unsigned long next;
pud_t *pudp;
// 步骤1:获取当前虚拟地址对应的 P4D 条目
p4d_t *p4dp = p4d_offset(pgdp, addr); // 计算 P4D 条目指针
p4d_t p4d = READ_ONCE(*p4dp); // 原子读取 P4D 条目值
// 步骤2:若 P4D 条目为空,分配新的 PUD 页表
if (p4d_none(p4d)) {
phys_addr_t pud_phys;
BUG_ON(!pgtable_alloc); // 确保分配函数有效
pud_phys = pgtable_alloc(PUD_SHIFT); // 分配 PUD 页表内存(通常为 4KB)
__p4d_populate(p4dp, pud_phys, PUD_TYPE_TABLE); // 填充 P4D 条目指向 PUD 页表
p4d = READ_ONCE(*p4dp); // 重新读取更新后的 P4D 条目
}
BUG_ON(p4d_bad(p4d)); // 检查 P4D 条目合法性
// 步骤3:通过 fixmap 临时映射 PUD 页表,获取其虚拟地址
pudp = pud_set_fixmap_offset(p4dp, addr);
do {
pud_t old_pud = READ_ONCE(*pudp); // 原子读取当前 PUD 条目
next = pud_addr_end(addr, end); // 计算当前 PUD 条目覆盖的地址范围
// 步骤4:尝试创建 1GB 大页映射(若条件满足)
if (use_1G_block(addr, next, phys) && (flags & NO_BLOCK_MAPPINGS) == 0) {
pud_set_huge(pudp, phys, prot); // 设置 PUD 大页项(物理地址 + 属性)
BUG_ON(!pgattr_change_is_safe(pud_val(old_pud), READ_ONCE(pud_val(*pudp))));
} else {
// 步骤5:无法使用大页时,递归初始化 PMD 级页表
alloc_init_cont_pmd(pudp, addr, next, phys, prot, pgtable_alloc, flags);
BUG_ON(pud_val(old_pud) != 0 && pud_val(old_pud) != READ_ONCE(pud_val(*pudp)));
}
phys += next - addr; // 更新物理地址偏移
} while (pudp++, addr = next, addr != end); // 处理下一个 PUD 条目
pud_clear_fixmap(); // 清除临时映射
}
三、关键参数与机制
参数/机制 作用
pgdp 上级页表(PGD/P4D)条目指针,用于定位当前虚拟地址对应的 P4D。
pgtable_alloc 页表内存分配回调函数,如 early_pgtable_alloc 使用 memblock 分配物理页。
flags 控制标志(如 NO_BLOCK_MAPPINGS 禁用大页映射)。
pud_set_fixmap_offset() 通过 fixmap 窗口临时映射 PUD 页表到虚拟地址,以便写入页表项。
use_1G_block() 判断是否满足 1GB 大页条件(地址对齐、物理连续、硬件支持)。
四、技术细节
1.大页映射条件
地址对齐:虚拟地址和物理地址均按 1GB 对齐(addr & (SZ_1G - 1) == 0)。
长度足够:剩余映射长度至少为 1GB(next - addr >= SZ_1G)。
硬件支持:CPU 支持块映射(如 ID_AA64MMFR0_EL1.TGran4 标志允许 1GB 页)。
2.Fixmap 临时映射
在早期启动阶段(MMU 已启用但完整页表未构建时),内核通过预留的 fixmap 窗口(固定虚拟地址区域)临时映射物理内存,以便操作页表。例如:
pud_set_fixmap_offset() 将 PUD 页表的物理地址映射到 fixmap 区域。
pud_clear_fixmap() 解除映射,防止后续访问冲突。
3.页表内存分配
pgtable_alloc(PUD_SHIFT) 根据 PUD_SHIFT(通常为 30,对应 1GB 映射)申请物理页。例如:
early_pgtable_alloc() 在启动阶段使用 memblock 分配器申请内存,并通过 fixmap 清零页表。
4.原子操作与并发安全
READ_ONCE() 确保原子读取页表项,避免编译器优化导致的数据竞争。
BUG_ON() 用于检测非法状态(如页表项被意外修改),触发内核异常终止。
五、典型调用场景
1.内核启动阶段
在 __create_pgd_mapping() 中调用,为内核代码段、数据段建立线性映射(phys = virt - PAGE_OFFSET)。例如:
__create_pgd_mapping(swapper_pg_dir, phys, virt, size, prot, early_pgtable_alloc, 0);
2.设备内存映射
通过 ioremap() 映射 MMIO 区域时,若设备地址范围跨越 1GB 边界,可能触发 PUD 级大页映射。
六、关联机制
1.多级页表架构
ARM64 默认采用 4 级页表(PGD→P4D→PUD→PMD→PTE),但 P4D 通常与 PGD 合并(退化级),实际操作为 PGD→PUD→PMD→PTE。
2.页表同步与 TLB
新映射建立后,需通过 flush_tlb_kernel_range() 刷新 TLB,确保新页表项生效。
3.内存属性控制
prot 参数指定页表项的访问权限(如 PROT_NORMAL 可缓存,PROT_DEVICE_nGnRnE 强序无缓存)。
总结:alloc_init_pud()
ARM64 内核页表构建链中的关键环节,通过动态分配 PUD 页表、尝试大页映射、递归初始化下级页表,实现灵活高效的内存映射。其设计紧密结合硬件特性(如大页支持)与软件需求(如启动阶段临时映射),是内核内存管理的核心基础设施。
八:关键函数解析:alloc_init_cont_pmd()
alloc_init_cont_pmd() 是 Linux 内核(ARM64 架构)中用于初始化连续 PMD(Page Middle Directory)级页表的核心函数,主要作用是为虚拟地址到物理地址的映射创建 PMD 级页表项,并支持 连续大页映射优化。以下是结合代码和搜索结果的详细解析:
一、核心功能
1.PMD 页表初始化
从 PUD 级页表向下遍历,为指定的虚拟地址范围(addr 到 end)分配 PMD 页表,并填充物理地址映射关系。若 PMD 条目未分配,则通过回调函数 pgtable_alloc 动态申请物理页。
2.连续大页映射优化
当虚拟地址、物理地址和长度均按 连续大页(如 2MB 或 16MB) 对齐时,启用 PTE_CONT 标志,创建连续页表项,减少页表层级遍历和 TLB 未命中。
3.动态页表分配与属性控制
通过 pgtable_alloc 申请物理页内存,并根据 prot 和 flags 设置页表属性(如读写权限、缓存策略)。
二、代码逻辑拆解
static void alloc_init_cont_pmd(pud_t *pudp, unsigned long addr,
unsigned long end, phys_addr_t phys,
pgprot_t prot,
phys_addr_t (*pgtable_alloc)(int), int flags)
{
unsigned long next;
pud_t pud = READ_ONCE(*pudp); // 原子读取当前 PUD 条目
// 1. 合法性检查:禁止 PUD 级大页映射(必须为表项)
BUG_ON(pud_sect(pud));
// 2. 若 PUD 条目未分配,申请 PMD 页表并填充
if (pud_none(pud)) {
phys_addr_t pmd_phys;
BUG_ON(!pgtable_alloc);
pmd_phys = pgtable_alloc(PMD_SHIFT); // 分配 PMD 页表内存(如 4KB)
__pud_populate(pudp, pmd_phys, PUD_TYPE_TABLE); // 填充 PUD 条目指向 PMD 页表
pud = READ_ONCE(*pudp);
}
BUG_ON(pud_bad(pud)); // 检查 PUD 条目合法性
// 3. 遍历虚拟地址范围,处理每个 PMD 条目
do {
pgprot_t __prot = prot;
next = pmd_cont_addr_end(addr, end); // 计算连续映射的结束地址
// 4. 判断是否启用连续大页映射
if ((((addr | next | phys) & ~CONT_PMD_MASK) == 0) &&
(flags & NO_CONT_MAPPINGS) == 0)
__prot = __pgprot(pgprot_val(prot) | PTE_CONT); // 添加连续标志
// 5. 初始化 PMD 条目(可能递归处理 PTE 或直接设置块映射)
init_pmd(pudp, addr, next, phys, __prot, pgtable_alloc, flags);
phys += next - addr; // 更新物理地址偏移
} while (addr = next, addr != end); // 处理下一个 PMD 条目
}
三、关键技术细节
1. 连续大页映射条件
地址对齐:
虚拟地址 addr、结束地址 next 和物理地址 phys 必须按 CONT_PMD_MASK 对齐(例如 2MB 或 16MB)。
标志允许:
flags 未设置 NO_CONT_MAPPINGS(由调用者控制是否禁用连续映射)。
硬件支持:
ARM64 处理器需支持 PTE_CONT 特性(通过 ID_AA64MMFR0_EL1.CNP 标志判断)。
2. 连续映射优势
减少页表层级:
若启用连续映射,一个 PMD 条目可覆盖更大地址范围(如 2MB),无需拆分到多个 PTE 条目。
降低 TLB 压力:
单次 TLB 缓存可覆盖更大物理内存区域,提升访问效率。
3. 页表内存分配
pgtable_alloc(PMD_SHIFT):
根据 PMD_SHIFT(通常为 21,对应 2MB 映射)申请物理页。在启动阶段,early_pgtable_alloc 通过 memblock 分配器申请内存,并通过 fixmap 窗口临时映射清零(参考搜索结果中 early_pgtable_alloc 的实现)。
4. 下游函数 init_pmd()
功能细分:
init_pmd() 可能进一步处理 PMD 条目,包括:
直接映射大页:若地址按 PMD 大小对齐,直接设置块映射(如 2MB)。
递归处理 PTE:若需要更细粒度,调用 alloc_init_pte() 初始化 PTE 级页表。
四、典型调用场景
1.内核启动阶段
在 paging_init() → map_kernel() → __create_pgd_mapping() 链中调用,用于映射内核代码段、数据段或设备内存区域。例如:
映射 0x80000000~0x80200000 为可读写内存(MT_MEMORY_RW)。
映射 0x80200000~0x81000000 为可执行内存(MT_MEMORY_RWX)。
2.设备驱动映射
通过 ioremap() 映射 MMIO 区域时,若设备地址范围连续对齐,可能触发连续 PMD 映射。
五、关联机制
1.Fixmap 临时映射
在 PMD 页表分配后,通过 fixmap 窗口(如 pte_set_fixmap())临时映射物理页,以便写入页表内容(参考搜索结果中 early_pgtable_alloc 的描述)。
2.内存属性控制
prot 参数定义页表项的访问权限(如 PROT_NORMAL 可缓存,PROT_DEVICE 无缓存)。
PTE_CONT 标志通过 pgprot_val() 合并到属性中,指示硬件启用连续映射优化。
3.错误检测机制
BUG_ON(pud_sect(pud)):确保 PUD 条目未使用大页映射(必须为表项)。
BUG_ON(pud_bad(pud)):检查 PUD 条目是否损坏(如非法权限位)。
总结:alloc_init_cont_pmd()
ARM64 页表构建链中的关键环节,通过动态分配 PMD 页表、支持连续大页映射、递归初始化下级页表,实现灵活高效的内存映射。其设计紧密结合硬件特性(如连续页表支持)与内核启动阶段的内存管理需求,是内核线性映射和设备内存映射的核心基础设施。
九:关键函数解析:alloc_init_cont_pte() 和 init_pte()
alloc_init_cont_pte() 和 init_pte() 是 Linux 内核(ARM64 架构)中用于初始化 PTE(Page Table Entry,页表项) 级页表的核心函数,其核心作用是通过动态分配 PTE 页表并设置连续映射标志,完成虚拟地址到物理地址的细粒度映射。以下结合代码逻辑与搜索结果进行解析:
一、函数功能总览
1.alloc_init_cont_pte()
从 PMD 级页表向下遍历,为虚拟地址范围(addr 到 end)分配 PTE 页表。
若虚拟地址、物理地址及长度按 连续小页(如 64KB) 对齐,启用 PTE_CONT 标志,优化 TLB 效率。
调用 init_pte() 初始化具体的 PTE 条目。
2.init_pte()
通过 Fixmap 窗口临时映射 PTE 页表,逐项填充物理地址与属性。
确保页表项修改安全(仅允许权限属性更新,禁止意外覆盖)。
二、代码逻辑拆解
1. alloc_init_cont_pte()
static void alloc_init_cont_pte(pmd_t *pmdp, unsigned long addr,
unsigned long end, phys_addr_t phys,
pgprot_t prot,
phys_addr_t (*pgtable_alloc)(int),
int flags)
{
unsigned long next;
pmd_t pmd = READ_ONCE(*pmdp); // 原子读取当前 PMD 条目
BUG_ON(pmd_sect(pmd)); // 禁止 PMD 级大页映射(必须为表项)
// 若 PMD 条目为空,分配 PTE 页表
if (pmd_none(pmd)) {
phys_addr_t pte_phys;
BUG_ON(!pgtable_alloc);
pte_phys = pgtable_alloc(PAGE_SHIFT); // 分配 PTE 页表内存(如 4KB)
__pmd_populate(pmdp, pte_phys, PMD_TYPE_TABLE); // 填充 PMD 条目
pmd = READ_ONCE(*pmdp);
}
BUG_ON(pmd_bad(pmd)); // 检查 PMD 条目合法性
do {
pgprot_t __prot = prot;
next = pte_cont_addr_end(addr, end); // 计算连续映射结束地址
// 判断是否启用连续映射(64KB 对齐且未禁用)
if ((((addr | next | phys) & ~CONT_PTE_MASK) == 0) &&
(flags & NO_CONT_MAPPINGS) == 0)
__prot = __pgprot(pgprot_val(prot) | PTE_CONT); // 添加连续标志
init_pte(pmdp, addr, next, phys, __prot); // 初始化 PTE 条目
phys += next - addr;
} while (addr = next, addr != end);
}
2. init_pte()
static void init_pte(pmd_t *pmdp, unsigned long addr, unsigned long end,
phys_addr_t phys, pgprot_t prot)
{
pte_t *ptep = pte_set_fixmap_offset(pmdp, addr); // 通过 Fixmap 映射 PTE 页表
do {
pte_t old_pte = READ_ONCE(*ptep);
// 设置 PTE 条目:物理地址转为页帧号(pfn),合并属性
set_pte(ptep, pfn_pte(__phys_to_pfn(phys), prot));
// 确保仅更新权限属性(如可读→只读),禁止意外覆盖现有条目
BUG_ON(!pgattr_change_is_safe(pte_val(old_pte), READ_ONCE(pte_val(*ptep))));
phys += PAGE_SIZE;
} while (ptep++, addr += PAGE_SIZE, addr != end);
pte_clear_fixmap(); // 清除临时映射
}
三、关键技术细节
1. 连续映射优化(PTE_CONT)
对齐条件:
虚拟地址 addr、结束地址 next 和物理地址 phys 必须按 CONT_PTE_MASK(如 64KB)对齐。
硬件支持:
ARM64 处理器需支持 PTE_CONT 特性(通过 ID_AA64MMFR0_EL1.TGran64K 标志判断)。
优势:
单次 TLB 缓存可覆盖多个连续页(如 16 个 4KB 页),减少 TLB 未命中。
2. 页表内存分配
pgtable_alloc(PAGE_SHIFT):
分配一页(4KB)物理内存用于存储 PTE 页表(含 512 个 PTE 条目,覆盖 2MB 地址空间)。
分配方式:在启动阶段通过 early_pgtable_alloc() 调用 memblock_alloc() 申请内存(参考搜索结果中 early_pgtable_alloc 的实现)。
3. Fixmap 临时映射
pte_set_fixmap_offset():
将 PTE 页表的物理地址临时映射到 Fixmap 窗口(如 FIX_PTE 区域),以便通过虚拟地址访问页表。
pte_clear_fixmap():
解除临时映射,避免后续操作冲突。
4. 安全修改检查
pgattr_change_is_safe():
确保仅允许更新页表项的保护位(如 PTE_RDONLY),禁止修改物理地址或其他关键属性,防止意外破坏现有映射。
四、典型调用场景
1.内核启动阶段
在 paging_init() → map_kernel() → __create_pgd_mapping() 链中调用,用于映射内核的精细内存区域(如 .data、.bss 段)。
示例:__create_pgd_mapping(swapper_pg_dir, phys, virt, size, prot, early_pgtable_alloc, 0)。
2.用户进程页表扩展
当用户进程通过 mmap 申请内存时,内核可能调用类似逻辑动态扩展 PTE 页表。
五、关联机制
1.线性映射(Linear Mapping)
通过 __phys_to_virt() 将物理地址转换为内核虚拟地址(如 PAGE_OFFSET 偏移),建立直接映射(参考搜索结果中 __map_memblock 的实现)。
2.页表同步与 TLB 刷新
新映射建立后需调用 flush_tlb_kernel_range() 刷新 TLB,确保新页表项生效。
3.内存属性控制
prot 参数定义页表项的访问权限(如 PROT_NORMAL 可缓存,PROT_DEVICE 无缓存)。
PTE_CONT 标志通过 pgprot_val() 合并到属性中,指示硬件启用连续映射。
总结:alloc_init_cont_pte 和 init_pte 是 ARM64
页表构建链的最底层环节,负责动态分配 PTE 页表、设置连续映射标志,并逐项填充物理地址与属性。其设计结合了硬件特性(如连续页支持)与内核内存管理需求,是内核启动阶段线性映射和动态内存分配的核心基础设施。