適用于: 下載 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 站點(英文)。 |
文章來源于領測軟件測試網 http://www.kjueaiud.com/