首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

Windows 服務開發深度解析 (四):服務停止競態條件、控制處理程序設計與最佳實務

     編輯:關於C++

前言:服務開發的隱藏地雷區

前三章涵蓋服務架構、安裝、啟動、基本控制處理。本章聚焦**高階應用開發者必須面對的核心難題**:服務停止時的競態條件、控制處理程序與服務主執行緒的正確協作模式、狀態回報時機、避免「服務無回應」誤判。內容基於實戰驗證,極具實務價值。

核心問題:誰來負責停止服務?

架構回顧:

  • 控制處理程序:由 **SCM 主執行緒** 呼叫 (CtrlHandler),接收停止/暫停/繼續等控制碼。**必須極速返回** (毫秒級),否則阻塞同進程其他服務的控制處理。
  • 服務主函式:由 **專屬服務執行緒** 執行 (ServiceMain),執行實際業務邏輯。

錯誤做法:在 CtrlHandler 直接呼叫 TerminateThread 強殺服務執行緒 → 資源洩漏、DLL 無通知、資料損毀。
正確做法:CtrlHandler **通知** 服務執行緒停止,服務執行緒**優雅結束**並回報最終狀態。

競態條件實戰重現

場景:快速連續發送兩次「暫停」指令

實驗:修改服務,CtrlHandler 收到 SERVICE_CONTROL_PAUSE 立即回報 SERVICE_PAUSE_PENDING,啟動新執行緒 Sleep 5 秒後回報 SERVICE_PAUSED。在兩個命令提示字元視窗快速連續執行 net pause MyService。

觀察到的事件日誌序列

SERVICE_PAUSE_PENDING  (第一次請求)
SERVICE_PAUSE_PENDING  (第二次請求!服務已在暫停中卻又收到請求)
SERVICE_PAUSED         (第一次完成)
SERVICE_PAUSED         (第二次完成)

根因:SCM **不阻擋** 重複控制碼。net.exe 繞過「服務」管理工具的 UI 禁用邏輯,直接送請求給 SCM。SCM 轉發給 CtrlHandler,服務未同步狀態導致重複處理。

解決方案:狀態機同步 + 單一責任原則

原則一:CtrlHandler 只做「排隊通知」,不做狀態變更


// 全域/類別成員
enum ServiceState { RUNNING, PAUSING, PAUSED, STOPPING, STOPPED };
volatile ServiceState g_desiredState = RUNNING; // 原子變數或 Interlocked
HANDLE g_hStateChangeEvent = CreateEvent(NULL, TRUE, FALSE, NULL); // 手動重設事件

VOID WINAPI CtrlHandler(DWORD dwControl) {
  switch (dwControl) {
    case SERVICE_CONTROL_PAUSE:
      if (InterlockedCompareExchange((LONG*)&g_desiredState, PAUSING, RUNNING) == RUNNING)
        SetEvent(g_hStateChangeEvent); // 通知服務執行緒
      break;
    case SERVICE_CONTROL_CONTINUE:
      if (InterlockedCompareExchange((LONG*)&g_desiredState, RUNNING, PAUSED) == PAUSED)
        SetEvent(g_hStateChangeEvent);
      break;
    case SERVICE_CONTROL_STOP:
      if (InterlockedCompareExchange((LONG*)&g_desiredState, STOPPING, RUNNING) == RUNNING ||
          InterlockedCompareExchange((LONG*)&g_desiredState, STOPPING, PAUSED) == PAUSED)
        SetEvent(g_hStateChangeEvent);
      break;
    case SERVICE_CONTROL_INTERROGATE:
      // 立即回報當前狀態
      SetServiceStatus(g_hStatus, &g_status);
      break;
  }
  // **不在此呼叫 SetServiceStatus 回報 PENDING 狀態** (見原則二)
}

原則二:服務主執行緒獨佔狀態轉換與 SetServiceStatus


VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv) {
  // 註冊 CtrlHandler、初始化狀態結構...
  g_status.dwCurrentState = SERVICE_START_PENDING;
  SetServiceStatus(g_hStatus, &g_status);

  // 初始化業務資源...
  g_status.dwCurrentState = SERVICE_RUNNING;
  SetServiceStatus(g_hStatus, &g_status);

  // 主迴圈:等待狀態變更事件
  while (g_desiredState != STOPPING) {
    DWORD wait = WaitForSingleObject(g_hStateChangeEvent, 1000); // 定期輪詢業務邏輯
    ResetEvent(g_hStateChangeEvent);

    // 處理業務邏輯 (非阻塞、可中斷)
    DoWorkIteration();

    // 檢查並執行狀態轉換
    switch (g_desiredState) {
      case PAUSING:
        PauseWork(); // 停止接收新工作、等待進行中工作完成
        g_status.dwCurrentState = SERVICE_PAUSED;
        SetServiceStatus(g_hStatus, &g_status);
        g_desiredState = PAUSED; // 更新為穩定狀態
        break;
      case RUNNING: // 從 PAUSED 回復
        ResumeWork();
        g_status.dwCurrentState = SERVICE_RUNNING;
        SetServiceStatus(g_hStatus, &g_status);
        break;
      case STOPPING:
        // 迴圈條件會結束,跳出執行清理
        break;
    }
  }

  // 清理資源、關閉控制代碼、釋放記憶體...
  g_status.dwCurrentState = SERVICE_STOPPED;
  g_status.dwWin32ExitCode = NO_ERROR;
  SetServiceStatus(g_hStatus, &g_status); // 最後一次回報
}

優點總結

  • 無競態:狀態轉換僅在單一執行緒 (服務主執行緒) 發生,InterlockedCompareExchange 確保控制碼冪等。
  • CtrlHandler 極速:僅原子操作 + SetEvent,微秒級返回,不阻塞 SCM。
  • 語意清晰:PENDING 狀態由服務執行緒在**真正開始處理**時回報,而非控制碼到達時虛報。
  • 可擴充:新增控制碼 (自訂 SERVICE_CONTROL_USER+X) 只需擴充狀態機。

進階議題:SuspendThread 陷阱

嘗試在 CtrlHandler 直接 SuspendThread 服務執行緒以實現「暫停」:**極度危險**。

  • 服務執行緒可能持有關鍵區、Heap 鎖、Loader Lock → 死鎖整個進程 (含其他服務)。
  • 無法優雅處理進行中的 I/O、交易、網路連線。
  • 無法實現「暫停後仍可停止」:SuspendThread 後無法喚醒處理停止邏輯,只能 TerminateThread (再次違反原則)。

結論:暫停 = 業務層面停止接收新工作 + 等待進行中工作完成,**非執行緒層面暫停**。

SCM 「服務無回應」誤判機制與對策

SCM 發送控制碼後,預期服務在合理時間內 (預設 ~30 秒,可由 dwWaitHint 延長) 回報新狀態。若服務主執行緒忙於長時間運算/阻塞 I/O,無法處理 g_hStateChangeEvent,SCM 彈出「服務未回應」對話框。

對策

  1. 服務主迴圈非阻塞化:所有 I/O 採用非同步 (IOCP、Overlapped I/O、WSAPoll/select)、運算切分為小單元並定期 WaitForSingleObject(event, 0) 檢查狀態。
  2. 專用控制執行緒:若業務邏輯極難非阻塞化,可建立**額外執行緒**專責 WaitForSingleObject(g_hStateChangeEvent, INFINITE) 處理狀態轉換並呼叫 SetServiceStatus,主執行緒專心業務。但需妥善同步共享資料。
  3. 設定合理 dwWaitHint:在回報 PENDING 時設定 dwWaitHint = 60000 (60秒) + dwCheckPoint 遞增,告知 SCM 仍在處理中。

完整服務骨架程式碼 (C, 最小功能:安裝、啟動、停止、記錄事件)


#include 
#include 
#include 

#define SVC_NAME TEXT("MyDemoService")
#define SVC_DISPLAY TEXT("My Demo Service")

SERVICE_STATUS_HANDLE g_hStatus = NULL;
SERVICE_STATUS g_status = {0};
HANDLE g_hStopEvent = NULL;
HANDLE g_hStateChangeEvent = NULL;
volatile LONG g_desiredState = 0; // 0=RUNNING, 1=STOPPING

