天天看点

Windows CE .NET 高级内存管理

<!--[if !vml]-->

<!--[endif]-->show toc

摘要

Microsoft_ Windows CE 的优点之一是它的 Microsoft Win32_ 应用程序编程接口 (API) 支持。无数 Windows 程序员可以利用他们的 Win 32 API 和 MFC 知识相对容易地转移到 Windows CE。Windows CE 可以实现 Win32 API 的子集,但程序员应当永远不要忘记 Windows CE 是与 Windows XP 完全不同的操作系统,Windows CE 具有不同的要求和不同的实现。在设计应用程序或诊断问题时知道 Windows CE 如何实现它的 Win32 兼容性是一件非常重要的事情。

内存管理是 Windows CE 和 Windows XP 之间在实现方面存在很明显差异的地方之一。虽然 Windows CE 支持几乎每个 Win32 内存管理函数(除了我们不赞成对它使用全局堆函数),但是这些内存管理 API 的实现是完全不同的。这些差异可以妨碍那些不熟悉 Windows CE 与 Windows 的桌面版本之间的细微差异的程序员。要了解这些问题位于哪里,必须首先了解 Windows CE 如何管理内存。

系统内存映射

Windows XP 和 Windows CE 都是 32 位操作系统,都同样支持 4 GB 的虚拟地址空间。Windows XP 将地址空间划分为两个 2 GB 区域。上半部地址空间是为系统保留的。下面 2 GB 地址空间则由每个正在运行的应用程序重复使用。

<!--[if !vml]-->

Windows CE .NET 高级内存管理

<!--[endif]-->

图 1. Windows XP 虚拟内存空间

第一眼看,Windows CE 的虚拟地址空间是以类似系统保留区域的方式组织的,并且是重复的应用程序空间。图 2 显示了 Windows CE 地址空间。在这里,上部 2GB 地址空间也是为系统保留的。下半部地址空间则划分为很多区域。该区域的大多数(几乎一半空间)被定义为大型内存区域 (Large Memory Area)。该区域用来分配通常用于内存映射文件的大型内存空间块。

在大型内存区域的下面是另一个大型区域,本文称为保留区域。在保留区域的下面,在内存空间的最底端,是一个 64 MB 的区域。该 64 MB 区域,更准确地说是最下面的 32 MB 区域,是每个正在运行的应用程序重复使用的区域。

<!--[if !vml]-->

Windows CE .NET 高级内存管理

<!--[endif]-->

图 2. Windows CE 虚拟内存空间

Windows CE 应用程序内存映射

最下面的 64 MB 虚拟地址空间是驻留 Windows CE 应用程序的地方。图 3 显示了该应用程序虚拟地址空间。像在 Windows XP 应用程序中一样,应用程序代码从虚拟地址 0x10000 开始加载。应用程序启动时,将在地址空间中为所有代码保留足够的空间。然后,在需要实际代码时,这些代码将被按需分页调度进该地址空间。

在为代码保留的区域上面,页是为只读和读/写静态数据区域保留的。此外,还为本地堆和应用程序中运行的每个线程的堆栈保留了区域。当线程启动时,为每个堆栈保留的区域的大小是固定的。只在堆栈增长时才会提交实际的 RAM。另一方面,堆保留了需要在堆中分配 RAM 块时增长的区域。

当加载“现场执行”(XIP) DLL 时,将从 64 MB 空间的顶部向下加载这些 DLL。当创建 ROM 时,每个 XIP DLL 都会被定址(确定在地址空间中的位置)。加载非 XIP DLL 时,将把它放在 32 MB 边界的下面。非 XIP 的 DLL(也叫作基于 RAM 的 DLL)是指那些从对象存储区加载的 DLL、从 ROM 解压缩的 DLL或从外部文件系统(例如 Compact Flash 卡)加载的 DLL。应用程序虚拟内存空间中靠上位置的 32 MB 仅用于 XIP DLL。

<!--[if !vml]-->

