原文出處:Creating a Simple Win32 Service in C++
下載 NTService 例子原始碼
下載 NTServCpl 例子原始碼
下載 NTServCtrl 例子原始碼
摘要
本文描述如何用 Visual C++ 建立 Windows NT 服務程式。建立該服務僅用到一個 C++ 類,這個類提供服務與作業系統之間一個簡單的介面。使用這個類實現自己的服務非常簡單,只要改寫少數幾個基類中的虛擬函數即可。在本文有三個原始碼參考例子:
- NTService 是一個簡單的 Win32 服務,它就是用本文所描述的方法建立的;
- NTServCpl 是一個控制面版程式,用來控制 NTService 服務;
- NTServCtrl 是一個獨立的程式例子,用它可以監控某個 Win32 服務;
簡介
Windows NT 中的服務實際上是一個程式,只要電腦作業系統一啟動,服務就可以運行其中。它不需要使用者登陸。服務程式是一種與使用者無關的任務,比如目錄複寫,進程監控或網路上供其它機器使用的服務,比如 HTTP 協議支援。
建立 Windows NT 服務程式並不是很難。但調試某個服務程式不是一件容易的事。就我自己而言,我喜歡用 Visual C++ 編寫自己的 C++ 程式。大多數 Win32 服務都是用 C 寫的,所以我覺得如果用某個 C++ 類來實現 Win32 服務的準系統一定很有意思。有了這個 C++ 類,誰要想用 C++ 建立 Win32 服務就是一件很簡單的事情了。我為此開發了一個 C++ 基類,用它作為編寫 Win32 服務的起點應該沒有什麼大問題。
建立服務程式除了編寫服務代碼外,還必須做一些其它額外的編碼工作:
- 在系統日誌或應用程式記錄檔中報警示告資訊和出錯資訊,不能用輸出到螢幕的方式,因為使用者根本就沒有登陸。
- 服務程式的控制即可以通過單獨的應用程式,也可以通過控制面版程式。這取決於你的服務實現什麼樣的通訊機制。
- 從系統中安裝和卸載服務
大多數服務程式都是使用一個安裝程式來安裝,而用另外一個程式來卸載。本文我將這些功能內建在服務程式自身當中,使之一體化,這樣只分發一個.EXE檔案即可。你可以從命令列直接運行服務程式,並且可以隨心所欲地安裝和卸載或報告其版本資訊。NTService 支援下列的命令列參數:
- -v, 報表服務的名字和版本號碼;
- -i, 安裝服務;
- -u, 卸載服務;
預設情況下,當系統啟動該服務時沒有命令列參數傳遞。
建立應用程式架構
我一直都是建立基於 MFC 的應用程式。當我剛接觸 Win32 服務程式時,我先是用 Visual C++ AppWizard 建立一個 SDI/MFC 程式。然後去掉其中的文檔和視圖類、表徵圖以及其它一些無用的東西,只剩下架構。結果到最後什麼都去掉了,包括主視窗(服務程式不能有這個東東),什麼也沒有留下,非常愚蠢。我不得不 又回過頭到 AppWizard,並用單個的源檔案建立控制台程式,此源檔案包含main 入口函數,我將這個檔案命名為 NTServApp.cpp。我用此 cpp 擴充而不是用 C,因為我只想用C++ 來寫程式,而不是直接用 C。稍後我們會討論該檔案代碼實現。
因為我想用 C++ 類來構建服務,所以我建立了 NTService.h 和 NTService.cpp 檔案,用它們來實現 CNTService 基類。我還建立了 MyService.h 和 MyService.cpp 檔案用於實現自己的服務類(CMyService),它派生於 CNTService。稍後我們會看到代碼。
建立新工程時,我喜歡儘快看到運行結果,所以我決定服務程式要做的第一件事情是建立一個系統應用程式記錄檔記錄。藉助這個日誌記錄機制,我能Tracing Service何時啟動, 何時停止等等。我還可以記錄服務中發生的任何出錯資訊。建立這個日誌記錄比我想象的要複雜得多。
建立日誌記錄
我想,既然記錄檔是作業系統的一部分,那麼肯定有API(API)來支援建立日誌記錄。所以我開始搜尋 MSDN CD,直到發現 ReportEvent 函數為止。如果你不熟悉這個函數,你可能會想,這個函數應該知道在哪個記錄檔建立記錄,以及你想要插入的文本資訊。沒錯,這都是它要做的事情,但是為了簡化出錯資訊的國際化,該函數有一個訊息 ID 作為參數,並在你提供的訊息表中尋找訊息。所以問題無非是你想將什麼訊息放入日誌,以及如何將這些訊息添加到你的應用程式中,下面我們一步一步來做:
- 以 .MC 為副檔名建立一個包含訊息描述的文字檔。我將它命名為 NTServMsg.mc。該檔案的格式非常特別,具體細節參見 Platform SDK 文檔;
- 針對你的源檔案運行訊息編譯器(mc.exe),預設情況下它建立名為 MSG00001.BIN 的輸出檔案。編譯器還建立一個標頭檔(在我的例子程式中,該標頭檔是 NTServMsg.h)和一個.RC 檔案(NTServMsg.rc)。只要你修改工程的 .MC 檔案就必須重複這一步,所以把工具加到 Visual C++ 的工具菜單裡做起來會很方便;
- 為工程建立一個 .RC 檔案,將 WINDOWS.H 標頭檔以及訊息編譯器產生的 .RC 檔案包含到其中;
- 在主工程標頭檔中包含訊息編譯器產生的標頭檔,以便模組可以存取符號訊息名;
下面讓我們仔細一下這些檔案,以便弄明白你自己需要建立什麼,以及訊息編譯器要為你建立些什麼。我們不用研究整個訊息集,只要看看其中一二個如何工作的即可。下面是例子程式訊息源檔案 NTServMsg.mc 的第一部分:
MessageId=100SymbolicName=EVMSG_INSTALLEDLanguage=EnglishThe %1 service was installed..MessageId=SymbolicName=EVMSG_REMOVEDLanguage=EnglishThe %1 service was removed..MessageId=SymbolicName=EVMSG_NOTREMOVEDLanguage=EnglishThe %1 service could not be removed..
每一條都有一個訊息ID,如果不特別設定,那麼 ID 的取值就是指其前面所賦的值。每一條還有一個代碼中使用的符號名,語言標示符以及訊息文本。訊息可以跨多個行,並用含有一個句號的單獨一行終止。
訊息編譯器輸出一個庫檔案,該庫檔案被用作應用程式的資源,此外還輸出兩個要在代碼中包含的檔案。下面是我的 .RC 檔案:
// NTServApp.rc#include <windows.h>// 包含由訊息編譯器(MC)產生的訊息表資源指令碼#include "NTServMsg.rc"Here''s the .RC file the message compiler generated:LANGUAGE 0x9,0x11 11 MSG00001.bin
正像你所看到的,這些檔案中內容不多!
訊息編譯器產生的最後一個檔案是你要包含到代碼中的標頭檔,下面就是這個標頭檔的部分內容:
[..........]//// MessageId: EVMSG_INSTALLED//// MessageText://// The %1 service was installed.//#define EVMSG_INSTALLED 0x00000064L//// MessageId: EVMSG_REMOVED//// MessageText://// The %1 service was removed.//#define EVMSG_REMOVED 0x00000065L[...........]
你可能已經注意到了有幾個訊息包含參數替代項(如 %1)。讓我們看看將訊息寫入某個系統記錄檔時如何在代碼中使用訊息ID和參數替代項。以事件記錄中記錄成功安裝資訊的部分安裝代碼為例。也就是 CNTService::IsInstalled 函數部分:
[....]LogEvent(EVENTLOG_INFORMATION_TYPE, EVMSG_INSTALLED, m_szServiceName);[....]
LogEvent 是另一個 CNTService 函數,它使用事件類型(資訊,警告或錯誤),事件訊息的 ID,以及形成日誌訊息的最多三個參數的替代串:
// This function makes an entry into the application event log.void CNTService::LogEvent(WORD wType, DWORD dwID,const char* pszS1,const char* pszS2,const char* pszS3){const char* ps[3];ps[0] = pszS1;ps[1] = pszS2;ps[2] = pszS3;int iStr = 0;for (int i = 0; i < 3; i++) {if (ps[i] != NULL) iStr++;}// Check to see if the event source has been registered,// and if not then register it now.if (!m_hEventSource) {m_hEventSource = ::RegisterEventSource(NULL, // local machinem_szServiceName); // source name}if (m_hEventSource) {::ReportEvent(m_hEventSource,wType,0,dwID,NULL, // sidiStr,0,ps,NULL);}}
如你所見,其主要工作是由 ReportEvent 系統函數處理。
至此,我們已經可以通過調用 CNTService::LogEvent 在系統日誌中記錄事件了。接下來我們將考慮建立服務本身的一些代碼。
編寫服務代碼
為了建構一個簡單的 Win32 服務,你需要知道的大多數資訊都可以在 Platform SDK 中找到。其中的範例代碼都是用C語言寫的,並且很好理解。我的 CNTService 類就是基於這些代碼。
一個服務主要包括三個函數:
- main函數,這是代碼的入口。我們正是在這裡解析任何命令列參數並進行服務的安裝,移除,啟動等等。
- 在例子中,提供真正服務代碼的入口函數叫 ServiceMain。你可以隨便叫它什麼。在服務第一次啟動的惡時候,將該函數的地址傳遞給服務管理員。
- 處理來自服務管理員命令訊息的函數。在例子中,這個函數叫 Handler,這個名字可以隨意取。
服務回呼函數
因為 ServiceMain 和 Handler 函數都是由系統來調用,所以它們必須遵循作業系統的參數傳遞規範和調用規範。也就是說,它們不能簡單地作為某個 C++ 類的成員函數。這樣就給封裝帶來一些不便,因為我們想把 Win32 服務的功能封裝在一個 C++ 類中。為瞭解決這個問題,我將 ServiceMain 和 Handler 函數建立成 CNTService 類的靜態成員。這樣就使我得以建立可以由作業系統調用的函數。 但是,這樣做還沒有完全解決問題,因為系統不允許給被調用的函數傳遞任何形式的使用者資料,所以我們無法確定對 C++ 對象特定執行個體的 ServiceMain 或 Handler 的調用。用了一個非常簡單但有局限的方法來解決這個問題。我建立一個包含 C++ 對象指標的靜態變數。這個變數是在該對象首次建立是進行初始化的。這樣便限制你每個服務應用只有一個C++對象。我覺得這個限制並不過分。下面是 NTService.h 檔案中的聲明:
class CNTService{ [...] // 待用資料 static CNTService* m_pThis; // nasty hack to get object ptr [...]};
下面是初始化 m_pThis 指標的方法:
CNTService::CNTService(const char* szServiceName){ // Copy the address of the current object so we can access it from // the static member callback functions. // WARNING: This limits the application to only one CNTService object. m_pThis = this; [...]}
CNTService 類
當我建立 C++ 對象封裝 Windows 函數時,我嘗試為我封裝的每個 Windows API 除了建立成員函數外,還做一些別的工作,我嘗試讓對象更容易使用,降低實現特定項目所需的程式碼數。因此我的對象是基於“我想讓這個對象做什嗎?”而不是“Windows 用這些 APIs 做什嗎?”
CNTService 類包含一些用來解析命令列的成員函數,為了處理服務的安裝和拆卸以及事件記錄的記錄,你得在衍生類別中重寫一些虛擬函數來處理服務控制管理員的請求。下面我們將通過本文的例子服務實現來研究這些函數的使用。
如果你想建立儘可能簡單的服務,只需要重寫 CNTService::Run 即可,它是你編寫代碼實現具體服務任務的地方。你還需要實現 main 函數。如果服務需要實現一些初始化。如從註冊表讀取資料,還需重寫 CNTService::OnInit。如果你要向服務發送命令訊息 ,那麼可以在服務中使用系統函數 ControlService,重寫 CNTService::OnUserControl 來處理請求。
在例子應用程式中使用 CNTService
NTService 在 CMyService 類中實現了它的大多數功能,CMyService 由 CNTService 派生。 MyService.h 標頭檔如下:
// myservice.h#include "ntservice.h"class CMyService : public CNTService{public: CMyService(); virtual BOOL OnInit(); virtual void Run(); virtual BOOL OnUserControl(DWORD dwOpcode); void SaveStatus(); // Control parameters int m_iStartParam; int m_iIncParam; // Current state int m_iState;};
正像你所看到的,CMyService 改寫了 CNTService 的 OnInit、Run 和 OnUserControl。它還有一個函數叫 SaveStatus,這個函數被用於將資料寫入註冊表,那些成員變數用來儲存目前狀態。例子服務每隔一定的時間對一個整型變數進行增量處理。開始值和增量值都存在註冊表的參數中。這樣做並沒有別的意圖。只是為了簡單示範。下面我們看看這個服務是如何?的。
實現 main 函數
有了從 CNTService 派生的 CMyService,實現 main 函數很簡單,請看 NTServApp.cpp 檔案:
int main(int argc, char* argv[]){ // 建立服務物件 CMyService MyService; // 解析標準參數 (安裝, 卸載, 版本等.) if (!MyService.ParseStandardArgs(argc, argv)) { // 未發現任何標準參數,所以啟動服務, // 取消下面 DebugBreak 程式碼的注釋, // 當服務啟動後進入調試器, //DebugBreak(); MyService.StartService(); } // 到這裡,服務已經停止 return MyService.m_Status.dwWin32ExitCode;}
這裡代碼不多,但執行後卻發生了很多事情,讓我們一步一步來看。首先,我們建立一個 MyService 類的執行個體。建構函式設定初始化狀態和服務名字(MyService.cpp):
CMyService::CMyService():CNTService("NT Service Demonstration"){ m_iStartParam = 0; m_iIncParam = 1; m_iState = m_iStartParam;}
接著調用 ParseStandardArgs 檢查命令列是否包含服務安裝(-i)、卸載(-u)以及報告其版本號碼(-v)的請求。CNTService::ParseStandardArgs 分別調用 CNTService::IsInstalled,CNTService::Install 和 CNTService::Uninstall 來處理這些請求。如果沒有可識別的命令列參數,則假設該服務控制管理員試圖啟動該服務並調用 StartService。該函數直到服務停止運行才返回。當你調試完代碼,即可把用於調試的程式碼注釋掉或刪除。
安裝和卸載服務
服務的安裝由 CNTService::Install 處理,它用 Win32 服務管理員註冊服務並在註冊表中建立一個條目以支援服務運行時日誌訊息。
服務的卸載由 CNTService::Uninstall 處理,它僅僅通知服務管理員該服務已經不再需要。CNTService::Uninstall 不會刪除服務實際的可執行檔。
編寫服務代碼
現在我們來編寫實現服務的具體代碼。對於 NTService 例子,有三個函數要寫。他們涉及初始化,運行服務的細節和響應控制請求。
初始化
註冊表有一個給服務用來儲存參數的地方:
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services
我就是選擇這裡來儲存我的服務配置資訊。我建立了一個 Parameters 鍵,並在此儲存我要儲存的值。所以當服務啟動時,OnInit 函數被調用;這個函數從註冊表中讀取初始設定。
BOOL CMyService::OnInit(){ // Read the registry parameters. // Try opening the registry key: // HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services//Parameters HKEY hkey; char szKey[1024]; strcpy(szKey, "SYSTEM//CurrentControlSet//Services//"); strcat(szKey, m_szServiceName); strcat(szKey, "//Parameters"); if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, szKey, 0, KEY_QUERY_VALUE, &hkey) == ERROR_SUCCESS) { // Yes we are installed. DWORD dwType = 0; DWORD dwSize = sizeof(m_iStartParam); RegQueryValueEx(hkey, "Start", NULL, &dwType, (BYTE*)&m_iStartParam, &dwSize); dwSize = sizeof(m_iIncParam); RegQueryValueEx(hkey, "Inc", NULL, &dwType, (BYTE*)&m_iIncParam, &dwSize); RegCloseKey(hkey); } // Set the initial state. m_iState = m_iStartParam; return TRUE;}
現在我們有了服務參數,我們便可以運行服務了。
運行服務
當 Run 函數被調用時將執行服務的主體代碼。本文例子的這部分很簡單:
void CMyService::Run(){ while (m_bIsRunning) { // Sleep for a while. DebugMsg("My service is sleeping (%lu)...", m_iState); Sleep(1000); // Update the current state. m_iState += m_iIncParam; }}
注意,只要服務不終止,這個函數就不會退出。當有終止服務的請求時,CNTService::m_bIsRunning 標誌被置成 FALSE。如果在服務終止時,你要實現清除操作,那麼你還可以改寫 OnStop 和/或 OnShutdown。
響應控制請求
你可以用任何適合的方式與服務通訊——具名管道,思想交流,便條等等——對於一些簡單的請求,用系統函數 ControlService 很容易實現。CNTService 提供了一個處理器專門用於通過 ControlService 函數發送的非標準訊息(也就是使用者發送的訊息)。本文例子用單一訊息在註冊表中儲存當前服務的狀態,以便其它應用程式能看到它。我不建議用這種方法來監控服務,因為它不是最佳方法,這隻是比較容易編碼實現而已。ControlService 所能處理的使用者訊息必須在 128 到 255 這個範圍。我定義了一個常量 SERVICE_CONTROL_USER,128 作為基值。範圍內的使用者訊息被發送到 CNTService:: OnUserControl,在例子服務中,處理此訊息的細節如下:
BOOL CMyService::OnUserControl(DWORD dwOpcode){ switch (dwOpcode) { case SERVICE_CONTROL_USER + 0: // Save the current status in the registry. SaveStatus(); return TRUE; default: break; } return FALSE; // say not handled}
SaveStatus 是一個局部函數,用來在註冊表中儲存服務狀態。
調試 Win32 服務
main 函數中包含一個對 DebugBreak 的調用,當服務第一次被啟動時,它會啟用系統調試器。你可以監控來自調試器命令視窗中的服務調試資訊。你可以在服務中用 CNTService::DebugMsg 來報告調試期間感興趣的事件。
為了調試服務代碼,你需要按照 Platform SDK 文檔中的要求安裝 系統調試器(WinDbg)。你也可以用 Visual Studio 內建的調試器調試 Win32 服務。
有一點很重要,那就是 當它被服務管理員控制時,你不能終止服務和逐步執行,因為服務管理員會讓服務要求 逾時並終止服務線程。所以你只能讓服務吐出訊息,跟蹤其過程並在調試器視窗查看它們。
當服務啟動後(例如,從控制台的“服務”中),調試器將在服務線程的掛起後啟動。你需要通過單擊“Go”按鈕或按 F5 讓繼續運行。然後在調試器中觀察服務的運行過程。
下面是啟動和終止服務的調試輸出例子:
Module Load: WinDebug/NTService.exe (symbol loading deferred)Thread Create: Process=0, Thread=0Module Load: C:/NT351/system32/NTDLL.DLL (symbol loading deferred)Module Load: C:/NT351/system32/KERNEL32.DLL (symbol loading deferred)Module Load: C:/NT351/system32/ADVAPI32.DLL (symbol loading deferred)Module Load: C:/NT351/system32/RPCRT4.DLL (symbol loading deferred)Thread Create: Process=0, Thread=1*** WARNING: symbols checksum is wrong 0x0005830f 0x0005224f for C:/NT351/symbols/dll/NTDLL.DBGModule Load: C:/NT351/symbols/dll/NTDLL.DBG (symbols loaded)Thread Terminate: Process=0, Thread=1, Exit Code=0Hard coded breakpoint hitHard coded breakpoint hit[](130): CNTService::CNTService()Module Load: C:/NT351/SYSTEM32/RPCLTC1.DLL (symbol loading deferred)[NT Service Demonstration](130): Calling StartServiceCtrlDispatcher()Thread Create: Process=0, Thread=2[NT Service Demonstration](174): Entering CNTService::ServiceMain()[NT Service Demonstration](174): Entering CNTService::Initialize()[NT Service Demonstration](174): CNTService::SetStatus(3026680, 2)[NT Service Demonstration](174): Sleeping...[NT Service Demonstration](174): CNTService::SetStatus(3026680, 4)[NT Service Demonstration](174): Entering CNTService::Run()[NT Service Demonstration](174): Sleeping...[NT Service Demonstration](174): Sleeping...[NT Service Demonstration](174): Sleeping...[NT Service Demonstration](130): CNTService::Handler(1)[NT Service Demonstration](130): Entering CNTService::Stop()[NT Service Demonstration](130): CNTService::SetStatus(3026680, 3)[NT Service Demonstration](130): Leaving CNTService::Stop()[NT Service Demonstration](130): Updating status (3026680, 3)[NT Service Demonstration](174): Leaving CNTService::Run()[NT Service Demonstration](174): Leaving CNTService::Initialize()[NT Service Demonstration](174): Leaving CNTService::ServiceMain()[NT Service Demonstration](174): CNTService::SetStatus(3026680, 1)Thread Terminate: Process=0, Thread=2, Exit Code=0[NT Service Demonstration](130): Returned from StartServiceCtrlDispatcher()Module Unload: WinDebug/NTService.exeModule Unload: C:/NT351/system32/NTDLL.DLLModule Unload: C:/NT351/system32/KERNEL32.DLLModule Unload: C:/NT351/system32/ADVAPI32.DLLModule Unload: C:/NT351/system32/RPCRT4.DLLModule Unload: C:/NT351/SYSTEM32/RPCLTC1.DLLThread Terminate: Process=0, Thread=0, Exit Code=0Process Terminate: Process=0, Exit Code=0>
總結
也許用 C++ 建立 Win32 服務並不是最理想的,但使用單一的類來派生你自己的服務的確方便了你的服務開發工作。