Windows 服務 (舊稱 NT 服務) 是**無使用者介面、長時間執行、可在使用者登入前啟動、支援系統帳戶權限**的 Win32 應用程式。典型例子:資料庫引擎 (SQL Server)、Web 伺服器 (IIS)、防毒掃描器、備份代理、列印佇列、Windows Update。
| 組件 | 角色 | 執行身分 | 關鍵點 |
|---|---|---|---|
| SCM (Service Control Manager) | 服務控制管理器 — 核心權威 | LocalSystem (services.exe) | 啟動/停止/暫停服務、維護註冊資料庫 (HKLM\SYSTEM\CurrentControlSet\Services)、RPC 介面支援遠端管理 |
| 服務程式 | 實際業務邏輯 (您的 .exe) | LocalSystem / NetworkService / LocalService / 指定用戶帳戶 | 包含一或多個服務、實作 ServiceMain/CtrlHandler、向 SCM 回報狀態 |
| SCP (Service Control Program) | 服務控制程式 — 管理介面 | 互動使用者 (管理員) | services.msc, sc.exe, net.exe, PowerShell, 自訂管理工具 — 透過 SCM RPC 介面操作服務 |
services.exe (SCM) 啟動服務程式進程 → 進程主執行緒呼叫 StartServiceCtrlDispatcher 傳入 SERVICE_TABLE_ENTRY 陣列 (服務名稱 + ServiceMain 指標) → SCM 為**每個服務**建立一條**專屬執行緒**執行對應的 ServiceMain。因此單一進程天然多執行緒。
/SUBSYSTEM:CONSOLE) 即可。install, remove, debug)、建立 SERVICE_TABLE_ENTRY、呼叫 StartServiceCtrlDispatcher、等待所有服務結束、進程清理。
VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR* lpszArgv) {
// dwArgc: 參數個數 (含服務名稱)
// lpszArgv: 參數字串陣列 (啟動參數可由 SCM 傳遞)
}
生命週期:
g_hStatus = RegisterServiceCtrlHandler(szServiceName, CtrlHandler)。SERVICE_START_PENDING,呼叫 SetServiceStatus。SERVICE_RUNNING;失敗 → SERVICE_STOPPED + 錯誤碼。SERVICE_STOPPED → 返回 (執行緒結束)。
VOID WINAPI CtrlHandler(DWORD dwControl) {
switch (dwControl) {
case SERVICE_CONTROL_STOP: // 停止
case SERVICE_CONTROL_PAUSE: // 暫停
case SERVICE_CONTROL_CONTINUE: // 繼續
case SERVICE_CONTROL_INTERROGATE: // 狀態查詢
case SERVICE_CONTROL_SHUTDOWN: // 系統關機
case SERVICE_CONTROL_PARAMCHANGE: // 參數變更
case SERVICE_CONTROL_NETBINDADD/REMOVE/ENABLE/DISABLE: // 網路綁定變更
// 自訂控制碼: SERVICE_CONTROL_USER + 128 ~ 255
}
// 關鍵:僅設定旗標/觸發事件,**不**執行耗時操作、**不**直接呼叫 SetServiceStatus 回報 PENDING (見進階章節)
}
安裝本質:在上述路徑建立以服務名稱命名的子鍵,設定關鍵值:
| 值名稱 | 類型 | 說明 |
|---|---|---|
| ImagePath | REG_EXPAND_SZ | 服務執行檔完整路徑 (含參數),如 "C:\MySvc\Svc.exe" |
| DisplayName | REG_SZ | 顯示名稱 (services.msc 顯示) |
| Description | REG_SZ | 描述文字 |
| Start | REG_DWORD | 啟動類型: 2=AUTO, 3=DEMAND, 4=DISABLED, 0=BOOT, 1=SYSTEM |
| ErrorControl | REG_DWORD | 錯誤控制: 0=IGNORE, 1=NORMAL, 2=SEVERE, 3=CRITICAL |
| Type | REG_DWORD | 服務類型: 0x10=WIN32_OWN_PROCESS, 0x20=WIN32_SHARE_PROCESS, 0x100=INTERACTIVE_PROCESS (舊) |
| ObjectName | REG_SZ | 執行帳戶: LocalSystem, NT AUTHORITY\NetworkService, NT AUTHORITY\LocalService, 域\用戶 |
| DependOnService | REG_MULTI_SZ | 相依服務名稱清單 |
| FailureActions | REG_BINARY | 失敗復原動作 (重啟、執行程式、重開機) |
void InstallService(const char* svcName, const char* displayName, const char* exePath) {
SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
if (!hSCM) throw Exception("OpenSCManager 失敗: " + IntToStr(GetLastError()));
SC_HANDLE hSvc = CreateService(
hSCM, svcName, displayName, SERVICE_ALL_ACCESS,
SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
exePath, NULL, NULL, NULL, NULL, NULL
);
if (!hSvc) {
DWORD err = GetLastError();
CloseServiceHandle(hSCM);
throw Exception("CreateService 失敗: " + IntToStr(err));
}
// 可選:設定描述
SERVICE_DESCRIPTION sd = { (LPSTR)"My custom service description" };
ChangeServiceConfig2(hSvc, SERVICE_CONFIG_DESCRIPTION, &sd);
CloseServiceHandle(hSvc);
CloseServiceHandle(hSCM);
}
| 帳戶類型 | 權限範圍 | 網路存取 | 典型用途 |
|---|---|---|---|
| LocalSystem (預設) | 本機完整權限 (相當於核心模式) | **無**網路身分 (無法存取網路共享、SQL 遠端) | 本機系統服務、驅動程式、硬體抽象層 |
| NetworkService | 有限本機權限 (類似 Users 群組) | **有**網路身分 (電腦帳戶 DOMAIN\MACHINE$) | 需網路存取的服務 (IIS 應用程式集區、更新服務) |
| LocalService | 有限本機權限 (匿名登入) | **無**網路身分 (匿名) | 不需網路、低權限原則服務 |
| 指定用戶帳戶 | 依用戶權限 | 依用戶權限 | 需特定資源權限、Kerberos 委派、SQL 整合驗證 |
最佳實務:遵循**最小權限原則**,優先使用 NetworkService/LocalService,避免 LocalSystem 除非必要。指定用戶帳戶需管理密碼輪換 (Managed Service Accounts / Group Managed Service Accounts 可自動化)。
服務無 UI,除錯/審計全靠事件日誌。
void LogEvent(const char* fmt, ...) {
HANDLE h = RegisterEventSource(NULL, "MyService");
if (h) {
char buf[1024]; va_list ap; va_start(ap, fmt); vsprintf(buf, fmt, ap); va_end(ap);
LPCSTR msgs[1] = { buf };
ReportEvent(h, EVENTLOG_INFORMATION_TYPE, 0, 0, NULL, 1, 0, msgs, NULL);
DeregisterEventSource(h);
}
}
EVENTLOG_ERROR_TYPE, WARNING_TYPE, INFORMATION_TYPE, AUDIT_SUCCESS/FAILURE。在 main() 偵測參數 debug 或 /console,直接呼叫 ServiceMain (模擬 SCM),輸出日誌至主控台,方便單步除錯。
int main(int argc, char* argv[]) {
if (argc > 1 && strcmp(argv[1], "debug") == 0) {
// 模擬 SCM 環境
SERVICE_TABLE_ENTRY table[] = { { "MyService", ServiceMain }, { NULL, NULL } };
// 此處不呼叫 StartServiceCtrlDispatcher,直接在主執行緒跑 ServiceMain
// 需自行處理 Ctrl+C 信號模擬停止
ServiceMain(0, NULL);
return 0;
}
if (argc > 1 && strcmp(argv[1], "install") == 0) { InstallService(...); return 0; }
if (argc > 1 && strcmp(argv[1], "remove") == 0) { RemoveService(...); return 0; }
SERVICE_TABLE_ENTRY table[] = { { "MyService", ServiceMain }, { NULL, NULL } };
StartServiceCtrlDispatcher(table);
return 0;
}
| 錯誤 | 現象 | 修正 |
|---|---|---|
| 使用 WinMain / GUI 子系統 | 服務啟動後立即結束、SCM 報錯 1053 | 改用 main()、/SUBSYSTEM:CONSOLE |
| ServiceMain 內阻塞不返回 | SCM 認為啟動逾時 (預設 30秒),強制終止服務 | 快速完成初始化並進入主迴圈,或設定 dwWaitHint、dwCheckPoint 定期回報 |
| CtrlHandler 執行耗時操作 | 同進程其他服務控制碼無回應、SCM 認為服務掛起 | CtrlHandler 僅設旗標/觸發事件,毫秒級返回 |
| 未呼叫 SetServiceStatus 回報 RUNNING | 服務永遠停留在 START_PENDING,SCM 終止 | 初始化成功後務必回報 RUNNING |
| 硬編碼服務名稱/路徑 | 安裝多實例衝突、部署困難 | 從命令列/設定檔/資源讀取 |
| 忽略 64 位元 / UAC / Session 0 隔離 | Vista+ 服務在 Session 0,無法互動桌面 (UI、訊息盒失效) | 不建立 UI、使用 WTSSendMessage 或命名管道/IPC 與使用者進程通訊 |
services.msc, sc.exe (查詢/啟動/停止/設定), regedit (檢視註冊鍵值), Event Viewer (查看日誌), Process Explorer (檢視服務進程、執行緒、控制代碼)。掌握 SCM/服務/SCP 三角關係、ServiceMain/CtrlHandler 分工、狀態回報流程、安裝註冊鍵值、帳戶權限模型,即具備開發穩健 Windows 服務的基礎。下一階段將深入:服務停止競態條件、暫停/繼續語意、非同步 I/O 架構 (IOCP)、恢復動作設定、事件日誌最佳實務、安裝程式整合 (WiX/Inno Setup)。