第 7 章 虚拟内存系统
7.1. 物理内存管理 vm_page_t
vm_page_t物理内存是通过 vm_page_t 结构逐页管理的。物理内存的页面通过将各自的 vm_page_t 结构放置到多个分页队列中的一个来分类。
页面可以处于有线、活动、非活动、缓存或空闲状态。除了有线状态外,页面通常被放置在代表其当前状态的双向链表队列中。有线页面不会被放置到任何队列中。
FreeBSD 实现了更复杂的缓存和空闲页面分页队列,用于实现页面着色。每个状态都涉及多个队列,队列按处理器的 L1 和 L2 缓存大小排列。当需要分配新页面时,FreeBSD 会尝试获取一个相对于该页面所分配的 VM 对象,在 L1 和 L2 缓存视角下合理对齐的页面。
此外,页面可以通过引用计数来持有或通过忙碌计数来锁定。VM 系统还通过页面标志中的 PG_BUSY 位实现了一个“终极锁定”状态。
通常,每个分页队列按 LRU(最近最少使用)方式操作。页面最初通常会放置在有线或活动状态下。当页面被设置为有线时,通常会与某个页表相关联。VM 系统通过扫描更活跃的分页队列(LRU)中的页面以将它们移到不那么活跃的分页队列,从而对页面进行“老化”。被移动到缓存中的页面仍然与 VM 对象关联,但可以立即重新使用。空闲队列中的页面是完全空闲的。FreeBSD 尽量减少空闲队列中的页面数量,但必须保持一定数量的真正空闲页面,以便在中断时进行页面分配。
如果进程尝试访问其页表中不存在的页面,但该页面在某个分页队列中存在(例如非活动队列或缓存队列),则会发生一个开销相对较小的页面重新激活错误,导致页面被重新激活。如果页面根本不存在于系统内存中,进程必须阻塞,直到页面从磁盘中加载进来。
FreeBSD 动态地调整其分页队列,并尝试保持各队列中页面的合理比例,同时努力维持干净页面与脏页面的合理分布。重平衡的幅度取决于系统的内存负载。此重平衡由 pageout 守护进程实现,涉及清洗脏页面(将其同步到备份存储)、监测页面是否被活跃引用(重置其在 LRU 队列中的位置或在队列之间移动它们)、当队列失衡时在队列之间迁移页面等。FreeBSD 的 VM 系统愿意接受一定数量的重新激活页面错误,以确定页面实际是活跃还是空闲。这有助于更好地决定何时清洗或换出页面。
7.2. 统一缓冲区缓存 vm_object_t
vm_object_tFreeBSD 实现了通用的“VM 对象”概念。VM 对象可以与不同类型的备份存储相关联:无备份、交换备份、物理设备备份或文件备份存储。由于文件系统使用相同的 VM 对象来管理内存中与文件相关的数据,结果便形成了一个统一的缓冲区缓存。
VM 对象可以被 shadowed(影子化),即可以堆叠在一起。例如,你可以将一个交换备份的 VM 对象堆叠在一个文件备份的 VM 对象上,以实现 MAP_PRIVATE 的 mmap() 映射。这种堆叠也用于为派生(fork)的地址空间实现包括写时复制(copy-on-write)在内的各种共享属性。
需要注意的是,vm_page_t 同一时刻只能与一个 VM 对象关联。VM 对象的影子化实现了在多个实例之间共享同一页面的效果。
7.3. 文件系统 I/O struct buf
struct buf由 vnode 支持的 VM 对象,如文件支持的对象,通常需要维护独立于 VM 系统的干净/脏信息。例如,当 VM 系统决定将物理页面同步到其备份存储时,VM 系统需要在页面实际写入备份存储之前将其标记为干净。此外,文件系统还需要能够将文件或文件元数据的部分映射到 KVM 中,以便对其进行操作。
用于管理这些操作的实体被称为文件系统缓冲区,即 struct buf 或 bp。当文件系统需要操作 VM 对象的一部分时,它通常会将对象的一部分映射到一个 struct buf 中,然后将 struct buf 中的页面映射到 KVM。同样,磁盘 I/O 通常通过将对象的部分映射到缓冲区结构中,然后对缓冲区结构发起 I/O 操作来实现。在 I/O 的整个过程中,底层的 vm_page_t 通常会被置为忙碌状态。文件系统缓冲区也有其自己的忙碌概念,这对于文件系统驱动代码非常有用,因为它们更倾向于操作文件系统缓冲区而不是硬 VM 页面。
FreeBSD 保留了一定量的 KVM 来存放 struct buf 的映射,但需要明确的是,这部分 KVM 仅用于存放映射,并不限制数据缓存的能力。物理数据缓存严格来说是 vm_page_t 的功能,而非文件系统缓冲区。然而,由于文件系统缓冲区用于占位 I/O 操作,它们本质上限制了可以并发执行的 I/O 数量。不过,由于通常有几千个文件系统缓冲区可用,这通常不是问题。
7.4. 映射页表 vm_map_t, vm_entry_t
vm_map_t, vm_entry_tFreeBSD 将物理页表拓扑与 VM 系统分开。所有硬件的每进程页表可以动态重建,通常被视为临时的。管理 KVM 的特殊页表通常会被永久预分配。这些页表不是临时的。
FreeBSD 通过 vm_map_t 和 vm_entry_t 结构将 vm_object 的部分与虚拟内存中的地址范围关联。页表是直接从 vm_map_t/vm_entry_t/vm_object_t 层级合成的。回想一下,我提到过物理页面只直接与 vm_object 关联;这并不完全准确。vm_page_t 还会链接到它们实际关联的页表中。一个 vm_page_t 可以被链接到多个 pmap(即页表的别名)中。不过,层级关联依然成立,因此同一对象中对同一页面的所有引用都会引用相同的 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 的主要机制是 zone 分配器。zone 分配器将一块 KVM 内存分割成固定大小的内存块,用于分配特定类型的结构。你可以使用 vmstat -m 命令查看当前按 zone 分类的 KVM 使用情况。
7.6. 调整 FreeBSD VM 系统
经过协同努力,FreeBSD 内核可以动态调整自身配置。通常情况下,你无需修改除 maxusers 和 NMBCLUSTERS 内核配置选项以外的任何内容。也就是说,内核编译选项通常在 /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。
Sysctl 提供了一种在运行时调整内核参数的方法。通常,你无需修改任何 sysctl 变量,尤其是与 VM 相关的变量。
运行时的 VM 和系统调整相对简单。首先,尽可能在你的 UFS/FFS 文件系统上使用 Soft Updates。/usr/src/sys/ufs/ffs/README.softupdates 包含了配置它的说明(和限制)。
其次,配置足够的交换空间。你应该在每个物理磁盘上配置一个交换分区,最多四个,即使是你的“工作”磁盘。交换空间的大小应至少是主内存的两倍,如果内存较小,可能需要更多的交换空间。你还应该根据你计划在机器上配置的最大内存量来调整交换分区的大小,以便以后无需重新分区。如果你希望能够容纳崩溃转储,你的第一个交换分区必须至少与主内存大小相同,并且 /var/crash 必须有足够的空闲空间来存储转储。
基于 NFS 的交换在 4.X 或更高版本的系统上完全可以接受,但你必须意识到,NFS 服务器将承受大部分的分页负载。
最后更新于