Windows CE .NET 高级内存管理

<!--[endif]-->

图 3. Windows CE .NET 应用程序虚拟内存空间

由应用程序通过创建单独的堆或直接调用 VirtualAlloc API 所分配的任何其他内存将从底部向上进行分配,分配时,系统将查找第一个足够大、可满足分配的可用区域。

限制因素

尽管 Windows CE 应用程序的一个限制因素是可用于应用程序的 RAM 数量,但还有另一个主要的限制是应用程序的相对很小的 32 MB 虚拟地址空间。尽管 XIP DLL 被加载在 32 MB 空间的上端,但所有其他内存分配和任何基于 RAM 的 DLL 都必须能够放进应用程序的 32 MB 内存空间中。这个 32 MB 限制“框”不是 Windows CE 程序员所面临的很大问题,因为它是一个可被克服的挑战。

要了解这个看似很大的内存空间有些什么限制性,必须了解 VirtualAlloc API 的操作原理。VirtualAlloc 是任何 Microsoft Win32 操作系统中最基础的内存分配调用。它在页级别分配内存;页是可以被 CPU 分配或释放的最小的内存单位。Windows CE .NET CPU 的页大小是 1024 或 4096 字节,这取决于 CPU。最广泛使用的是 4 KB 页大小。

VirtualAlloc 调用分配内存的过程分为两个步骤。第一步,保留虚拟内存空间的区域。这种保留不会消耗任何 RAM;它只是防止一部分虚拟地址空间被用于其他用途。保留内存空间之后,就可以提交 (commit) 部分或整个区域,这个过程是指将实际物理内存映射到保留区域。VirtualAlloc 函数用于保留内存空间和提交内存。下面显示了 VirtualAlloc 函数的原型。

LPVOID VirtualAlloc (LPVOID lpAddress, DWORD dwSize,

                     DWORD flAllocationType,

                     DWORD flProtect);

VirtualAlloc 的第一个参数是要分配的内存区域的虚拟地址。使用 VirtualAlloc 提交先前保留的内存块时,使用 lpAddress 参数来标识先前保留的内存块。如果该参数是 NULL,则由系统确定从哪里分配内存区域,并以 64 KB 为边界。第二个参数是 dwSize,它是要分配或保留的区域的大小。因为该参数是以字节而不是页为单位指定的,所以系统会将所请求的大小以下一个页边界为限进行舍入。

flAllocationType 参数指定分配的类型。可以指定以下标志的组合:MEM_COMMIT、MEM_AUTO_COMMIT 和 MEM_RESERVE。MEM_COMMIT 标志用于分配程序使用的内存。MEM_RESERVE 用于保留要随后提交的虚拟地址空间。保留页是无法访问的,直到通过指定区域并使用 MEM_COMMIT 标志进行了另一个 VirtualAlloc 调用为止。MEM_AUTO_COMMIT 标志唯一用于 Windows CE 并且很好用,但它不是本文的主题。

因此,要使用 VirtualAlloc 来分配可使用的 RAM,应用程序必须调用 VirtualAlloc 两次,一次保留内存空间,再一次则提交物理 RAM;或者调用 VirtualAlloc 一次,这需要在 flAllocationType 参数中组合使用 MEM_RESERVE 和 MEM_COMMIT 标志。

组合保留和提交标志方式所使用的代码更少,并且更快、更简单。该技术通常用在 Windows XP 应用程序中,但用在 Windows CE 应用程序中不是很好。以下代码片段演示了存在的问题。

INT i;

PVOID pMem[512];

for (i = 0; i < 512; i++) {

   pMem[i] = VirtualAlloc (0, PAGE_SIZE, MEM_RESERVE | MEM_COMMIT,

                           PAGE_READWRITE);

}

该代码片段似乎是无害的。它分配了 512 块内存,每块内存的大小为 1 页。问题是:在 Windows CE 系统上,甚至是在有数兆字节可用 RAM 的系统上,该代码总是会失败。问题在于 Win32 操作系统保留内存区域的方式。

