天天看點

Windows服務架構與服務的編寫

從NT核心開始,服務程式已經變為一種非常重要的系統程序,一般的駐守程序和普通的程式必須在桌面登入的情況下才能運作,而許多系統的基礎程式必須在使用者登入桌面之前就要運作起來,而利用服務,可以很友善的實作這種功能,而且服務程式一般不予使用者進行互動,可以安靜的在背景執行,合理的利用服務程式可以簡化我們的系統設計,比如Windows系統的日志服務,IIS服務等等。

服務程式本身是依附在某一個可執行檔案之中,系統将服務安裝在系統資料庫中的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services位置,當需要執行服務程式時,由系統的服務控制管理器在系統資料庫中對應的位置讀取服務資訊,并啟動對應的程式。

下面從幾個方面詳細說明服務程式的基本架構

服務程式的架構

服務程式本身也是依附在exe或者dll檔案中,一般一個普通的可執行檔案中可以包含一個或者多個服務,但是為了代碼的維護性,一般一個程式總是隻包含一個服務。

服務程式是由服務管理器負責排程,控制的,是以我們在編寫服務程式的時候必須滿足服務控制管理器的排程,必須包含:

1. 立即調用StartServiceCtrlDispatchar函數把程序的主線程連接配接到ServiceControlManager的主函數

2. 在程序中運作的各個服務的入口點函數ServiceMain

3. 在程序中運作的各個服務的控制處理函數Handler

ServiceControlManager函數的原型如下:

BOOL WINAPI StartServiceCtrlDispatcher(
  __in  const SERVICE_TABLE_ENTRY* lpServiceTable
);      

函數參數是一個SERVICE_TABLE_ENTRY類型的指針,這個類型的定義如下:

typedef struct _SERVICE_TABLE_ENTRY 
{  
    LPTSTR lpServiceName;  
    LPSERVICE_MAIN_FUNCTION lpServiceProc;
} SERVICE_TABLE_ENTRY,  *LPSERVICE_TABLE_ENTRY;      

這個結構是一個服務名稱和對應入口函數指針的映射。在傳入的時候必須給一個該類型的數組,數組的每一項都代表一個服務與其入口函數指針的映射,同時這個數組的最後一組必須為NULL

當啟動服務的時候,系統會啟動對應的程序,當程序代碼執行到StartServiceCtrlDispatcher時,程式由服務控制管理器接管,服務控制管理器根據需要啟動的服務名稱,在傳入的數組指針中,找到對應的入口函數,然後調用它,當對應的入口函數傳回時結束服務,并将後續代碼的控制權轉交給對應主程序,由主程序接着執行後面的代碼

在入口函數中我們必須給服務一個控制管理程式,這個程式主要是用來處理服務程式接受到的各種控制消息,比如啟動服務,暫停服務,停止服務等,這個函數有點類似于Windows 視窗程式中的視窗過程。這個函數由我們自己編寫,然後調用函數RegisterServiceCtrlHandler(Ex) 将服務名稱與對應的控制函數綁定,每當有一個控制事件發生時都會調用我們注冊的函數進行處理,RegisterServiceCtrlHandler函數會傳回一個句柄,作為服務的控制句柄。當我們要自己向服務控制管理器報告服務的目前狀态時需要這個句柄。

服務的啟動過程

已經安裝的服務,被系統存儲在系統資料庫的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services

位置處,這個系統資料庫項紀錄了服務所依賴的exe或者dll檔案,它的啟動類型等資訊,當我們嘗試啟動服務的時候,系統會在系統資料庫的對應位置查找是否存在對應服務的表項,如果存在則啟動對應的程序。當程序的代碼執行到StartServiceCtrlDispatcher函數時,該程序将由服務控制管理器接管,服務控制管理器将會根據填入的SERVICE_TABLE_ENTRY,找到服務所對應的入口函數開啟對應的服務線程并調用,在入口函數處會注冊一個控制句柄,然後應該向服務控制管理程式報告目前狀态為正在啟動,然後執行服務的正式代碼。(注意:由于服務的入口函數需要自己編寫,是以這裡提到的注冊控制句柄,報告狀态都應該是由程式員自己編寫代碼實作)

Handler函數

handler函數用來處理服務的控制請求,這個函數由RegisterServiceCtrlHandler(Ex)函數注冊到系統,當服務控制請求到來時,由服務的主線程的控制分發線程來調用。綜合上面的内容,可以看到一個服務程式應該是至少涉及到3個線程,程序的主線程,服務線程,控制分發線程,RegisterServiceCtrlHandler(Ex)的原型如下:

