用 C++ 創建簡單的 Win32 服務程序

用 C++ 創建簡單的 Win32 服務程序

作者:Nigel Thomson(MSDN 技術組)
翻譯:NorthTibet

原文出處: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。稍後我們會看到代碼。
  建立新工程時,我喜歡儘快看到運行結果,所以我決定服務程序要做的第一件事情是建立一個系統應用程序日誌記錄。藉助這個日誌記錄機制,我能跟蹤服務何時啓動, 何時停止等等。我還可以記錄服務中發生的任何出錯信息。創建這個日誌記錄比我想象的要複雜得多。

建立日誌記錄
  我想,既然日誌文件是操作系統的一部分,那麼肯定有應用程序編程接口(API)來支持建立日誌記錄。所以我開始搜索 MSDN CD,直到發現 ReportEvent 函數爲止。如果你不熟悉這個函數,你可能會想,這個函數應該知道在哪個日誌文件建立記錄,以及你想要插入的文本信息。沒錯,這都是它要做的事情,但是爲了簡化出錯信息的國際化,該函數有一個消息 ID 作爲參數,並在你提供的消息表中查找消息。所以問題無非是你想將什麼消息放入日誌,以及如何將這些消息添加到你的應用程序中,下面我們一步一步來做:

  1. 以 .MC 爲擴展名創建一個包含消息描述的文本文件。我將它命名爲 NTServMsg.mc。該文件的格式非常特別,具體細節參見 Platform SDK 文檔;
  2. 針對你的源文件運行消息編譯器(mc.exe),默認情況下它創建名爲 MSG00001.BIN 的輸出文件。編譯器還創建一個頭文件(在我的例子程序中,該頭文件是 NTServMsg.h)和一個.RC 文件(NTServMsg.rc)。只要你修改工程的 .MC 文件就必須重複這一步,所以把工具加到 Visual C++ 的工具菜單裏做起來會很方便;
  3. 爲工程創建一個 .RC 文件,將 WINDOWS.H 頭文件以及消息編譯器產生的 .RC 文件包含到其中;
  4. 在主工程頭文件中包含消息編譯器產生的頭文件,以便模塊可以存取符號消息名;

  下面讓我們仔細一下這些文件,以便弄明白你自己需要創建什麼,以及消息編譯器要爲你創建些什麼。我們不用研究整個消息集,只要看看其中一二個如何工作的即可。下面是例子程序消息源文件 NTServMsg.mc 的第一部分:

MessageId=100
SymbolicName=EVMSG_INSTALLED
Language=English
The %1 service was installed.
.

MessageId=
SymbolicName=EVMSG_REMOVED
Language=English
The %1 service was removed.
.

MessageId=
SymbolicName=EVMSG_NOTREMOVED
Language=English
The %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,0x1
1 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 machine
			m_szServiceName); // source name
	}
	
	if (m_hEventSource) {
		::ReportEvent(m_hEventSource,
			wType,
			0,
			dwID,
			NULL, // sid
			iStr,
			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=0
Module 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.DBG
Module Load: C:/NT351/symbols/dll/NTDLL.DBG (symbols loaded)
Thread Terminate: Process=0, Thread=1, Exit Code=0
Hard coded breakpoint hit
Hard 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.exe
Module Unload: C:/NT351/system32/NTDLL.DLL
Module Unload: C:/NT351/system32/KERNEL32.DLL
Module Unload: C:/NT351/system32/ADVAPI32.DLL
Module Unload: C:/NT351/system32/RPCRT4.DLL
Module Unload: C:/NT351/SYSTEM32/RPCLTC1.DLL
Thread Terminate: Process=0, Thread=0, Exit Code=0
Process Terminate: Process=0, Exit Code=0
>

總結

  也許用 C++ 創建 Win32 服務並不是最理想的,但使用單一的類來派生你自己的服務的確方便了你的服務開發工作。

發佈了37 篇原創文章 · 獲贊 0 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章