在任何 Win32 操作系统(包括 Windows CE .NET)上,当一个虚拟内存空间区域被保留时,它会将保留区域对齐 64 KB 边界。因而,上面的代码片段试图将保留的 512 个区域的每个区域都对齐 64 KB 边界。Windows CE 应用程序的问题是它们必须位于 32 MB 虚拟内存空间的范围内。在整个应用程序内存空间中该空间的大小只有 512 64 KB,并且它们中的一部分需要用作应用程序代码、本地堆、堆栈和应用程序所加载的每个 DLL 的区域。通常,在对 VirtualAlloc 进行大约 470 次调用之后上面的代码片段将失败。

上述问题的解决方案是首先保留足够用于整个分配的较大区域,然后在需要时提交 RAM,如下所示。

INT i;

PVOID pBase, pMem[512];

pBase = VirtualAlloc (0, 512*PAGE_SIZE, MEM_RESERVE, PAGE_READWRITE);

for (i = 0; i < 512; i++) {

   pMem[i] = VirtualAlloc (pBase + (i * PAGE_SIZE), PAGE_SIZE,

                           MEM_COMMIT, PAGE_READWRITE);

}

避免该问题的关键是知道这个情况。这只是 Windows CE 应用程序的地址空间中只有 512 个区域所带来的问题影响应用程序的很多地方中的一个。

分配大型内存块

Windows CE .NET 应用程序的地址空间局限于 32 MB 所引起的另一个问题是如何分配大型内存块。当应用程序的整个地址空间被限制在 32 MB 以内时,如果应用程序需要一块 8、16 或 32 Mb RAM 用于具体用途,它怎样才能分配该内存?回答是应用首先用在 Windows CE .NET 早期版本中针对视频驱动程序的一个修复程序。有了它,如果 Windows CE .NET 检测到一个对 VirtualAlloc 的调用请求保留超过 2 MB 的地址空间,该地址空间将不会保留在 32 MB 的限制大小中。该内存块将保留在大型内存区域 (Large Memory Area) 中,大型内存区域位于全局内存空间中,正好在 2 GB 系统保留空间的下面。

内存空间已经保留后,应用程序就可以通过调用 VirtualAlloc 来提交在保留空间内的具体页。这就允许应用程序或驱动程序使用大型内存块,即使它存活在 32 MB 的大小的限制内。下面的代码显示了分配 64 MB 块然后提交保留区域的一页的简单情形。

   PVOID ptrVirt, ptrMem;

   ptrVirt = VirtualAlloc (0, 1024 * 1024 * 64, MEM_RESERVE,

                        PAGE_NOACCESS);

   if (!ptrVirt) return 0;

   ptrMem = VirtualAlloc ((PVOID)((int)ptrVirt+4096),

                          4096, MEM_COMMIT, PAGE_READWRITE);

   if (!ptrMem) {

      VirtualFree (ptr, 0, MEM_RELEASE);

      return 0;

   }

   return ptrMem;

前面的代码还显示了直接处理虚拟内存 API 所具有的一个特性。这就是您可以创建大型稀疏数组,而不会消耗大量 RAM。在上面的代码中,64 MB 保留区域不会消耗任何物理 RAM。在该示例中,唯一被消耗的 RAM 是在第二次调用 VirtualAlloc 以提交页时使用的一个页(4096 字节)。

DLL 加载问题

目前,有很多在 Pocket PC 2002 上编程的 Windows CE 程序员。有一个重要问题会影响 Pocket PC 2002 程序员,这个问题与应用程序加载 DLL 有关,尽管对 Windows CE .NET 内存体系结构所作的更改修复了这个问题。要理解该问题,首先必须理解在 Windows CE .NET 如何不同于 Windows CE 3.0 与两个版本的 Windows CE 如何加载和管理 DLL 之间存在的一个主要差异。

