1、pgd_addr_end
根据当前虚拟地址 addr 和目标结束地址 end,计算当前 PGD 项 能够覆盖的最大虚拟地址范围的结束地址 next。
- 如果 addr 和 end 跨越多个 PGD 项(即 end 超出当前 PGD 项的地址范围),则返回当前 PGD 项的地址边界。
- 如果 end 在当前 PGD 项的地址范围内,则返回 end。
- 在建立页表映射时,内核需要逐个处理每个 PGD 项对应的地址范围。通过 pgd_addr_end,可以确定当前 PGD 项需要处理的地址范围 [addr, next),并更新 addr 为 next,以便处理下一个 PGD 项。
页表建立参考
do {next = pgd_addr_end(addr, end); // 计算当前 PGD 项的地址范围alloc_init_pud(pgd, addr, next, phys, prot, alloc); // 初始化 PUD 页表phys += next - addr; // 更新物理地址偏移
} while (pgd++, addr = next, addr != end);
- 流程解释:
- 循环处理每个 PGD 项:通过 do-while 循环,逐个处理 PGD 项。
- 计算当前 PGD 项的地址范围:通过 pgd_addr_end 确定当前 PGD 项的地址范围 [addr, next)。
- 初始化下级页表:调用 alloc_init_pud 为当前 PGD 项分配并初始化 PUD 页表。
- 更新物理地址偏移:根据当前处理的地址范围大小(next - addr)调整物理地址 phys。
- 移动到下一个 PGD 项:更新 addr 为 next,并继续处理下一个 PGD 项,直到 addr == end。
2、pgd_offset_k
pgd_offset_k(addr)
是 Linux 内核中用于 获取内核页全局目录(PGD)中对应虚拟地址 addr
的页表项地址 的宏。它是内核页表操作的核心宏之一,主要用于内核虚拟地址空间的映射初始化(如 early_fixmap_init
)和页表建立(如 create_mapping
)等场景。
1. 宏的定义与展开
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
init_mm
:内核的全局内存描述符(mm_struct
),表示内核的地址空间(与进程的mm
分离)。pgd_offset(mm, addr)
:根据mm
的 PGD 基地址和addr
计算对应的 PGD 项地址。
进一步展开:
#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr))
(mm)->pgd
:内核 PGD 的基地址(init_mm.pgd
)。pgd_index(addr)
:从addr
中提取 PGD 项的索引。
2. pgd_index(addr)
的计算
#define pgd_index(addr) (((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
PGDIR_SHIFT
:定义 PGD 项的位移量,决定 PGD 项覆盖的地址范围。例如:- 对于 ARM64(4 级页表),
PGDIR_SHIFT = 39
,每个 PGD 项覆盖2^(48-39) = 512GB
地址空间。
- 对于 ARM64(4 级页表),
PTRS_PER_PGD
:PGD 项的数量(通常为1 << (PGDIR_SHIFT - VA_BITS_SHIFT)
,例如 512 项)。- 作用:通过右移
addr
并掩码,提取 PGD 项的索引。
3. 典型使用场景
3.1 早期内核映射初始化(early_fixmap_init
)
在内核启动阶段,early_fixmap_init
函数会使用 pgd_offset_k
初始化固定映射区域(fixmap
)的页表:
pgd_t *pgd = pgd_offset_k(FIXADDR_START);
__pgd_populate(pgd, __pa_symbol(bm_pud), PUD_TYPE_TABLE);
- 作用:将
bm_pud
(预分配的 PUD 页表)的物理地址写入 PGD 项中,建立从FIXADDR_START
到bm_pud
的映射。 - 后续步骤:通过
pud_offset
、pmd_offset
、pte_offset
逐级填充页表。
3.2 通用页表建立(create_mapping
)
在内核中,create_mapping
函数会通过 pgd_offset_k
遍历虚拟地址范围并建立页表:
pgd_t *pgd = pgd_offset_k(virt_addr);
do {next = pgd_addr_end(addr, end);alloc_init_pud(pgd, addr, next, phys, prot);phys += next - addr;addr = next;
} while (pgd++, addr != end);
- 作用:逐个处理每个 PGD 项的地址范围,初始化下级页表(PUD → PMD → PTE)。
4. ARM64 架构示例
假设:
addr = 0xffff7ffffabfe000
(FIXADDR_START
)init_mm.pgd = 0xffff800000ef0000
PGDIR_SHIFT = 39
,PTRS_PER_PGD = 512
4.1 计算 pgd_index(addr)
pgd_index(addr) = (0xffff7ffffabfe000 >> 39) & 0x1ff = 0xff
- 解释:
addr
的高 9 位(0x1ff
)即为 PGD 项的索引。
4.2 计算 pgd_offset_k(addr)
pgd_offset_k(addr) = init_mm.pgd + 0xff * sizeof(pgd_t)
- ARM64 中
pgd_t
占 8 字节,因此:pgd_offset_k(addr) = 0xffff800000ef0000 + 0xff * 8 = 0xffff800000ef07f8
- 结果:这是
addr
在内核 PGD 中对应项的虚拟地址。
5. 关键点总结
-
目的:
- 快速定位内核虚拟地址
addr
在 PGD 中的项地址。 - 为后续页表建立(如
PUD
、PMD
、PTE
)提供起点。
- 快速定位内核虚拟地址
-
层级关系:
pgd_offset_k
属于页表操作宏的第一级(PGD),后续通过pud_offset
、pmd_offset
、pte_offset
逐级展开。
-
架构依赖:
PGDIR_SHIFT
和PTRS_PER_PGD
的定义依赖于架构(如 ARM64、x86)和页表层级(4 级或 2 级)。
-
应用场景:
- 早期内核映射(
fixmap
、ioremap
)。 - 动态内存映射(
vmalloc
、ioremap
)。 - 设备树(DTB)的加载(通过
fixmap
映射物理地址)。
- 早期内核映射(
6. 常见问题
Q1:为什么 pgd_offset_k
使用 init_mm
?
- 原因:内核地址空间是全局的,所有进程共享同一个内核 PGD(
init_mm.pgd
)。通过init_mm
可直接访问内核页表。
Q2:如何验证 pgd_offset_k
的正确性?
- 调试方法:在
early_fixmap_init
中打印pgd_offset_k(addr)
的值,并检查其是否对应预期的 PGD 项地址(如通过pr_err
输出)。
Q3:ARM64 中为何需要乘以 8?
- 原因:ARM64 的每个 PGD 项占用 8 字节(64 位),因此索引
0xff
对应的偏移量为0xff * 8 = 0x7f8
。
7. 扩展:页表层级划分(以 ARM64 为例)
层级 | 宏 | 地址位移 | 页表项大小 | 映射范围 |
---|---|---|---|---|
PGD | pgd_index(addr) | >> 39 | 8 字节 | 512GB |
PUD | pud_index(addr) | >> 30 | 8 字节 | 1GB |
PMD | pmd_index(addr) | >> 21 | 8 字节 | 2MB |
PTE | pte_index(addr) | >> 12 | 8 字节 | 4KB |
总结
pgd_offset_k(addr)
是内核页表操作的基础,通过计算虚拟地址 addr
在 PGD 中的项地址,为后续的页表建立提供起点。理解其工作原理对于调试内核内存管理、分析页表初始化流程至关重要。