虚拟化可以通过许多不同的方式来实现。它可以有硬件支持,也可以没有硬件支持。虚拟化操作系统可以预期会被修改以准备虚拟化,或者可以预期在不修改的情况下工作。无论如何,软件开发者必须努力实现 Gerald Popek 和 Robert Goldberg 提出的虚拟化的三个目标:保真度、性能和安全性。1
我们可以为每个目标做出妥协。例如,在某些情况下,人们可以接受牺牲一些性能。事实上,这几乎总是性能的必然结果:与在裸机上执行操作系统相比,执行虚拟化操作系统需要付出更多努力,并且必须为此付出代价。
本文是关于开发者在处理各种类型的虚拟化时必须意识到的折衷。忽视这些问题可能意味着程序的执行速度会不必要地降低。即使虚拟化的保真度达到 100%,也可能导致故障。块和网络 I/O 的虚拟化影响已在本期 Queue 的其他地方讨论,因此本文不专门讨论这些内容。这里只讨论相关的 DMA(直接内存访问)主题。
首先,我们应该了解一些关于常用虚拟化技术的细节。
在处理器和芯片组获得硬件虚拟化支持之前,商品硬件上的虚拟化实现就已经被开发出来。硬件的现有功能允许虚拟化操作系统的实现,但代价是
这种类型的虚拟化——半虚拟化——很受欢迎,但开发者需要了解它的缺陷,以避免主要的性能损失。尽管存在这些性能损失,虚拟化还是实现了某些技术,例如迁移和检查点。由于在执行的操作系统下方有一段软件——VMM(虚拟机监视器)或 hypervisor——控制着操作系统域的执行,因此可以简单地停止执行它们。VMM 还知道分配给已执行域的所有资源。这意味着它可以存储所有这些信息,并在以后恢复执行,就像什么都没发生过一样。
使用几乎相同的技术,也可以在不同的机器上恢复执行。这种域迁移为系统架构师的武器库增加了一种强大的武器,因为它可以用来提高系统利用率和可用性。
然而,如果机器网络是异构的,迁移就会成为一个问题。如果操作系统和/或在其上运行的应用程序依赖于 CPU 功能,则迁移的域必须依赖并仅使用所有机器上都可用的功能。
为了安全地实现虚拟化,在虚拟机上运行的各个域必须隔离。隔离的程度取决于用例。
至少,各个域必须不能相互崩溃(即,一个虚拟机中的错误或恶意攻击不得对其他域产生影响)。因此,VMM 集中控制内存等硬件资源至关重要,并且各个域只能修改分配给它们的内存。最终,这意味着不能授予各个域对实际物理内存的管理权限。
这对其他硬件设备也是如此。多个域直接访问硬盘驱动器或 NIC(网络接口卡)是一个安全问题。因此,在许多情况下,域无法访问该硬件。相反,这些域可以访问由 VMM 实现的虚拟设备。数据从真实的底层硬件设备间接传输到域。
在某些情况下,这种间接性是不可接受的。对于服务器而言,硬盘驱动器和网络带宽以及延迟至关重要。虚拟设备实现会妨碍这两者。对 3D 显卡的支持至少同样复杂。因此,域会获得专用的硬件设备,然后它们可以自行控制这些设备。为了提高效率,单个硬件设备可以将其自身表示为 VMM 和域的多个设备。在这种情况下,虚拟化的安全方面被下推到硬件(或直接在硬件上运行的固件)。
然而,对于内存,硬件级别的这种分离尚未被提出,并且不太可能永远被提出,因为共享内存优化仍然有用且应用广泛。
VMM 是一段独立于在域中运行的操作系统内核的软件。因此,从客户操作系统内核中使用 VMM 中的功能需要一种以前不存在的执行转换。这些 VMM 进入和退出操作可以通过各种方式实现。在半虚拟化内核中,它可能是一个专门的跳转,而在硬件辅助虚拟化环境中,它是一组新的指令。无论采用哪种方法,都有一个共同因素:进入和退出都不便宜。
在进入 VMM 时,CPU 必须被置于更特权的状态。必须保存和恢复大量的内部状态。这可能会非常耗费资源,并且与系统调用情况一样,一些 CPU 缓存的内容会丢失。可以玩一些技巧来避免显式刷新缓存(即使是那些使用虚拟地址的缓存),但是 VMM 代码需要缓存行来存储其代码和数据,这意味着一些旧的条目将被逐出。
这些缓存效应不容低估。VMM 调用可能很频繁,此时成本是可衡量的。调用通常是隐式发生的,由内存处理触发。
出于这个原因,今天的某些处理器(以及未来可能所有的处理器)都具有避免某些缓存刷新的方法。在 VMM 进入和退出方面,最关键性能的缓存是 TLB(转换后备缓冲区)。此缓存存储从虚拟地址转换而来的物理地址。这些转换可能非常耗费资源,并且缓存使用在不同客户域之间共享的虚拟地址进行索引。
如果不采用一些技巧,则在每次进入或退出 VMM 时都必须刷新 TLB 缓存。可以通过扩展缓存的索引来避免这种情况:除了虚拟地址之外,还使用一个对每个客户域和 VMM 唯一的令牌(TLB 被标记)。这样,就不必刷新 TLB 缓存,并且进入/退出 VMM 不会自动导致 TLB 未命中。
如前所述,对物理内存的访问必须限制为 VMM。没有迹象表明即将出现对内存分区的直接硬件支持。
这意味着客户域操作系统内核只能访问虚拟化的物理内存,并且用于实现虚拟内存的处理器功能被重用来实现第二级虚拟化。对于半虚拟化内核,这意味着使用访问权限的环形结构(至少在 x86 和 x86-64 处理器中实现)。只有 VMM 具有环形级别零的访问权限(这意味着对物理内存的完全访问权限)。客户域操作系统内核仅具有环形级别一的访问权限,并且仅限于 VMM 显式分配给它们的内存页。客户域操作系统内核反过来可以细分内存,使其在更高的环形级别(通常为三)对各个进程可用。
对于硬件辅助虚拟化,处理器实现了一个或多个额外的环,VMM 可以使用这些环来虚拟化内存。客户域操作系统内核像往常一样运行,像往常一样使用环,但是它们用于环零访问的内存实际上仅限于 VMM 分配给它们的内存。因此,这不是真正的环零内存访问。
复杂性并没有随着环的使用而停止。内存使用量随着系统的正常运行时间而变化。如果为每个域分配特定数量的物理内存,然后启动使用它的进程,事情就会很简单。一旦所有页表都设置好,内存处理将是静态的。然而,这与大多数系统的实际情况相去甚远。
实际情况是,应该根据域当前和近期未来的需求为其分配内存。这使得虚拟化环境能够更高效地运行,因为更少的资源处于未使用状态。这类似于虚拟内存处理中的过度提交情况。此外,客户域操作系统内核会创建新的进程,这些进程必须获得自己的页表树。这些树必须为 VMM 所知。
对于半虚拟化操作系统内核,需要 VMM 协作的内存操作作为对 VMM 的显式请求来执行。在硬件辅助虚拟化中,情况更加复杂:VMM 必须猜测客户域操作系统内核执行的操作并模仿该操作。VMM 必须维护所谓的影子页表,它是 CPU 实际使用的页表,而不是 CPU 自己的页表。
这两种方法都会产生巨大的成本。客户域操作系统内核的每个处理内存分配的操作都会变得明显更慢。最新版本的 x86 和 x86-64 处理器包含更多对内存的硬件支持,这通过在硬件中实现虚拟物理内存来减少管理开销。这种方法仍然涉及两组页表,但它们是互补的。客户虚拟地址首先被转换为主机虚拟地址,然后主机虚拟地址又被转换为真实的物理地址。图 1 中的示意图显示了两个页表树如何协同工作。(有关该图的详细说明,请参阅我的文章《每个程序员都应该了解的内存》。2)这意味着地址转换变得更加复杂和缓慢。
一个微基准测试比千言万语更能显示所产生的性能损失。以下程序是一个简单的指针追踪程序,它使用不同的总内存量
struct list {
struct list *n;
long pad[NPAD];
};
void chase(long n) {
l = start_of_list;
while (n->0)l = l->n;
}
该程序测量沿着指针跟踪闭合列表所花费的时间。(关于该程序、数据结构、可以用它收集的数据以及其他与内存相关的信息,可以在我之前提到的文章中找到)。结果如图 2 所示,该图显示了相同的程序在虚拟客户域中运行时与在没有虚拟化的情况下运行代码相比所产生的速度减慢。
每个图都有两个不同的区域:当处理器的数据缓存足以容纳程序所需的数据时,速度减慢接近于零;但是一旦数据不适合数据缓存,速度减慢就会迅速增加。不同的处理器对情况的处理方式也不同。在英特尔处理器上,虚拟化在性能方面大约损失 17%,而在 AMD 处理器上大约损失 38%。 (在这种特定情况下,AMD 处理器的成本较高可能是由于所用处理器的特定版本造成的。在大多数情况下,差异可能不会那么大。)这些都是重要的数字,可以在具有大内存需求的应用程序的现实生活中衡量。这应该让开发者警醒。
此时,值得进一步了解客户域中的内存访问。我们在这里区分两种情况:影子页表的使用和虚拟物理内存的使用。需要关注的重要事件是 TLB 未命中和页错误。
使用影子页表,处理器实际使用的页表树是由 VMM 维护的。每当发生 TLB 未命中时,就会使用此页表树来解决缓存未命中并计算物理地址。这需要三到五个内存访问,具体取决于平台。当发生页错误时,会调用客户域操作系统内核。它确定要执行的操作。在半虚拟化内核的情况下,它将调用 VMM 以设置新页面的映射。在硬件辅助虚拟化的情况下,内核会修改自己的页表树。但是,此更改对处理器不可见。当程序继续时,它将再次出错。这次,VMM 注意到其页表与客户域操作系统内核的页表不同步,并进行适当的更改以反映该内核中的更改。
如果处理器支持虚拟物理内存,则 TLB 未命中会变得更加复杂。现在我们有两棵页表树要处理。首先,程序中的虚拟地址使用客户域操作系统内核的页表树转换为虚拟物理地址。然后,必须使用 VMM 的页表树将此地址转换为真实的虚拟地址。这意味着该过程现在可能需要比以前多达两倍的内存访问。这种复杂性通过 VMM 中简化的内存处理来弥补。不再需要影子页表。客户域操作系统内核在响应页错误而被调用时,会修改其页表树并继续执行程序。由于其处理器使用刚刚修改的树,因此不需要进一步的步骤。
虚拟物理内存的实现带来了一些更多的问题。第一个问题主要与 I/O 设备的使用有关,但也为用户级应用程序所感受到。为了实现高效的 I/O,大多数现代设备在传输数据时会绕过 CPU,并直接从内存读取数据或向内存写入数据。为了保持设备和操作系统实现的简单性,大多数情况下的 I/O 设备都不知道虚拟内存。这意味着在 I/O 请求期间,使用的内存区域必须是固定的(锁定是操作系统开发人员的行话),并且此时不能用于任何其他目的。
这些设备也不知道任何关于虚拟化的信息。这意味着内存的锁定必须扩展到客户物理内存实现之外。正确地实现这一点意味着额外的工作和成本。
如前所述,内存锁定问题并非完全是操作系统内核的问题。用户级应用程序可以请求锁定内存(这是同一个概念的另一个术语,这次源于 POSIX),以便没有虚拟内存实现伪像会影响该内存区域的使用。此锁定是一项特权操作,因为它消耗宝贵的系统资源。因此,在用户级代码中锁定内存需要在操作系统内核和 VMM 中获得适当的支持,以便有权执行该操作,并且系统在客户域操作系统内核和 VMM 中都有足够的资源来遵守。实时、低延迟和高安全性编程——内存锁定主要用于这三种情况——在虚拟化环境中要困难得多。
一个密切相关的问题是,客户域操作系统内核需要控制物理内存,以提供对大内存页面的支持。大内存页面是某些操作系统提供的一种优化,用于减少使用大量内存时内存访问的成本。我们不会深入探讨细节(请参阅我之前提到的文章),但足以知道速度提升可能很显着,并且优化是基于通过将大量正常的、小内存页面视为一个大页面来降低 TLB 未命中率。
由于之前解释的原因,这种优化在虚拟环境中甚至更重要,因为 TLB 未命中的成本更高。它要求组成大页面的页面在物理内存中是连续的。由于碎片,仅凭这一点就很难实现。如果 VMM 控制物理内存,则问题就更加严重,因为 VMM 和客户域操作系统内核都需要协调。即使使用客户物理内存,也需要这种协调:客户内核可以使用大页面,但除非它们也映射到 VMM 的页表树中的大页面,否则不会有大的收益。
我们应该讨论的另一个与内存相关的问题是,这个问题的重要性将日益增加,并且需要在应用程序级别进行优化。未来几乎所有的多处理器商品系统都将具有 NUMA(非统一内存架构)。这是将内存库连接到系统中的每个处理器而不是仅连接到(一组)内存控制器的结果。结果是,访问不同物理内存地址的成本可能会有所不同。
这是一个众所周知的问题,可以在今天的操作系统内核和其上的程序中处理。然而,使用虚拟物理内存,虚拟化操作系统内核可能不知道物理内存放置的确切细节。结果是,除非可以从 VMM 将完整的 NUMA 状态传达给客户域操作系统内核,否则虚拟化将阻止某些 NUMA 优化。
CPU 的产品线显示出越来越多的功能,所有这些功能都希望能够提高操作系统内核和应用程序的性能。因此,建议软件组件使用新功能。
然而,在虚拟机中,这可能是一个问题。VMM 也需要支持这些功能。有时,这可能就像将有关功能存在的信息传递给客户域操作系统内核一样简单。有时,这意味着必须在客户管理中完成更多的工作。这些问题可以在一个地方(VMM)处理。
然而,有一个密切相关的问题,但不太容易解决。当虚拟化用于迁移并且机器网络不是完全同构时,向客户域宣布某个功能的存在可能会导致麻烦。如果客户域首先在具有可用相应功能的机器上执行,然后迁移到缺少该功能的机器,则操作系统内核或应用程序可能无法继续运行。
这可以被视为管理问题,因为应该简单地避免此类问题。实际上,这通常是一个无法实现的目标,因为所有组织都在使用不同年代的机器。使用功能的最小公分母是处理这种情况的一种方法,但这可能会牺牲很大一部分性能。更好的解决方案是动态处理可用功能的变化(即,当功能不再可用时,程序停止使用这些功能,反之亦然)。今天的操作系统根本不支持这一点,并且需要进行广泛的更改。尽管如此,开发者可能希望在设计代码时牢记这一点。未能处理不断变化的 CPU 功能可能会导致崩溃和不正确的结果。
前面的章节重点介绍了程序在虚拟机中执行时所经历的变化。以下是开发者必须注意的要点摘要。
在虚拟机中访问设备(例如硬盘驱动器、NIC 和显卡)的成本可能会显着增加。 已经开发出一些缓解某些情况下成本变化的措施,但开发者应该更加努力地使用缓存并避免不必要的访问。
虚拟环境中的 TLB 未命中也明显更昂贵。需要提高 TLB 缓存的效率,以免损失性能。 操作系统开发者必须使用 TLB 标记,并且每个人都必须通过在虚拟地址空间中尽可能紧凑地分配内存来减少任何时候使用的 TLB 条目数量。TLB 标记只会增加缓存压力。
开发者必须考虑减小程序代码大小并对程序的代码和数据进行排序。 这最大限度地减少了任何时候的占用空间。
页错误也明显更昂贵。 减小程序代码和数据大小也有助于解决这个问题。也可以预先调入内存页面,或者至少让内核知道使用模式,以便它可以一次调入多个页面。
应该更严格地控制处理器功能的使用。 理想情况下,每次使用都意味着检查 CPU 功能的可用性。这可以有多种形式,不一定是显式测试。程序应该准备好看到功能集在进程的运行时发生变化,并为操作系统提供一种方法来发出更改信号。或者,操作系统可以在较旧的处理器上提供较新功能的模拟。
今天的虚拟化技术在很大程度上实现了虚拟化的三个目标:保真度、性能和安全性。因此,程序员通常不必关心他们的代码是在虚拟环境中运行还是在非虚拟环境中运行。
为了避免重大的性能损失,某些始终有益的优化变得更加迫切。开发者必须转移他们的重点。
尤其重要的是优化内存处理,而这在今天却鲜为人知。对于低级语言,程序员必须自己动手并优化他们的代码。对于高级、解释型和/或脚本语言,这项工作主要必须由这些系统的实现者完成。开发者有责任选择合适的实现,或者在失败的情况下,选择更适合高性能编程的编程语言。
ULRICH DREPPER 是红帽公司的咨询工程师,他在该公司工作了 12 年。他对各种低级编程都感兴趣,并且参与 Linux 已有近 15 年。
最初发表于 Queue vol. 6, no. 1—
在 数字图书馆 中评论本文
Mendel Rosenblum, Carl Waldspurger - I/O 虚拟化
“虚拟”一词被过度使用,从云中运行的虚拟机到跨越虚拟世界运行的化身,一切都让人联想到虚拟。即使在计算机 I/O 的狭隘背景下,虚拟化也具有悠久而多样的历史,逻辑设备就是典型的例子,它们刻意与其物理实例化分离。
Scot Rixner - 网络虚拟化:打破性能壁垒
虚拟化最近的人气复苏导致其在越来越多的环境中得到使用,其中许多环境都需要高性能网络。例如,考虑服务器整合。网络虚拟化的效率直接影响可以有效整合到单台物理机器上的网络服务器数量。不幸的是,现代网络虚拟化技术会产生巨大的开销,这限制了可实现的网络性能。我们需要新的网络虚拟化技术,以在网络密集型领域充分实现虚拟化的优势。
Werner Vogels - 超越服务器整合
虚拟化技术在 1960 年代后期被开发出来,目的是更有效地利用硬件。硬件很昂贵,而且可用硬件不多。处理工作主要外包给少数拥有计算机的地方。在单台 IBM System/360 上,可以并行运行多个环境,这些环境保持完全隔离,并让每个客户都产生拥有硬件的错觉。虚拟化是以粗粒度级别实现的分时,而隔离是该技术的关键成就。
Tom Killalea - 认识 Virts
当你深入研究那些所谓的“一夜成名”的故事时,你经常会发现它们实际上已经酝酿多年。虚拟化已经存在了 30 多年,从你们中的一些人还在向非常物理的机器中输入成堆的穿孔卡片的时代起,但在 2007 年,情况发生了转变。VMware 是当年 IPO 的轰动事件;2007 年 11 月,不少于四家主要的操作系统供应商(微软、Oracle、红帽和 Sun)宣布了重要的新虚拟化功能;在时尚的技术专家中,虚拟似乎已成为新的潮流。