Windows CE .NET 的新功能之一是将应用程序的虚拟地址空间从 Windows CE 早期版本的 32 MB 扩展到 64 MB。虚拟空间中可用于 XIP DLL 的高端 32 MB 不可用于 Windows CE 3.0。因此,运行在基于 Windows CE 3.0 的系统上的应用程序必须将它们的 XIP DLL、它们的代码和它们的所有数据加载到 32 MB 虚拟地址空间中。图 4 显示了一个 Windows CE 3.0 应用程序的应用程序内存空间。图 4 展示了 Windows CE 3.0 应用程序内存空间的关系图。

<!--[if !vml]-->

Windows CE .NET 高级内存管理

<!--[endif]-->

图 4. Windows CE3.0 应用程序虚拟内存空间

因为 Pocket PC 2002 是基于 Windows CE 3.0 的,因此运行在该平台上的应用程序会受到该虚拟内存空间的限制。

DLL 加载

除了在加载 XIP DLL 时 Windows CE 的早期版本与 Windows CE .NET 有额外 32 MB 空间的差异以外,Windows CE 用来加载 DLL 的技术与用于 Windows CE .NET 的技术是相同的。

当发出加载一个 DLL 的请求时,内核首先检查该 DLL 是否先前已被另一个应用程序加载,如果没有,并且 DLL 不是 XIP DLL,则内核将使用经过修改的从上到下搜索在 32 MB 虚拟内存映射中查找第一个可用的空间。搜索被认为经过修改,是因为内核将避免使用由另一个 DLL 使用的任何地址,即使该 DLL 不是由当前进程加载的。该搜索技术确保了将系统中的每个 DLL 加载在唯一、非重叠的地址中。

之所以必须使用唯一地址,是因为如果 DLL 是由多个进程加载的,则在所有过程中它必须位于相同的虚拟地址中。通过用唯一的地址加载每个不同的 DLL,内核可以确保如果应用程序想加载由另一个进程先前加载的 DLL,则在其他进程中 DLL 所映射的虚拟地址可用于请求该 DLL 的进程。图 5 显示了三个进程分别加载一系列 DLL 的关系图。在该图中,DLL A 由所有三个进程加载在相同地址上。进程 2 加载 DLL C,后者位于比进程 1 所加载的 DLL B 和 DLL A 更低的地址空间中。进程 C 随后加载 DLL A 和它自己的 DLL D。注意,在每个进程中,相同的 DLL 加载在相同的地址中,而每个不同的 DLL 则加载在唯一的地址中。

<!--[if !vml]-->

Windows CE .NET 高级内存管理

<!--[endif]-->

图 5. 加载一系列 DLL 的三个进程

现在考虑如果遇到潜在的问题该怎么办。假设进程 2 加载了很大的 DLL C(如图 6 所示)。注意,进程 3 正好是一个大型 .exe 文件,并且在进程 2 已加载了相当大的 DLL C 之后进程 3 也要加载 DLL。很显然,如果进程 3 试图加载还没有被其他进程加载的任何其他 DLL,它很可能遇到麻烦。该示例有点故意设计的成分,因为 DLL C 必须具有难以置信的大小,或者进程 2 必须加载大量 DLL,之后该问题才会自然发生。

<!--[if !vml]-->

Windows CE .NET 高级内存管理

<!--[endif]-->

图 6. 三个进程加载一系列 DLL,其中进程 2 加载大型 DLL

讨论了常规加载 DLL 后,现在是讨论如何处理复杂的 XIP 和非 XIP DLL 的时候了。当 OEM 创建 ROM 镜像时,每个现场执行 DLL 都将被定址在一个唯一地址上。以这种方式,所有 XIP DLL 就能在相互不发生冲突的情况下被加载。因为它们是 XIP,所以包含 DLL 代码的 ROM 可以直接映射到请求它的任何应用程序的虚拟地址空间。XIP DLL 在被进程加载时不能再定址到另一个地址,因为更改基址将涉及修改只读代码。

