0%

操作系统虚拟化:虚拟机和容器

VMvare

大致总结

1. 虚拟机(Virtual Machine)的兴起与硬核实现

1.1 虚拟机的基本思想:一切皆状态机

课程从计算机系统的抽象视角出发,明确核心观点——“一切皆状态机(Everything is a State Machine)”。这一思想为虚拟化技术奠定了理论基础,具体内涵如下:

  • 计算机的执行状态由寄存器、内存、外设状态共同构成;
  • 虚拟机的本质是对另一台计算机状态机的完整复现与控制
  • 只要能够精确维护并推进该状态机,即可在单台物理机上实现“另一台计算机”的运行。

1.2 早期实现:软件模拟与性能瓶颈

1.2.1 纯软件模拟(Emulation)

早期虚拟机系统的核心实现方式为全软件模拟,典型代表包括教学场景或调试工具中的模拟器(如NEMU),其技术特征如下:

  • 客户机(Guest)的每条指令均由宿主机(Host)程序解释执行;
  • 需在软件层面完整模拟CPU、内存、I/O设备等计算机核心组件;
  • 具备极高的可移植性与执行可控性。
1.2.2 性能瓶颈与核心问题

纯软件模拟虽能满足功能需求,但存在不可忽视的性能缺陷:

  • 指令级的解释执行导致数量级的性能损失
  • 实际运行速度通常仅为原生系统的1%左右;
  • 难以适配通用操作系统或生产环境的性能需求。

这一现状引发关键思考:能否在不完全模拟硬件的前提下,实现接近原生系统的执行性能?

1.3 VMware 的关键突破:软件虚拟化

1.3.1 核心设计思想

1998年成立的VMware提出了革命性的工程实现方案,其核心思想跳出“全模拟”框架:让虚拟机中的大多数代码直接在物理CPU上运行,仅对关键环节进行干预。具体包括:

  • 在宿主操作系统内核中加载虚拟化驱动模块
  • 对客户机的执行环境进行精细化控制;
  • 仅在必要时(如特权操作执行)进行拦截与处理。
1.3.2 关键技术机制

VMware通过以下核心技术实现性能突破:

  1. 地址空间重映射
    • “悄悄”修改进程的物理内存映射关系;
    • 将虚拟机内的进程“迁移”至虚拟机外,以原生方式直接运行。
  2. 特权指令处理
    • 普通指令无需拦截,直接在物理CPU上执行;
    • 特权指令通过陷入(trap)或二进制翻译机制,交由虚拟机监控器(VMM)处理。
1.3.3 技术里程碑意义

该方案在无额外硬件支持的前提下,实现了三大突破:

  • 执行性能接近原生系统;
  • 实现对完整操作系统的透明虚拟化;
  • 推动虚拟机从“学术研究工具”走向“商业化可行产品”。

这一突破标志着虚拟化技术正式从学术领域迈向工业基础设施。

1.4 硬件虚拟化支持:Intel VT-x 与 EPT

1.4.1 VT-x:CPU级虚拟化原生支持

为进一步简化虚拟化实现、提升性能与稳定性,Intel推出VT-x(Virtualization Technology)技术,其核心特性如下:

  • 在CPU硬件层面引入Guest/Host双执行模式
  • 当客户机执行敏感指令或特权指令时,自动触发VM Exit机制;
  • 安全地将控制权转移至虚拟机监控器(Hypervisor)。

VT-x的技术优势体现在:

  • 减少复杂的二进制翻译流程;
  • 提升虚拟化系统的可靠性与可维护性;
  • 显著降低虚拟机监控器(VMM)的实现复杂度。
1.4.2 EPT:扩展页表(Extended Page Tables)

EPT技术针对性解决了虚拟内存虚拟化中的地址转换核心问题。在虚拟化场景中,地址转换需经历三级链路:
客户机虚拟地址(GVA)→ 客户机物理地址(GPA)→ 宿主机物理地址(HPA)

EPT的引入实现了关键优化:

  • 三级地址转换由硬件自动完成,无需软件干预;
  • 大幅减少页表维护的软件开销;

为什么虚拟机修改CR3或者访问内存需要VMM管理?不能直接由虚拟机的内核管理吗,即让虚拟机自己维护一个CR3,虚拟机内核自己计算出GPA后,(因为虚拟机本身就是主机的一个进程),可以将这个GPA视为一个普通的VA,利用主机的地址转换机制得到HPA?

