Windows 如何跟踪正在使用的内存位置?

Windows 如何跟踪正在使用的内存位置?

我对内存泄漏的(基本)定义的理解是内存中一个已被保留的位置,但是保留该内存的应用程序内指向该内存的指针已被删除/丢失。

我的问题是:为了让 Windows(或任何操作系统)知道内存仍然被保留用于管理应用程序内部内存的指针的销毁,它必须独立跟踪正在使用的内存。它在哪里做到这一点?如何做到的?

答案1

您说得对 - 每个操作系统都分别跟踪内存的使用和分配。这是它的重要功能之一。出于许多原因,将内存的完全控制权交给用户空间进程根本就没有意义 - 安全性就是一个例子:您的进程可以将敏感信息存储在内存中,因此操作系统应该管理哪些进程可以访问它。

通常,所有操作系统都有一个名为“内存管理”的子系统(在 Windows 中称为“内存管理器”),用于跟踪虚拟内存。操作系统的另一部分“硬件抽象”随后将该虚拟内存映射到物理设备。当然,这禁止任何用户进程与物理内存之间的直接交互。

可以通过公共 API 直接与操作系统的这一部分进行交互 - 请参阅内存管理函数对于 Windows。在 Linux 中,有几种方法可以访问内存,这一页将是开始的最佳方式。

答案2

嗯,是的,这是操作系统内存管理器的一部分的功能,但实际上这只是将问题稍微推后了一点。OP 不仅问了“在哪里”,还问了“如何”……所以,下面就是如何。

Windows 内存管理器(缩写为 Mm)通过一组称为虚拟地址描述符。对于用户模式地址空间,这是基于每个进程完成的。当进程首次创建时,它没有 VAD。当虚拟地址空间的一块从空闲变为非空闲时(非空闲可能是保留、提交、映射或一两个不太常见的选项),将创建一个 VAD。VAD 包含区域的起始地址(始终是页面对齐的)和大小(也始终是页面对齐的)、分配类型(保留、提交等)以及其他一些内容。

VAD 描述的区域可以重新定义和/或细分。例如,将较大的保留范围的一部分更改为“已提交”是很常见的。这会导致原始 VAD 被修改以反映保留区域的新长度,并创建新的 VAD 来描述已提交区域。如果已提交区域超出原始 VAD 定义的范围的“中间”,则必须创建两个新的 VAD。

每个进程的 VAD 被组织成一种称为平衡二叉树,或者更具体地说,伸展树,按每个区域的起始虚拟地址排序。当尝试确定给定地址是否正在使用,如果是,则确定其使用方式时,内存管理器会“遍历”树以查找包含该地址的 VAD - 如果没有,则查找不包含该地址的 VAD。

由于这种树的性质,此操作非常高效,既可用于查找现有 VAD,也可用于添加和删除 VAD。对于查找现有 VAD,它的效率与“二分搜索”在简单有序列表中的效率相同。例如,如果树中有 31 到 64 个 VAD,则最多需要查看六个 VAD 才能查找任何给定的地址。对于 65 到 128 个 VAD,搜索只需七个步骤,等等。但与简单有序列表不同,添加或删除条目不需要重新调整所有现有条目。

当必须分配新区域时,必须“遍历”树以找到足够大小的空闲区域。这是一个成本适中的操作,但没关系,因为这种情况并不经常发生。

关于“指针的销毁”——指针只是内存中的一个位置。当我分配虚拟内存时(在 Windows API 中,对此的低级调用是 VirtualAlloc),我会得到一个指向已分配区域的指针。用零覆盖指针,甚至释放存储它的虚拟内存,都不会告诉操作系统我已经完成了该区域的处理。毕竟,我可能已经将该指针值复制到其他地方了。真正告诉操作系统“我已经完成了该区域处理”的是调用 VirtualFree。这会删除描述该区域的 VAD。当然,所有每个进程的虚拟地址空间都是进程的一个属性,当进程被删除时,任何仍分配的内容都会消失(因为所有 VAD 也会消失)。

虚拟页码和物理页码之间的关联在称为页表。这些也有助于管理虚拟地址空间。有一个页表,由 512页表项并占用一页,每 2 MB 虚拟地址空间;每个 PTE 描述一页。PT 被组织成一个简单的层次结构,在 32 位 x86 上有三层深度,在 x64 上有四层深度。对应于 2 MB 跨度的 vas 的页表全部是空闲的(即没有 VAD 描述 2 MB 中的任何内容),实际上不存在;因此上层 PT 中的相应条目为零。页表也由操作系统的内存管理器(而不是另一个答案中声称的“硬件抽象层”)维护和更新。

(上一段假设 x86 上有 PAE。如果没有 PAE,每个 PT 有 1024 个 PTE,并且树中只有两级 PT。)

另一个数据集“PFN 数组”跟踪每个物理页面的状态。它是一个简单的结构数组,按物理页号(“页框号”)索引。数组的每个元素都包含有关相应物理页面状态的信息:它位于哪个系统范围的页面列表中(空闲、待机、清零、已修改)或者它是否位于一个或多个进程工作集中,它位于多少个进程中,等等。

相关内容