第7章 虚拟内存系统

7.1. 物理内存管理 vm_page_t

物理内存是通过vm_page_t结构以页面为基础进行管理的。物理内存页面通过其各自的vm_page_t结构放置在几个分页队列之一来进行分类。

一个页面可以处于绑定、活动、非活动、缓存或空闲状态。除了绑定状态外,页面通常被放置在一个双向链表队列中,该队列表示其所处的状态。绑定页面不会放置在任何队列中。

FreeBSD 为缓存和空闲页面实现了更复杂的分页队列,以实现页面着色。每个状态都包含多个队列,这些队列根据处理器的 L1 和 L2 缓存的大小进行排列。当需要分配新页面时,FreeBSD 会尝试获取一个从 L1 和 L2 缓存的角度来看与分配页面所在的 VM 对象合理对齐的页面。

此外,一个页面可以使用引用计数来持有,或者使用繁忙计数来锁定。VM 系统还使用页面标志中的 PG_BUSY 位为页面实现了“最终锁定”状态。

一般来说,每个分页队列都以 LRU 方式运行。页面通常最初处于绑定或活动状态。绑定时,页面通常与某个地方的页表相关联。VM 系统通过扫描更活跃的分页队列(LRU)中的页面来使页面老化,以便将其移动到不太活跃的分页队列。被移动到缓存中的页面仍然与 VM 对象相关联,但可以立即重复使用。空闲队列中的页面确实是空闲的。FreeBSD 试图将空闲队列中的页面数量降到最低,但必须维护一定数量的真正空闲页面,以便在中断时适应页面分配。

如果进程尝试访问其页表中不存在但存在于某个分页队列(例如非活动或缓存队列)中的页面,则会发生相对廉价的页面重新激活错误,这会导致页面被重新激活。如果页面根本不存在于系统内存中,则进程必须阻塞,直到页面从磁盘中调入。

FreeBSD 动态调整其分页队列,并尝试维护各个队列中页面的合理比率,以及尝试维护干净页面与脏页面之间的合理分解。发生的重新平衡量取决于系统的内存负载。此重新平衡由页面交换守护进程实现,并涉及清洗脏页面(将其与它们的备份存储同步)、注意到页面何时被活动引用(重置它们在 LRU 队列中的位置或在队列之间移动它们)、在队列不平衡时在队列之间迁移页面,等等。FreeBSD 的 VM 系统愿意接受合理的页面重新激活错误来确定页面的活动程度或空闲程度。这导致在何时清洗或交换页面方面做出更好的决策。

7.2. 统一缓冲区缓存 vm_object_t

FreeBSD 实现了通用“VM 对象”的概念。VM 对象可以与各种类型的备份存储相关联,例如无备份、交换备份、物理设备备份或文件备份存储。由于文件系统使用相同的 VM 对象来管理与文件相关的内存中数据,因此结果是统一的缓冲区缓存。

VM 对象可以被隐藏。也就是说,它们可以彼此叠加。例如,您可能有一个交换备份的 VM 对象叠加在一个文件备份的 VM 对象之上,以实现 MAP_PRIVATE mmap()。此叠加还用于实现各种共享属性,包括写时复制,用于派生地址空间。

需要注意的是,vm_page_t一次只能与一个 VM 对象相关联。VM 对象隐藏实现了在多个实例中共享同一页面的感知。

7.3. 文件系统 I/O struct buf

vnode 备份的 VM 对象,例如文件备份对象,通常需要维护自己的干净/脏信息,这些信息独立于 VM 系统对干净/脏的概念。例如,当 VM 系统决定将物理页面同步到其备份存储时,VM 系统需要在页面实际写入其备份存储之前将其标记为干净。此外,文件系统需要能够将文件或文件元数据的某些部分映射到 KVM 以对其进行操作。

用于管理这些实体的实体称为文件系统缓冲区、struct bufbp。当文件系统需要操作 VM 对象的一部分时,它通常将对象的一部分映射到 struct buf 中,然后将 struct buf 中的页面映射到 KVM 中。以同样的方式,磁盘 I/O 通常通过将对象的部分映射到缓冲区结构中,然后在缓冲区结构上发出 I/O 来发出。底层的 vm_page_t 通常在 I/O 持续期间处于繁忙状态。文件系统缓冲区也有自己的繁忙概念,这对于希望操作文件系统缓冲区而不是硬 VM 页面的文件系统驱动程序代码很有用。

FreeBSD 保留了一定数量的 KVM 来保存来自 struct buf 的映射,但应该明确的是,此 KVM 仅用于保存映射,并且不限制缓存数据的能力。物理数据缓存严格是 vm_page_t 的功能,而不是文件系统缓冲区。但是,由于文件系统缓冲区用于占位 I/O,因此它们本身确实限制了可能的并发 I/O 量。但是,由于通常有几千个文件系统缓冲区可用,因此这通常不是问题。

