🐧 第五章 内存管理
📄 一句话概括

程序看到的是连续地址,背后是散落的物理页。翻译时低 12 位不动,只换页号那一段——这就是映射的全部含义。

📝 本节和前面几节的关系

5.3 说明了【为什么必须绕过去】,这一节讲绕过去之后长什么样。下一节(5.5)讲那张翻译表本身。

一、一句话说清

每个进程都以为自己独占一整片连续的内存,从 0 号地址一直排上去。 实际上它的每一页被放在物理内存的任意位置,中间还隔着别人的页。 把【我以为的地址】翻译成【真实的地址】的那张表,叫页表。 所以虚拟内存不是【一块内存】,是【一层翻译】。

二、一个地址怎么变成物理地址

第一步:把地址切成两段。低 12 位是页内偏移(因为页是 4 KB,2 的 12 次方正好 4096),高位是页号。 第二步:拿页号去查页表,得到物理页号。 第三步:物理页号乘 4 KB,加上原来的偏移,就是物理地址。 关键在第三步:偏移一位都没变。变的只有页号——所以同一页内的相对位置永远不变。 这就是为什么程序不用关心自己被打散成什么样:它在页内怎么排,搬过去还是怎么排。

三、顺手解决的三件事

隔离:每个进程一张自己的页表,它翻不到别人的页。 共享:两张页表里都填同一个物理页号,两个进程就共享了同一页物理内存(5.11 的共享内存就是这么做的)。 保护:页表项里带权限位,只读页上写一个字节,CPU 直接抛异常——这是硬件拦的,绕不过去。 这三件事都不是【软件约定】,是【翻译本身的副产品】。

四、代价是什么

每一次访存都要先翻译一次——这是虚拟内存唯一的代价。 如果每次都去内存里查表,速度会跌一半以上(一次访存变两次)。 所以 CPU 里有一个专门的缓存叫 TLB,记住最近翻译过的页号(6.7 详述)。命中就不查表。 这就是【用一点点缓存,换掉绝大部分翻译开销】的经典设计。

五、动手 Demo:虚拟内存——连续的地址,散落的页

这一节手工搭一张四个页的页表,让四个连续的虚拟页落到四个不相邻的物理页上,然后亲手算一次地址翻译。

PAGE = 4096
mapping = [7, 2, 5, 1]      # 虚拟页 -> 物理页

addr = 0x00002A17
vpn = addr // PAGE
off = addr % PAGE
phys = mapping[vpn] * PAGE + off

print("虚拟地址", hex(addr))
print("页号", vpn, "偏移", off)
print("物理页", mapping[vpn])
print("物理地址", hex(phys))

实跑输出:

--------------------------------------------------------------------
虚拟内存——连续的地址,散落的页
--------------------------------------------------------------------
① 四个连续的虚拟页,落到四个不相邻的物理页

  虚拟页     虚拟地址范围              →     物理页     物理地址范围
  ----------------------------------------------------------------------------
  #0         0x00000000-0x00000FFF     →     #7         0x00007000-0x00007FFF
  #1         0x00001000-0x00001FFF     →     #2         0x00002000-0x00002FFF
  #2         0x00002000-0x00002FFF     →     #5         0x00005000-0x00005FFF
  #3         0x00003000-0x00003FFF     →     #1         0x00001000-0x00001FFF

  **左边一列是连着的,右边一列是散的。**
  **程序只看见左边**,所以它永远不用关心【我到底被放在哪块内存条上】。

② 一个地址怎么变成物理地址

  步骤                 算式                  结果
  ------------------------------------------------------
  给一个虚拟地址       —                     0x00002A17
  除以页大小取页号     10775 ÷ 4096          页号 2
  查页表               页号 2 → 物理页 5     物理页 #5
  余数当页内偏移       10775 mod 4096        偏移 2583
  拼出物理地址         5 × 4096 + 2583       0x00005A17

  **注意最后一行:虚拟地址和物理地址的低 12 位是一样的。**
  **变的只有页号那一段——这就是【映射】的全部含义。**

③ 顺带解决的三件事

  想要                     怎么做到
  --------------------------------------------------------
  进程之间互不可见         各自的页表,看不见对方的物理页
  两个进程共享一份代码     两个页表里都填同一个物理页号
  只读的代码真的改不了     页表项里带权限位,写就报错

注意结果:0x00002A17 变成了 0x00005A17。 低 12 位全是 0xA17,一点没变。变得只有高位那个 2 变成了 5。 这不是巧合,是设计:偏移不动,所以页内布局天然保留。 再看第一张表:左边虚拟页 0、1、2、3 连号,右边物理页 7、2、5、1 乱序——程序永远只看见左边那一列。

📋 Linux 上验证翻译对不对:pmap -x <pid> 会列出每一段的虚拟地址与对应的物理地址(需要权限),你会看到它们根本不连续。

30 秒实验

  1. pmap -x $$,看 shell 自己的地址空间分了几段。
  2. 把一个地址右移 12 位再左移 12 位,验证低 12 位被清零(这就是页对齐)。
  3. cat /proc/self/maps | head,看最前面几段的起始地址是不是 4 KB 对齐的。

常见误区

  1. 【虚拟内存就是把硬盘当内存用】。那是 Swap。虚拟内存是一层地址翻译,跟硬盘没关系。
  2. 【虚拟地址就是物理地址】。只在没有开 MMU 的单片机上成立。
  3. 【虚拟内存让程序变慢】。有 TLB 挡着,绝大多数访问和物理寻址一样快。
自测三问(点开对答案) 1. 地址翻译时哪一部分不变?为什么? —— **低 12 位(页内偏移)不变**,因为页大小 4 KB 固定,**同一页内的相对位置搬过去还是一样。** 2. 虚拟内存顺手解决了哪三件事? —— **隔离、共享、保护**。三者都是**翻译表可以各写各的**带来的副产品。 3. 虚拟内存唯一的代价是什么?靠什么补? —— **每次访存都要翻译一次**。靠 **TLB**——**CPU 里缓存最近翻译的页号,命中就不查表。**

相关笔记