1. EPT到底是什么:解决内存虚拟化“翻译”老大难
1.1 虚拟化里的“地址翻译”问题是哪来的
做过KVM虚拟化或者看过《系统虚拟化》这类书的朋友,应该都体会过:内存虚拟化是整个虚拟化里最绕、也最影响性能的一块。原因不复杂——虚拟机里跑的是客户机操作系统,客户机内核管理的是“客户机物理地址”(Guest Physical Address,GPA),但真正落到硬件上的必须是宿主机物理地址(Host Physical Address,HPA)。于是每次内存访问,都等于要经过两层“翻译”:客户机虚拟地址(GVA)先按客户机的页表转成GPA,然后GPA还要再转成HPA。
这个“GPA到HPA”的转换,就是扩展页表(Extended Page Tables,EPT)要解决的问题。
在Intel没有把EPT做进硬件之前,虚拟化平台普遍用的是影子页表方案。所谓影子页表,说白了就是VMM(虚拟机监视器)替客户机维护一份“已翻译好”的页表,直接让硬件去查。这个方案的思路很直接,但代价极其沉重:客户机里每次发生页表更新,比如进程创建、内存映射、mmap、缺页处理,VMM都必须同步去更新影子页表。一次更新操作可能触发几十次VM-Exit,也就是CPU从客户机模式切回根模式让VMM处理,处理完再切回去。如果工作负载是内存密集型,比如Redis、数据库、JVM应用,频繁的上下文切换会直接把性能拖垮。
我当时在一台双路服务器上跑Java微服务压测,开虚机之后吞吐量只有裸机的六成左右,用perf一看,VM-Exit次数高得离谱,八成以上的退出原因都是页表相关的影子页表维护操作。那个年代做虚拟化优化,基本就是在跟VM-Exit数量作斗争。
1.2 影子页表的痛点与EPT的思路
影子页表的问题可以归结为两点:一是维护成本高,客户机每动一次页表,VMM都要跟着动;二是内存成本高,每跑一个进程上下文,VMM都可能需要为它准备一份对应的影子页表。
EPT的思路简单说就是:既然客户机的地址空间和宿主机的地址空间是两层,那索性在硬件里搞两张页表,各查各的。客户机虚拟地址到客户机物理地址,走客户机自己的页表;客户机物理地址到宿主机物理地址,走VMM维护的EPT。CPU在硬件层面自动把两段翻译串起来完成最终访问,完全不需要为每次页表更新都切回根模式。
AMD那边对应的方案叫NPT(Nested Page Tables),也叫RVI(Rapid Virtualization Indexing),思路和EPT类似。两家的实现细节不一样,但解决的都是同一个核心痛点。这也是我为什么一直跟团队里新来的同学强调:别把EPT当成一个“功能开关”,它的本质是硬件帮你把内存虚拟化的翻译路径从“软件模拟”变成了“硬件流水线”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. EPT的工作原理与硬件细节
2.1 两阶段转换:GVA、GPA、HPA怎么串起来
先看一条完整的内存访问路径。
虚拟机里的进程访问一个虚拟地址,CPU先根据客户机CR3指向的页表,把GVA翻译成GPA。这一层和普通操作系统里的虚拟地址到物理地址翻译一模一样,客户机内核完全无感,它以为自己操作的就是真实物理内存。
关键在第二步:CPU拿到GPA之后,并不会直接拿着这个地址去访问内存,而是会再去查一张VMM维护的EPT页表,把GPA翻译成HPA。只有拿到了HPA,内存总线上的访问才开始真正执行。
所以整条路径是:
GVA →(客户机页表)→ GPA →(EPT页表)→ HPA
Intel手册里把这两段分别称为阶段1翻译(GVA到GPA)和阶段2翻译(GPA到HPA),合起来叫嵌套分页或两阶段地址转换。硬件会自动完成两段页表的遍历,对客户机软件完全透明。
这里面有个很重要的点:客户机的页表本身也是存放在内存里的,而这些“存放页表的内存”对应的GPA