总结来说,虚拟机不能直接管理 CR3 或自行完成地址转换,核心原因有三点:

  1. 权限与安全(隔离性)
    如果允许虚拟机内核直接控制 CR3 并在物理 CPU 上运行,它就能将虚拟地址映射到物理机内存的任何角落(包括宿主机内核或其他虚拟机的空间)。为了保证宿主机的安全,物理 CR3 的控制权必须由最高特权的 VMM (Root 模式) 掌握。
  2. 物理地址冲突(认知偏差)
    虚拟机内核认为自己拥有从零开始的连续“物理内存”(GPA),但实际上它只是宿主机进程里的一块虚拟空间。如果没有 VMM 介入,虚拟机算出来的地址(GPA)直接丢给内存总线,会指向错误的、甚至不存在的物理硬件。
  3. 硬件设计的局限(在 EPT 出现前)
    传统的 CPU 内存管理单元(MMU)是一次性映射的,它只认一套页表。它没有“先翻译一次(GVA→GPA),再把结果当成虚拟地址翻译第二次(GPA→HPA)”的功能。

演进结果

  • 早期方案(影子页表):强制拦截。由于硬件太“笨”,VMM 必须拦截所有 CR3 操作,手工合成一套“影子页表”来骗过 CPU,让 CPU 直接跨过 GPA 这一层。
  • 现代方案(EPT):硬件辅助。硬件在 MMU 里增加了第二层翻译逻辑。正如你所想,现在虚拟机确实可以自己管理一个“假 CR3”,而 CPU 硬件会自动在后台帮它完成第二步转换。

简而言之:不让它管是为了“防盗”(安全)和“防撞”(冲突),而现在能管是因为“硬件变聪明了”(EPT)。


在实现虚拟机时,请分别举一个不使用和使用intel VT-x的具体例子帮助我理解VT-x的作用;请举一个具体的使用扩展页表的例子帮助我理解扩展页表的作用?

实现虚拟机时,硬件辅助虚拟化技术(如 Intel VT-x)极大地简化了监控程序(VMM/Hypervisor)的设计,并提升了性能。

以下通过对比和具体场景为你解释这两个核心概念:

1. Intel VT-x 的作用:从“模拟器”到“直通车”

不使用 VT-x 的例子:二进制翻译 (Binary Translation)

在没有 VT-x 之前(如早期的 VMware 或现有的纯软件模拟器 QEMU),如果 Guest OS(虚拟机操作系统)想要执行一条特权指令(比如 LGDT 加载全局描述符表,或者修改控制寄存器 MOV CR3, EAX):

  1. 无法直接运行:Guest OS 运行在非特权级(Ring 3 或 Ring 1),直接执行这些指令会触发 CPU 异常或静默失败(Silent Failure)。
  2. 软件扫描与替换:VMM 必须像翻译员一样,在 Guest 代码运行前,逐行扫描二进制指令。
  3. 动态修补:发现特权指令后,VMM 将其“挖掉”,替换成一段跳转到 VMM 内部的模拟代码。
  4. 代价:这就像一边翻译外语一边看书,性能损耗巨大,且 VMM 逻辑极其复杂,必须处理指令集的各种边角案例。

使用 VT-x 的例子:硬件自动拦截 (VM-Exit)

有了 VT-x,CPU 引入了两种模式:Root 模式(VMM 运行)和 Non-Root 模式(Guest 运行)。

  1. 直接执行:Guest OS 可以直接在 CPU 上以原生的速度运行大部分指令。
  2. 精准捕获:当 Guest OS 执行修改 CR3 的指令时,硬件会自动触发一个 VM-Exit
  3. 硬件保存现场:CPU 自动根据 VMCS(虚拟机控制结构)保存当前 Guest 的所有寄存器状态,并跳回 VMM。
  4. VMM 处理:VMM 检查 VMCS 中的退出原因,帮 Guest 修改一下映射,然后执行 VMLAUNCH/VMRESUME 让 CPU 回到 Guest 继续运行。
  5. 作用:VT-x 让硬件承担了“监视和拦截”的工作,VMM 从“翻译员”变成了“管理员”。

2. 扩展页表 (EPT) 的作用:消除“影子”

在没有 EPT(Extended Page Tables)时,地址转换非常痛苦,因为存在两层映射

  • GVA (Guest 虚拟地址) GPA (Guest 物理地址)
  • GPA (Guest 物理地址) HPA (Host 物理地址)

具体例子:修改内存条目

假设你有一个虚拟机正在运行一个浏览器。浏览器申请了一块内存(GVA 为 0x1234),在 Guest 看来,这对应了它的内存条 0x5678 处(GPA)。但实际上,宿主机分配给这个飞地的真实内存是在 0x9ABC(HPA)。

