虚拟地址与物理地址:地址空间的概念、虚拟地址到物理地址的映射、页表的作用

好,咱们今天聊聊地址空间这回事。说实话,我刚入行那会儿,对虚拟地址和物理地址的理解也仅限于「一个给CPU看,一个给内存看」。直到我在一个嵌入式项目里,遇到了内存碎片导致系统崩溃的问题,才真正体会到——没有虚拟地址,现代操作系统根本跑不起来。

地址空间的概念

先说说物理地址。物理地址就是内存芯片上实实在在的地址。比如你的板子上焊了一颗512MB的DDR3,那物理地址范围就是0x00000000到0x1FFFFFFF。每个字节都有一个唯一的物理地址。

但问题来了——如果同时跑多个程序,每个程序都直接操作物理地址,会怎样?

  • 程序A 把数据写到了0x1000
  • 程序B 也把数据写到了0x1000
  • 两个程序互相覆盖,系统直接崩了

这就是我当年遇到的坑。一个跑着Linux的ARM板,两个用户态进程同时访问同一块物理内存,结果数据全乱了。后来查了半天,发现是MMU没配好,虚拟地址映射出了问题。

虚拟地址就是为了解决这个问题而生的。每个进程都有自己的虚拟地址空间,32位系统是4GB,64位系统更大。进程以为自己独占整个内存,实际上MMU在背后偷偷做转换。

核心概念:虚拟地址是进程看到的地址,物理地址是硬件实际访问的地址。MMU负责在两者之间做映射。

虚拟地址到物理地址的映射

映射过程,说白了就是查表。CPU发出一个虚拟地址,MMU拿到这个地址,去查页表,找到对应的物理地址,然后去内存里取数据。

我画了一张图,帮你理解这个过程:

虚拟地址到物理地址的映射流程 CPU 发出虚拟地址 虚拟地址 MMU 地址转换 查页表 页表 虚拟页号 → 物理页框号 权限、属性等 物理地址 物理内存 数据访问 虚拟地址 = 虚拟页号 + 页内偏移 物理地址 = 物理页框号 + 页内偏移 分页机制示例 虚拟地址: 0x1234 → 页号=1, 偏移=0x234 页表[1] → 物理页框号=0x5000 物理地址: 0x5000 + 0x234 = 0x5234 TLB加速 最近用过的页表项会缓存到TLB 命中TLB时,无需访问内存中的页表 TLB Miss → 走慢路径,查页表

嗯,这里要注意——页表不是一张大表,而是多级结构。为什么?因为4GB的虚拟地址空间,如果每页4KB,就需要1M个页表项。每个页表项4字节,光页表就要4MB。如果每个进程都搞一张4MB的表,内存根本扛不住。

多级页表就解决了这个问题。以ARMv8的4级页表为例:

级别 名称 粒度 覆盖范围
L0 PGD 512GB 整个地址空间
L1 PUD 1GB 大块区域
L2 PMD 2MB 中等区域
L3 PTE 4KB 基本页面

只有真正用到的虚拟地址范围,才会分配页表。没用到的区域,页表项直接标记为无效。这样内存占用就小多了。

页表的作用

页表不只是做地址转换。它还干了很多别的事:

  • 权限控制:读、写、执行权限,用户态/内核态隔离
  • 属性控制:缓存策略(Cacheable/Non-cacheable)、共享属性
  • 访问记录:脏位(Dirty bit)、访问位(Access bit)
  • 大页支持:2MB、1GB的大页,减少TLB Miss

个人经验:我在调试一个视频编解码驱动时,发现解码出来的图像总是花屏。查了两天,最后发现是页表里把DMA缓冲区的内存属性配成了Cacheable。CPU读的时候从Cache拿数据,DMA写的时候直接写内存,两边数据不一致。改成Non-cacheable就正常了。

页表项的结构,以ARMv8的L3页表项为例:

// ARMv8 L3页表项(4KB页面)
// 位域定义
Bit [1:0]  : 描述符类型 (0b11 = 页表项)
Bit [4:2]  : 属性索引 (Attridx)
Bit [5]    : 非安全 (NS)
Bit [6]    : 访问权限 (AP, 0=EL1可读写, 1=EL1只读)
Bit [7]    : 共享性 (SH, 0=非共享, 1=共享)
Bit [9:8]  : 访问权限扩展 (AP[2:1])
Bit [10]   : 非全局 (nG)
Bit [11]   : 访问位 (AF, 0=未访问, 1=已访问)
Bit [15:12]: 保留
Bit [50:12]: 物理地址[47:12] (物理页框号)
Bit [51]   : 连续位 (Contiguous)
Bit [52]   : PXN (特权执行永不)
Bit [53]   : UXN (用户执行永不)
Bit [54]   : 写入权限 (Write)
Bit [55]   : 读取权限 (Read)
Bit [63:56]: 保留

你看,一个64位的页表项,真正用来存物理地址的只有39位(50:12),剩下的全是控制信息。这就是为什么页表能同时完成地址转换和权限管理。

避坑指南:我曾经在移植Linux内核时,忘了配置页表的共享属性。结果两个CPU核访问同一块内存,一个改了数据,另一个看不到。这个问题极其隐蔽,因为单核跑的时候一切正常,双核就出问题。最后用DSB指令和共享属性才搞定。

页表的另一个重要作用是支持虚拟内存。当物理内存不够时,操作系统可以把不常用的页面换到磁盘上。页表项里有一个Present位,如果为0,说明页面不在内存中。CPU访问这个页面时,会触发缺页异常,操作系统再把数据从磁盘加载回来。

这个过程对应用程序是完全透明的。程序只管读写虚拟地址,剩下的MMU和操作系统帮你搞定。

总结一下今天的内容:

  • 地址空间:虚拟地址让每个进程拥有独立的地址空间,互不干扰
  • 地址映射:MMU通过查页表,把虚拟地址转换成物理地址
  • 页表作用:不只是地址转换,还管权限、缓存策略、缺页处理

说实话,地址映射这块内容,光看书很难真正理解。我建议你找个开发板,自己写个简单的页表配置代码,跑几个测试用例。踩过坑之后,这些概念就刻在脑子里了。

专注资料整理