Linux内核页表全解析:从pgd到pte的四级地址转换
【免费下载链接】linux Linux kernel source tree 项目地址: https://gitcode.com/GitHub_Trending/li/linux
你是否在调试内核时被pgd_t、pud_t等页表类型搞得晕头转向?本文将用图解和通俗语言带你轻松理解Linux内核的四级页表结构,掌握地址转换的核心逻辑。读完你将能够:区分四种页表类型的层级关系、看懂页表项定义代码、理解虚拟地址到物理地址的转换流程。
页表(Page Table)基础概念
页表是Linux内核实现虚拟内存管理的核心数据结构,用于将程序使用的虚拟地址(Virtual Address)转换为物理内存中的实际地址(Physical Address)。现代CPU通常采用多级页表设计,Linux内核从2.6版本开始支持四级页表结构,分别对应四种页表类型:pgd_t(页全局目录)、pud_t(页上级目录)、pmd_t(页中间目录)和pte_t(页表项)。这种分层设计既能减少内存占用,又能提高地址转换效率。
页表类型的定义主要位于内核头文件include/linux/pgtable.h中,该文件包含了页表操作的核心数据结构和函数声明。
四级页表结构全景图
Linux内核的四级页表采用层层索引的方式工作,就像使用邮政编码查找地址一样:先找省(pgd)、再找市(pud)、接着找区(pmd)、最后找到具体街道(pte)。以下是64位系统中典型的页表层级划分:
四种页表类型详解
pgd_t:页全局目录(Page Global Directory)
pgd_t是四级页表的顶层结构,每个进程拥有一个独立的pgd表。在64位系统中,pgd表通常包含512个表项(由PTRS_PER_PGD宏定义),每个表项指向一个pud_t结构。
关键代码定义:
// 简化自[include/linux/pgtable.h](https://link.gitcode.com/i/a0947a07dbf7950ab4d01e26e783a4b1)第89-91行
#define pgd_index(a) (((a) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
#define pgd_offset(mm, address) pgd_offset_pgd((mm)->pgd, (address))
pud_t:页上级目录(Page Upper Directory)
pud_t是页表结构的第二层,在大多数系统中(如x86_64),pud表同样包含512个表项(PTRS_PER_PUD),每个表项指向一个pmd_t结构。在32位系统或某些嵌入式平台中,pud层可能被折叠(Folded),此时pud_t与pgd_t等价。
关键代码定义:
// 简化自[include/linux/pgtable.h](https://link.gitcode.com/i/a0947a07dbf7950ab4d01e26e783a4b1)第80-86行
static inline unsigned long pud_index(unsigned long address)
{
return (address >> PUD_SHIFT) & (PTRS_PER_PUD - 1);
}
#define pud_offset(p4d, address) p4d_pgtable(*(p4d)) + pud_index((address))
pmd_t:页中间目录(Page Middle Directory)
pmd_t是页表结构的第三层,作用与pud_t类似,用于进一步细化地址索引。pmd表同样包含512个表项(PTRS_PER_PMD),每个表项指向一个pte_t结构。在支持大页(Huge Page)的系统中,pmd表项可以直接指向一个2MB或1GB的物理页面,从而跳过pte层加速地址转换。
关键代码定义:
// 简化自[include/linux/pgtable.h](https://link.gitcode.com/i/a0947a07dbf7950ab4d01e26e783a4b1)第72-78行
static inline unsigned long pmd_index(unsigned long address)
{
return (address >> PMD_SHIFT) & (PTRS_PER_PMD - 1);
}
#define pmd_offset(pud, address) pud_pgtable(*(pud)) + pmd_index((address))
pte_t:页表项(Page Table Entry)
pte_t是四级页表的最底层,直接指向物理内存页面。pte表包含512个表项(PTRS_PER_PTE),每个表项记录了物理页框号(PFN)以及页面属性(如读写权限、缓存策略等)。当CPU访问虚拟地址时,最终会通过pte_t找到对应的物理页面。
关键代码定义:
// 简化自[include/linux/pgtable.h](https://link.gitcode.com/i/a0947a07dbf7950ab4d01e26e783a4b1)第67-70行
static inline unsigned long pte_index(unsigned long address)
{
return (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1);
}
#define pte_offset_kernel(pmd, address) (pte_t *)pmd_page_vaddr(*pmd) + pte_index(address)
四种页表类型对比表格
| 页表类型 | 全称 | 索引位数 | 表项数量 | 典型大小 | 作用 | 定义位置 |
|---|---|---|---|---|---|---|
| pgd_t | Page Global Directory | 9位 | 512 | 4KB | 指向pud_t表 | include/linux/pgtable.h |
| pud_t | Page Upper Directory | 9位 | 512 | 4KB | 指向pmd_t表 | include/linux/pgtable.h |
| pmd_t | Page Middle Directory | 9位 | 512 | 4KB | 指向pte_t表或大页 | include/linux/pgtable.h |
| pte_t | Page Table Entry | 9位 | 512 | 4KB | 指向物理页或页框 | include/linux/pgtable.h |
地址转换实例分析
以64位系统中的虚拟地址0xffff888000000000为例,地址转换过程如下:
- 提取pgd索引:右移
PGDIR_SHIFT(通常为39位)得到高9位索引值,从进程的pgd表中找到对应的pgd_t表项。 - 提取pud索引:右移
PUD_SHIFT(通常为30位)得到中间9位索引值,从pgd_t指向的pud表中找到pud_t表项。 - 提取pmd索引:右移
PMD_SHIFT(通常为21位)得到下9位索引值,从pud_t指向的pmd表中找到pmd_t表项。 - 提取pte索引:右移
PAGE_SHIFT(通常为12位)得到低9位索引值,从pmd_t指向的pte表中找到pte_t表项。 - 计算物理地址:pte_t表项中的高44位为物理页框号(PFN),左移12位(
PAGE_SHIFT)加上虚拟地址的低12位偏移量,得到最终物理地址。
关键转换函数调用链:
// 简化自[include/linux/pgtable.h](https://link.gitcode.com/i/a0947a07dbf7950ab4d01e26e783a4b1)第168-170行
pgd_t *pgd = pgd_offset(mm, address); // 获取pgd表项
pud_t *pud = pud_offset(pgd, address); // 获取pud表项
pmd_t *pmd = pmd_offset(pud, address); // 获取pmd表项
pte_t *pte = pte_offset_kernel(pmd, address); // 获取pte表项
实际应用与注意事项
在编写内核模块或调试内核问题时,理解页表结构非常重要。以下是一些常见应用场景:
- 内存泄漏调试:通过遍历进程的pgd->pud->pmd->pte链,统计物理页面使用情况,定位内存泄漏点。
- 性能优化:合理配置大页(通过pmd_t直接映射2MB/1GB页面)可以减少TLB缓存未命中,提升内存访问性能。
- 地址空间隔离:不同进程拥有独立的pgd表,确保进程间地址空间隔离,提高系统安全性。
需要注意的是,页表操作必须在关中断或持有页表锁的情况下进行,防止CPU在地址转换过程中修改页表导致不一致。相关锁机制可参考内核源码mm/page_alloc.c中的实现。
总结与进阶学习
Linux内核的四级页表结构是现代操作系统内存管理的经典设计,通过pgd_t、pud_t、pmd_t和pte_t的分层索引,实现了高效的虚拟地址到物理地址的转换。核心要点:
- 层级关系:pgd(顶层)→ pud → pmd → pte(底层)
- 表项作用:每级表项都包含下一级表的物理地址和属性标志
- 地址拆分:64位地址通常按9+9+9+9+12位拆分(4级索引+页内偏移)
想要深入学习页表实现,可以参考:
- 官方文档:Documentation/vm/pgtable.rst(假设存在该文件)
- 内核源码:mm/memory.c中的
__get_user_pages函数实现 - 架构相关代码:arch/x86/include/asm/pgtable_64.h(以x86_64为例)
希望本文能帮助你打通Linux页表的任督二脉!如果觉得有用,请点赞收藏,下期我们将讲解页表操作的核心API函数。
【免费下载链接】linux Linux kernel source tree 项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