没有 EPT 时:影子页表 (Shadow Page Tables)
  1. 同步负担:VMM 必须在内核中维护一套“影子页表”,直接存储 GVA HPA
  2. 频繁拦截:每当 Guest OS 修改自己的页表(比如进程切换修改 CR3),都会触发一次 VM-Exit。
  3. 计算量:VMM 必须手动遍历 Guest 的页表,计算出对应的 HPA,填入影子页表。如果 Guest 频繁切换进程,VMM 就会忙于更新影子页表,导致系统卡顿。
使用 EPT 时:硬件级二级跳

有了 EPT,CPU 内部集成了两套页表遍历引擎。

  1. 硬件自动翻译
  • 第一阶段:CPU 先按 Guest 的页表把 GVA 转为 GPA
  • 第二阶段:CPU 自动根据 VMM 设置好的 EPT 把 GPA 转为 HPA
  1. 零拦截:Guest OS 可以随意修改自己的页表(修改 CR3),而不会产生任何 VM-Exit。因为无论 Guest 怎么改第一级映射,第二级映射(EPT)是由宿主机掌控的,始终安全可靠。
  2. 作用:EPT 极大地减少了内存管理带来的上下文切换次数,虚拟机的内存性能几乎接近物理机。

总结

  • VT-x:解决了指令执行的权力和安全问题,变“软件翻译”为“硬件自动拦截”。
  • EPT:解决了内存翻译的效率问题,变“人工维护影子表”为“硬件二级映射”。

可以这样理解吗:无论是影子页表还是EPT,都是为了实现虚拟机以为自己有的从0开始的物理地址的幻想,从而定义了一些软件或硬件实现的映射规则?

你的理解完全正确,而且抓住了虚拟化的本质。

虚拟化技术的终极目标就是“欺骗”:让虚拟机里的内核觉得自己是这台电脑唯一的、至高无上的主人,拥有从 0 开始的完整内存。

你可以从以下两个维度来巩固这个“幻想”与“映射”的理解:

1. 幻想的内容是什么?

在计算机体系结构中,操作系统的“世界观”是基于地址空间的。

  • 虚拟机内核的幻想:“我拥有物理地址 到 。我可以随意把任何程序映射到这些地址上。”
  • 残酷的现实:它以为的 可能是宿主机内存里的 ;它以为连续的内存,在宿主机上可能是碎片化的,甚至被交换到了硬盘上。

2. 规则的两种实现路径

影子页表和 EPT 就是为了维护这个幻想而制定的两套“翻译规则”

影子页表 (Shadow Page Tables) —— “软件映射规则”

  • 原理:既然硬件(旧款 CPU)只认一张页表,VMM 就把虚拟机的幻想(GVA GPA)和宿主机的现实(GPA HPA)强行压缩成一张表(GVA HPA)。
  • 代价:虚拟机每想改一次自己的幻想(修改页表),都会被 VMM 拦截并重新计算压缩规则。这导致虚拟机“想得越多,卡得越凶”。

EPT (Extended Page Tables) —— “硬件映射规则”

  • 原理:硬件(新款 CPU)支持了两套并行的规则。
  • 第一套(虚拟机管):负责维护“幻想”(GVA GPA)。
  • 第二套(硬件+VMM管):负责把“幻想”落地到“现实”(GPA HPA)。
  • 优势:由于这两套规则在 CPU 内部是解耦的,虚拟机可以自由地在自己的幻想世界里折腾页表,而不需要惊动宿主机。硬件会在访问内存的一瞬间,自动完成这两步跳跃。

总结

不管是软件模拟还是硬件辅助,它们存在的意义都是解开“物理地址”这一层硬性耦合

如果没有这些规则,虚拟机就像一个试图在别人的领地上盖房子的违章建筑;有了这些规则,虚拟机就像是在一个平行宇宙里盖房子,它看到的土地(地址)是它的,但底层支撑的物质(内存)是由宿主机提供的。


Container

我们可以把容器的实现逻辑和本质归纳为一句话:

容器的本质是宿主机上一个(或一组)受到“视觉隔离”和“资源控制”的特殊进程。

以下是其实现的最终总结图景:


1. 容器实现的三大支柱(技术细节)