SERVICE_STATUS_HANDLE WINAPI RegisterServiceCtrlHandlerEx(
  __in      LPCTSTR lpServiceName,
  __in      LPHANDLER_FUNCTION_EX lpHandlerProc,
  __in_opt  LPVOID lpContext
);      

不帶Ex的版本隻有前兩個參數,帶Ex版本的第3個參數是一個傳入到對應的控制函數中的參數。對應提供的控制管理函數的原型如下:

DWORD WINAPI HandlerEx(
  __in  DWORD dwControl,
  __in  DWORD dwEventType,
  __in  LPVOID lpEventData,
  __in  LPVOID lpContext
);      

第一個參數是一個控制碼,類似于GUI程式中的消息,根據這個控制碼就可以知道對應的控制消息,下面列舉常見的控制碼:

控制碼 含義
SERVICE_CONTROL_STOP 請求服務停止
SERVICE_CONTROL_PAUSE 請求暫停服務
SERVICE_CONTROL_CONTINUE 請求恢複暫停的服務
SERVICE_CONTROL_INTERROGATE 請求服務立即更新它的目前狀态資訊給服務控制管理程式
SERVICE_CONTROL_SHUTDOWN 請求服務執行清理任務,因為系統正在關機.由于隻有非常有限的時間用來關機,是以這個控制隻應由絕對需要關機的服務使用.例如:事件登入服務需要清理維護的檔案中的髒位元組,或服務需要關機以便當系統在關機狀态時網絡連接配接不能進行. 如果服務關鍵要花時間,并發出STOP_PENDING狀态資訊,強烈建議這些消息包括一個等待提示使得服務控制程式知道在給系統指明服務關機完成之前要等多長時間.系統給服務控制管理器有限的時間(約20秒)完成服務關機,在這個時間後無論服務關機動作是否完成都進行系統關機

第二個參數是事件類型,對于有的控制碼,它可能含有子控制類型來較長的描述它,就好像WM_COMMAND消息中有子控件的相關消息

第三個參數是事件參數,這個參數是子控制碼對應的參數

第四個參數是上面帶Ex的函數第三個參數傳進來的内容

每次Handler函數被調用時,服務必須調用SetServiceStatus函數把狀态報告給服務管理器程式注意:即使狀态無變化也要報告

服務控制管理器

在服務中一般有3類對象(在這并不是指Windows系統的核心對象,這裡隻是為了便于了解給出的一個分類):

1. 服務程式對象:服務本身的代碼,一般是服務主要完成的功能代碼

2. 服務控制對象:用來控制服務,向服務發送執行

3. 服務管理對象:用來響應對應的控制碼,主要是指服務的handler函數

與GUI程式相類比,服務對象就好比GUI程式本身,服務控制對象就好像我們在操作GUI程式,比如點選滑鼠,而服務控制對象就像視窗的視窗過程

服務管理器由SCManager對象代表。SCManager對象是持有服務對象的容器對象。SCManager對象和服務對象的句柄類型是SC_HANDLE。我們可以使用函數OpenService來在服務管理器中打開對應服務擷取服務對象的句柄,或者使用函數CreateService在服務管理器中建立一個新服務并傳回服務的句柄

後面關于服務的控制操作請參考本人之前寫的一篇關于服務控制管理器的編寫的部落格​​​點選這裡​​​

下面通過一個封裝的Service庫來說明服務程式的架構。這個簡單的類的詳細代碼請​​​點選這裡下載下傳​​​

該項目中主要定義了三個類,其中CFSZService類是所有服務類的基類,CServiceCtrl是服務的控制類,該類用于控制服務,這個類中的所有函數都是靜态函數。另外為了測試我從CFSZService類上派生了一個類——CTestService,用來編寫服務的具體代碼。如果以後想要使用這個項目中的代碼,可以進行如下操作:

1. FSZService類中派生一個新類,并重載基類的RunService,在這個服務中編寫具體的服務代碼即可

2. 在相應位置調用DECLARE_SERVICE_TABLE_ENTRY宏,用來聲明一個SERVICE_TABLE_ENTRY變量,用來綁定服務和對應的入口函數

3. 在相應位置添加代碼:

IMPLAMENT_SERVICE_MAIN(GetSystemInfoService, CTestService)

BEGIN_SERVICE_MAP()
    ON_SERVICE_MAP(GetSystemInfoService, CTestService)
END_SERIVCE_MAP()      

第一個宏用來定義了一個函數,該函數是服務的入口函數,需要傳入服務名稱,服務的類名稱。

第二個宏用來将服務名和它對應的入口函數進行綁定。

4. 在主函數處調用CFSZService::RegisterService(),在該函數裡面會調用StartServiceCtrlDispatcher,一遍讓服務控制管理程式來接管服務代碼

代碼的整體說明

服務基類的定義如下:

class CFSZService
{
public:
    typedef CAtlMap<CString, CFSZService *> CFSZServiceMap;  //服務名稱和對應的服務對象

    CFSZService(const CString& csSrvName);
    ~CFSZService(void);
    virtual DWORD Run(DWORD dwArgc, LPTSTR* lpszArgv);
    virtual BOOL OnInitService(DWORD dwArgc, LPTSTR* lpszArgv); //初始化服務
    virtual DWORD RunService(); //運作服務 
    void SetServiceStatusHandle(SERVICE_STATUS_HANDLE);

    static DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);
    static BOOL RegisterService();
    //服務指令處理函數
protected:
    virtual DWORD OnStop();
    virtual DWORD OnUserControl(DWORD dwControl);
    virtual DWORD OnStart();
    virtual DWORD OnContinue();
    virtual DWORD OnPause();
    virtual DWORD OnShutdown();
    virtual DWORD OnInterrogate();
    virtual DWORD OnShutDown();

protected://裝置變更事件通知處理 SERVICE_CONTROL_DEVICEEVENT
    virtual DWORD OnDeviceArrival(PDEV_BROADCAST_HDR pDbh){return 0;}
    virtual DWORD OnDeviceRemoveComplete(PDEV_BROADCAST_HDR pDbh){return 0;}
    virtual DWORD OnDeviceQueryRemove(PDEV_BROADCAST_HDR pDbh){return 0;}
    virtual DWORD OnDeviceQueryRemoveFailed(PDEV_BROADCAST_HDR pDbh){return 0;}
    virtual DWORD OnDeviceRemovePending(PDEV_BROADCAST_HDR pDbh){return 0;}
    virtual DWORD OnCustomEvent(PDEV_BROADCAST_HDR pDbh){return 0;}
protected://硬體配置檔案發生變動 SERVICE_CONTROL_HARDWAREPROFILECHANGE
    virtual DWORD OnConfigChanged(){return 0;}
    virtual DWORD OnQueryChangeConfig(){return 0;}
    virtual DWORD OnConfigChangeCanceled(){return 0;}
protected://裝置電源事件 SERVICE_CONTROL_POWEREVENT
    virtual DWORD OnPowerSettingChange(PPOWERBROADCAST_SETTING pPs){return 0;}
protected://session 發生變化 SERVICE_CONTROL_SESSIONCHANGE
    virtual DWORD OnWTSConsoleConnect(PWTSSESSION_NOTIFICATION pWn){return 0;}
    virtual DWORD OnWTSConsoleDisconnect(PWTSSESSION_NOTIFICATION pWns){return 0;}
    virtual DWORD OnWTSRemoteConnect(PWTSSESSION_NOTIFICATION pWns){return 0;}
    virtual DWORD OnWTSRemoteDisconnect(PWTSSESSION_NOTIFICATION pWns){return 0;}
    virtual DWORD OnWTSSessionLogon(PWTSSESSION_NOTIFICATION pWns){return 0;}
    virtual DWORD OnWTSSessionLogoff(PWTSSESSION_NOTIFICATION pWns){return 0;}
    virtual DWORD OnWTSSessionLock(PWTSSESSION_NOTIFICATION pWns){return 0;}
    virtual DWORD OnWTSSessionUnLock(PWTSSESSION_NOTIFICATION pWns){return 0;}
    virtual DWORD OnWTSSessionRemoteControl(PWTSSESSION_NOTIFICATION pWns){return 0;}
protected:
    //内部的工具方法,設定服務為一個指定的狀态
    BOOL SetStatus(DWORD dwStatus,DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0 ,DWORD dwExitCode = 0,DWORD dwAcceptStatus = SERVICE_CONTROL_INTERROGATE);
    BOOL SetStartPending(DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0); //設為正在啟動狀态
    BOOL SetContinuePending(DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0); //設為正在繼續運作狀态
    BOOL SetPausePending(DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0); //設為正在暫停狀态
    BOOL SetPause(); //設為暫停狀态
    BOOL SetRunning(); //設為以啟動狀态
    BOOL SetStopPending(DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0); //設為正在停止狀态
    BOOL SetStop(DWORD dwExitCode = 0); //設為以停止狀态
    BOOL ReportStatus(DWORD, DWORD, DWORD);//向服務管理器報告目前服務狀态
protected:
    CString m_csSrvName; //服務名稱
    DWORD m_dwCurrentStatus; //目前狀态
    SERVICE_STATUS_HANDLE m_hCtrl; //控制句柄
public:
    static CFSZServiceMap ms_SrvMap;
};      

在這個基類中主要定義了3類函數,分别是:

1. 服務本身的代碼函數:用來處理服務的業務,實作服務的功能

2. 服務控制管理函數:包括各種控制消息的響應函數和服務控制句柄的管理函數

3. 服務狀态設定函數:主要用來設定服務的狀态

該項目使用Atl 和CString,一般在控制台程式中想要使用這二者隻需要包含頭檔案:atlcoll.h、atlstr.h即可

CFSZServiceMap 成員

該成員是用來将服務名稱和對應的類對象關聯起來,這樣以後根據服務名稱就可以找到對應的服務類的對象指針,該類型定義如下:

typedef CAtlMap<CString, CFSZService *> CFSZServiceMap;      

在每個類的構造函數中進行初始化:

CFSZService::CFSZService(const CString& csSrvName)
{
    m_csSrvName = csSrvName;
    ms_SrvMap.SetAt(m_csSrvName, this);
}      

服務的入口函數

服務的入口函數是利用宏定義的一個函數,每當需要添加一個服務的時候都需要調用宏IMPLAMENT_SERVICE_MAIN來定義一個對應的服務入口ServiceMain,該函數的定義如下:

#define IMPLAMENT_SERVICE_MAIN(srvName, className)\
    VOID WINAPI _ServiceMain_##className(DWORD dwArgc, LPTSTR* lpszArgv)\
    {\
        CFSZService *pThis = NULL;\
        if(!CFSZService::ms_SrvMap.Lookup(_T(#srvName), pThis))\
        {\
            pThis = dynamic_cast( new className(_T(#srvName)) );\
        }\
        else\
        {\
            return;\
        }\
        assert(NULL != pThis);\
        SERVICE_STATUS_HANDLE hss = RegisterServiceCtrlHandlerEx(_T(#srvName), CFSZService::HandlerEx, reinterpret_cast(pThis));\
        assert(NULL != hss);\
        pThis->SetServiceStatusHandle(hss);\
        pThis->Run(dwArgc, lpszArgv);\
        delete dynamic_cast<##className*>(pThis);\
    }      

上面的代碼首先根據傳入的類名動态建立了一個服務類(由于這裡服務對象都是動态建立和銷毀的,是以在其他地方不需要建立服務對象),然後調用RegisterServiceCtrlHandlerEx構造了一個服務控制句柄,然後調用類的SetServiceStatusHandle函數來将對應的服務控制句柄儲存起來最後調用Run函數來運作服務的正式代碼,最後當Run函數執行完畢後,服務的相應工作也做完了,這個時候删除了這個類。Run函數的定義如下:

DWORD CFSZService::Run(DWORD dwArgc, LPTSTR* lpszArgv)
{
    assert(NULL != this);
    if (OnInitService(dwArgc, lpszArgv))
    {
        RunService();
    }
    return 0;
}      

這個函數中使用了OnInitService函數來進一步初始化服務相關資訊,該函數提供了一個服務初始化的時機。比如調用相關函數進行socket的初始化或者對com環境進行初始化等等。然後調用RunService執行服務正式的代碼。

HandlerEx函數

DWORD dwRet = ERROR_SUCCESS;

    if( NULL == lpContext )
    {
        return ERROR_INVALID_PARAMETER;
    }

    CFSZService*pService =  reinterpret_cast<CFSZService*>(lpContext);
    if( NULL == pService )
    {
        return ERROR_INVALID_PARAMETER;
    }

    switch(dwControl)
    {
    case SERVICE_CONTROL_STOP:  //0x00000001 停止伺服器
        {
            dwRet = pService -> OnStop();
        }
        break;
        ...
    }