技术开发 频道

Windows CE .NET 高级内存管理

 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 应用程序内存空间的关系图。

 图 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 则加载在唯一的地址中。

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

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

 图 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 位于虚拟地址空间中低很多的位置。

 图 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 解决了该问题,已经在使用的数百万设备仍然需要应用程序。知道从哪里查找问题是找到并解决问题的第一步。

0
相关文章