可视化 Windows Mobile 虚拟内存怪物






4.93/5 (40投票s)
本文介绍了 VirtualMemory.exe,这是一款内存监视器应用程序,可以图形化地可视化 Windows Mobile 的虚拟内存模型,从而可以快速轻松地识别常见的内存问题,例如臭名昭著的 DLL 压缩、普通的内存泄漏以及 device.exe 内存不足。
引言
Windows Mobile 的内存模型与大多数 32 位操作系统截然不同。典型的 32 位操作系统允许大量应用程序访问数 GB 的虚拟内存,通常远超系统上的物理内存量。例如,Windows XP 允许数十个进程各拥有 2 GB 的虚拟内存。然而,在 Windows Mobile 上,**最多只能存在 32 个应用程序**,并且每个应用程序通常**只能访问 32 MB 的虚拟内存**,这远少于现代设备的平均物理内存量。
当然,这样做是出于可以理解的原因——性能。十年前,Windows Mobile 设备处理器性能低下,内存非常有限。如果处理器不必执行太多工作,限制应用程序可以访问的虚拟内存量是有意义的。然而,如今处理器功能强大,内存廉价且充足。
因此,这一十年前的性能决策现在已经颠倒过来。Windows Mobile 的内存模型如今非但不能帮助系统,反而会引起难以调试的头疼问题。即使有充足的可用物理内存,应用程序也可能遇到内存不足的错误,或者某些功能可能无法加载。更令人困惑的是,系统上运行的其他程序,或者 OEM 设备制造商或运营商添加的功能,都可能直接导致这些问题的发生。
当然,微软提供了许多诊断工具,例如 DumpMem 来分析虚拟内存使用情况,以及 Application Verifier 来跟踪内存泄漏。它们生成的日志文件非常详细。但是,当问题涉及多个程序的交互时,很难从数页的十六进制输出中找出问题的根源。本文介绍了 *VirtualMemory.exe*,这是一款内存监视器应用程序,可以图形化地可视化 Windows Mobile 的虚拟内存模型,从而可以快速轻松地识别常见的内存问题,例如臭名昭著的 DLL 压缩、普通的内存泄漏以及 *device.exe* 内存不足。
背景
对 Windows Mobile 内存模型的全面阐述远远超出了本文的讨论范围。有关详细信息,请参阅以下精彩的博客文章(本文标题的灵感来源):
图形化输出
如上图所示,*VirtualMemory.exe* 为设备上的每个进程槽显示一列。槽 0 分配给当前正在运行的进程,因此不进行绘制。这样就剩下 32 个槽。槽 1 包含存储在 ROM 中的 DLL,通常是满的。接下来的几个槽通常是系统进程,例如 *filesys.exe*、*device.exe*、*gwes.exe* 等,它们在启动时尽早加载。之后是用户应用程序。每列的底部是从 0 开始的最低虚拟内存地址。这是程序代码所在的位置,并且向上增长的堆空间在此分配。列的顶部是最高的虚拟内存地址,达到 32 MB,DLL 驻留在该处并向下加载。
显然,Windows Mobile 设备显示器上的像素不足以清晰地显示每个单独虚拟内存块的状态。为了解决这个问题,每列的刻度颜色是根据其代表的内存块混合而成的。红色表示完全空闲。蓝色表示内存空间已保留。绿色表示内存空间已提交。混合颜色表示这三种状态的某种组合。正如预期的那样,大部分显示是红色的——空闲虚拟内存。也有大量的蓝色(保留)和一些绿色(提交)。请记住,Windows CE 在访问包含代码的地址空间之前不会提交它们。为了加快屏幕刷新速度,绘制是在屏幕外位图上进行的,然后将其blit到显示器上。
使用应用程序
启动内存监视器将显示设备当前的虚拟内存状态。左右导航按钮会突出显示当前槽,并显示相应的进程名称。单击“快照”软按钮将刷新虚拟内存视图。单击“退出”软按钮将退出应用程序。
幕后
分析虚拟内存
为了查看虚拟内存如何在设备上使用,使用了 `VirtualQuery` 系统调用。如下所示,它为每段具有相同分配类型的连续内存返回一个数据结构。为了使图形例程更容易处理,这些数据结构被扁平化,形成一个数组,详细说明每个内存块在每个槽中的状态(空闲、保留或已提交)。
void GetVirtualMemoryStatus(VIRTUALDATA *pvd)
{
MEMORY_BASIC_INFORMATION mbi;
int idx;
DWORD addr;
BYTE state;
memset(pvd->pageAllocated,0x00,sizeof(pvd->pageAllocated));
for(idx=STARTBAR;idx<STARTBAR+NUMBARS;idx++)
{
DWORD offset;
addr = idx * 0x02000000;
for( offset = 0; offset < 0x02000000; offset += mbi.RegionSize )
{
unsigned int i;
memset(&mbi,0x00,sizeof(MEMORY_BASIC_INFORMATION));
if(VirtualQuery( (void*)( addr + offset ), &mbi,
sizeof( MEMORY_BASIC_INFORMATION ) )==0)
break;
state=(BYTE)((mbi.State>>12)&(VMEMCOMMIT|VMEMRESERVE));
if(state)
{
for(i=(offset)/4096;i<(offset+mbi.RegionSize)/4096;i++)
pvd->pageAllocated[idx-STARTBAR][i]=state;
}
}
}
}
获取进程名称
识别哪个槽有问题还不够——为了使开发人员更容易,还应该确定占用该槽的进程的名称。下面的例程通过使用 `CreateToolhelp32Snapshot` 和 `OpenProcess` 系统调用来获取该信息。进程的槽是通过将基地址除以 32 MB 来找到的。
void GetProcessNames(VIRTUALDATA *pvd)
{
HANDLE hProcessSnap;
HANDLE hProcess;
PROCESSENTRY32 pe32;
int slot;
for(slot=STARTBAR;slot<STARTBAR+NUMBARS;slot++)
swprintf(pvd->szExeName[slot-STARTBAR],
TEXT("Slot %d: empty"),slot);
if((1-STARTBAR)>=0)
wcscpy(pvd->szExeName[1-STARTBAR],
TEXT("Slot 1: ROM DLLs"));
// Take a snapshot of all processes in the system.
hProcessSnap =
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS|TH32CS_SNAPNOHEAPS, 0 );
if( hProcessSnap != INVALID_HANDLE_VALUE )
{
memset(&pe32,0x00,sizeof(PROCESSENTRY32));
pe32.dwSize = sizeof( PROCESSENTRY32 );
if( Process32First( hProcessSnap, &pe32 ) )
{
do
{
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION,
FALSE,pe32.th32ProcessID );
if( hProcess != NULL )
{
slot=pe32.th32MemoryBase/0x02000000;
if(slot-STARTBAR<NUMBARS)
swprintf(pvd->szExeName[slot-STARTBAR],TEXT("Slot %d: %s"),
slot,pe32.szExeFile);
CloseHandle( hProcess );
}
} while( Process32Next( hProcessSnap, &pe32 ) );
}
CloseToolhelp32Snapshot( hProcessSnap );
}
}
如果使用的设备具有两层安全模型(大多数 Windows Mobile 标准设备都是如此),除非请求的应用程序经过特权签名,否则 `CreateToolhelp32Snapshot` 可能无法返回系统中所有进程的属性。*VirtualMemory.exe* 由特权 SDK 测试证书签名,该证书允许它检索所有模拟器映像上的所有进程名称。但是,此证书并未安装在实际设备上。要将其安装到设备上,必须先在设备上安装平台 SDK 中的 *SdkCerts.cab*,将 SDK 证书放入根存储区。否则,*VirtualMemory.exe* 将无法获取每个槽的所有进程名称。
常见问题
DLL 压缩
大多数应用程序使用动态链接库 (DLL) 来在应用程序之间共享代码。为了使 CPU 更容易地在进程之间共享 DLL,Windows Mobile 会尝试允许系统中每个进程中的 DLL 拥有虚拟地址空间,无论该进程是否使用该 DLL。这意味着一个进程的 DLL 会减少系统中每个其他进程的虚拟内存。因此,大多数进程在开始运行之前,其可用虚拟内存就远少于 32 MB。其中很大一部分已经被其他进程的 DLL 占用了。
这种虚拟内存的缩小可能导致一个称为 DLL 压缩的问题。随着越来越多的应用程序运行 DLL,空闲虚拟地址空间的上限不断降低。最终,应用程序的堆(向上增长)可能会与这个上限发生冲突。当发生这种情况时,该进程就没有更多的可用虚拟内存了。分配会失败,DLL 也无法加载,尽管系统上仍然有充足的物理内存。
本文开头图片显示了这个问题。最后 6 个槽已加载了需要三个不同、大型 DLL 的程序。从早期加载的程序建立的上限开始,`LoadLibrary` 将这三个 DLL(蓝色大块)逐渐加载到地址空间的较低位置(以防其他程序想要使用它们)。最终,没有更多的地址空间留给任何**新**的 DLL 加载。请注意,仍然有可能创建更多的进程(至少直到达到 32 个的限制),使用**相同**的 DLL。如图片所示,相同的前三个程序加载的第二个实例成功了。这是因为 Windows CE 会为每个 DLL 在所有进程中保留空间(这正是 DLL 压缩的根本原因!)。避免 DLL 压缩的一种常见方法是静态链接应用程序并避免使用 DLL。
内存泄漏
在虚拟地址空间的底部是可执行代码,其上方是堆。当程序分配更多内存时,堆会逐渐向上增长。一个正常运行的程序在运行时应该使用相对恒定的内存量。然而,如果在 *VirtualMemory.exe* 的连续快照中堆持续增长,这表明存在内存泄漏。最终,堆将达到 DLL 上限,应用程序将耗尽虚拟地址空间。使用 Application Verifier 追踪这些内存泄漏并修复它们!
Device.exe 内存已满
32 MB 虚拟内存限制最大的牺牲品之一是 *device.exe*。*Device.exe* 是 Windows Mobile 负责加载系统中所有设备驱动程序的进程。这意味着来自微软、OEM 设备制造商、运营商和 ISV 的驱动程序都必须挤在同一个 32 MB 的空间内。当然,驱动程序因设备而异,具体取决于 OEM 制造商和运营商决定安装的内容,因此第三方驱动程序可用的空闲虚拟内存量也各不相同。结果是,需要驱动程序的 ISV 应用程序在一个设备上可能运行正常,但在另一个设备上甚至可能无法加载!
本文开头的图片显示了 Windows Mobile 5 模拟器映像上 *device.exe* 的槽。正如您所见,它相对空闲。然而,真实世界的设备包含相机和其他占用大量内存的设备。配备大型视频缓冲区的多百万像素相机可以轻松消耗剩余的空闲虚拟内存。结果,使用以下方法加载售后驱动程序的 ISV 可能会遇到 `FILE_NOT_FOUND` 错误。
hISVdriver = ActivateDevice(TEXT("Drivers\\ISVdriver"), 0);
这个错误有点误导。驱动程序文件就在那里,但 *device.exe* 中容纳它的内存空间却不存在。唯一避免这种情况的方法是仔细分析您希望支持的每个运营商的每个设备。有些设备可能没有多少剩余内存了。
未来
随着 Windows Mobile 设备的功能越来越强大,虚拟内存问题变得越来越普遍。因此,OEM 厂商在设备上增加更多物理内存或更大的摄像头是没有意义的,因为这只会使虚拟内存问题变得更糟。幸运的是,微软的帮助即将到来。
Windows CE 6 的内存模型与 Windows CE 5(Windows Mobile 5 和 6 的基础)完全不同。应用程序数量不是 32 个,而是 32,000 个,每个应用程序都可以访问完整的 2 GB 虚拟内存。对于开发人员来说,这意味着随着 Windows Mobile 7 的推出,虚拟内存问题应该会完全消失。对于用户来说,这也意味着功能更强大、内存量大得多的设备即将到来!
关注点
*VirtualMemory.exe* 是为诊断在某个手机上加载设备驱动程序的问题而创建的。所有其他设备都工作正常,除了这个特定的型号,它返回了 `FILE_NOT_FOUND` 错误。问题最终是 *device.exe* 完全满了,这是因为运营商加载了额外的代码。
历史
- 2009 年 1 月 6 日 - 初始版本 (WJB)。