Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

地址转换

复习

  • 分段:按代码、数据和栈划分内存,以及外部碎片从何而来
  • 页与分页:把虚拟内存和物理内存都切成固定大小的块
  • 页表:记录虚拟页与物理页帧的对应关系

TL;DR

  • 虚拟地址 = 页号 + 页内偏移
  • 用页号查页表,得到物理页帧号
  • 物理地址 = 页帧号 + 页内偏移(偏移原封不动)
  • 这一整套换算,由 CPU 里的 MMU 硬件自动完成

正文

  前面把工具都备齐了:分页、页表。现在把最关键的一步走通——一个虚拟地址,究竟怎样变成物理地址?

把地址拆成两段

  还记得吗,虚拟地址可以拆成两部分:

虚拟地址 = [ 页号 ][ 页内偏移 ]

  转换的过程,正好分工明确:

  1. 页号:拿去查页表,找出对应的物理页帧号
  2. 页内偏移:原样保留,不动

  最后拼起来:

物理地址 = [ 物理页帧号 ][ 页内偏移 ]

  为什么偏移可以不动?因为虚拟内存和物理内存切成了同样大小的块,页内的相对位置不会变——页里的第 12 个字节,搬到物理页帧后还是第 12 个字节。

走一个例子

  假设页大小是 4 KB(页内偏移占 12 位),某进程的页表里记着“虚拟页 2 → 物理页帧 9”。

虚拟地址:页号 2,偏移 100
查页表:  2 → 页帧 9
物理地址:页帧 9,偏移 100

  不管虚拟页 2 被放到了物理内存的哪里,只要页表记着它,就能找到;而偏移 100 从头到尾没变。

谁来做这件事

  这么频繁的换算,当然不可能让程序自己做。CPU 里专门有个硬件负责它,叫内存管理单元(MMU,Memory Management Unit)。

  它的工作就两件:

  • 拿页号去查页表(页表的起始位置由内核通过一个寄存器告诉它)
  • 拼出物理地址;如果发现页表说“这一页不合法”或“不在内存”,就触发异常交给内核

  对程序来说,这一切都是透明的:它只管用自己的虚拟地址,完全不知道背后发生了查表和拼接。又是一层被藏起来的细节。

新问题:查表太慢

  思路清楚了,可性能问题也冒了出来。每访问一次内存,都要先查一次页表——而页表本身也在内存里。于是一次内存访问,可能变成两次:先读页表,再读写真正的数据。

  程序每执行一步都在访存,这样翻倍的开销谁受得了?下一章,我们给页表加一层缓存来解决它。

思考题

  地址转换里,为什么页内偏移可以直接照搬,而页号必须查表?如果页大小不是 2 的整数次幂,这种“拆成两段”的办法还能用吗?

小结

知识点

  • 虚拟地址拆成页号与页内偏移
  • 页号查页表得页帧号,偏移保持不变
  • 物理地址 = 页帧号 + 页内偏移
  • MMU 负责整个换算过程

参考资料

  1. Wikipedia(zh):内存管理单元:MMU
  2. Wikipedia(zh):分页:地址转换

思考题答案(仅供参考)

  页内偏移可以直接照搬,是因为虚拟内存和物理内存切成同样大的块,页内的相对位置不会改变;只要知道“这一页搬到哪个页帧”,页内第几个字节就还是第几个字节。页号则没有这个便利——每一页被放到哪个页帧是任意的,必须查表才知道。至于“拆成两段”,它依赖页大小是 2 的整数次幂,这样页号的位数和偏移的位数刚好是简单的二进制切分,用位运算就能拆。若页大小不是 2 的幂,拆分和拼接就会变复杂,所以现实中页大小几乎总是 2 的整数次幂。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