页表就是【页号 → 物理页号】的表,一项 8 字节。除了页号,里面还有 P、可写、用户态可用、访问过四个关键位——一位就能撑起请求分页和 Swap。
5.4 讲了翻译这件事,这一节讲翻译表本身长什么样、里面记了什么。5.16 再讲它怎么被拆成多级。
一、地址怎么切
48 位虚拟地址 = 36 位页号 + 12 位偏移。 36 位页号意味着页表要有 2 的 36 次方项——这是个天文数字。 12 位偏移意味着页大小固定 4 KB,所以偏移那一段翻译时原样搬过去。 注意【48 位】不是【64 位】:x86-64 实际只用 48 位(后来的扩展到 57 位),上面那些位全是 0 或 1 的符号扩展。
二、单级页表为什么撑不住
页表项数 = 虚拟地址空间 ÷ 页大小 = 2 的 36 次方 = 687 亿项。 每项 8 字节,乘起来是 512 GB。 而这是【每个进程一份】——光页表就能把整台机器撑爆。 更荒唐的是【绝大多数项是空的】:一个进程只用几个 GB,剩下 99.9% 的表项永远没人查。 所以真正的做法不是【把表做小】,而是【用到才建】——这就是多级页表(5.16)。
三、页表项里的位
高位:物理页号——翻译结果。 P(Present,在不在):0 表示这一页还没分配,一访问就触发缺页。这一位是请求分页和 Swap 的总开关。 R/W(可写吗):写只读页会触发保护异常——只读代码段就靠它。 U/S(用户态能用吗):挡住用户程序碰内核内存,这是进程隔离的最后一道闸。 A/D(访问过/改过):给换出算法做参考——久没访问过的页优先换出去。 看清一件事:权限、状态、映射结果挤在同一个 8 字节里,所以一次查表把所有事都查清了。
四、页表在哪
页表在内核内存里,用户程序看不见。 CPU 里有一个寄存器指着【当前进程的页表】(x86 上叫 CR3)。 进程切换时,这个寄存器一换,整个地址空间就换了——这就是为什么切换进程比切换线程贵(线程共用同一张表)。 所以【进程隔离】落实到硬件上,就是【一个寄存器值不同】。
五、动手 Demo:页表——一个 4KB 的页,怎么管 256TB 的地址
这一节把一个 48 位地址切开,算出单级页表到底要多大,再把页表项里的位一个一个列出来。
vbits = 48
addr = 0x00007FF6C1A2B3C8
print("页号", hex(addr >> 12))
print("偏移", hex(addr & 0xFFF))
entries = 1 << (vbits - 12)
print("虚拟空间(TB)", 1 << (vbits - 40))
print("页表项数", entries)
print("页表大小(GB)", entries * 8 / 1024 ** 3)
实跑输出:
--------------------------------------------------------------------
页表——一个 4KB 的页,怎么管 256TB 的地址
--------------------------------------------------------------------
① 先把地址切开
部分 占多少位 这一段的值
-------------------------------------------------
页号(高位) 36 位 0x7FF6C1A2B
页内偏移(低位) 12 位 0x3C8
合起来 48 位 0x7FF6C1A2B3C8
**12 位偏移 = 4KB 页**:低 12 位在同一页内,搬家时不用改。
**高 36 位是页号**:这是页表要负责翻译的部分。
② 单级页表要多大
算的是 公式 结果
-----------------------------------------------------------
虚拟地址空间 2 的 48 次方字节 256 TB
页的总数 2^48 ÷ 4KB 68719476736 个
每项 8 字节 页号 + 权限位 + 状态位 —
页表总大小 68719476736 × 8 字节 512.00 GB
**512.00 GB 的页表,每个进程一份**——这就是单级页表撑不住的原因。
**而且绝大多数进程只用了几个 GB,剩下的表全是空的。**
③ 页表项里都记了什么
位 名字 作用
-----------------------------------------------------------
高位 物理页号 翻译结果指向哪一页
P 在不在 0 表示这一页还没分配,一碰就缺页
R/W 可写吗 写只读页会触发异常
U/S 用户态能用吗 挡住用户程序碰内核内存
A/D 访问过/改过 给换出算法做参考
**最有用的是 P 位**:**它把【有没有分配】和【在不在内存】统一成了一个位。**
**5.13 的请求分页、5.15 的 Swap,全靠这一位。**
页号占 36 位、偏移占 12 位,加起来正好 48 位。 页表项数是 687 亿项,总大小 512 GB——这个数字就是单级页表的死刑判决书。 而且要注意:这 512 GB 是【每个进程一份】,不是【整机一份】。 所以后面 5.16 的多级页表不是优化,是【不这么做根本没法用】。
📋 Linux 上能直接看到页大小:getconf PAGESIZE;看某个进程的页表规模:grep VmPTE /proc/<pid>/status。
30 秒实验
getconf PAGESIZE,确认这台机器的页大小是 4096。grep -E 'VmPTE|VmSize|VmRSS' /proc/self/status,看页表本身占了多少。- 把任意地址
addr & ~0xFFF,验证这就是它的页起始地址。
常见误区
- 【页表存在磁盘上】。它在内核内存里,被换出的是进程的页,不是页表本身。
- 【页表项里只有物理页号】。还有在不在、可写、用户态可用、访问过四个位,权限与状态全挤在这 8 字节里。
- 【64 位 CPU 就有 64 位地址】。x86-64 实际只用 48 位(后来扩到 57 位),高位是符号扩展。
自测三问(点开对答案)
1. 48 位地址、4 KB 页,页号和偏移各占多少位? —— **页号 36 位、偏移 12 位**。页大小 4 KB 就是 2 的 12 次方,**所以偏移固定 12 位。** 2. 单级页表为什么不可行? —— **要给整个虚拟地址空间建表:687 亿项 × 8 字节 = 512 GB,每个进程一份。**而绝大多数项永远是空的。 3. 页表项里的 P 位有什么特别? —— **它把【没分配】和【不在内存】统一成一个位**。**请求分页(5.13)和 Swap(5.15)全靠它。**- ☐ 5.6 实验
相关笔记
- 虚拟内存 — 翻译这件事
- 请求分页 — P 位的第一个用处
- 多级页表 — 512 GB 怎么被砍掉
- 🐧 Linux是怎样工作的 · 索引