UML软件工程组织

.NET Compact Framework 的实时行为
Maartin Struys
Michel Verhagen
PTS Software

转载Microsoft

适用于:
    Microsoft® Windows® CE .NET
    Microsoft Visual Studio® .NET
    Microsoft Visual Basic® .NET
    Microsoft Visual C#®
    Microsoft .NET Compact Framework

下载 CFInRT_used_for_actual_measurements.exe

下载 RTCF.exe

下载 Win32CEAppInRT.exe

摘要:Visual Studio .NET 2003 的到来为智能设备可编程性提供了集成的支持,从而可以使用托管代码为多种设备开发应用程序。软件开发人员现在可以在设备开发过程中使用象 Visual Basic .NET 和 Visual C# 这样的新型语言。尽管这听起来很令人鼓舞,但仍有一个问题需要回答:使用托管代码为嵌入式设备编写应用程序时,是否可以利用 Windows CE .NET 的实时功能?本文将回答这个问题,并提出一种可能的方案,将实时行为与 Microsoft .NET 功能结合起来。

目录

托管环境和非托管环境

象 Microsoft® 公共语言运行库这样的托管环境的某些优势(例如,编写更安全且平台独立的软件)在实时环境中可能会成为劣势。一般来说,您不能在使用一种方法之前等待实时 (JIT) 编译器编译这种方法,也不能等待内存回收器通过删除不使用的资源来清除以前分配的内存。而这两种特性都会影响确定性的系统行为。可以通过调用 GC.Collect() 来强制内存回收器履行其职责。但您希望内存回收器能够自己执行任务,因为它经过了高度优化。为实现真正的实时行为,如果有一种方法可以将用本机或非托管 Microsoft Win32® 代码编写的真正的实时功能与用托管代码编写的其他功能区分开,那就太好了。利用平台调用 (P/Invoke),您就可以做到这一点。

有效的平台调用

根据 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.ShowValuetd.SetTimerPointer 还会创建画笔。由于每次屏幕更新时,td.SetTimerPointer 都会被调用两次,因此每次屏幕更新期间共创建六个对象。由于每 50 毫秒更新一次屏幕,因此每秒创建 120 个对象。在 10 分钟的执行时间里,共创建 72,000 个对象。所有这些对象都可能由内存回收器管理。在表 1 中,已分配对象的数目大致等于这些理论值。

计数器 n 平均值 最小值 最大值
程序总运行时间 603752 0 0 0 0
已分配的最大字节数 1115238 0 0 0 0
已分配的对象数 66898 0 0 0 0
已分配的字节数 1418216 66898 21 8 24020
简单回收数 0 0 0 0 0
按简单回收收集的字节数 0 0 0 0 0
简单回收后使用的字节数 0 0 0 0 0
简单回收时间 0 0 0 0 0
精简回收数 1 0 0 0 0
按精简回收收集的字节数 652420 1 652420 652420 652420
精简回收后使用的字节数 134020 1 134020 134020 134020
精简回收时间 357 1 357 357 357
完整回收数 0 0 0 0 0
按完整回收收集的字节数 0 0 0 0 0
完整回收后使用的字节数 0 0 0 0 0
完整回收时间 0 0 0 0 0
由应用程序导致的回收的 GC 数 0 0 0 0 0
GC 滞后时间 357 1 357 357 357
抖动的字节数 14046 259 54 1 929
抖动的本机字节数 70636 259 272 35 3758
抖动的方法数 259 0 0 0 0
间隔的字节数 0 0 0 0 0
间隔的方法数 0 0 0 0 0
异常数 0 0 0 0 0
调用数 3058607 0 0 0 0
虚拟调用数 1409 0 0 0 0
虚拟调用缓存命中数 1376 0 0 0 0
平台调用数 176790 0 0 0 0
回收后使用的总字节数 421462 1 421462 421462 421462

表 1:测试运行五分钟后 .NET Compact Framework 的性能结果

结果中分别包含了运行 10 分钟和运行 100 分钟的性能计数器结果。此数据是在实际测试过程中记录的。可以看出,运行 10 分钟后,发生了内存回收,且性能没有明显下降。表 2 显示了运行大约 100 分钟后的性能计数器。此次运行过程中发生了完整内存回收。在此次运行过程中,仅创建了 461,499 个对象,而不是预期的 720,000 个。这比预期对象数大约少 35%。此差异很可能是由于性能计数器所致。按照 Microsoft 的测试结果,在托管应用程序中,性能计数器会导致大约 30% 的性能损失。但是系统的实时行为未受到影响,如图 4 所示。

