在上一篇文章中,我們概略介紹了 Windows 服務的基本架構。本文將深入探討服務程式內部的運作細節。服務程式的進入點函數(通常是 main 或 WinMain)的核心任務是初始化一個 SERVICE_TABLE_ENTRY 結構陣列,這個陣列記錄了該程式所包含的所有服務名稱及其對應的 ServiceMain 回呼函數位址。
SERVICE_TABLE_ENTRY service_table_entry[] =
{
{ "MyFTPd", FtpdMain },
{ "MyHttpd", Httpserv },
{ NULL, NULL },
};
陣列中的每個元素包含兩個成員:第一個是服務名稱,第二個是該服務的 ServiceMain 函數指標。最後一個 {NULL, NULL} 標記陣列結束。上述範例定義了兩個服務:MyFTPd 和 MyHttpd。
初始化完成後,將陣列位址傳遞給 StartServiceCtrlDispatcher 函數:
BOOL StartServiceCtrlDispatcher(
LPSERVICE_TABLE_ENTRY lpServiceStartTable
);
此函數會通知服務控制管理器(SCM)目前進程包含的服務。SCM 啟動服務進程後,會等待主執行緒呼叫此函數。若在兩分鐘內未呼叫,SCM 會以 TerminateProcess 強制終止進程,因此主執行緒必須儘快呼叫它。
StartServiceCtrlDispatcher 不會立即返回,而是進入一個循環,為陣列中每個非空元素建立一個新執行緒,分別執行對應的 ServiceMain。在循環中,它會等待兩類事件:
CtrlHandler 處理,處理完畢後回到等待狀態。ServiceMain 原型雖接受參數,但通常忽略。服務設定最好存放於登錄檔:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ServiceName\Parameters
外部應用程式修改設定時,服務可用 RegNotifyChangeKeyValue 即時感知並重新配置。
根據 MSDN,ServiceMain 必須立即執行兩項關鍵操作:
RegisterServiceCtrlHandler 登錄控制處理函數。SetServiceStatus 回報初始化狀態。原因在於 SCM 設有啟動逾時機制(Windows NT 4.0 為 80 秒),若超時未完成初始化,SCM 會判定啟動失敗。
SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler(
LPCTSTR lpServiceName, // 服務名稱
LPHANDLER_FUNCTION lpHandlerProc // CtrlHandler 函數位址
);
參數 lpServiceName 必須與 SERVICE_TABLE_ENTRY 中的名稱一致。函數返回 SERVICE_STATUS_HANDLE(32 位元控制碼),SCM 用它唯一識別服務,後續狀態回報必須攜帶此控制碼。SCM 要求在 1 秒內完成註冊,否則視為啟動失敗(NT 4.0 不會終止進程但回報錯誤,Windows 2000 已修正此行為)。
註冊成功後,必須立即呼叫 SetServiceStatus 告知 SCM 服務正在初始化:
BOOL SetServiceStatus(
SERVICE_STATUS_HANDLE hService,
LPSERVICE_STATUS lpServiceStatus
);
SERVICE_STATUS 結構包含七個成員,必須正確初始化:
typedef struct _SERVICE_STATUS {
DWORD dwServiceType; // 服務類型
DWORD dwCurrentState; // 當前狀態
DWORD dwControlsAccepted; // 接受的控制碼
DWORD dwWin32ExitCode; // Win32 錯誤碼
DWORD dwServiceSpecificExitCode;// 服務專用錯誤碼
DWORD dwCheckPoint; // 進度檢查點
DWORD dwWaitHint; // 預估等待時間(毫秒)
} SERVICE_STATUS, *LPSERVICE_STATUS;
SERVICE_WIN32_OWN_PROCESS,多服務共用進程用 SERVICE_WIN32_SHARE_PROCESS;需與桌面互動時加上 SERVICE_INTERACTIVE_PROCESS。此值生命週期內不可變。SERVICE_START_PENDING。SERVICE_ACCEPT_STOP、SERVICE_ACCEPT_PAUSE_CONTINUE、SERVICE_ACCEPT_SHUTDOWN 等。NO_ERROR;需回報 Win32 錯誤時填寫前者;服務專用錯誤時將前者設為 ERROR_SERVICE_SPECIFIC_ERROR 並在後者填入自訂碼。dwCheckPoint=0,dwWaitHint 設為合理預估毫秒數。每次呼叫 SetServiceStatus 可遞增 dwCheckPoint 指示步驟。初始化完成後,將 dwCurrentState 改為 SERVICE_RUNNING,並將兩者歸零。初始化完成並回報 SERVICE_RUNNING 後,服務通常進入一個循環,掛起等待網路請求或控制通知。收到請求時喚醒處理,處理完畢繼續等待。
若收到停止或關機通知,服務執行緒跳出迴圈,執行清理工作並返回。ServiceMain 返回導致執行緒終止,StartServiceCtrlDispatcher 內的等待執行緒喚醒,遞減服務計數,進入上述的結束邏輯。