地址映射基础:物理地址、虚拟地址、总线地址的区别;MMU与MPU的作用

刚入行那会儿,我总把物理地址、虚拟地址、总线地址搅成一锅粥。后来在调试一个PCIe设备时,因为地址理解错位,整整耗了两天才找出问题。说白了,三种地址服务的对象不同,但做XIP映射时必须把它们的转换路径刻在脑子里。今天咱们就把这件事理清楚。

物理地址、虚拟地址、总线地址——三个不同的世界

物理地址(Physical Address, PA)是CPU核心看到的真实地址总线上的值。我习惯把它想象成“内存条上具体的哪个字节”。处理器发出的访存请求,最终地址译码器就是拿这个去选中内存颗粒或外设寄存器。

虚拟地址(Virtual Address, VA)则是程序员眼里看到的地址空间。每个进程都以为自己占用了4GB(32位)或更大连续空间,其实这全是MMU变出来的魔术。实际读写时,MMU会把VA页表一步步翻译成PA。

总线地址(Bus Address, BA)最容易让人迷糊——它其实是设备端视角的地址。举个例子,DMA控制器看到的地址不是CPU的PA,而是总线域内的地址。PCIe设备的BAR空间就工作在总线地址域,CPU要通过PCIe Bridge做地址映射才能访问它。

我的避坑经验:在ARM系统中,我经常遇到一个误区:认为CPU读到设备寄存器用的是PA。其实CPU发出的地址是VA,经过MMU才变PA,PA再通过系统总线走到IO空间,中间还可能跨过IOMMU/SMMU。如果你在做XIP(eXecute In Place)映射,务必搞清楚你的代码是跑在哪个地址域——直接映射到Flash还是先经过MMU?我当初就因为忽略了SMMU的地址转换,导致XIP代码在虚拟机里死活跑不起来。

三种地址的核心区别一览

地址类型 使用者 典型宽位 主要特点
虚拟地址 (VA) 应用程序、CPU指令 32/48/64位 连续、独立、受MMU保护
物理地址 (PA) 内存控制器、Cache 32/36/48位 真实硬件引脚、非连续
总线地址 (BA) 设备DMA、PCIe桥 32/40/64位 设备域、可能经过IOMMU转换

这三者之间通常不是一对一关系。比如VA 0x1000可能被MMU映射到PA 0x8000,而同一PA在总线域下又可能被IOMMU另映射到BA 0xC000。XIP映射的一致性必须贯穿这三个域,否则CPU执行一条指令读到的是旧数据。

MMU——虚拟地址到物理地址的“翻译官”

MMU(Memory Management Unit)是CPU内部的核心硬件模块。它的主要职责就两个:地址翻译和访问权限检查。每个进程切换时,操作系统会更新页表基址寄存器(比如ARM的TTBR0_EL1),MMU自动根据页表完成VA→PA的查表。

翻译速度很关键,TLB(Translation Lookaside Buffer)为此而生。我记得在优化一个嵌入式导航仪项目时,发现XIP代码因为频繁的TLB miss,执行性能比SRAM差了一倍。后来我强制把XIP区域设为大页(2MB页),TLB命中率直接拉到99%。

MMU的关键能力:

  • 支持多种页大小(4KB/64KB/2MB/1GB)
  • 提供缓存属性控制(Normal/Device/Strongly Ordered)
  • 区分特权级(EL0-EL3)
  • 捕获非法访问触发Page Fault

MPU——没有MMU时的“保底方案”

MPU(Memory Protection Unit)常出现在Cortex-M系列或R系列处理器中。它没有页表,也没有TLB,只靠一组预先配置的区域寄存器(Region)来定义地址空间属性。每个区域包含基地址、大小、访问权限和缓存策略。

换句话说,MPU不做地址翻译。地址空间始终是物理的。我头疼的是,很多工程师把MPU当成“精简版MMU”,结果在配置区域重叠时出了大问题。比如你设了Region0覆盖0x0000-0xFFFF,Region1覆盖0x0000-0x7FFF,MPU的优先级规则是区域编号越小优先级越高。我以前在某一款车规芯片上踩过这个坑——因为区域配置反了,导致XIP的Flash区域被当成SRAM区域执行,数据一致性问题查了三天。

警告:使用MPU时,区域边界必须对齐到区域支持的最小粒度(通常32字节)。未对齐的配置会导致硬件异常或者直接忽略。在我经手的项目中,至少有两成的MPU故障是由地址对齐引起的。

地址映射的灵魂三问:为何需要MMU/MPU?

  1. 隔离:让每个进程只能访问自己的空间。MMU通过页表权限位,阻止恶意代码读写内核或其它进程的数据。
  2. 简化编程模型:程序员只需关心连续的虚拟地址,不需要打理物理内存碎片。这在大内存场景下特别爽。
  3. 缓存与排序控制:XIP映射最头疼的是指令Cache和DMA的冲突。MMU/MPU可以对每个页面设置Shareable、Cacheable等属性。我习惯把XIP区域设成Normal Inner Write-Back Outer Write-Back,而与DMA共享的设成Non-Cacheable。

为什么会需要MMU/MPU同时存在?有些系统(比如带Linux和RTOS的AMP架构)会在一个CPU核上跑MMU(Linux端),另一个核上跑MPU(裸机端)。地址映射策略必须各自独立设计,但共享内存区域需要两边的MMU/MPU属性一致,否则Cache一致性协议会乱掉。

一张图看懂地址映射框架

下面这张SVG图我按照自己实战总结的“三域两关键”来画的。你可以看到进程发出VA,经过MMU(及TLB)变成PA,PA再通过系统总线送到内存或目标设备;而设备DMA发起BA访问时,IOMMU(SMMU)会把BA翻译回PA,与CPU的Cache做一致性同步。

三域地址映射与XIP一致性的关键路径 CPU核心 / 进程 发出VA请求 MMU 页表 + TLB VA → PA 物理内存 / Flash PA域 设备 / DMA 发出BA请求 IOMMU / SMMU BA → PA 一致性协议 XIP关键点 VA/BA/PA三域 属性必须一致 VA PA BA PA或IO地址 一致性协议 (snoop)

这张图里,紫色框中MMU负责VA→PA,青色框中IOMMU负责BA→PA。绿色虚线代表两者之间需要传递一致性的监听流量。只要一个环节的属性配置错了,XIP就可能读到过时的指令缓存,或者设备写入的数据无法被CPU看到。

实践建议:配置MMU/MPU时的检查清单

  1. 分清目标设备——哪些是普通内存,哪些是非易失存储器(Flash/EEPROM),哪些是外设寄存器。XIP一般只对Flash类存储器有效。
  2. 设置正确的Cache策略——XIP代码段建议Cacheable + Write-Back,但是如果该区域同时会被DMA写,就必须改成Non-Cacheable或强制Cache刷新。
  3. 地址对齐检查——MMU页表条目必须按页边界对齐,MPU区域必须按粒度对齐。我习惯写一个脚本在编译后检查映射表的对齐性。
  4. 多核一致性——如果两个核通过MMU映射到同一块物理地址,必须配置成相同Shareability属性(Inner Shareable或Outer Shareable),否则硬件自动干预会失效。
  5. 启动阶段初始设置——启动时MMU通常是关闭的,CPU直接使用PA。在使能MMU之前,我建议先把XIP区域的页表提前填好,并且将指令Cache invalidate一遍,避免乱序执行带来的脏数据。

再次提醒:对于XIP场景,最隐蔽的问题是总线地址与物理地址映射不一致。比如PCIe设备BAR空间是256MB,但是CPU物理地址只映射了128MB,越界访问会导致Unsupported Request。我曾经在调试一款FPGA加速卡时,因为忘记调整PCIe地址窗口,XIP启动一直卡住,最后用逻辑分析仪抓到地址越界。血泪教训啊。