Windows服務(NT Service)專為後台長期運行、無用戶交互任務設計。雖然C++封裝類開發效率高,但面向對象封裝掩蓋了底層Win32 API調用細節,不利於初學者理解「服務控制管理器(SCM)如何調度服務、何時調用什麼函數」。C語言直擊API本質,是學習服務機制的最佳語言。本文以「定期查詢可用物理內存並寫入日誌」為例,完整演示服務的創建、安裝、配置、測試全生命周期。
main() → StartServiceCtrlDispatcher(分派表)
↓
SCM創建控制分派器線程 → 為每個服務啟動ServiceMain線程
↓
ServiceMain → RegisterServiceCtrlHandler(註冊控制處理器)
↓
初始化 → SetServiceStatus(SERVICE_RUNNING)
↓
工作循環(查詢內存、寫日誌、Sleep) ← ControlHandler處理STOP/SHUTDOWN請求修改狀態
↓
服務停止 → 清理 → 退出
#include <windows.h>
#include <stdio.h>
#define SLEEP_TIME 5000 // 查詢間隔5秒
#define LOGFILE "C:\\MyServices\\memstatus.txt" // 日誌路徑
int WriteToLog(char* str) {
FILE* log = fopen(LOGFILE, "a+");
if (!log) return -1;
fprintf(log, "%s\n", str);
fclose(log);
return 0;
}
SERVICE_STATUS ServiceStatus; // 服務狀態結構
SERVICE_STATUS_HANDLE hStatus; // 狀態句柄
void ServiceMain(int argc, char** argv);
void ControlHandler(DWORD request);
int InitService();
main():只負責啟動分派器void main() {
SERVICE_TABLE_ENTRY ServiceTable[2];
ServiceTable[0].lpServiceName = "MemoryStatus";
ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;
ServiceTable[1].lpServiceName = NULL;
ServiceTable[1].lpServiceProc = NULL;
StartServiceCtrlDispatcher(ServiceTable); // 進入分派器,不再返回
}
關鍵結構 SERVICE_TABLE_ENTRY:lpServiceName(服務名)、lpServiceProc(入口函數指針)。數組以雙NULL結尾。StartServiceCtrlDispatcher 將主線程轉為控制分派器,必須在30秒內調用否則報錯。
ServiceMain——服務的真正主函數void ServiceMain(int argc, char** argv) {
hStatus = RegisterServiceCtrlHandler("MemoryStatus", (LPHANDLER_FUNCTION)ControlHandler);
// 初始化 ServiceStatus 結構...
}
ServiceStatus 關鍵字段| 字段 | 初始值 | 含義 |
|---|---|---|
dwServiceType | SERVICE_WIN32 | Win32自有進程服務 |
dwCurrentState | SERVICE_START_PENDING | 正在啟動中 |
dwControlsAccepted | SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN | 接受停止/關機請求 |
dwWin32ExitCode / dwServiceSpecificExitCode | 0 | 退出碼(運行時更新) |
dwCheckPoint / dwWaitHint | 0 | 啟動超過30秒時用於心跳上報 |
調用 SetServiceStatus(hStatus, &ServiceStatus) 上報SCM。
int InitService() { return WriteToLog("Monitoring started."); }
// 在ServiceMain中:
int error = InitService();
if (error) {
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
ServiceStatus.dwWin32ExitCode = -1;
SetServiceStatus(hStatus, &ServiceStatus);
return; // 退出線程,服務終止
}
ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(hStatus, &ServiceStatus);
while (ServiceStatus.dwCurrentState == SERVICE_RUNNING) {
MEMORYSTATUS ms; ms.dwLength = sizeof(ms);
GlobalMemoryStatus(&ms);
char buf[32]; sprintf(buf, "%lu", ms.dwAvailPhys);
if (WriteToLog(buf)) break; // 寫失敗則退出循環
Sleep(SLEEP_TIME);
}
// 循環結束前上報停止狀態
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(hStatus, &ServiceStatus);
ControlHandler——響應SCM控制請求類似窗口消息循環,處理 SERVICE_CONTROL_STOP、SERVICE_CONTROL_SHUTDOWN:
void ControlHandler(DWORD request) {
switch (request) {
case SERVICE_CONTROL_STOP:
case SERVICE_CONTROL_SHUTDOWN:
WriteToLog("Monitoring stopped.");
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(hStatus, &ServiceStatus); // 必須上報
break;
default: break;
}
// PAUSE/CONTINUE 本例未處理
}
核心機制:工作循環檢查 ServiceStatus.dwCurrentState,ControlHandler修改該全局變量實現線程間通信。
MemoryStatus.exe,拷貝至 C:\MyServices\sc.exe (Win32 Platform SDK工具) 安裝:
sc create MemoryStatus binpath= c:\MyServices\MemoryStatus.exe
// 注意 binpath= 後有空格!
services.msc) 可見 MemoryStatus,啟動類型默認為「手動」。sc delete MemoryStatus(標記刪除,重啟後生效)。// 啟動服務後,memstatus.txt 內容示例:
Monitoring started.
273469440
273379328
273133568
273084416
Monitoring stopped.
故障注入測試:將日誌文件設為「只讀」→ 啟動服務失敗(初始化寫入失敗)→ 服務狀態顯示「已停止」;運行中設為只讀 → 服務自動停止(工作循環寫入失敗跳出)。
ServiceMain 中創建多個工作線程,主線程負責監控與狀態上報。掌握這五步,你已擁有了編寫任意Windows後台服務的核心能力。動手改造 MemoryStatus,實現你的專屬服務吧!