适用于: 下载 CFInRT_used_for_actual_measurements.exe。 下载 RTCF.exe。 摘要:Visual Studio .NET 2003 的到来为智能设备可编程性提供了集成的支持,从而可以使用托管代码为多种设备开发应用程序。软件开发人员现在可以在设备开发过程中使用象 Visual Basic .NET 和 Visual C# 这样的新型语言。尽管这听起来很令人鼓舞,但仍有一个问题需要回答:使用托管代码为嵌入式设备编写应用程序时,是否可以利用 Windows CE .NET 的实时功能?本文将回答这个问题,并提出一种可能的方案,将实时行为与 Microsoft .NET 功能结合起来。 目录 托管环境和非托管环境象 Microsoft® 公共语言运行库这样的托管环境的某些优势(例如,编写更安全且平台独立的软件)在实时环境中可能会成为劣势。一般来说,您不能在使用一种方法之前等待实时 (JIT) 编译器编译这种方法,也不能等待内存回收器通过删除不使用的资源来清除以前分配的内存。而这两种特性都会影响确定性的系统行为。可以通过调用 有效的平台调用根据 MSDN® 帮助,平台调用是公共语言运行库提供的功能,它使托管代码能够调用非托管的本机动态链接库 (DLL) 入口点。换句话说,平台调用提供了一条从托管 Microsoft .NET 代码到非托管 Win32 代码的切换路径。为了能够在 Microsoft Windows® CE .NET 内使用此机制,必须将要调用的本机 Win32 函数在 DLL 中定义为外部公开。由于托管 .NET 环境不知道 C++ 名称混成的任何情况,因此从托管应用程序内调用的函数还应具有 C 命名规则。为了能够使用 DLL 中的功能,需要在托管应用程序内的功能入口点周围构建一个包装类。列表 1 显示了一个小型非托管 DLL 的示例。列表 2 显示如何从托管代码中调用非托管 DLL。由于此机制适用于所有输出的 DLL 函数,而且几乎所有 Win32 API 都被输出到 coredll.dll 中,因此此机制也提供了一种方法,用来调用几乎所有的 Win32 API。我们在测试中使用了平台调用,以便从托管应用程序中调用非托管实时线程。 // 这就是函数 GetTimingInfo,位于 // 非托管 Win32 DLL 中。此函数需要一些信息, // 这些信息来自同一 DLL 中的一个中断服务 // 线程。请求托管应用程序时,使用 // 双缓冲机制来复制计时信息。 RTCF_API DWORD GetTimingInfo(LPDWORD lpdwAvgPerfTicks, LPDWORD lpdwMax, LPDWORD lpdwMin, LPDWORD lpdwDeltaMax, LPDWORD lpdwDeltaMin) { g_bRequestData = TRUE; if (WaitForSingleObject(g_hNewDataEvent, 1000)==WAIT_OBJECT_0) { *lpdwAvgPerfTicks = g_dwBufferedAvgPerfTicks; *lpdwMax = g_dwBufferedMax; *lpdwMin = g_dwBufferedMin; *lpdwDeltaMax = g_dwBufferedDeltaMax; *lpdwDeltaMin = g_dwBufferedDeltaMin; return 1; } else return 0; } // GetTimingInfo 原型 #ifdef RTCF_EXPORTS #define RTCF_API __declspec(dllexport) #else #define RTCF_API __declspec(dllimport) #endif extern "C" { RTCF_API BOOL Init(); RTCF_API BOOL DeInit(); RTCF_API DWORD GetTimingInfo(LPDWORD lpdwAvgPerfTicks, LPDWORD lpdwMax, LPDWORD lpdwMin, LPDWORD lpdwDeltaMax, LPDWORD lpdwDeltaMin); } 列表 1:要从托管代码中调用的 Win32 DLL // 能够平台调用到 DLL 中的包装类 // DLL 中的输出函数由此包装类 // 导入。请注意,如何使用编译器属性来识别 // 集成了输出函数的实际 DLL。 using System; using System.Runtime.InteropServices; namespace CFinRT { public class WCEThreadIntf { [DllImport("RTCF.dll")] public static extern bool Init(); [DllImport("RTCF.dll")] public static extern bool DeInit(); [DllImport("RTCF.Dll")] public static extern uint GetTimingInfo( ref uint perfAvg, ref uint perfMax, ref uint perfMin, ref uint perfTickMax, ref uint perfTickMin); } } // 从托管代码中调用非托管函数 public void CollectValue() { if (WCEThreadIntf.GetTimingInfo(ref aveSleepTime, ref maxSleepTime, ref minSleepTime, ref curMaxSleepTime, ref curMinSleepTime) != 0) { curMaxSleepTime = (uint)(float)((curMaxSleepTime * scaleValue) / 1.19318); curMinSleepTime = (uint)(float)((curMinSleepTime * scaleValue) / 1.19318); aveSleepTime = (uint)(float)((aveSleepTime * scaleValue) / 1.19318); maxSleepTime = (uint)(float)((maxSleepTime * scaleValue) / 1.19318); minSleepTime = (uint)(float)((minSleepTime * scaleValue) / 1.19318); } StoreValue(); counter = (counter + 1) % samplesInMinute; } 列表 2:调用非托管代码 实时方案系统需要真正的实时功能从外部数据源检索信息。信息存储在系统中,并且会以某种图形化的形式向用户显示。图 1 显示了解决此问题的一种可能的方案。 图 1:使用托管和非托管代码的实时方案 位于本机 Win32 DLL 中的实时线程接收外部数据源的中断。该线程会处理中断并存储要向用户显示的相关信息。在右边,用托管代码编写的单独 UI 线程将读取实时线程以前存储的信息。由于在进程之间切换环境的代价非常大,因此您希望让整个系统位于同一进程内。如果通过将实时功能放到 DLL 中并在该 DLL 与系统的其他部分之间提供接口,把实时功能与用户界面功能分开,这样就实现了用单一进程来处理系统的所有部分的目标。UI 线程与实时 (RT) 线程之间的通信是通过使用平台调用进入本机 Win32 代码来实现的。 实际测试您希望使测试具有代表性,但要尽可能简单,以便使它能够很容易地在其他系统上重复。为此,可以下载源代码来亲自进行实验。 此测试需要提供一种向系统通知中断的方法,还要求能输出探测来测量系统性能。 使用由信号生成器生成的方波来通知系统。 当然,Windows CE .NET 操作系统应能够集成 .NET Compact Framework。 Paul Yao 写的一篇文章中提到应使用哪些 Windows CE .NET 模块和组件来运行托管应用程序。 请参阅适用于 Windows CE .NET 的 Microsoft .NET Compact Framework。 测试的目的不只是具有代表性和可重复性,而且也包括为输入找到适当的中断源。 列表 3 显示了如何将物理中断挂接到中断服务线程上。 RTCF_API BOOL Init() { BOOL bRet = FALSE; DWORD dwIRQ = IRQ; // 在我们的例子中,IRQ = 5 // 获取指定 IRQ 的 SysIntr if (KernelIoControl(IOCTL_HAL_TRANSLATE_IRQ, &dwIRQ, sizeof(DWORD), &g_dwSysIntr, sizeof(DWORD), NULL)) { // 创建一个事件来激活 IST g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); if (g_hEvent) { // 将中断连接到事件,并 // 创建中断服务线程。 // 实际的 IST 显示在列表 4 中 InterruptDisable(g_dwSysIntr); if (InterruptInitialize(g_dwSysIntr, g_hEvent, NULL, 0)) { g_bFinish = FALSE; g_hThread = CreateThread(NULL, 0, IST, NULL, 0, NULL); if (g_hThread) { bRet = TRUE; } else { InterruptDisable(g_dwSysIntr); CloseHandle(g_hEvent); g_hEvent = NULL; } } } } return bRet; } 列表 3:将物理中断连接到中断服务线程 为了利用托管代码和 .NET Compact Framework 来测试应用程序的实时行为,我们基于 Standard SDK 创建了 Windows CE .NET 平台。我们还在平台中包含了 .NET Compact Framework 的 RTM 版本。操作系统在频率为 300 MHz 的 Geode GX1 上运行。通知系统时使用方波,它会立即连接到 PC104 总线(第 23 针)上的 IRQ5 线。方波的频率为 10 kHz。在上升的侧面,会生成一个中断。该中断由中断服务线程 (IST) 处理。在 IST 中,我们将探测脉冲发送到并行端口,以便查看输出信号。还利用高精度 QueryPerformanceCounter API 来存储激活 IST 的时间。为了能够测量较长时间段内的计时信息,除了平均时间以外,我们还存储了最长和最短时间。从中断发生到探测输出的这段时间表示 IRQ - IST 滞后时间。高精度计时器获得的计时信息指示激活 IST 的时间。理想情况下,对于 10 kHz 的中断频率,此值应该为 100 微秒。所有计时信息均按照固定的时间间隔传递到图形用户界面。 由于 .NET Compact Framework 本身并不能在真正实时的情况(如前所述)下使用,因此,我们决定将其仅用于显示目的,而对于所有实时功能,则使用由嵌入式 Microsoft Visual C++® 4.0 编写的 DLL。为了在 DLL 与 .NET Compact Framework 图形用户界面 (GUI) 之间进行通信,我们结合使用了双缓冲机制和平台调用。GUI 利用 System.Threading.Timer 对象,按照固定的时间间隔来请求新的计时信息。DLL 决定何时有时间将信息传递给 GUI。数据准备好之前会禁用 GUI。GUI 中显示的信息的刷新率可由用户选择。在我们的测试中,使用了 50 毫秒的刷新率。 以下伪代码解释了 IST 的操作以及 GUI 检索本机 Win32 DLL 中存储的信息的机制。 Interrupt Service Thread: Wait On IRQ 5 send probe pulse to the parallel port Measure time with QueryPerformanceCounter Store measured time (min, max, current, average) locally if (userInterfaceRequestsData) { copy measured time information reset statistic measure values set dataReady event userInterfaceRequestsData = false } 托管代码定期更新显示数据: disable timer // 请参阅“缺陷” call with P/Invoke into the DLL // 以下代码在 DLL 中实现 userInterfaceRequestsData = true wait for dataReady event return measured values draw measured values on the display, each time using new graphics objects update marker // 显示屏上的滚动垂直条 enable timer 在测试过程中,我们挂接了一个示波器,并在实验中安排了 10 分钟,同时打印输出范围和 Windows CE .NET 图形显示。图 2 显示了使用示波器测量的中断滞后时间。在最佳情况下,滞后时间为 14.0 微秒,在最差情况下,滞后时间为 54.4 微秒,即抖动为 40.4 微秒。图 3 显示了激活 IST 的周期。此图是实际用户界面的屏幕快照。理想情况下,IST 应该每 100 微秒运行一次,即我们测量过程中的平均时间(中间的蓝线)。除了 50 毫秒的采样周期(白色方块)内的最短和最长时间以外,我们还测量了总体最短(绿色)和最长(红色)时间。测试周期内的偏差不超过 ±40 微秒。 图 2:托管应用程序:IRQ.IST 滞后时间 图 3:托管应用程序:运行 10 分钟后 IST 激活的次数 结果我们用了较长的时间进行测量,以确保内存回收器和 JIT 编译器经常处于活动状态。感谢 Microsoft 人员提供了性能计数器的注册项,使我们能够监视 .NET Compact Framework 的行为。使用此注册项,可以在 .NET Compact Framework 中激活多个性能计数器。我们主要使用了此性能信息来验证确实运行了 JIT 编译器和内存回收器。此性能信息还明确显示出测试过程中使用的对象数目。 // 要用来收集新数据和 // 刷新屏幕的定期计时器方法 private void OnTimer(object source) { // 临时停止计时器,以防止 // 调用全部的 OnTimer if (theTimer != null) { theTimer.Change(Timeout.Infinite, dp.Interval); } Pen blackPen = new Pen(Color.Black); Pen yellowPen = new Pen(Color.Yellow); Graphics gfx = CreateGraphics(); td.SetTimePointer(dp.CurrentSample, gfx, blackPen); for (int i = 0; i < dp.SamplesPerMeasure; i++) { td.ShowValue(dp.CurrentSample, dp[i], gfx, i); } dp.CollectValue(); td.SetTimePointer(dp.CurrentSample, gfx, yellowPen); gfx.Dispose(); yellowPen.Dispose(); blackPen.Dispose(); // 为下一次更新重新启动计时器 if (theTimer != null) { theTimer.Change(dp.Interval, dp.Interval); } } 列表 4:在托管环境中处理计时器消息 如列表 4 所示,每当周期性地更新屏幕时,都会实例化多个对象。这些对象(两个笔对象和一个图形对象)是在每次屏幕更新期间创建的。函数 td.ShowValue 和 td.SetTimerPointer 还会创建画笔。由于每次屏幕更新时,td.SetTimerPointer 都会被调用两次,因此每次屏幕更新期间共创建六个对象。由于每 50 毫秒更新一次屏幕,因此每秒创建 120 个对象。在 10 分钟的执行时间里,共创建 72,000 个对象。所有这些对象都可能由内存回收器管理。在表 1 中,已分配对象的数目大致等于这些理论值。
表 1:测试运行五分钟后 .NET Compact Framework 的性能结果 结果中分别包含了运行 10 分钟和运行 100 分钟的性能计数器结果。此数据是在实际测试过程中记录的。可以看出,运行 10 分钟后,发生了内存回收,且性能没有明显下降。表 2 显示了运行大约 100 分钟后的性能计数器。此次运行过程中发生了完整内存回收。在此次运行过程中,仅创建了 461,499 个对象,而不是预期的 720,000 个。这比预期对象数大约少 35%。此差异很可能是由于性能计数器所致。按照 Microsoft 的测试结果,在托管应用程序中,性能计数器会导致大约 30% 的性能损失。但是系统的实时行为未受到影响,如图 4 所示。
表 2:测试运行 100 分钟后 .NET Compact Framework 的性能结果 图 4:托管应用程序:运行 100 分钟后 IST 激活的次数 远程进程查看器提供了多种证据,证明内存回收器和 JIT 编译器不影响实时行为。图 5 显示了用于托管应用程序的远程进程查看器的屏幕转储。应用程序中的所有线程(优先级为 0 的实时线程除外)都以正常优先级 (251) 运行。在我们的测量中,没有发现 JIT 编译器和内存回收器需要内核阻断才能执行任务。 图 5:显示托管应用程序的远程进程查看器 缺陷在测试过程中,提高方波的频率会在托管应用程序中产生意外的结果。尤其是当某些屏幕区域无效而需要频繁重画时,应用程序会随机地挂起系统。进一步调查显示,此问题是由于经验丰富的 Win32 程序员的疏忽造成的。在 Win32 应用程序中,每当计时器到期时,使用计时器都会产生一条 结果证明为了能够将我们的实验结果与相同设置中的典型结果进行比较,我们还编写了一个 Win32 应用程序,该应用程序调用具有实时功能的同一个 DLL。Win32 应用程序在功能上与托管应用程序相同。它可以为系统提供一个图形用户界面,计时信息显示在其中的一个窗口中。当收到 图 6:Win32 应用程序:运行 10 分钟后 IST 激活的次数 在图 7 中,当激活 IST 时,显示周期时间(对于 Win32 应用程序也如此)。同样,这些结果与使用 .NET Compact Framework 托管应用程序的结果也相同。托管应用程序和 Win32 应用程序的源代码都可以通过下载得到。 图 7:Win32 应用程序:运行 10 分钟后 IST 激活的次数 小结我们并非建议将 .NET Compact Framework 单独用于某项实时工作,而是希望能将其用作表示层,这一点很重要。在这样一个系统中,.NET Compact Framework 可与实时功能“和平共存”,而不会影响 Windows CE .NET 的实时行为。在本文中,我们没有对 .NET Compact Framework 的图形功能进行基准测试。在我们的测试中,没有发现完全用 Win32 编写的应用程序与部分在托管环境中用 C# 编写的应用程序之间有任何明显差别。.NET Compact Framework 可以提高程序员的工作效率,并且可以提供丰富的功能,因此用托管代码编写表示层、并用非托管代码编写绝对实时功能具有很多优势。这些不同类型的功能之间的明显区别可以通过此方法来消除。 致谢我们已经考虑了很久,希望在实时情况下测试 .NET Compact Framework 的可用性。但是此测试需要与能够提供所需硬件和测量设备的人员和公司共同完成。因此,我们要感谢 Getronics 的 Willem Haring 在此项目中为我们提供了支持、意见和热情的招待。我们还要感谢 Delem 的人员对我们的热情招待,以及为我们提供了测试所需的设备。 关于作者Michel Verhagen 在荷兰的 PTS Software 工作。Michel 是一名 Windows CE .NET 顾问,在 Windows CE 方面已经积累了四年经验。他的主要专长在 Platform Builder 领域。 Maarten Struys 也在 PTS Software 工作,负责实时和嵌入式方面的内容。Maarten 是一名经验丰富的 Windows (CE) 开发人员,从推出 Windows (CE) 时起,就开始从事 Windows CE 方面的工作。自 2000 年以来,Maarten 开始在 .NET 环境中使用托管代码。他还是荷兰有关嵌入式系统开发领域的两家权威杂志的自由撰稿人。他最近开设了一个 Web 站点,用来提供有关嵌入式环境中 .NET 的信息。 其他资源有关 Windows CE .NET 的详细信息,请参阅 Windows Embedded Web 站点(英文)。 有关 Windows CE .NET 中包含的联机文档和上下文相关帮助,请参阅 Windows CE .NET 产品文档(英文)。 有关 Microsoft Visual Studio® .NET 的详细信息,请参阅 Visual Studio Web 站点(英文)。 |
文章来源于领测软件测试网 https://www.ltesting.net/