内核在为非 XIP DLL 查找可用的虚拟地址时,它会从最低定址 XIP DLL 的下面开始搜索可用的虚拟地址。这不是应用程序已加载的最低定址 XIP DLL,而是整个系统中的最低定址 XIP DLL,无论它是否是由任何应用程序加载的。在这里,该技术再次保证了当前加载的每个 DLL 都可以被其他进程加载。尽管该系统运行得很好,但因为某个 DLL 在 Pocket PC 2002 的 Windows CE .NET 中可能只能有唯一的实现,所以有时 DLL 不会被其他进程加载。

在 Pocket PC 2002 上的 Windows CE .NET 实现利用了 Windows CE 3.0 中的功能,该功能允许在设备上使用多个 ROM。该功能允许在系统中使用多个 ROM,即使它们没有连续的物理地址。

上面已经提到,DLL 需要经过特殊的处理才能成为 XIP。因为对 DLL 的定址需要更改 DLL 的代码,所以在创建 ROM 镜像时 DLL 必须被定址。创建第一个 ROM 时,ROM 创建工具将对每个 DLL 定址,以便它不会与 ROM 中的任何其他 DLL 发生重叠。

使用多个 XIP 区域意味着 DLL 加载问题需要内核设计者重新考虑。要确保在多个 XIP 区域系统上 XIP DLL 永远不会重叠,必须将第二个 ROM 上的 DLL 定址到比第一个 ROM 镜像的最低 DLL 更低的虚拟地址。如果使用了其他 ROM,则这些 XIP 区域中的 DLL 还必须定址到比前一个 ROM 更低的地址。

由于其他原因,使用多个 ROM 镜像很容易。如果 OEM 或 Microsoft 想更新 Windows CE 镜像的一部分,它们可以为具体 ROM 发出更新,而不必更新整个系统。为了保证一个 ROM 的更新不需要有对另一个 ROM 的更改,Microsoft 鼓励不要将定址于较低镜像中的 DLL 定址到前一个镜像中最低 DLL 的地址,而应当定址在比它更低的地址上,以在一组 DLL 和另一组 DLL 之间人为引入虚拟内存空隙。

负责 Pocket PC 2002(基于 Windows CE 3.0)的 Microsoft 内部开发人员最大限度地利用了多个 XIP 区域。大多数 Pocket PC 实现都有五个或更多个 XIP 区域。问题是区域之间的空隙太大。Pocket PC 2002 镜像中的最低定址 XIP DLL 通常定址在 0x0100000 以下。因为 Windows CE 将基于 RAM 的 DLL 放在最低 XIP DLL 的下面,所以可供基于 RAM 的 DLL、应用程序代码、它的堆和堆栈使用的空间没有限制在 32 MB 虚拟地址空间的范围内,而是在最低 XIP DLL 下面的空间中(小于 16 MB)。

图 7 显示了 Pocket PC 2002 的问题。注意,XIP DLL 的虚拟内存空间中的区域相当大。事实上,这幅图很保守,因为它没有显示 XIP 区域接管虚拟内存空间的一半的情形,而 Pocket PC 2002 上通常就是这种情况。注意基于 RAM 的 DLL 的加载;A、B、C 和 D 位于虚拟地址空间中低很多的位置。

<!--[if !vml]-->

Windows CE .NET 高级内存管理

<!--[endif]-->

图 7. 在 Pocket PC 2002 上加载 DLL,大部分虚拟地址空间被 XIP DLL 使用

对于处理海量数据的公司应用程序,公司开发人员被迫在他们的 Windows CE 应用程序中使用大型数据库。通常数据库引擎被实现为 DLL,而它通常很大。在上面的示例中,数据库 DLL 是制造麻烦的 DLL C。可用于 Pocket PC 2002 应用程序的虚拟内存空间小于 16 MB,而人们又需要大型的、基于 RAM 的 DLL,这使得很多开发人员发现他们的应用程序将由于缺少空间而无法运行 — 不是缺少 RAM,而是虚拟内存空间。

