4. 内存虚拟化:GVA到GPA再到HPA的地址转换、影子页表与EPT对比、大页内存配置

好,咱们进入内存虚拟化这个核心话题。说实话,搞虚拟化这么多年,我觉得内存这块是最容易出幺蛾子的地方。CPU虚拟化搞不好顶多是性能差点,内存要是搞不好,直接给你来个蓝屏或者虚拟机崩溃,那叫一个酸爽。

今天咱们就聊聊内存虚拟化里最绕不开的三个东西:地址怎么转的、影子页表和EPT到底选哪个、以及大页内存怎么配。嗯,我尽量用大白话讲清楚。

4.1 从GVA到HPA:一次地址的“三连跳”

先问个问题:虚拟机里的进程看到的地址,和物理机上的真实内存地址,是一回事吗?

当然不是。这里头其实经历了三次地址转换,我管它叫“三连跳”。

  • GVA(客户机虚拟地址):虚拟机里的进程看到的地址。比如你在VM里跑个MySQL,它以为自己独占4GB内存,其实那是假的。
  • GPA(客户机物理地址):虚拟机操作系统认为的“物理地址”。注意,这个“物理”是打引号的,它其实是Hypervisor给VM分配的一段假地址空间。
  • HPA(宿主机物理地址):真正的物理内存条上的地址。这个才是真家伙。

转换流程是这样的:

GVA → (客户机页表) → GPA → (Hypervisor页表) → HPA

说白了,就是虚拟机里的进程先通过自己的页表,把GVA转成GPA。然后Hypervisor再插一脚,把GPA转成真正的HPA。我在项目里遇到过新手问:“为什么我在虚拟机里看到的内存和宿主机对不上?”——对得上才怪,中间隔了一层呢。

关键点:每次内存访问,CPU都要做两次地址转换。如果处理不好,性能直接腰斩。

4.2 影子页表 vs EPT:新老两代技术

好,地址转换的流程清楚了。那问题来了:谁来干这个转换的活?

早期Intel处理器没有硬件辅助,全靠软件模拟。那时候用的就是影子页表

4.2.1 影子页表:软件硬扛

影子页表的思路很简单:Hypervisor自己维护一份“合并后的页表”,直接把GVA映射到HPA。虚拟机里的进程访问内存时,CPU查的是这份影子页表,一步到位。

听起来挺聪明对吧?但代价也大:

  • 每次虚拟机更新自己的页表(比如进程切换),Hypervisor都得拦截下来,同步更新影子页表。
  • 这导致VM exit频繁发生,性能损耗严重。

我记得以前做Xen的时候,一台物理机跑十几个VM,光页表同步就能吃掉30%的CPU。那时候我就在想,这玩意肯定不是长久之计。

避坑指南:我曾经在旧版KVM上遇到过一个问题——虚拟机频繁创建销毁线程,结果影子页表同步跟不上,直接导致宿主机OOM。后来升级到支持EPT的内核才解决。

4.2.2 EPT:硬件来帮忙

到了Intel Nehalem架构,EPT(Extended Page Tables)出现了。说白了,就是把地址转换的第二跳(GPA→HPA)交给硬件来做。

CPU里现在有两套页表:

  • 客户机页表:GVA→GPA,由VM自己管。
  • EPT页表:GPA→HPA,由Hypervisor管。

CPU一次硬件查两级页表,直接得到HPA。整个过程不需要VM exit,性能提升非常明显。

对比项 影子页表 EPT
实现方式 纯软件模拟 硬件辅助
VM exit频率 高(页表同步频繁) 低(只在EPT缺页时触发)
内存开销 每个VM一份影子页表 每个VM一份EPT页表
性能表现 内存密集型场景差 接近原生性能

我个人习惯,只要硬件支持EPT,坚决不用影子页表。你想想看,硬件能干的活,何必让软件去折腾?

小技巧:在ESXi里可以用 esxcli system settings kernel list 查看是否启用了EPT。如果没启用,先检查BIOS里VT-x和VT-d开了没。

4.3 大页内存配置:性能提升的捷径

好,地址转换和页表机制聊完了。接下来咱们说说怎么让内存访问更快——大页内存。

默认情况下,x86用4KB大小的页。一个进程如果占2GB内存,那页表里得有50多万条记录。TLB(快表)就那么点容量,根本装不下,频繁miss导致性能下降。

大页内存的思路很简单:把页变大。比如2MB甚至1GB。页表条目少了,TLB命中率自然就高了。

4.3.1 透明大页 vs 显式大页

Linux里配大页有两种方式:

  • 透明大页(THP):内核自动把连续的内存合并成大页。好处是省心,坏处是可能引起内存碎片,甚至导致某些应用卡顿。
  • 显式大页(HugeTLB):手动预留大页内存,应用通过mmap或共享内存来用。可控性强,性能更稳定。

我在生产环境里吃过透明大页的亏。有一次跑数据库,THP自动合并内存时,竟然把正在使用的页面也合并了,导致数据库响应延迟飙升。从那以后,我建议数据库类应用一律关掉THP,改用显式大页。

4.3.2 配置示例

显式大页的配置其实不复杂。以Linux为例:

# 预留1024个2MB的大页
echo 1024 > /proc/sys/vm/nr_hugepages

# 查看是否成功
grep HugePages_Total /proc/meminfo

然后在KVM里给虚拟机分配大页:

# 启动VM时指定内存后端为大页
qemu-system-x86_64 -m 2048 -mem-prealloc -mem-path /dev/hugepages

注意:大页内存一旦预留,其他进程就不能用了。所以要根据实际需求来配,别贪多。我一般预留物理内存的50%-70%给大页,剩下的给系统和其他进程。

4.3.3 性能实测

我之前做过一个测试:同样跑一个内存密集型的Java应用,用4KB页时TLB miss率是12%,换成2MB大页后直接降到0.3%。应用吞吐量提升了将近40%。

嗯,这个提升幅度,说实话我自己都吓了一跳。所以如果你在搞虚拟化,大页内存绝对是性价比最高的优化手段之一。

建议:在VMware vSphere里,可以在虚拟机的高级参数里设置 sched.mem.lpage.enable1GBPage = TRUE 来启用1GB大页。不过前提是CPU和操作系统都得支持。

小结

今天咱们把内存虚拟化的核心捋了一遍:

  • 地址转换的三层结构:GVA→GPA→HPA,每一层都有它的作用。
  • 影子页表是软件方案,EPT是硬件方案。有EPT就别用影子页表。
  • 大页内存能显著提升TLB命中率,但要注意透明大页的坑,生产环境推荐显式大页。

下一章咱们聊聊I/O虚拟化,那又是另一个有意思的话题。到时候见。