7.4. 映射页表 vm_map_t, vm_entry_t

FreeBSD 将物理页表拓扑结构与虚拟机系统分离。所有硬性的每个进程页表都可以在运行时重建,通常被认为是临时的。特殊的页表,例如管理 KVM 的页表,通常是永久预分配的。这些页表不是临时的。

FreeBSD 通过vm_map_tvm_entry_t结构将虚拟对象的部分与虚拟内存中的地址范围关联起来。页表直接从vm_map_t/vm_entry_t/vm_object_t层次结构中合成。回想一下我提到的物理页面仅直接与vm_object关联;这并不完全正确。vm_page_t也链接到它们积极关联的页表中。一个vm_page_t可以链接到多个pmaps(页表的别称)。但是,层次关联仍然有效,因此对同一对象中同一页面的所有引用都引用同一个vm_page_t,从而使我们在整个系统中实现了缓冲区缓存的统一。

7.5. KVM 内存映射

FreeBSD 使用 KVM 来保存各种内核结构。KVM 中保存的单个最大实体是文件系统缓冲区缓存。也就是说,与struct buf实体相关的映射。

与 Linux 不同,FreeBSD 不会将所有物理内存映射到 KVM 中。这意味着 FreeBSD 在 32 位平台上可以处理高达 4G 的内存配置。事实上,如果 mmu 支持,FreeBSD 理论上可以在 32 位平台上处理高达 8TB 的内存配置。但是,由于大多数 32 位平台只能映射 4GB 的内存,因此这是一个无关紧要的问题。

KVM 通过多种机制进行管理。管理 KVM 的主要机制是区域分配器。区域分配器获取一块 KVM 并将其分成固定大小的内存块,以便分配特定类型的结构。您可以使用vmstat -m来获取按区域划分的当前 KVM 使用情况概述。

7.6. 调优 FreeBSD 虚拟机系统

已经做出了协调一致的努力,使 FreeBSD 内核能够动态地自我调整。通常,您无需修改除maxusersNMBCLUSTERS内核配置选项之外的任何内容。也就是说,在(通常是)/usr/src/sys/i386/conf/CONFIG_FILE中指定的内核编译选项。所有可用内核配置选项的描述可以在/usr/src/sys/i386/conf/LINT中找到。

在大型系统配置中,您可能希望增加maxusers。值通常在 10 到 128 之间。请注意,将maxusers设置得太高会导致系统溢出可用的 KVM,从而导致不可预测的操作。最好将maxusers保留在某个合理的数字,并添加其他选项(例如NMBCLUSTERS)来增加特定资源。

如果您的系统要大量使用网络,您可能需要增加NMBCLUSTERS。典型值范围从 1024 到 4096。

NBUF参数也传统上用于扩展系统。此参数确定系统可以用来映射文件系统缓冲区以进行 I/O 的 KVA 量。请注意,此参数与统一缓冲区缓存没有任何关系!此参数在 3.0-CURRENT 及更高版本的内核中是动态调整的,通常不需要手动调整。我们建议您不要尝试指定NBUF参数。让系统自行选择。值太小会导致文件系统操作效率极低,而值太大则会导致过多的页面被绑定,从而使页面队列饥饿。

默认情况下,FreeBSD 内核未经优化。您可以使用内核配置中的makeoptions指令设置调试和优化标志。请注意,除非您可以容纳由此产生的较大的(通常为 7 MB 以上)内核,否则不应使用-g

makeoptions      DEBUG="-g"
makeoptions      COPTFLAGS="-O -pipe"

Sysctl 提供了一种在运行时调整内核参数的方法。通常,您无需修改任何 sysctl 变量,尤其是与 VM 相关的变量。

运行时 VM 和系统调优相对简单。首先,尽可能在您的 UFS/FFS 文件系统上使用 Soft Updates。/usr/src/sys/ufs/ffs/README.softupdates包含有关如何配置它的说明(和限制)。

其次,配置足够的交换空间。您应该在每个物理磁盘上配置一个交换分区,最多四个,即使在您的“工作”磁盘上也是如此。您应该拥有至少与主内存 2 倍大小的交换空间,如果您内存不多,甚至可能更多。您还应该根据您打算在机器上使用的最大内存配置来调整交换分区的大小,这样以后就不必重新分区磁盘了。如果您希望能够容纳崩溃转储,则您的第一个交换分区必须至少与主内存一样大,并且/var/crash必须有足够的空间来保存转储。

基于 NFS 的交换在 4.X 或更高版本的系统上完全可以接受,但您必须意识到 NFS 服务器将承担分页负载的主要压力。


最后修改于:2024 年 3 月 9 日,作者 Danilo G. Baio