Windows Mobile 和 Windows CE 任务管理器






4.51/5 (20投票s)
编写自己的 Windows Mobile 或 Windows CE 设备任务管理器的源代码

引言
本文介绍了如何为 Windows Embedded CE 操作系统编写任务管理器。示例代码运行在 Windows Mobile 6.x 或任何其他基于 Windows Embedded CE 的操作系统镜像上。要从 Visual Studio 2005 或 2008 中开发 Windows Mobile 6.x 应用程序,您需要从 Microsoft 下载单独的 SDK。SDK 还附带了一个功能齐全的模拟器,这是一个强大且不可或缺的工具,用于测试您的应用程序。
经验丰富的开发人员会知道 Windows Mobile 6.x 基于 Windows Embedded CE 5.0。然而,该代码也已在采用不同内存模型的 Windows Embedded CE 6.0 上进行测试。由于目前还没有基于此版本 Windows Embedded CE 的 Windows Mobile 版本,您需要开发自己的操作系统镜像。如何做到这一点超出了本文的范围。
此示例代码使用 COM 和 Compact .NET Framework 3.5。
背景
我的工作是为实时控制机器编写软件,即软件执行代码的时间应该是确定性的和可预测的。这项工作的最低要求是您拥有一个实时操作系统 (RTOS)。几年来,我们为此目的使用了 Windows Embedded CE (5.0 和 6.0)。
为了监控我们应用程序的实时行为,我们使用了大量的监控和跟踪工具,其中包括一些自制的工具。一个用于判断我们当前系统整体行为(CPU 负载、内存分配、线程数...)的工具是任务管理器,在桌面 Windows 上也已知。不幸的是,Windows Embedded CE 默认不提供这样的工具。起初我们搜索了互联网,但找不到一个包含我们所有要求的便捷工具。所以我们决定自己编写一个,而且我们现在可以在需要时添加新功能。
要编写自己的任务管理器(无论是桌面 Windows 还是 Windows Embedded CE),您可以使用 Microsoft 的 ToolHelp
API 库(适用于 Windows Embedded CE 和桌面 Windows),它们可以帮助您查询内存使用情况、系统资源……为了编写需要良好用户界面的程序,我更喜欢使用 Compact .NET Framework (C#)。
Compact .NET Framework 没有直接访问 WIN32 ToolHelp
API 的库。所以我开始编写通过 P/Invoke 调用这些函数的代码。起初看起来运行良好,但我后来发现了一个棘手的问题。如果您查询一个在查询期间停止/终止的正在运行进程的属性,结果发现 ToolHelp
API 会使您的 .NET 应用程序崩溃。用 C# try catch
块包围 P/Invoke 调用并没有帮助。最终发现 MSDN 文档警告您代码中可能存在的缺陷,因为它指出所有 ToolHelp
调用都应该用 WIN32 __try __except
块保护。哎呀,您不能在 C# 中使用这种 Win32 (C++) 机制。
注意:如果您仔细检查代码,您会发现我没有使用
__try __except
,而是使用(更喜欢)标准的 C++try catch
语句。要将所有 SEH 异常导向标准 C++try catch
,您需要指定 /EHa 编译器选项。这将确保例如访问冲突或任何其他 SEH 异常都将被try { } catch (...)
捕获。
为了克服这个问题,我决定将应用程序拆分为 UI 部分和用 C++ 编写的 COM DLL,后者负责实际(棘手)的工作。C++ 允许我使用 WIN32 __try __except
机制,并将这种复杂性隐藏在 .NET 之外。此外,如果您将代码拆分为不同的层,它总是看起来更令人印象深刻和更专业,不是吗?
得益于 COM 互操作——从 Compact .NET Framework 2.0 开始支持——这不应该是一个大问题。但谁说编写软件很容易呢?在开发过程中,我发现了 Microsoft COM 互操作库中的两个主要错误。这些问题已报告给 Microsoft(并确认为错误),并有望在 Compact .NET Framework 的下一个版本中修复。尽管如此,我现在将向您介绍这些错误的解决方法,这也是我决定撰写本文的原因之一。
代码
该 COM DLL 名为 Toolbox.dll,并实现了以下 COM coclasses:
CpuLoad
:每秒发出一个事件,告知您系统的整体 CPU 负载ProcessList
:枚举当前系统上运行的所有“Process
”Process
:告知您有关进程的所有详细信息:使用的系统资源(虚拟内存、堆、DLL 计数、线程计数...)System
:为您提供一些系统范围的信息(总虚拟内存、总堆等)以及一些通用功能,例如终止或启动进程
CpuLoad
您如何确定系统当前的 CPU 负载?
答案基本上很简单。
如果您运行一个低优先级线程并测量它在某个时间段内消耗了多少时间,您就会知道系统没有花在做其他事情上的时间(服务中断、运行其他线程……)。幸运的是,Windows Embedded CE 支持 GetThreadTimes()
WIN32 API,它会告诉您需要知道的一切。
我们的应用程序将创建一个具有最低优先级的线程,并每秒在其上调用 GetThreadTimes()
。我们需要另一个正常优先级的线程,它将每秒短暂运行一次,计算 CPU 负载并触发一个 COM 事件。
错误 1
在我们的 C# 代码中,我们订阅了 COM 事件。但结果发现,当 Compact .NET Framework 垃圾回收器运行时,它错误地取消了我们的事件订阅。
如果您运行示例代码一段时间并在 CProxy_ICpuLoadEvents::Unadvise()
方法中设置断点,您就可以看到这一点。过了一段时间——通常是当您切换到另一个应用程序再切换回我们的示例应用程序时——GC 会启动并完全在我们无法控制的情况下取消我们的事件订阅。这当然是不希望发生的。
解决方案是手动从 C# 调用 CProxy_ICpuLoadEvents::Advise()
方法,如下所示...
System.Runtime.InteropServices.ComTypes.IConnectionPointContainer iCPC =
(System.Runtime.InteropServices.ComTypes.IConnectionPointContainer)m_cpuLoad;
System.Runtime.InteropServices.ComTypes.IConnectionPoint iCP;
Guid IID_ICpuLoadEvents = new Guid("9C6705E3-E8F9-4692-8FC8-FE15B6226022");
iCPC.FindConnectionPoint(ref IID_ICpuLoadEvents, out iCP);
iCP.Advise(this, out m_cookie);
... 而不是使用标准 C# 订阅事件的方式
m_cpuLoad.Measurement +=
new ToolBoxLib._ICpuLoadEvents_MeasurementEventHandler(cpuLoad_Measurement);
进程列表
ProcessList
实现了保留的 COM __NewEnum()
方法,该方法在 C# 中转换为 IEnumerable<T>
接口。此接口允许您使用 C# foreach() { }
语句遍历所有正在运行的进程。
错误 2
我们希望定期使用当前运行进程列表更新我们的 UI 并显示它们的属性。在我们的应用程序中,我们为此使用了 CpuLoad
事件,尽管您也可以为此使用另一个计时器事件。现在,如果代码运行一段时间,COM 互操作库会在使用 foreach
迭代进程列表时抛出意外的“坏变量类型” (0x80020008)。
原来这是 COM 互操作库中的一个 bug,它错误地解释了 CComEnum<T>::Next()
方法返回的 HRESULT
值。CComEnum<T>
辅助类通常用于实现由 get__NewEnum(IUnknown** retval)
方法返回的 EnumVariant
对象。
根据 MSDN 的说法,当返回 S_FALSE
时,EnumVariant
的最后一个元素已到达,并且不必包含有效数据。但是 COM 互操作库似乎没有这样做,而是检查 VARIANT
是否具有有效的 VARTYPE
。但现在,由于 VARIANT
中的数据无效(当返回 S_FALSE
时,CComEnum<T>
不会初始化它),嵌入的 VARTYPE
可以是任何东西。如果遇到未知的 VARTYPE
,就会发出“坏变量类型”错误。
为了克服这个问题,我们简单地创建了一个新的 CComEnum2<T>
模板类,它继承了 CComEnum<T>
的大部分功能,除了我们将重新实现的 CComEnum<T>::Next()
方法。
// CComEnum2<> extends ATL CComEnum<> that the Next() method initializes its T parameter
template <class Base, const IID* piid, class T, class Copy,
class ThreadModel = CComObjectThreadModel>
class CComEnum2 :
public CComEnum<Base, piid, T, Copy, ThreadModel>
{
public:
STDMETHOD(Next)(ULONG celt, T* rgelt, ULONG* pceltFetched)
{
*rgelt = T();
return CComEnum::Next(celt, rgelt, pceltFetched);
}
};
我们首先确保调用模板类型 T
的默认构造函数以正确初始化它,然后才调用基 CComEnum<T>::Next()
。这适用于大多数类型,但当使用 VARIANT
时,这还不够。因此,当模板类型是 VARIANT
类型时,我们创建了一个模板特化。
// CComEnum2<> template class specialization where T = VARIANT
// This will overcome a bug in CF3.5 that will check erroneously VARIANT vt type
// when the last item of an IEnumVariant is checked for validity.
// CComEnum<> returns than S_FALSE
template <class Base, const IID* piid, class Copy,
class ThreadModel/* = CComObjectThreadModel*/>
class CComEnum2<Base, piid, VARIANT, Copy, ThreadModel> :
public CComEnum<Base, piid, VARIANT, Copy, ThreadModel>
{
public:
STDMETHOD(Next)(ULONG celt, VARIANT* rgelt, ULONG* pceltFetched)
{
VariantInit(rgelt);
return CComEnum::Next(celt, rgelt, pceltFetched);
}
};
这个特殊版本的 CComEnum2<T>::Next()
会在 VARIANT
上调用 VariantInit()
,以确保 VARTYPE
被正确初始化为 VT_EMPTY
。这似乎可以避免这个错误。在我们的示例代码中,CProcessList::get__NewEnum(IUnknown** retval)
使用新的 CComEnum2<T>
模板类,而不是普通的 CComEnum<T>
模板类。
进程
Process
实现了一个接口,该接口将提供有关 Process
的所有信息。它将为此使用 ToolHelp
API。
系统
System
实现了一个用于查询系统范围内存使用的接口,以及一个用于结束(终止)进程的接口。
测试和调试
示例包包含一个 Visual Studio 2008 解决方案,其中包含 2 个项目:
- CETaskManager.vcproj C# 项目,构建 CETaskManager.exe 可执行文件
- ToolBox.vcproj C++ 项目,构建
Toolbox
COM DLL
默认情况下,您可以将组件部署到 Windows Mobile 6.x 模拟器,或者如果需要,部署到真正的智能设备。
提示:由于这是一个“混合平台”解决方案,您可能需要单独部署每个项目。如果您选择“部署解决方案”,Visual Studio 可能只会部署“启动项目”。另外,请确保 CEChart.dll 存在于部署目录中。
值得注意的是,模拟器非常好,它也存在与真实设备相同的错误。微软在创建错误模拟器方面也做得很好。
该代码(C# 和 COM C++ 项目)定义了两个字符串 BUG1
和 BUG2
(参见 Form1.cs 和 ProcessList.cpp)。当这些字符串被定义时,代码将在仍然启用错误的情况下运行。通过取消定义这些字符串,您将编译这些错误的修复程序,程序将正常工作。
对于感兴趣的读者,我还包含了一个解决方案,它将为桌面 Windows 编译 ToolBox.dll 和 TaskManager.exe。这在调试和比较行为时特别有用。事实证明,桌面版本没有提及的两个错误。但我必须提到,桌面版本 ToolHelp
API 的性能与 CE 版本相比不佳。尽管如此,比较这两个版本还是很有趣的。
安装
- 在您的设备上部署 Compact Framework 3.5 runtime
- 将 TaskManager.exe 复制到设备的文件夹中,通常是 “%CSIDL_PROGRAM_FILES%\CETaskManager”
- 将 ToolBox.dll 和 Interop.ToolBoxLib.dll 复制到同一文件夹并注册 ToolBox.dll(所有 COM DLL 都需要)
- 将 CEchart.dll 复制到同一文件夹。此组件是绘制 CPU 负载历史图表所必需的。
当然,您也可以在调试时使用 Visual Studio 将组件部署到您的设备。
还包括一个适用于基于 Windows Mobile 6.x 设备的 CAB 文件(ARMV4I)。
结论
如果您是 Windows Mobile 开发人员并经常使用 COM 互操作,您迟早可能会遇到这些问题。我已为您提出了针对每个问题的解决方案。我希望这能鼓励您继续为 Windows 智能设备编写软件。如果您遇到问题,并且确信它们是错误,您可以随时联系 Microsoft 并尝试解决它们。我做过好几次了,都成功了。
作为额外福利,本文向您展示了编写自己的功能齐全的 TaskManager
的示例代码,这是任何为 Windows 智能设备编写软件的开发人员都不可或缺的重要工具。祝您好运。
参考文献
- Microsoft Windows Embedded 主页
- Microsoft Windows Compact .NET Framework 主页
- Microsoft Windows Mobile 主页
- 将 DCOM 远程处理功能带到 Windows CE 和 .NET CF2.0
致谢
非常感谢我的同事兼“深夜代码编写者”朋友 Kurt Mampaey,他为我提供了 CEChart.dll 组件。
历史
- 2008年11月10日:初始版本
- 2008年11月11日:版本 2 - 更新了演示和源文件
- 2008年11月29日:更正了一些语法错误并重新措辞了一些解释