计数器 n 平均值 最小值 最大值
执行引擎启动时间 478 0 0 0 0
程序总运行时间 5844946 0 0 0 0
已分配的最大字节数 1279678 0 0 0 0
已分配的对象数 461499 0 0 0 0
已分配的字节数 8975584 461499 19 8 24020
简单回收数 0 0 0 0 0
按简单回收收集的字节数 0 0 0 0 0
简单回收后使用的字节数 0 0 0 0 0
简单回收时间 0 0 0 0 0
精简回收数 11 0 0 0 0
按精简回收收集的字节数 8514912 11 774082 656456 786476
精简回收后使用的字节数 1679656 11 152696 147320 153256
精简回收时间 5395 0 490 436 542
完整回收数 2 0 0 0 0
按完整回收收集的字节数 397428 2 198714 1916 395512
完整回收后使用的字节数 79924 2 39962 17328 62596
完整回收时间 65 2 32 2 63
由应用程序导致的回收的 GC 数 0 0 0 0 0
GC 滞后时间 5460 13 420 2 542
抖动的字节数 19143 356 53 1 929
抖动的本机字节数 95684 356 268 35 3758
抖动的方法数 356 0 0 0 0
间隔的字节数 85304 326 261 35 3758
间隔的方法数 385 0 0 0 0
异常数 0 0 0 0 0
调用数 21778124 0 0 0 0
虚拟调用数 1067 0 0 0 0
虚拟调用缓存命中数 1029 0 0 0 0
平台调用数 1996991 0 0 0 0
回收后使用的总字节数 5632119 13 433239 84637 493054

表 2:测试运行 100 分钟后 .NET Compact Framework 的性能结果

图 4:托管应用程序:运行 100 分钟后 IST 激活的次数

远程进程查看器提供了多种证据,证明内存回收器和 JIT 编译器不影响实时行为。图 5 显示了用于托管应用程序的远程进程查看器的屏幕转储。应用程序中的所有线程(优先级为 0 的实时线程除外)都以正常优先级 (251) 运行。在我们的测量中,没有发现 JIT 编译器和内存回收器需要内核阻断才能执行任务。

图 5:显示托管应用程序的远程进程查看器

缺陷

在测试过程中,提高方波的频率会在托管应用程序中产生意外的结果。尤其是当某些屏幕区域无效而需要频繁重画时,应用程序会随机地挂起系统。进一步调查显示,此问题是由于经验丰富的 Win32 程序员的疏忽造成的。在 Win32 应用程序中,每当计时器到期时,使用计时器都会产生一条 WM_TIMER 消息。但在消息队列中,WM_TIMER 消息的优先级很低,因此只有在不需要处理其他优先级较高的消息时,才会发布这些消息。由于 CreateTimer 不提供用于开始计时的精确计时器,因此此行为可能会导致丢失计时器触发。但这不是问题,尤其是在计时器用于更新图形用户界面 (GUI) 的情况下。不过,在托管应用程序中,我们使用 System.Threading.Timer 对象来创建计时器。每次计时器到期时,都会调用一个委托。委托是从某个线程池中的单独线程内进行调用的。如果系统正在忙于处理其他活动(例如,重画整个屏幕),则在完成前面激活的委托之前,会激活更多的计时器委托,而且每个委托位于单独的线程中。这可能会导致耗尽线程池中的所有可用线程,并使系统挂起。列表 4 中提供了用于防止这种情况的解决方案。每次激活一个计时器委托时,我们都通过调用 Timer 对象的 Change 方法来停止计时器对象,以表明在处理完当前计时器消息之前,我们不需要下一条消息。这可能会导致计时器时间间隔不精确。在我们的例子中,计时器只用于刷新屏幕,因此计时不精确算不上问题。

结果证明

为了能够将我们的实验结果与相同设置中的典型结果进行比较,我们还编写了一个 Win32 应用程序,该应用程序调用具有实时功能的同一个 DLL。Win32 应用程序在功能上与托管应用程序相同。它可以为系统提供一个图形用户界面,计时信息显示在其中的一个窗口中。当收到 WM_TIMER 消息时,该应用程序仅利用 Win32 API 绘制计时结果。在性能方面,我们没有发现任何明显的差异,如图 6 和图 7 所示。在图 6 中,使用示波器再次测量中断滞后时间。对于 Win32 应用程序,滞后时间为 14.4 微秒,在最差的情况下,滞后时间为 55.2 微秒,即抖动为 40.8 微秒。这些结果与使用 .NET Compact Framework 托管应用程序测试的结果大致相等。

图 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 站点(英文)。

 

版权所有:UML软件工程组织