VOID WINAPI LogEvent(LPCTSTR fmt, ...) {
  HANDLE h = RegisterEventSource(NULL, SVC_NAME);
  if (h) {
    TCHAR buf[1024]; va_list ap; va_start(ap, fmt); _vstprintf(buf, fmt, ap); va_end(ap);
    LPCTSTR msgs[1] = { buf };
    ReportEvent(h, EVENTLOG_INFORMATION_TYPE, 0, 0, NULL, 1, 0, msgs, NULL);
    DeregisterEventSource(h);
  }
}

VOID WINAPI CtrlHandler(DWORD ctrl) {
  switch (ctrl) {
    case SERVICE_CONTROL_STOP:
      if (InterlockedCompareExchange(&g_desiredState, 1, 0) == 0)
        SetEvent(g_hStateChangeEvent);
      break;
    case SERVICE_CONTROL_INTERROGATE:
      SetServiceStatus(g_hStatus, &g_status);
      break;
  }
}

VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv) {
  g_hStatus = RegisterServiceCtrlHandler(SVC_NAME, CtrlHandler);
  if (!g_hStatus) return;

  g_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
  g_status.dwCurrentState = SERVICE_START_PENDING;
  g_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
  g_status.dwCheckPoint = 1; g_status.dwWaitHint = 30000;
  SetServiceStatus(g_hStatus, &g_status);

  g_hStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
  g_hStateChangeEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

  // 初始化...
  LogEvent(TEXT("Service initializing..."));
  Sleep(2000); // 模擬初始化

  g_status.dwCurrentState = SERVICE_RUNNING;
  g_status.dwCheckPoint = 0; g_status.dwWaitHint = 0;
  SetServiceStatus(g_hStatus, &g_status);
  LogEvent(TEXT("Service started."));

  // 主迴圈
  while (g_desiredState == 0) {
    DWORD wait = WaitForSingleObject(g_hStateChangeEvent, 1000);
    if (wait == WAIT_OBJECT_0) ResetEvent(g_hStateChangeEvent);
    // DoWork()...
  }

  // 清理
  LogEvent(TEXT("Service stopping..."));
  CloseHandle(g_hStopEvent);
  CloseHandle(g_hStateChangeEvent);

  g_status.dwCurrentState = SERVICE_STOPPED;
  g_status.dwWin32ExitCode = NO_ERROR;
  SetServiceStatus(g_hStatus, &g_status);
  LogEvent(TEXT("Service stopped."));
}

void InstallService() {
  SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
  TCHAR path[MAX_PATH]; GetModuleFileName(NULL, path, MAX_PATH);
  SC_HANDLE svc = CreateService(scm, SVC_NAME, SVC_DISPLAY, SERVICE_ALL_ACCESS,
    SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
    path, NULL, NULL, NULL, NULL, NULL);
  CloseServiceHandle(svc); CloseServiceHandle(scm);
}

int _tmain(int argc, TCHAR* argv[]) {
  if (argc == 2 && _tcscmp(argv[1], TEXT("install")) == 0) {
    InstallService(); return 0;
  }
  SERVICE_TABLE_ENTRY dispatch[] = { { SVC_NAME, ServiceMain }, { NULL, NULL } };
  StartServiceCtrlDispatcher(dispatch);
  return 0;
}

總結:服務開發鐵律

  1. CtrlHandler = 郵差:只投遞控制碼 (設定旗標/事件),**不**處理業務、**不**回報 PENDING (除非極簡單同步服務)。
  2. ServiceMain = 廚師:獨佔狀態機、執行業務、決定何時回報何種狀態、優雅清理。
  3. 狀態轉換 = 原子 + 序列化:InterlockedCompareExchange + 單一執行緒狀態機,消除競態。
  4. 阻塞 = 服務殺手:主迴圈必須定期輪詢控制事件,或採用非同步/專用控制執行緒架構。
  5. 清理 = 最後責任:SERVICE_STOPPED 回報前,所有資源必須釋放、控制代碼關閉、記憶體歸還。

遵循此架構,您的服務將在壓力測試、快速重複控制、系統關機、叢集容錯移轉等極端場景下保持穩定、可控、可觀測。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved