大家好~可很多人对“内存映射”这四个字的理解,还停留在 mmap() 能把文件映射到内存。

(mmap内存映射原理)
实际上呢?
你在 Linux 里 malloc 的那一块内存、网卡 DMA 写进去的数据、NVMe SSD 直接提交的 IO、GPU 访问 CPU 内存、虚拟机里的 VFIO 直通、 容器跑 AI 推理时的 pinned memory,背后全是“映射”。

更准确地说,是地址翻译。
CPU 在翻,设备在翻, IOMMU 在翻,PCIe 设备甚至自己也在翻。
翻来翻去,最后大家都快翻吐了。

很多人第一次接触 MMU 的时候,会有一种特别诡异的感觉:“地址不就是地址吗?为什么还要翻译?”
哈哈。
这话就像问:“人不就是人吗?为什么还要身份证?”
因为世界太复杂了。
CPU 想要自己的秩序, 操作系统想保护内存, 虚拟机想隔离租户,设备想直接 DMA,GPU 想绕过 CPU。

于是乎,Linux 系统开始疯狂套娃:虚拟地址、物理地址、IO 虚拟地址、Guest Physical Address、Host Physical Address。最后大家看官方文档的时候,人已经麻了。
今天,咱们就按真实系统里的数据流来,把这层窗户纸捅破。
以前 32 位机器最经典的问题就是 4GB 内存天花板。
当时明明机器插了 8GB 内存,系统却只能认 4GB。
2^32 = 4GBCPU 地址线就这么宽,你还能咋办?
于是,问题就来了:进程怎么共享内存?怎么防止程序乱写?怎么让多个进程觉得自己独占全部内存?
这时候,MMU(Memory Management Unit,内存管理单元)被捣鼓出来了,其实本质就一句话:“CPU 看到的地址,不是真实地址。”
这事儿第一次听会非常反直觉。
你在代码里写:
int *p = malloc(4);
*p = 123;你以为 CPU 真访问了那个地址?
没有。CPU 看到的是虚拟地址(VA),真正落到 DRAM 上之前,还得经过 MMU 翻译成物理地址(PA)。
整个过程大概像这样:
CPU
↓
Virtual Address
↓
MMU
↓
Physical Address
↓
Memory那为什么非要搞这一层?
因为操作系统想骗人。
准确说,是想骗每个进程:“你拥有完整内存空间。”让每个进程都以为自己从 0x0 开始。
虚拟内存可以说是操作系统最大的骗局,刚毕业前几年,大概有一半面试会被问到:“malloc 之后真的分配内存了吗?”
很多人(包括当年的我)会脱口而出:“当然啊。”
但其实不是的,Linux 下,malloc 往往只是申请虚拟地址区间。真正物理页的分配,通常发生在第一次访问,也就是缺页异常(Page Fault)。
举个例子。
char *buf = malloc(1024 * 1024 * 1024);你申请了 1GB,系统马上给你 1GB 物理内存了吗?
想多了,服务器要真这么干,云厂商早赔死了。
操作系统只是:
“行,我先给你记账。”
直到你第一次写入数据:
buf[0] = 1;CPU 发现页表里没映射,于是触发 Page Fault,内核开始真正分配物理页、更新页表、刷新 TLB,最后程序继续运行。整个过程,其实是 CPU 和 OS 合伙演的一场戏。
页表这个东西,最早的时候,很简单,一个数组,虚拟页号对应物理页号。
但后来内存越来越大,页表也开始爆炸。假设 48 位虚拟地址、4KB 页
那么页数量:
2^48 / 2^12 = 2^36如果每个页表项 8 字节:
2^36 * 8 = 512GB光页表就 512GB。
还玩个锤子。
于是,多级页表来了。x86_64 最经典的是四级页表。
PGD
↓
PUD
↓
PMD
↓
PTE后来 Intel 又搞五级页表。
我刚学内核那会儿,满脑子全是:
pte_offset_map()pmd_offset()pud_offset(),感觉像在玩迷宫。
很多人研究页表研究半天,最后上线发现性能还是烂。
为什么?
因为真正影响性能的,是 TLB(Translation Lookaside Buffer)
本质上,它就是地址翻译缓存。
因为页表查找太慢。
CPU 不可能每次访存都走四级页表。
所以 MMU 会把最近翻译结果缓存到 TLB。
流程变成:
CPU 发起虚拟地址访问
↓
查 TLB
↓
命中 -> 直接得到物理地址
↓
未命中 -> Walk 页表问题来了。
TLB 特别小,比 L1 Cache 还珍贵。一旦 miss,性能直接雪崩。
我以前调优一个 Redis 实例,明明 CPU 利用率不高,吞吐就是上不去。后来 perf 一看,TLB-load-misses 爆炸。原因是业务搞了个超大的 hash table 随机访问,内存局部性烂穿,CPU 每次都得 page walk。最后只能上 HugePage(大页),TLB miss 立马下降,吞吐肉眼可见恢复。
大页HugePage 为什么能救命? 普通页是 4KB,HugePage 是 2MB 或 1GB。页变大,TLB 覆盖范围就变大。
举个例子。
TLB 假设只能存 1024 个条目。
如果是 4KB 页:
1024 * 4KB = 4MB只能覆盖 4MB。
但如果是 2MB HugePage:
1024 * 2MB = 2GB直接覆盖 2GB。
这差距就很离谱了。
现在的 AI 训练就特别喜欢 HugePage,因为大模型参数太大,TLB 根本扛不住,尤其 GPU Unified Memory 场景,页迁移频繁的时候,TLB shootdown 能把机器打哭。
不过,TLB 最大的问题还不是 miss,而是同步。
多核 CPU 下,每个核心都有自己的 TLB。假设一个页表改了,别的 CPU 核心怎么办?
必须通知它们“兄弟,缓存失效”。于是 IPI(处理器间中断)来了,操作系统开始跨核广播“flush TLB”。这玩意儿在高并发场景下特别恐怖。
CPU 解决完了,设备又不干了。网卡说“我要 DMA”,SSD 说“我要零拷贝”,GPU 说“我要直接读用户态内存”。设备 DMA 最大的问题是什么?它不经过 CPU,也就是说,它看不到 CPU 的虚拟地址空间,只能认物理地址。

于是,早期驱动开发特别痛苦。
驱动程序必须:
整个过程麻烦得要死,更致命的是安全性极差,设备如果 DMA 错地址,整个内存都能被写烂。
所以后来,IOMMU(输入输出内存管理单元)出现了。

IOMMU,本质上是给设备用的 MMU,这句话你一定记住。
MMU 给 CPU 翻译地址。
IOMMU 给设备翻译地址。
逻辑完全一样。

设备原本:
Device -> Physical Address有了 IOMMU:
Device
↓
IO Virtual Address
↓
IOMMU
↓
Physical Address是不是突然熟悉了?
没错,本质还是页表。
只不过对象从 CPU 变成了 PCIe Device。
在虚拟化时代,我们特别依赖 IOMMU。
以前裸机时代,设备 DMA 错了顶多系统 crash。但在云计算里,问题瞬间升级,如果一个租户的虚拟机直通了一块网卡,设备能随便 DMA,那它完全可以:
这已经不是 bug,这是灾难。
于是 Intel VT-d、AMD-Vi 开始普及,IOMMU 会给每个设备建立独立地址空间,比如:
NIC#1 -> 只能访问 VM1 内存
NIC#2 -> 只能访问 VM2 内存设备就像被关进笼子,你只能 DMA 自己那块区域。
VFIO 为什么离不开 IOMMU?
很多人第一次接触 VFIO,觉得特别神奇,虚拟机居然能直接用物理 GPU 且性能还几乎没损耗。其实关键就在 IOMMU。
VFIO 做的事情本质是把设备 DMA 权限限制在 Guest Memory,整个链路是套娃式的:
Guest VA
↓
Guest Page Table
↓
Guest PA
↓
Host IOMMU
↓
Host Physical Address看到没?Guest 以为自己看到的是物理地址,其实还是假的。虚拟化最牛逼的一点,就是所有人都被骗了,CPU 被骗,Guest OS 被骗,设备也被骗。
DMA 为什么会变成性能瓶颈?很多人以为 DMA 很快,其实只说对了一半。
DMA 的核心优势是不用 CPU 搬数据,但地址翻译本身也有成本。
设备 DMA 时,IOMMU 也得查页表。
于是,设备侧也开始有 TLB,也就是 IOTLB。
假设一块 100Gb 网卡每秒钟 DMA 几千万次,IOMMU 页表 walk 还能扛住吗?
根本扛不住。
于是大量 PCIe 设备开始卡在 IOTLB Miss。
于是大量 PCIe 设备开始卡在:
IOTLB Miss注意这个名字——IOTLB,本质就是 IOMMU 的 TLB。
AI 集群把 IOMMU 问题彻底放大,前几年很多公司做 AI 训练集群,发现 GPU 间通信延迟总是抖,尤其 RDMA 场景,后来一看,问题居然出在 IOMMU。GPU DMA 太猛了,尤其大规模参数同步时,如果 PCIe ATS 没开,IOTLB miss 爆炸,于是延迟像心电图。很多运维同学第一反应是:“是不是网卡坏了?”
结果不是,是地址翻译撑不住了。
这时候,ATS(Address Translation Service)来了。
这个东西看名字某种云服务,其实它是 PCIe 协议里的地址翻译机制。核心思想非常简单:“设备自己缓存地址翻译结果。” 听起来耳熟吗?对,跟 CPU TLB 一模一样。
以前设备 DMA 要问 IOMMU:
Device DMA
↓
IOMMU 查页表
↓
返回物理地址现在设备本地有 ATC(Address Translation Cache),命中直接 DMA。本质就是缓存,计算机世界很多高深技术扒开一看都是缓存:
Device
↓
ATC Cache
↓
命中直接 DMAATS 为什么意义巨大?
因为 PCIe 设备越来越聪明了。以前设备只是搬运工,现在 GPU 会页迁移,SmartNIC 会跑协议栈,DPU 会虚拟化,NVMe 会 peer-to-peer DMA。设备越来越像 CPU,那它就必须减少地址翻译开销。尤其 AI 时代,GPU 一秒几 TB 带宽,如果每次 DMA 都问 IOMMU:“哥,这地址对应哪?”,总线直接堵死。ATS 的意义其实就是让设备拥有部分 MMU 能力。
ATS 工作流程也很简单,设备第一次 DMA:
Device 发起地址翻译请求
↓
IOMMU 返回结果
↓
设备缓存到本地后续再访问同页:
直接命中结束。
本质就是缓存,计算机世界很多高深技术,扒开一看,都是缓存。
当然,ATS 带来了新问题:失效同步。缓存永远都有一致性问题。CPU TLB 有 shootdown,ATS 也一样。如果页表更新,设备缓存怎么办?必须 invalidation。PCIe 里专门有 Invalidate Request,设备收到后清掉对应 translation cache,否则设备可能 DMA 到错误地址,这问题非常严重。
5.1 PASID 又是什么鬼?
如果 ATS 只是缓存,那设备怎么知道“这是哪个进程的地址空间”?
于是,PASID(Process Address Space ID)来了。以前设备 DMA 默认只有一个地址空间,但现在 GPU、DPU、NVMe 都想直接访问用户态,于是设备必须支持多个进程、多个地址空间。
PASID 本质上,就是给地址空间编号。
设备 DMA 时:
VA + PASIDIOMMU 才知道该查哪个页表。
5.2 Shared Virtual Addressing
SVA(Shared Virtual Addressing)这个东西出来以后,CPU 和设备终于开始共享同一套虚拟地址。
以前 CPU 用 VA,设备用 IO VA,得来回映射。
现在 GPU 可以直接使用进程虚拟地址,比如 malloc(4096) 的 buffer,GPU 直接 DMA,不需要额外 pin,不需要 bounce buffer。
这就是现代异构计算的核心。
5.3 CUDA Unified Memory 背后其实全是 ATS
说实话,我第一次看见 CUDA Unified Memory 那会儿,真以为 N 卡开挂了——CPU 和 GPU 共用一个指针,都不用 cudaMemcpy 了。
看着神乎其神,其实背后全是IOMMU、ATS、PASID还有Page Fault Replay这一堆东西在背后协同干活。
简单说就是,GPU去访问某个地址的时候,如果对应的页不在自己本地显存里,就会触发页错误(fault)。这时候驱动就会自动把页面迁移过来,更新一下地址映射,完事儿GPU再接着执行。
这套东西第一次看真的会怀疑人生,因为 GPU 已经越来越像 CPU。
5.4 PRI
PRI(Page Request Interface)则是设备发现页不存在时,能主动请求 OS 补页。是不是越来越像 CPU page fault?没错,设备正在获得完整虚拟内存能力。
先看代码,Linux IOMMU 子系统核心逻辑之一:
int iommu_map(struct iommu_domain *domain,
unsigned long iova,
phys_addr_t paddr,
size_t size,
int prot)本质做什么?建立:
IOVA -> Physical Address设备 DMA 的时候,访问的是 IOVA,IOMMU 再翻译。这和 CPU 页表映射完全同构:
VA -> PA很多时候,理解系统最好的办法,不是背概念,而是找 "同构关系"。
这个问题特别经典。很多新手第一次跑 DPDK:
intel_iommu=off然后懵了。不是说 IOMMU 很安全吗?为什么关掉性能更高?
因为老设备不支持 ATS,DMA 每次都走 IOMMU,性能损耗明显。尤其早期 40G 网卡时代,IOTLB 压力非常大。所以很多公司直接关 IOMMU,用 hugepage + physical address DMA,暴力,但快。
不过现在情况开始变了,新一代 SmartNIC、GPU、NVMe 大量支持 ATS,IOMMU 开销正在被逐渐吃掉。
SR-IOV 很多人以为只是 "网卡虚拟化",其实底层核心还是映射。
每个 VF 都有独立 DMA、独立 Queue、独立中断、独立地址空间,最后靠 IOMMU 隔离。所以 SR-IOV 和 IOMMU 基本绑定,没有 IOMMU,很多 VFIO 场景根本不敢上线。
很多人调优只盯 CPU,其实 NUMA 才是真正的地狱。尤其双路服务器,内存不是统一的,每个 socket 有自己的 memory controller。
于是,地址翻译之后,还得决定访问哪块物理内存。跨 NUMA 节点访问,延迟直接上升。
AI 训练里最恐怖的场景之一:GPU 挂在 NUMA0,内存却分配在 NUMA1。最后 PCIe 流量绕远路,带宽直接腰斩。我见过一次,客户花几百万买 GPU,结果性能只有理论值 40%,最后发现只是 numa binding 没配,现场空气都安静了。
很多人低估 page walk 成本。一次 TLB miss,可能意味着:
现代 CPU 为了加速 page walk,甚至专门有 Page Walk Cache。你会发现,整个 CPU 架构越来越魔幻,缓存套缓存:
最后整个芯片,几乎全是缓存。
这几年 CXL 特别火,很多人觉得只是 "新总线",其实核心还是内存共享。CPU、GPU、FPGA、DPU 都想共享同一套 memory semantic,于是地址翻译复杂度再次爆炸。
CXL Type-3 Memory Device 本质上已经开始让外设变成 "内存扩展"。
那意味着什么?
意味着设备不再只是设备,而是系统内存的一部分。这时候 ATS、PASID、SVA 会越来越关键。
很多人学了几年内核,还是搞不懂内存映射,因为资料太碎了。一本书讲 MMU,一本书讲 PCIe,一本书讲虚拟化,没把整个链路串起来。
我以前带实习生,最喜欢让大家思考的一个问题:"GPU DMA 一个用户态 buffer,到底经过几次地址翻译?"
很多同学直接懵,其实这个问题,能一下看出你理解的是 API,还是系统。
咱们串一次完整链路:
用户态:
buf = malloc(4096);CPU 访问:
buf = malloc(4096);GPU DMA:
VA + PASID
↓
ATS Cache
↓ miss
IOMMU
↓
Host PA如果页不在:
Page Fault
↓
OS 补页
↓
更新页表
↓
Invalidate ATS
↓
GPU 重试看到没?现代系统已经复杂到设备和 CPU 在共同维护虚拟内存。
计算机发展到今天,本质还是在搬数据。大学那会儿,老师天天讲算法复杂度,以为 CPU 才是世界中心。工作十年后才发现,真正折磨的,往往不是计算,而是数据移动。
CPU 等内存,GPU 等 PCIe,网卡等 DMA,SSD 等 Queue。整个系统,都在等。而地址翻译,就是这场等待里的核心环节。
更新时间:2026-07-16
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight All Rights Reserved.
Powered By 61893.com 闽ICP备11008920号
闽公网安备35020302035593号