上一篇概述了服務基礎概念,本文深入核心細節:main() 如何初始化服務表、StartServiceCtrlDispatcher 如何與 SCM 協作、每個服務實例如何在獨立線程中運行 ServiceMain。掌握這一流程是編寫穩健多服務進程的關鍵。
SERVICE_TABLE_ENTRY定義進程內包含的所有服務名稱與對應的 ServiceMain 入口:
typedef struct _SERVICE_TABLE_ENTRY {
LPSTR lpServiceName; // 服務名稱 (註冊表中 ServiceName)
LPSERVICE_MAIN_FUNCTION lpServiceProc; // ServiceMain 函數指針
} SERVICE_TABLE_ENTRY;
// 範例:單進程承載兩個服務
SERVICE_TABLE_ENTRY dispatchTable[] = {
{ "MyFTPd", FtpdMain }, // FTP 服務入口
{ "MyHttpd", HttpdMain }, // HTTP 服務入口
{ NULL, NULL } // 結束標記 (必須)
};
StartServiceCtrlDispatcherBOOL StartServiceCtrlDispatcher(LPSERVICE_TABLE_ENTRY lpServiceStartTable);
StartServiceCtrlDispatcher。超時 SCM 會 `TerminateProcess` 殺死進程。StartServiceCtrlDispatcher 為表中**每個非 NULL 項目**創建一個**新線程**,各線程開始執行對應的 ServiceMain 函數。while (TRUE) {
// 1. SCM 發送控制通知給本進程某服務 → 喚醒
// 2. 某服務狀態變更需回報 (內部機制) → 喚醒
// 喚醒後:
// - 識別目標服務
// - 調用該服務的 CtrlHandler 處理控制碼
// - CtrlHandler 返回 → 循環繼續等待
}
ServiceMain 簽名與職責VOID WINAPI ServiceMain(DWORD dwArgc, LPSTR* lpszArgv);
// dwArgc: 啟動參數個數 (來自註冊表 ImagePath 或 SCM 啟動參數)
// lpszArgv: 參數字符串陣列 (argv[0] = 服務名)
g_statusHandle = RegisterServiceCtrlHandler(lpszArgv[0], CtrlHandler); 失敗即返回。dwServiceType, dwControlsAccepted 等。SERVICE_START_PENDING → 逐步 dwCheckPoint → 最終 SERVICE_RUNNING。進程主線程 (main)
│
├─► StartServiceCtrlDispatcher() ──► 進入調度迴路 (永久阻塞)
│ │
│ ├─► 服務 A 線程 ──► ServiceMain("MyFTPd") ──► FTP 核心迴路
│ │ │
│ │ └─► CtrlHandler_A (處理 FTP 服務控制碼)
│ │
│ ├─► 服務 B 線程 ──► ServiceMain("MyHttpd") ──► HTTP 核心迴路
│ │ │
│ │ └─► CtrlHandler_B (處理 HTTP 服務控制碼)
│ │
│ └─► ... 更多服務線程
│
└─► (無其他代碼執行)
| 問題現象 | 根因 | 解決方案 |
|---|---|---|
| 服務啟動後立即停止,事件日誌記 1053 錯誤 (ERROR_SERVICE_REQUEST_TIMEOUT) | 主線程未在 2 分鐘內調用 StartServiceCtrlDispatcher,或 ServiceMain 過早返回 | main() 第一時間調用 Dispatcher;ServiceMain 必須含無限迴路或等待事件,不可直接返回 |
| 無法停止服務,SCM 強制終止 | CtrlHandler 未及時回報 STOP_PENDING / STOPPED,或工作線程未響應停止信號 | CtrlHandler 立即回報 PENDING;用事件/標誌通知工作線程;工作線程定期檢查退出標誌 |
| 多服務進程中,僅第一個服務啟動 | DispatchTable 結束標記 {NULL,NULL} 缺失或位置錯誤 | 確保陣列以 {NULL,NULL} 結尾,且前面所有項目 Name/Proc 均非 NULL |
| 服務參數 (argv) 為空或錯誤 | 註冊表 ImagePath 格式錯誤,或 SCM 傳遞方式誤解 | ImagePath 應為 `"C:\Path\Svc.exe" "param1" "param2"`;ServiceMain 從 lpszArgv[1] 開始取業務參數 |
// svc.cpp
#include
#include
SERVICE_STATUS_HANDLE g_hStatus = NULL;
SERVICE_STATUS g_status = {0};
HANDLE g_hStopEvent = NULL;
VOID WINAPI CtrlHandler(DWORD dwCtrl) {
if (dwCtrl == SERVICE_CONTROL_STOP) {
g_status.dwCurrentState = SERVICE_STOP_PENDING;
g_status.dwCheckPoint = 1;
g_status.dwWaitHint = 5000;
SetServiceStatus(g_hStatus, &g_status);
SetEvent(g_hStopEvent); // 通知工作線程
}
}
VOID WINAPI ServiceMain(DWORD argc, LPSTR* argv) {
g_hStatus = RegisterServiceCtrlHandler("MyDemoSvc", CtrlHandler);
if (!g_hStatus) return;
g_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
g_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
g_status.dwCurrentState = SERVICE_START_PENDING;
g_status.dwCheckPoint = 1;
SetServiceStatus(g_hStatus, &g_status);
g_hStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
// ... 初始化資源 (Socket, DB, 執行緒池) ...
g_status.dwCurrentState = SERVICE_RUNNING;
g_status.dwCheckPoint = 0;
SetServiceStatus(g_hStatus, &g_status);
WaitForSingleObject(g_hStopEvent, INFINITE); // 核心等待
// ... 清理資源 ...
g_status.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(g_hStatus, &g_status);
}
int main() {
SERVICE_TABLE_ENTRY table[] = {
{ "MyDemoSvc", ServiceMain },
{ NULL, NULL }
};
StartServiceCtrlDispatcher(table); // 永不返回
return 0;
}
StartServiceCtrlDispatcher 是服務進程的「心臟起搏器」:它將單一主線程進程轉化為**多線程服務容器**,每個服務擁有獨立線程運行 ServiceMain,主線程轉型為**控制調度器**負責分發 SCM 指令至對應 CtrlHandler。理解這一「一主多從、調度分發」模型,是診斷服務啟動失敗、停止超時、多服務干擾等疑難雜症的基石。