组合 DLL

可用来减轻 Pocket PC 2002 上的该问题的技术有不少。首先,开发人员应当通过将小型 DLL 组合成更大的 DLL 来减少 DLL 的数目。每个 DLL 至少占据一个 64 KB 区域。如果应用程序有 4 个 DLL,每一个的大小是 20 KB,则 DLL 使用的总计内存空间是 256 KB。通过组合四个 DLL,所得到的大型 DLL 将仅消耗 64 KB 虚拟内存空间 — 代码只占用 60 KB,但最低内存使用量是 64 KB。常规规则是,将 DLL 组合成(但不超过)64 KB 的倍数的大小。在某些包含过多小型 DLL 的应用程序中,只需将 DLL 组合成几个大型 DLL 就能解决应用程序的 DLL 加载问题。

将 DLL 代码转移到应用程序

在 Pocket PC 2002 中减少 DLL 问题的另一个方法是将 DLL 中的代码转移到应用程序。即使多个进程共享代码,有时在多个进程中复制代码也是有利的,因为不同进程将独立于其他应用程序被加载到内存中。

首先,将代码移动到应用程序中似乎没有帮助 — 代码仍然在应用程序的 32 MB 虚拟空间中。但是,这里的关键是要使某些代码成为不需要大型的、基于 RAM 的 DLL 的大型应用程序,而使其他代码成为加载和使用基于 RAM 的 DLL 的小型应用程序。在该技术中,大型应用程序执行大多数业务逻辑和用于加载大型 DLL 的小型应用程序。如果大型应用程序需要得到大型 DLL 的服务,它必须使用进程间通信让较小的进程调用该 DLL,并通过再次使用进程间通信将数据返回给大型进程。

定义 DLL 加载顺序

减少 DLL 数或转移应用程序的代码还不够,下面讨论更基本的方法:手动指定 DLL 的加载顺序。加载顺序是重要的,因为如果大型 DLL 在早期加载,它将迫使所有随后的小型 DLL 的加载地址向下转移。通常,大型 DLL 被单个应用程序使用。但如果它被早期加载,它可以迫使其他应用程序 DLL 的加载地址向下转移到无法加载这些 DLL 的位置,从而冲击其他应用程序。

解决方案是首先加载小型 DLL,然后让会造成影响的大型 DLL 在晚期加载,甚至最后加载。这就产生了如何强制执行 DLL 加载顺序的问题。一个方式是对应用程序套件中不同进程的启动顺序进行排队,但这有时会有问题。

另一个定义 DLL 加载顺序的方式是编写一个运行于主要应用程序之前的小型应用程序,让它通过重复调用 Win32 函数 LoadLibrary,按定义好的顺序加载基于 RAM 的 DLL。DLL 加载程序在主应用程序的生存期内一直运行,然后终止。它甚至可以通过调用 CreateProcess 来启动主应用程序,并通过阻塞 CreateProcess 所返回的进程句柄而进入等待状态,直到主应用程序终止。加载 DLL 的应用程序不会使用很多 RAM,因为被加载的 DLL 最后全部都要由其他进程来加载。

为解决 Pocket PC 2002 上的 DLL 加载问题而讨论的所有解决方案都各有缺点。没有一个是完美的或不可辩驳的。但是,它们确实是开发人员用来开发其产品的解决方案。以后发布的 Pocket PC 应当解决该问题,但对开发 Pocket PC 2002 产品的开发人员来说,及时解决问题是关键的。

通过知道 Windows CE 如何管理内存,开发人员就能更快地避免缺陷,并诊断问题。理解 Windows CE 如何管理 DLL 将有助于避免 Pocket PC 2002 应用程序中潜在的问题。即使未来发布的 Pocket PC 解决了该问题,已经在使用的数百万设备仍然需要应用程序。知道从哪里查找问题是找到并解决问题的第一步。

继续阅读