〇、全景:页表项的"最后一跳"
#3 讲了多级页表(PGD/P4D/PUD/PMD/PTE)和缺页处理,但每一级的页表项(page table entry)本身长什么样 没展开------它才是"虚拟地址 → 物理地址"翻译的核心载体。本篇补上这块:一个 PTE 是一个 64 位的值,拆成"物理页帧号(PFN)+ 一堆属性位"两部分------高位存"指向哪个物理页",低位存"这个页能不能写、能不能执行、被访问过没"。
#mermaid-svg-Jpa9RWUY5ZHwr4Ja{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .error-icon{fill:#552222;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .marker.cross{stroke:#333333;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja p{margin:0;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .cluster-label text{fill:#333;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .cluster-label span{color:#333;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .cluster-label span p{background-color:transparent;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .label text,#mermaid-svg-Jpa9RWUY5ZHwr4Ja span{fill:#333;color:#333;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .node rect,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .node circle,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .node ellipse,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .node polygon,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .rough-node .label text,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .node .label text,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .image-shape .label,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .icon-shape .label{text-anchor:middle;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .rough-node .label,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .node .label,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .image-shape .label,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .icon-shape .label{text-align:center;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .node.clickable{cursor:pointer;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .arrowheadPath{fill:#333333;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .cluster text{fill:#333;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .cluster span{color:#333;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja rect.text{fill:none;stroke-width:0;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .icon-shape,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .icon-shape p,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .icon-shape .label rect,#mermaid-svg-Jpa9RWUY5ZHwr4Ja .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Jpa9RWUY5ZHwr4Ja :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 64 位 PTE
PFN(物理页帧号)
bits 12~51,40 位
属性位
present/RW/US/NX/accessed/dirty/...
翻译:物理地址 = PFN << 12 | offset
MMU 权限检查
present? RW? US? NX?
一句话主线:PTE 是 64 位值,高 40 位(bits 12~51)存物理页帧号 PFN,低 12 位 + 第 63 位存属性位(present/RW/US/accessed/dirty/NX 等);MMU 翻译时先逐级查页表、再按 PTE 权限位检查(可写?用户态?可执行?),最后
物理地址 = PFN << 12 | 页内偏移。其中 accessed/dirty 是 CPU 硬件自动置位的"使用痕迹"(内核靠它做 LRU 回收/回写),bit 9~11 是内核复用的软件位,大页 PTE 用 PSE 标志让 PFN 从更高的位开始。
一、PTE 位字段布局:一个 64 位值的完整分解
x86-64 的 PTE 位定义在 arch/x86/include/asm/pgtable_types.h,逐位看:
| 位 | 宏 | 含义 | 谁置位 |
|---|---|---|---|
| 0 | _PAGE_BIT_PRESENT |
present,页在内存里 | 内核 |
| 1 | _PAGE_BIT_RW |
writable,可写 | 内核 |
| 2 | _PAGE_BIT_USER |
userspace,用户态可访问(清 0 = 仅内核) | 内核 |
| 3 | _PAGE_BIT_PWT |
write-through(写穿缓存) | 内核 |
| 4 | _PAGE_BIT_PCD |
cache disabled(禁用缓存) | 内核 |
| 5 | _PAGE_BIT_ACCESSED |
被访问过 | CPU 硬件 |
| 6 | _PAGE_BIT_DIRTY |
被写过 | CPU 硬件 |
| 7 | _PAGE_BIT_PSE(大页)/ _PAGE_BIT_PAT(4KB) |
大页标志 / 页表属性位 | 内核 |
| 8 | _PAGE_BIT_GLOBAL |
全局 TLB 项(切换进程不刷) | 内核 |
| 9~11 | _PAGE_BIT_SOFTW1/2/3 |
软件位,内核复用 | 内核 |
| 12 | _PAGE_BIT_PAT_LARGE(大页) |
大页 PAT 位 | 内核 |
| 12~51 | PFN(物理页帧号) | 指向哪个物理页 | 内核 |
| 57~58 | _PAGE_BIT_SOFTW4/5 |
软件位 | 内核 |
| 59~62 | _PAGE_BIT_PKEY_BIT0~3 |
保护键(Protection Keys) | 内核 |
| 63 | _PAGE_BIT_NX |
No execute,不可执行 | 内核 |
源码就是这一串:
c
// arch/x86/include/asm/pgtable_types.h (v6.6, line 10)
#define _PAGE_BIT_PRESENT 0 /* is present */
#define _PAGE_BIT_RW 1 /* writeable */
#define _PAGE_BIT_USER 2 /* userspace addressable */
#define _PAGE_BIT_PWT 3 /* page write through */
#define _PAGE_BIT_PCD 4 /* page cache disabled */
#define _PAGE_BIT_ACCESSED 5 /* was accessed (raised by CPU) */
#define _PAGE_BIT_DIRTY 6 /* was written to (raised by CPU) */
#define _PAGE_BIT_PSE 7 /* 4 MB (or 2MB) page */
#define _PAGE_BIT_GLOBAL 8 /* Global TLB entry PPro+ */
#define _PAGE_BIT_SOFTW1 9 /* available for programmer */
#define _PAGE_BIT_SOFTW2 10 /* " */
#define _PAGE_BIT_SOFTW3 11 /* " */
// ... 57~58 SOFTW4/5、59~62 PKEY、63 NX
#define _PAGE_BIT_NX 63 /* No execute: only valid after cpuid check */
关键结构:低 12 位全是"属性 + 状态",高 40 位(bits 12~51)才是真正的物理地址信息(PFN)。下面逐个拆开讲。
二、PFN:物理页帧号怎么编码进 PTE
2.1 40 位 PFN,物理地址 >> 12
物理地址 = 页帧号(PFN)× 页大小 + 页内偏移。x86-64 物理地址 52 位(__PHYSICAL_MASK_SHIFT = 52,page_64_types.h:54),页大小 4KB = 2^12,所以 PFN 占 52 - 12 = 40 位,正好是 PTE 的 bits 12~51。低 12 位被属性占用,物理地址天然 4KB 对齐(低 12 位一定是 0),所以 PTE 里不需要存低 12 位。
c
// arch/x86/include/asm/pgtable_types.h (v6.6, line 284)
#define PTE_PFN_MASK ((pteval_t)PHYSICAL_PAGE_MASK) // = bits 12~51
#define PTE_FLAGS_MASK (~PTE_PFN_MASK) // = 低 12 位 + NX 等
提取和设置 PFN 就是对这两段做位运算:
c
// arch/x86/include/asm/pgtable.h (v6.6, line 223 / 763)
static inline unsigned long pte_pfn(pte_t pte)
{
phys_addr_t pfn = pte_val(pte);
pfn ^= protnone_mask(pfn); // 处理 PROTNONE(NUMA)
return (pfn & PTE_PFN_MASK) >> PAGE_SHIFT; // 取 PFN 段,右移 12
}
static inline pte_t pfn_pte(unsigned long page_nr, pgprot_t pgprot)
{
return __pte(((phys_addr_t)page_nr << PAGE_SHIFT) | pgprot_val(pgprot));
}
pte_pfn(提取)和 pfn_pte(构造)一进一出,PTE_PFN_MASK 就是 PFN 段、PAGE_SHIFT 就是 12。
2.2 页大小决定 PFN 从哪一位开始
这是大页的关键:PFN 的起始位随页大小变化------因为"页内偏移"的位数 = log2(页大小),页越大、偏移越宽、PFN 越短、起始位越高:
| 页大小 | 页内偏移 | PFN 占位 | 说明 |
|---|---|---|---|
| 4KB | 12 位 | bits 12~51(40 位) | 普通页,PTE 层 |
| 2MB | 21 位 | bits 21~51(31 位) | 大页,PMD 层 |
| 1GB | 30 位 | bits 30~51(22 位) | 巨页,PUD 层 |
所以同一套"PFN << shift | offset"翻译,对大页/巨页也成立,只是 shift 分别是 12/21/30------这正是 #4 大页里"在 PMD/PUD 层提前终止"的位级体现。
三、权限位:MMU 翻译时做硬检查
PTE 里的权限位,MMU 每次访问都会硬件检查,不通过就触发 #PF(缺页/保护违例,见 #3):
- present(bit 0) :页在不在物理内存。清 0 → 访问触发缺页(#3 的
do_pte_missing按需分配),或表示页被换出(do_swap_page); - RW(bit 1) :可写。页只读(写时复制 COW)时,写访问触发
do_wp_page; - USER(bit 2):用户态可访问。清 0 表示内核页,用户态代码访问会触发保护违例(这就是内核地址空间对用户不可见的硬件机制);
- NX(bit 63) :不可执行。置位后该页不能取指执行(防在数据页上执行代码,是 DEP/W^X 的硬件基础)。注意注释
"only valid after cpuid check"------要先确认 CPU 支持 NX(X86_FEATURE_NX)。
这四个位是"权限"的核心,present/RW/US/NX 的检查全在 MMU 硬件翻译的那一跳里完成。
四、accessed / dirty:CPU 硬件自动置位的"使用痕迹"
accessed(bit 5)和 dirty(bit 6)是两个特殊的位------不是软件设的,是 CPU 硬件在访存时自动置 1 的 (注释 "raised by CPU"):
- accessed :页被读或写过,CPU 自动置 1。内核用
pte_mkold清除它,观察"最近有没有被访问",这是 LRU 页回收(决定先回收哪些页)的硬件基础; - dirty:页被写过,CPU 自动置 1。文件页被写后 dirty 置位,内核据此知道"这页要回写磁盘"(呼应 #5 的 writeback)。
这两个位让内核不用软件插桩就能知道页的使用情况------硬件在每次访存时顺带记账,内核定期读、用完清,形成"使用 → 观察 → 回收"的闭环。
五、软件位:内核复用的"私有"位
bit 9~11(SOFTW1/2/3)和 57~58(SOFTW4/5)硬件不管,内核复用它们做自己的标记:
c
// arch/x86/include/asm/pgtable_types.h (v6.6, line 32)
#define _PAGE_BIT_SPECIAL _PAGE_BIT_SOFTW1 // 标记"特殊页"(如 zero page、PFN MAP)
#define _PAGE_BIT_UFFD_WP _PAGE_BIT_SOFTW2 // userfaultfd 写保护
#define _PAGE_BIT_SOFT_DIRTY _PAGE_BIT_SOFTW3 // 软件脏页跟踪(CGROUP 内存回收)
#define _PAGE_BIT_DEVMAP _PAGE_BIT_SOFTW4 // 设备映射(devmap,GPU 等)
#define _PAGE_BIT_SAVED_DIRTY _PAGE_BIT_SOFTW5 // 保存的 dirty 位(暂停时)
SPECIAL 最典型------do_anonymous_page 里读匿名页时,用 pte_mkspecial 把零页映射成 special 页,表示"这不是普通物理页、是共享的特殊页"(#3 §5.1 的零页)。这些位"硬件无视、软件专用",是 PTE 给内核留的私有空间。
六、大页 PTE:PSE 标志 + 更宽的页内偏移
大页(#4)在 PMD/PUD 层"提前终止",靠的是 PSE(Page Size Extension,bit 7):
c
#define _PAGE_BIT_PSE 7 /* 4 MB (or 2MB) page */
#define _PAGE_BIT_PAT_LARGE 12 /* On 2MB or 1GB pages */
- 普通 4KB 页的 PTE 里,bit 7 是 PAT(缓存类型位);
- 2MB 大页的 PMD 里,bit 7 = PSE 置位 ,表示"这个 PMD 直接指向一个 2MB 物理大页,不再往下走 PTE";此时 PFN 从 bit 21 开始(§2.2),bit 12 变成
PAT_LARGE; - 1GB 大页同理在 PUD 层,PFN 从 bit 30 开始。
所以"大页"在 PTE 层面就是:PSE 标志 + PFN 起始位后移 + 省掉下一级页表,三个动作一次完成。
七、缓存类型:PAT + MTRR 决定最终内存类型
PTE 的 bit 3(PWT)、bit 4(PCD)、bit 7(PAT,4KB 页)、bit 12(PAT_LARGE,大页)合起来是页表里的内存类型编码 (PAT = Page Attribute Table)。它和上一章 CPU 系列刚讲的 MTRR 是两套并行机制:
- MTRR(CPU 寄存器):按物理地址范围定缓存类型(粗粒度,BIOS 设置,全 CPU 一致);
- PAT(页表项位):按页定缓存类型(细粒度,内核设置,per-PTE)。
最终某个物理地址的缓存类型 = PAT 和 MTRR 结合的结果(两者都指定时按 Intel 的规则取"更保守"的那个)。现代 x86 主要靠 PAT 做细粒度控制(如标记 MMIO 为 uncacheable),MTRR 保留做 BIOS 兜底。这正是 PTE 属性位里 PWT/PCD/PAT 的用途。
八、MMU 翻译全景:虚拟地址怎么变成物理地址
把 #3 的层级 + 本篇的位字段串起来,一次完整翻译:
虚拟地址(48 位 / LA48):| PGD(9) | PUD(9) | PMD(9) | PTE(9) | offset(12) |
│
┌───────────────────────────┘ 逐级用"当前级索引"查表
▼
PGD → PUD → PMD → PTE(每一级页表项 = 下一级表的物理地址 + 权限位)
│
┌───────────────────────────┘ 最后一级 PTE 指向真实物理页
▼
PTE = PFN(40 位) | 属性位
物理地址 = PFN << 12 | offset ← 这一跳就是本篇讲的"PFN 编码"
每一级查表时,MMU 都先检查该级页表项的 present/权限位,不通过就 #PF(#3);最后一级 PTE 里取出 PFN,PFN << 12 | offset 拼出物理地址,再交给 cache(按 §七 的 PAT/MTRR 决定缓存类型)去取数。
九、为什么这样设计(Why 层)
9.1 为什么 PTE 要"高位 PFN + 低位属性"这种布局
因为物理地址天然页对齐 (低 12 位一定是 0),这 12 位"空出来"正好塞属性位------不额外占空间就同时表达了"指向哪"(PFN)和"怎么用"(权限/状态)。把必然为 0 的位复用成属性,是位字段设计的经典做法。
9.2 为什么 accessed/dirty 要 CPU 硬件置位,而不是软件跟踪
软件跟踪"哪页被访问/写过"需要在每次访存插桩,代价不可接受。而 CPU 在翻译后写入页表项时顺带 置这两个位(MMU 本来就写了页表项),几乎零成本。"把使用痕迹记在本来就动的页表项上",是硬件配合内存管理的默契。
9.3 为什么 PFN 起始位随页大小变化(而不是固定)
因为"页内偏移"的宽度 = log2(页大小),物理地址 = PFN × 页大小 + 偏移,页越大偏移越宽、PFN 越窄。这个统一的 PFN << shift | offset 公式让 4KB/2MB/1GB 三级页共享同一套翻译逻辑,只是 shift 不同------大页的"省层级"就落在"shift 变大"上。
9.4 为什么要留软件位(bit 9~11 等)
硬件定义的位(present/RW/US/NX)是 x86 架构标准,但内核有自己的记账需求(特殊页、软脏页、userfaultfd 写保护),这些架构标准里没有对应位 。留几个"硬件无视、软件专用"的位,内核就能在不碰硬件语义的前提下扩展自己的标记------"架构位"和"软件位"分层,让内核能演进而不受限于硬件。
9.5 为什么缓存类型要 PAT + MTRR 两套
MTRR 是早期的粗粒度机制(按地址范围),BIOS 用它设显存等大块;但它粒度粗、且全 CPU 要一致。PAT 是后来的细粒度机制(按页),能精确到每一页。两者并存是历史演进:MTRR 保留兼容、PAT 承担细粒度------取"两者更保守"保证正确性。
十、与已有博客的交叉引用
| 已有博客 | 本篇覆盖的关系 |
|---|---|
| 内存 #3 页表缺页 | #3 讲层级和"状态位决定缺页行为",本篇把"状态位"落到每一位:present→do_pte_missing、!present→do_swap_page、!RW→do_wp_page(COW)、NX→执行保护违例 |
| 内存 #4 大页 | 大页 = PSE 标志 + PFN 起始位后移(bit 7 置位、PFN 从 21/30 位开始),是 #4"PMD/PUD 提前终止"的位级实现 |
| CPU #13 特性位 / #2 MTRR | 本篇 §七 的 PAT 和 CPU 系列的 MTRR 是"定缓存类型"的两套机制;PTE 的 NX 位要先 X86_FEATURE_NX 确认 CPU 支持 |
| IOMMU #8 页表填充 | IOMMU 页表项也是"PFN + 属性位"结构(见 \[blog-iommu-expansion]),和 MMU 的 PTE 是两套独立页表、同一套"PFN<<shift|offset"思想 |
附:本篇关键源码索引
| 符号 | 位置 |
|---|---|
_PAGE_BIT_PRESENT ~ _PAGE_BIT_NX(PTE 位定义) |
arch/x86/include/asm/pgtable_types.h:10 ~ :30 |
| 软件位复用(SPECIAL/UFFD_WP/SOFT_DIRTY/DEVMAP) | arch/x86/include/asm/pgtable_types.h:32 / :34 / :35 / :36 |
_PAGE_PRESENT / _PAGE_RW 等位掩码 |
arch/x86/include/asm/pgtable_types.h:49 ~ :69 |
PTE_PFN_MASK / PTE_FLAGS_MASK |
arch/x86/include/asm/pgtable_types.h:284 / :290 |
__PHYSICAL_MASK_SHIFT(52 位物理地址) |
arch/x86/include/asm/page_64_types.h:54 |
pte_pfn(提取 PFN) |
arch/x86/include/asm/pgtable.h:223 |
pfn_pte(构造 PTE) |
arch/x86/include/asm/pgtable.h:763 |
pte_present / pte_mkspecial |
arch/x86/include/asm/pgtable.h:966 / :483 |