容器并不是一个真实的实体,而是由 Linux 内核的三项技术“拼凑”出来的:

  • 隔离(Namespaces)—— 虚拟出的“平行宇宙”
    它通过修改进程对系统资源的视图,实现隔离。每个容器都认为自己拥有独立的 PID 1、独立的网卡 IP 和独立的主机名。

    本质: 内核在处理系统调用时,根据进程所属的 Namespace 动态过滤数据。

  • 限制(Cgroups)—— 强加的“资源配额”
    它通过树状层级结构,管控进程组对 CPU、内存、磁盘 I/O 等物理资源的使用。

    本质: 内核调度器在分配资源前,先检查该进程所在 Cgroup 的“账本”是否超支。

  • 环境(Rootfs / Mount Namespace)—— 带来的“样板间”
    通过 pivot_root 指令,将进程的根目录切换到镜像文件所在的目录。

    本质: 进程虽然在宿主机运行,但它看到的 /bin/etc 全是镜像里的,从而实现环境一致性。


2. 容器究竟是什么?(本质定义)

要理解容器,必须跳出“它是一台小电脑”的错觉:

  1. 它就是进程
    在宿主机执行 ps -ef,你能直接看到容器里的进程。它和 vimls 等普通进程在内核调度层面没有区别,都共用同一个内核(Kernel)。
  2. 它是 Namespace 的集合
    容器并不对应一个单一的 OSID,而是对应一组 Namespace 对象的引用。凡是共享这一组引用的进程,就属于同一个容器。
  3. 它是“静态”与“动态”的结合
  • 静态(Image):是一堆只读的文件层,提供了程序运行的“土壤”。
  • 动态(Container):是基于镜像跑起来的、带了“面具”(Namespace)和“手铐”(Cgroup)的进程。

3. 终极比喻:进程的“楚门的世界”

  • 普通进程:生活在真实的城市里,能看到街道上的所有人,能去任何想去的地方(受限于基本权限)。
  • 容器进程:就像楚门(Truman)。导演(VMM/Docker)为他建造了一个巨大的摄影棚:
  • Namespace 是布景墙:让他以为这个小镇就是全世界,以为自己是这里的 1 号人物。
  • Cgroups 是剧组预算:规定他每天只能吃两顿饭,不能跑太快。
  • Rootfs 是道具:家里所有的家具(文件)都是剧组提前放好的样板。
  • 本质:楚门依然是一个活生生的人(进程),但他生活在一个被严格限制的、虚假的隔离环境里。

4. 总结对比:VM vs Container

维度 虚拟机 (VM) 容器 (Container)
本质 硬件级虚拟化 操作系统级虚拟化
隔离边界 整个硬件(通过 Hypervisor) 进程(通过内核 Namespaces)
启动速度 分钟级(需启动 Guest OS) 毫秒级(直接启动进程)
资源损耗 高(需运行完整内核 + EPT 映射) 极低(直接共享宿主机内核)
安全性 极强(硬件屏障) 较弱(共享内核,存在逃逸风险)

所以容器究竟对应的是进程还是进程组呢?

这是一个非常敏锐的问题。简单的回答是:容器在 Linux 内核眼中是一个或一组进程,但在管理层面,它通常被视为一个“进程组”。

为了彻底理清这个概念,我们需要从“初始进程”“Namespace 共享”两个角度来看:

1. 容器的起点:PID 1 进程

当你启动一个容器(例如 docker run nginx)时,内核实际上只启动了一个进程。

  • 这个进程就是容器的初始进程(Entrypoint)
  • 在该容器的 PID Namespace 内部,这个进程的 PID 是 1
  • 这个 PID 1 进程是整个容器的“根”。如果这个进程退出了,内核会认为容器已经停止,从而回收所有的 Namespace 和 Cgroup 资源。

2. 容器的扩展:进程的集合

虽然容器启动时只有一个进程,但这个进程可以产生子进程。

  • 比如,你在 Nginx 容器里执行 ls 命令,或者 Nginx 启动了多个 Worker 进程。
  • 这些子进程会继承父进程的 Namespace 和 Cgroup 限制。
  • 此时,这个容器就变成了一个进程组(这些进程共享同一套隔离环境和资源配额)。

3. 核心结论:用 Namespace 来定义边界

在 Linux 底层,并没有“容器”这个专门的数据结构。容器其实是“共享同一组 Namespace 的所有进程的集合”。

我们可以这样拆解:

  • 从内核数据结构看:它是多个 task_struct(进程结构体),它们的指针都指向了同一组 nsproxy(Namespace 代理对象)。
  • 从资源控制(Cgroup)看:它是一个 Control Group,里面记录了一连串的 PID。无论这个容器里运行了 1 个还是 100 个进程,它们产生的功耗都会累加在这个 Cgroup 账本上。

既然容器只是进程,为什么它能在 Windows 或 Mac 上运行?(提示:它们其实在偷偷跑一个极小的 Linux 内核)