地址转换
复习
- 分段:按代码、数据和栈划分内存,以及外部碎片从何而来
- 页与分页:把虚拟内存和物理内存都切成固定大小的块
- 页表:记录虚拟页与物理页帧的对应关系
TL;DR
- 虚拟地址 = 页号 + 页内偏移
- 用页号查页表,得到物理页帧号
- 物理地址 = 页帧号 + 页内偏移(偏移原封不动)
- 这一整套换算,由 CPU 里的 MMU 硬件自动完成
正文
前面把工具都备齐了:分页、页表。现在把最关键的一步走通——一个虚拟地址,究竟怎样变成物理地址?
把地址拆成两段
还记得吗,虚拟地址可以拆成两部分:
虚拟地址 = [ 页号 ][ 页内偏移 ]
转换的过程,正好分工明确:
- 页号:拿去查页表,找出对应的物理页帧号
- 页内偏移:原样保留,不动
最后拼起来:
物理地址 = [ 物理页帧号 ][ 页内偏移 ]
为什么偏移可以不动?因为虚拟内存和物理内存切成了同样大小的块,页内的相对位置不会变——页里的第 12 个字节,搬到物理页帧后还是第 12 个字节。
走一个例子
假设页大小是 4 KB(页内偏移占 12 位),某进程的页表里记着“虚拟页 2 → 物理页帧 9”。
虚拟地址:页号 2,偏移 100
查页表: 2 → 页帧 9
物理地址:页帧 9,偏移 100
不管虚拟页 2 被放到了物理内存的哪里,只要页表记着它,就能找到;而偏移 100 从头到尾没变。
谁来做这件事
这么频繁的换算,当然不可能让程序自己做。CPU 里专门有个硬件负责它,叫内存管理单元(MMU,Memory Management Unit)。
它的工作就两件:
- 拿页号去查页表(页表的起始位置由内核通过一个寄存器告诉它)
- 拼出物理地址;如果发现页表说“这一页不合法”或“不在内存”,就触发异常交给内核
对程序来说,这一切都是透明的:它只管用自己的虚拟地址,完全不知道背后发生了查表和拼接。又是一层被藏起来的细节。
新问题:查表太慢
思路清楚了,可性能问题也冒了出来。每访问一次内存,都要先查一次页表——而页表本身也在内存里。于是一次内存访问,可能变成两次:先读页表,再读写真正的数据。
程序每执行一步都在访存,这样翻倍的开销谁受得了?下一章,我们给页表加一层缓存来解决它。
思考题
地址转换里,为什么页内偏移可以直接照搬,而页号必须查表?如果页大小不是 2 的整数次幂,这种“拆成两段”的办法还能用吗?
小结
知识点
- 虚拟地址拆成页号与页内偏移
- 页号查页表得页帧号,偏移保持不变
- 物理地址 = 页帧号 + 页内偏移
- MMU 负责整个换算过程
参考资料
- Wikipedia(zh):内存管理单元:MMU
- Wikipedia(zh):分页:地址转换
思考题答案(仅供参考)
页内偏移可以直接照搬,是因为虚拟内存和物理内存切成同样大的块,页内的相对位置不会改变;只要知道“这一页搬到哪个页帧”,页内第几个字节就还是第几个字节。页号则没有这个便利——每一页被放到哪个页帧是任意的,必须查表才知道。至于“拆成两段”,它依赖页大小是 2 的整数次幂,这样页号的位数和偏移的位数刚好是简单的二进制切分,用位运算就能拆。若页大小不是 2 的幂,拆分和拼接就会变复杂,所以现实中页大小几乎总是 2 的整数次幂。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