【linux内存管理】【基础知识 1】【pgd,p4d,pud,pmd,pte,pfn,pg,ofs,PTRS概念介绍】
一、背景
在开始内存管理之前,我们先对 linux 内存管理 相关的术语进行介绍。
二、关键术语与常用宏/函数介绍(基于 Linux 6.0.9 )
注:页表层次通常自上而下为 PGD -> P4D -> PUD -> PMD -> PTE(映射粒度由页大小决定)。有的平台会“fold”(折叠)某些层(例如 p4d 在很多配置是 folded),但抽象层名仍存在。
2.1 基本术语
1. PGD
- PGD(Page Global Directory)
顶层页目录条目类型,类型名pgd_t
。在多级页表体系中负责顶层索引。对用户态 mm,用pgd_offset(mm, addr)
得到pgd_t *
。
2. P4D
- P4D(page 4th-level directory)
在支持 5 层页表的平台存在(或在某些架构上被折叠)。类型p4d_t
。在某些内核配置下p4d
是“folded”(即其实质和pud
合并),但 API 仍然可用。
3. PUD
- PUD(Page Upper Directory)
次顶层目录,类型pud_t
。
4. PMD
- PMD(Page Middle Directory)
中层目录,类型pmd_t
。PMD 可以直接映射大页(hugepage)或指向 PTE 表。
5. PTE
- PTE(Page Table Entry)
最底层的页表条目,类型pte_t
,包含物理页帧号(PFN)及权限位(可读/写/exec/COW/dirty/accessed 等)。
6. PFN
- PFN(Page Frame Number)
物理页帧号——物理页的编号。通常:phys_addr = pfn << PAGE_SHIFT
。
7. pg/ofs/PTRS
-
pg / ofs / PTRS
-
pg
在不同上下文含义不同:有时表示struct page
,有时表示页表 page(页表页)。 -
ofs
常用于表示页内偏移(offset),或页表索引偏移的局部变量名。 -
PTRS
常见如PTRS_PER_PGD
,PTRS_PER_PTE
等,表示该级页表条目的数量(power-of-two)。这些通常由页表位宽与每条目大小决定。
例如索引计算常用:index = (addr >> SHIFT) & (PTRS_PER_xxx - 1)
。
-
2.2 常用宏或者函数介绍
xxx
可以是: pgd、p4d、pud、pmd、pte
1. xxx_index/ xxx_offset
- xxx_index: 表示线性地址在对应的级别中的 偏移量
- xxx_offset: 用来获取该级别目标偏移量对应项的指正
这些宏/函数通常成对出现:
xxx_index(addr)
给出某层的索引,xxx_offset(...)
/xxx_offset_k(...)
返回指向下一级结构的指针(或条目指针)。
1. pgd_index / pgd_offset
-
pgd_index(addr)
计算地址在 PGD 层的索引(仅位运算)。等价思想:(addr >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1)
。用于低级页表遍历或调试时快速得到索引。 -
pgd_offset(mm, addr)
/pgd_offset_k(addr)
返回pgd_t *
(顶层条目指针)。pgd_offset_k
专为内核全局init_mm
/ kernel 映射使用。使用场景:从mm
和addr
开始逐层遍历页表。
2. p4d_index/p4d_offset
p4d_index(addr)
/p4d_offset(pgd, addr)
与 PGD 相同思路。通常若配置折叠,p4d_*
可能是内联/映射到pud_*
的别名。使用场景:跨架构统一遍历代码。
3. pud_index/pud_offset
pud_index(addr)
/pud_offset(p4d, addr)
上同,返回pud_t *
。
4. pmd_index/pmd_offset
pmd_index(addr)
/pmd_offset(pud, addr)
计算 pmd 索引或从 pud 得到 pmd 指针。使用场景:当要检查一个addr
是否被 large page(PMD 大页)映射,或需要走到 PTE 层时先拿到 PMD。
5. pte_index/pte_offset/pte_offset_map/pte_offset_kernel
-
pte_index(pmd, addr)
/pte_offset(pmd, addr)
计算并返回pte_t *
(即指向对应 PTE 表的入口)。常见变体:-
pte_offset_map(pmd, addr)
:在需要映射页表页到内核地址空间时(有些架构 PTE 表本身也在高端内存),返回可直接读取/写的pte_t *
(并可能需要pte_unmap()
)。 -
pte_offset_kernel(pmd, addr)
:内核模式下直接返回pte_t *
(当 PTE 表可直接以内核地址访问时)。使用场景:在内核需要直接读取/修改一个地址的 PTE(例如 mm 管理工具、debug、swap 或特殊内存管理场景),通常在持有恰当锁的前提下使用。
-
-
pgd_index
,p4d_index
,pud_index
,pmd_index
,pte_index
(xxx_index)
一般用于纯算法/调试中计算某级的数组索引。
2. __xxx
/ xxx_val
-
__pte(x)
/__pmd(x)
/__pgd(x)
将一个原始数值(通常unsigned long
)包装为相应类型 (pte_t
,pmd_t
…) 的构造器。用于从数值构造条目(非正式更新条目时)或测试。不要在没有正确锁与同步情况下随意把数值写回页表。 -
pte_val(pte)
/pmd_val(pmd)
/pgd_val(pgd)
将条目抽成底层数值(unsigned long
),便于位操作或打印。使用场景:调试、打印或对位域进行分析。 -
xxx_pfn
(例如pte_pfn(pte)
)
从一个 PTE 中取出 PFN(即页帧号)。常见用于:将 PTE 转为struct page *
(配合pfn_to_page()
)或计算物理地址。
3. set_xxx
/ xxx_clear
替换/写入用法(写操作必须小心)
-
set_pmd(pmdp, new_pmd)
/pmd_clear(pud, addr, pmdp)
等-
set_pmd
:把pmd_t
写入pmd
条目(常见在创建/修改 pmd 映射时使用),通常需在持有pmd_spinlock
/ 块级锁 或在mm
的页表写锁保护下执行(不同架构同步机制不同)。 -
pmd_clear
/pte_clear
:从页表中移除条目(解除映射),通常在释放映射、unmap、swap out 等场景使用。清除 PTE 必须保证 CPU TLB 被刷新/无竞争。
-
-
set_pte_at(mm, addr, ptep, pte)
常用于设置用户地址空间的 PTE(写入带有 mm 信息的替代),通常要在page_table_lock
或相应写锁保护下做,并在之后 flush_tlb/flush_icache 等。
4. lookup_address
lookup_address(unsigned long addr, unsigned int *level)
在内核中用于查找某个虚拟地址在页表中对应的最底层条目(返回pte_t *
或pmd_t
等,level
表示返回条目是哪一级)。非常适合只读查询 PTE,而不需要手动多级遍历的场景。 这个函数只在 x86 下支持, arm64 中是没有的
5. pfn_to_page/page_to_pfn
pfn_to_page(pfn)
/page_to_pfn(page)
PFN 与struct page *
之间的转换。
6. virt_to_page/virt_to_phys
virt_to_page()
/virt_to_phys()
(仅对直接映射的内核虚拟地址安全)
可从内核虚拟地址直接得到struct page *
/ 物理地址。对 vmalloc 地址不适用(vmalloc 映射非连续)。
7. pte_unmap/pte_offset_map
pte_unmap()
/pte_offset_map()
如果pte_offset_map
为架构映射页表页提供了映射,需要在读完后pte_unmap()
。在某些架构pte_offset_kernel
不需映射,pte_offset_map
则会做必要的映射。
2.3 x86 与 arm64 的主要差别(对上述概念/宏的影响)
-
页表层数与折叠(folding)
-
x86 (legacy) 可支持 4-level(PGD→PUD→PMD→PTE)或在启用 PAE/5-level 时更多层次。内核中常见
p4d
层通常在 x86 被“folded”或作为占位,API 仍存在但实现为 pass-through。 -
arm64 通常使用 4 级(PGD/PUD/PMD/PTE)但实现细节与索引位偏移不同;也支持长线性映射与不同页大小(4K、16K、64K)在特定实现中。
-
-
页大小与大页(Huge pages)支持
-
x86 常见 4KB(可选 2MB(PMD large)或 1GB(PUD large))。
-
arm64 在硬件上也支持 4KB/16KB/64KB page sizes(实现依平台)。因此
PMD
是否能表示 large page 与页大小有关。
-
-
条目位布局(flags, PFN bit positions)
pte_val()
/pmd_val()
抽出的原始数值位域在不同架构上具体含义不同(低位通常是标志位,PFN 位从某个位起)。因此用pte_pfn()
/pmd_pfn()
抽象函数获取 PFN,而不是直接位移pte_val
。
-
访问 PTE 表的方式(是否需要
pte_offset_map
映射)- 在某些架构上(尤其物理内存直接映射的 x86)内核可以直接访问页表页;而在某些实现中需要通过
pte_offset_map
明确把页表页映射到可读内核虚拟地址。API 层面pte_offset_kernel
与pte_offset_map
提供兼容方案。
- 在某些架构上(尤其物理内存直接映射的 x86)内核可以直接访问页表页;而在某些实现中需要通过
-
名称与宏的差异
- 有些宏在某平台会被定义为别名或内联空操作(例如
p4d
fold)。因此编写跨架构代码时常用pgd_offset()
/p4d_offset()
/pud_offset()
等链式调用,而不要直接假设p4d
一定存在。
- 有些宏在某平台会被定义为别名或内联空操作(例如
三、测试驱动
下面给出一个 kernel module(leo_study/pgtable/pgtable_sig_test.c
)。目的:
-
安全地分配一页内存(
vmalloc
),演示如何读取该地址在内核页表中的 PGD/P4D/PUD/PMD/PTE 信息(使用pgd_offset_k
、p4d_offset
、pud_offset
、pmd_offset
、pte_offset_kernel
、lookup_address
、pgd_index
等); -
展示
__pte
、pte_val
、pte_pfn
、pfn_to_page
、page_to_pfn
的用法; -
展示
PTRS_PER_*
/*_index
的含义; -
给出注释掉的
set_pte_at
/pte_clear
/set_pmd
用法示例(非常危险,需在受控环境并加锁执行),并解释如何正确加锁/刷新 TLB(示例注释中说明)。
请在一台测试机或 QEMU 上运行该模块(不要在生产机器上测试写操作部分)。
3.1 code
// File: pgtable_demo.c
// Compile against Linux 6.0.9 headers. Example: make with a simple Kbuild
// This module demonstrates reading page table entries and related macros.
// WARNING: no destructive modifications are performed. Destructive examples are commented.#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/vmalloc.h>
#include <linux/highmem.h>
#include <linux/sched.h>
#include <linux/pagemap.h>
#include <linux/pgtable.h>
#include <linux/huge_mm.h>MODULE_LICENSE("GPL");
MODULE_AUTHOR("leo");
MODULE_DESCRIPTION("Page table demo: pgd/p4d/pud/pmd/pte and related macros (Linux 6.0.9 style)");// size for vmalloc region (one page)
#define DEMO_SIZE PAGE_SIZEstatic void *vmaddr = NULL;pte_t *my_lookup_address(unsigned long addr, unsigned int *level)
{pgd_t *pgd;p4d_t *p4d;pud_t *pud;pmd_t *pmd;pte_t *pte = NULL;// 内核地址空间 mm == NULL; 故这里使用 current->active_mmpgd = pgd_offset(current->active_mm, addr); // 第一级页表if (pgd_none(*pgd) || pgd_bad(*pgd))return NULL;printk("%s: pgd=%px *pgd=%#llx\n", __func__, pgd,(unsigned long)pgd_val(*pgd));p4d = p4d_offset(pgd, addr); // 第四级页表if (p4d_none(*p4d) || p4d_bad(*p4d))return NULL;printk("%s: p4d=%px *p4d=%#llx\n", __func__, p4d,(unsigned long)p4d_val(*p4d));pud = pud_offset(p4d, addr); // 第二级页表if (pud_none(*pud) || pud_bad(*pud))return NULL;printk("%s: pud=%px *pud=%#llx\n", __func__, pud,(unsigned long)pud_val(*pud));pmd = pmd_offset(pud, addr); // 第三级页表if (pmd_none(*pmd) || pmd_bad(*pmd))return NULL;printk("%s: pmd=%px *pmd=%#llx\n", __func__, pmd,(unsigned long)pmd_val(*pmd));if (pmd_leaf(*pmd)) { // 大页映射*level = 2;return (pte_t *)pmd;}pte = pte_offset_kernel(pmd, addr); // 最后一级页表if (!pte)return NULL;printk("%s: pte=%px *pte=%#llx\n", __func__, pte,(unsigned long)pte_val(*pte));*level = 3;return pte;
}static void dump_indices(unsigned long addr)
{unsigned long pgd_i = pgd_index(addr);
#ifdef p4d_indexunsigned long p4d_i = p4d_index(addr);
#elseunsigned long p4d_i = 0;
#endifunsigned long pud_i = pud_index(addr);unsigned long pmd_i = pmd_index(addr);unsigned long pte_i = pte_index(addr); /* pte_index uses pmd pointer in some impl; result only depends on addr bits */printk("pgd_index(%lu) = (((%#llx) >> PGDIR_SHIFT_%d) & (PTRS_PER_PGD_%d - 1)) = %lu\n",addr, addr, PGDIR_SHIFT, PTRS_PER_PGD, pgd_i);// printk("p4d_index(%lu) = (((%lu) >> P4D_SHIFT_%d) & (PTRS_PER_P4D_%d - 1)) = %lu\n", addr, addr, P4D_SHIFT, PTRS_PER_P4D, p4d_i);printk("pud_index(%#llx) = (((%#llx) >> PUD_SHIFT_%d) & (PTRS_PER_PUD_%d - 1)) = %lu\n",addr, addr, PUD_SHIFT, PTRS_PER_PUD, pud_i);printk("pmd_index(%#llx) = (((%#llx) >> PMD_SHIFT_%d) & (PTRS_PER_PMD_%d - 1)) = %lu\n",addr, addr, PMD_SHIFT, PTRS_PER_PMD, pmd_i);printk("pte_index(%#llx) = (((%#llx) >> PAGE_SHIFT_%d) & (PTRS_PER_PTE_%d - 1)) = %lu\n",addr, addr, PAGE_SHIFT, PTRS_PER_PTE, pte_i);pr_info("ADDR 0x%lx indices: pgd=%lu p4d=%lu pud=%lu pmd=%lu pte=%lu\n",addr, pgd_i, p4d_i, pud_i, pmd_i, pte_i);
}/* Helper to do safe lookup of PTE using lookup_address */
static void inspect_address(unsigned long addr)
{unsigned int level = 0;pte_t *ptep;pmd_t *pmdp;pud_t *pudp;p4d_t *p4dp;pgd_t *pgdp;/* lookup_address returns pointer to lowest-level entry that maps addr (pte or upper-level for huge page)and fills `level` with the level found (e.g., PG_LEVEL_NONE/PG_LEVEL_*). */
#if defined(CONFIG_X86) || defined(CONFIG_X86_64)ptep = lookup_address(addr, &level);printk(KERN_INFO "Architecture: x86/x86_64\n");#elif defined(CONFIG_ARM64)printk(KERN_INFO "Architecture: ARM64 (with PAC support)\n");// ARM64 上使用安全的ptep = my_lookup_address(addr, &level);#elif defined(CONFIG_ARM)printk(KERN_INFO "Architecture: ARM\n");#elseprintk(KERN_INFO "Architecture: Unknown\n");
#endifpr_info("lookup_address: addr=0x%lx returned ptep=%px level=%u\n", addr,ptep, level);/* Walk down manually from kernel top-level for demonstration (kernel mapping) */pgdp = pgd_offset_k(addr);pr_info(" pgd at %px, val=0x%lx\n", pgdp, pgd_val(*pgdp));p4dp = p4d_offset(pgdp, addr);pr_info(" p4d at %px, val=0x%lx\n", p4dp, p4d_val(*p4dp));pudp = pud_offset(p4dp, addr);pr_info(" pud at %px, val=0x%lx\n", pudp, pud_val(*pudp));pmdp = pmd_offset(pudp, addr);pr_info(" pmd at %px, val=0x%lx\n", pmdp, pmd_val(*pmdp));/* If pmd is huge mapping, pte_offset_kernel may behave differently; still we can try pte_offset_kernel */{pte_t *pte_k = NULL;/* pte_offset_kernel returns pte pointer for kernel (if supported) */pte_k = pte_offset_kernel(pmdp, addr);pr_info(" pte_offset_kernel -> %px\n", pte_k);if (pte_k) {unsigned long pval = pte_val(*pte_k);unsigned long pfn = pte_pfn(*pte_k);struct page *pg = pfn_to_page(pfn);pr_info(" pte_val=0x%lx pte_pfn=%lu pfn_to_page=%px\n",pval, pfn, pg);}}/* show use of __pte / pte_val */if (ptep) {unsigned long raw = pte_val(*ptep);pte_t copy = __pte(raw);pr_info(" lookup pte raw 0x%lx ; reconstructed via __pte -> pte_val=0x%lx\n",raw, pte_val(copy));}
}/* Print PTRS_PER_* values */
static void dump_ptrs(void)
{pr_info("PTRS_PER_PGD=%lu PTRS_PER_P4D=%lu PTRS_PER_PUD=%lu PTRS_PER_PMD=%lu PTRS_PER_PTE=%lu\n",(unsigned long)PTRS_PER_PGD,
#ifdef PTRS_PER_P4D(unsigned long)PTRS_PER_P4D,
#else0,
#endif(unsigned long)PTRS_PER_PUD, (unsigned long)PTRS_PER_PMD,(unsigned long)PTRS_PER_PTE);
}static int __init pgtable_demo_init(void)
{pr_info("pgtable_demo: init\n");dump_ptrs();/* allocate a vmalloc page (non-contiguous physical) to show vmalloc address handling */// vmaddr = kmalloc(DEMO_SIZE, GFP_KERNEL);vmaddr = vmalloc(DEMO_SIZE);if (!vmaddr) {pr_err("vmalloc failed\n");return -ENOMEM;}/* touch the page to ensure mapping exists */memset(vmaddr, 0x5a, DEMO_SIZE);pr_info("vmalloc returned %px %lu %#llx\n", vmaddr,(unsigned long)vmaddr, (unsigned long)vmaddr);/* dump indices for the allocated kernel virtual address */dump_indices((unsigned long)vmaddr);/* Inspect page-table entries via safe helper */inspect_address((unsigned long)vmaddr);/* Show virt_to_page where applicable (only for direct mapped memory; vmalloc not direct) */pr_info("virt_to_page(vmaddr) may be invalid for vmalloc; virt_to_page(vmaddr) -> %px\n",virt_to_page(vmaddr));/* Show page and pfn conversions for an alloc_page */{struct page *p = alloc_page(GFP_KERNEL);if (p) {unsigned long pfn = page_to_pfn(p);pr_info("alloc_page -> page=%px pfn=%lu pfn_to_page=%px\n",p, pfn, pfn_to_page(pfn));__free_page(p);}}pr_info("pgtable_demo: init done\n");return 0;
}static void __exit pgtable_demo_exit(void)
{pr_info("pgtable_demo: exit\n");if (vmaddr) {vfree(vmaddr);vmaddr = NULL;}pr_info("pgtable_demo: cleaned\n");
}module_init(pgtable_demo_init);
module_exit(pgtable_demo_exit);