程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> Windows服務編寫原理及探討(4)

Windows服務編寫原理及探討(4)

編輯:關於C++

(四)一些問題的討論

前面幾章的內容都是服務的一些通用的編寫原理,但裡面隱含著一些問題,編寫簡單的服務時看不出來,但遇到復雜的應用就會出現一些問題,所以本章就是用來分析、解決這些問題的,適用於高級應用的開發人員。我這一章的內容都是經過實驗得到的,很有實際意義。

我在第一章裡面就說過,是由一個服務的主線程執行CtrlHandler函數,它將收到各種控制命令,但是真正處理命令,執行操作的是ServiceMain的線程。現在,當一個SERVICE_CONTROL_STOP到達之後,你作為一個開發者,要怎樣停止這個服務?在我看過的一些源代碼裡,大部分只是簡單的調用TerminateThread函數去強行殺掉服務進程。但應該稍稍有點線程編程的常識就應該知道TerminateThread函數是可用的調用中最為糟糕的一個,服務線程將得不到任何機會去做應該的清理工作,諸如清除內存、釋放核心對象,Dlls也得不到任何線程已經被毀的通知。

所以停止服務的適當方法是以某種方式激活服務線程,讓它停止繼續提供服務功能,然後執行完當前操作和清除工作後返回。這就表示你必須在CtrlHandler線程和ServiceMain線程之間執行適當的線程通信。現在已知的最好的內部線程通信機制是I/O Completion Port(I/O 完成端口),假如你編寫的是一個大型的服務,需要同時處理為數眾多的請求,並且運行在多處理器系統上面,這個模型就可以提供最佳的系統性能。但也正因為它的復雜性較高,在小規模的應用上面不值得花費很多的時間和精力,這時作為開發者可以適當的選取其它的通信方式,諸如異步過程調用隊列、套接字和窗口消息,以適應實際情況。

開發服務時的另外一個重要問題就是調用SetServiceStatus函數時的所有狀態報告問題。很多的服務開發者為了在什麼時候調用SetServiceStatus的問題而常常產生爭論,一般推薦的方法就是:先調用SetServiceStatus函數,報告SERVICE_STOP_PENDING狀態,然後將控制代碼傳給服務線程或者再建立一個新的線程,讓它去繼續執行操作,當該線程即將執行完操作之前,再由它將服務的狀態設置成SERVICE_STOPPED,然後服務正好停止。

上面的主意從兩個方面來講還是很不錯的。首先服務可以立即確認收到了控制代碼,並將在它認為適當的時候進行處理;然後就是因為前面說過的,執行CtrlHandler函數的是主線程,如果按照這種工作方法,CtrlHandler函數可以迅速的返回,不會影響到其它服務可能收到的控制請求,對含有多個服務的程序來說,響應各個服務的控制代碼的速度會大大的提高。可是,隨之而來的是問題—— race condition 即“競爭條件”的產生。

擺在下面的就是一個競爭條件的例子,我花了一點時間來修改我的基本服務的代碼,意圖故意引發“競爭條件”的發生。我添加了一個線程,CtrlHandler函數的線程在收到請求後立刻作出反應,將當前的服務狀態設置成“請求正在被處理”即..._PENDING,然後由我添加的線程在睡眠了5秒之後再將服務狀態設置成“請求已完成”狀態——以模擬服務正在處理一些不可中止的事件,只有處理完成後才會更改服務的狀態。一切就緒之後,我嘗試在短時間內連續發送兩個“暫停”請求,如果“競爭條件”不存在的話應該只有先發送的那個請求能夠到達SCM,而另一個則應該返回請求發送失敗的信息,天下太平。

事實上很不幸的,我成功了。當我在兩個不同的“命令提示符”窗口分別同樣的輸入下面的命令:

net pause kservice

之後在“事件查看器”裡面,我找到了我的服務在“應用程序日志”裡添加的事件記錄,結果是我得到了這樣的事件列表:

SERVICE_PAUSE_PENDING

SERVICE_PAUSE_PENDING

SERVICE_PAUSED

SERVICE_PAUSED

看上去很奇怪是不是?因為服務處於正在暫停狀態的時候,它不應該被再次暫停的。但事實擺在眼前,很多服務都曾明確的報告過上面的順序狀態。我曾經認為這時SCM應該說些什麼或做些什麼,以阻止“競爭狀態”的出現,但實驗結果告訴我SCM似乎對此無能為力,因為它不能控制狀態代碼在什麼時候被發送。當用戶使用“管理工具”裡面的“服務”工具來管理服務的狀態的時候,在一個“暫停”請求已經發出之後不能再次用這個工具向它發出“暫停”請求,如果正在暫停服務,會有一個對話框出現,阻止你按下它後面的“服務”工具的工具欄上的任何按鈕,如果已經暫停,“暫停“按鈕將變成灰色。但是這時用命令行工具 net.exe 就可以很順利地將暫停請求再次送到服務。證據就是我添加的其他事件記錄裡面記下了SetServiceStatus的調用全都成功了,這更進一步的說明了我提交的兩個暫停請求都經過SCM,然後到達了我的服務。

接下來我又進行了其它的測試,例如先發送“暫停”請求,後發送“停止”請求,和先發送“停止”請求,再發送“暫停”或“停止”請求。前一種情況更加糟糕,先發送的“暫停”請求和後發送的“停止”請求都沒有得到什麼好下場,雖然SCM老老實實的先暫停了服務,後停止了服務,但 net.exe 的兩個實例的調用均告失敗。不過在測試先發送停止“請求”的時候,所有的現象都表示這兩個請求只有先發送的“停止”到達了SCM,這還算是個好消息...

為了解決這個問題,當服務得到一個“停止”“暫停”或“繼續”請求的時候,應該首先檢查服務是否已經在處理另外的一個請求,如果是,就依情況而定:是不調用SetServiceStatus直接返回還是暫時忍耐直到前一個請求動作完成再調用SetServiceStatus,這是你作為一個開發者要自己決定的。

如果說前面的問題已經足夠麻煩了,下面的問題會令你覺得更加怪異。它其實是一種可以解決上面的問題的方法:當CtrlHandler函數的線程收到SERVICE_PAUSE_PENDING請求之後,它調用SetServiceStatus報告服務正在暫停,然後由它自己調用SuspendThread來暫停服務的線程,然後再由它自己調用SetServiceStatus報告服務已經被暫停。這樣做的確避免了“競爭條件”的出現,因為所有的工作都是由一個函數來做的。現在需要注意的不是“競爭條件”而是服務本身,掛起服務的線程會不會暫停服務呢?答案是會的。但是暫停服務意味著什麼呢?

假如我的服務是用來處理網絡客戶的請求,那麼暫停對於我的服務來說應該是停止接受新的請求。如果我現在正處在處理請求的過程中,那麼我應該怎麼辦?也許我應該結束它,使客戶不至於無限期懸掛。但如果我只是簡單的調用SuspendThread,那麼不排除服務線程正處於孤立的中間狀態的可能,或者正在調用malloc函數去嘗試分配內存,如果運行在同一個進程中的另一個服務也調內存分配函數,那麼它也會被掛起,這肯定不是我期望的結果。

還有一個問題:用戶認為自己可以被允許去停止一個已經被暫停了的服務嗎?我認為是這樣的,而且很明顯的,微軟也這麼認為。因為當我們在“服務”管理工具裡面選中一個已暫停的服務之後,“停止”按鈕是可以被按下的。但我要怎樣停止一個由於線程被掛起才處於暫停狀態的服務呢?不,不要TerminateThread,請別跟我提起它。

解決這所有的混亂的最好方法,就是有一個能夠把所有事做好的線程,而且它應該是服務線程,而不是CtrlHandler線程。當CtrlHandler函數得到控制代碼之後,它要迅速的將控制代碼通過線程內部通訊手段送到服務線程中排隊,然後CtrlHandler函數就應該返回,它決不應該調SetServiceStatus。這樣,服務可以隨心所欲的控制每件事情,因為沒有什麼比它更有發言權的了,沒有“競爭條件”。服務決定暫停意味著什麼,服務能夠允許自己在已經暫停的情況下停止,服務決定什麼內部通訊機制是最好的——並且CtrlHandler函數必須簡單的與這種機制相一致。

事情沒有完美的,上面的方法也不例外,它僅有一個小缺陷:就是假定當服務收到控制代碼後,在較短的時間內就能做出應有的響應。如果服務線程正在忙於處理一個客戶的請求,控制代碼可能進入等待隊列,而且SetServiceStatus可能也無法迅速的被調用。如果真是這樣的話,負責發送通知的SCP可能會認為你的服務已經失敗,並向用戶報告一個消息框。事實上服務並沒有失敗,而且也不會被終止。

這種情況夠糟糕了,沒有用戶會去責怪SCP——雖然SCP將他們引導到了錯誤的狀態,他們只會責怪服務的作者——就是我或你...因此,在服務中怎麼做才能防止這種問題發生呢?很簡單,使服務快速有效的運行,並且總保持一個活動線程等待去處理控制代碼。

說起來好像很容易,但實際做起來就被那麼簡單了,這也不是我能夠向各位解釋的了,只有認真的調試自己的服務,才能找出最為適合處理方法。所以我的文章也真的到了該結束的時候了,感謝各位的浏覽。如果我有什麼地方說的不對,請不吝賜教,謝謝。

下面是我寫的一個服務的源代碼,沒什麼功能,只能啟動、停止和安裝。

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <tchar.h>
#define SZAPPNAME "basicservice"
#define SZSERVICENAME "KService"
#define SZSERVICEDISPLAYNAME "KService"
#define SZDEPENDENCIES ""
void WINAPI KServiceMain(DWORD argc, LPTSTR * argv);
void InstallService(const char * szServiceName);
void LogEvent(LPCTSTR pFormat, ...);
void Start();
void Stop();
SERVICE_STATUS ssStatus;
SERVICE_STATUS_HANDLE sshStatusHandle;
int main(int argc, char * argv[])
{
  if ((argc==2) && (::strcmp(argv[1]+1, "install")==0))
  {
   InstallService("KService");
   return 0;
  }
SERVICE_TABLE_ENTRY  service_table_entry[] =
  {
   { "KService", KServiceMain },
   { NULL, NULL }
  };
  ::StartServiceCtrlDispatcher(service_table_entry);
  return 0;
}
void InstallService(const char * szServiceName)
{
  SC_HANDLE handle = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
  char szFilename[256];
  ::GetModuleFileName(NULL, szFilename, 255);
  SC_HANDLE hService = ::CreateService(handle, szServiceName,
  szServiceName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
  SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, szFilename, NULL,
  NULL, NULL, NULL, NULL);
  ::CloseServiceHandle(hService);
  ::CloseServiceHandle(handle);
}
SERVICE_STATUS servicestatus;
SERVICE_STATUS_HANDLE servicestatushandle;
void WINAPI ServiceCtrlHandler(DWORD dwControl)
{
  switch (dwControl)
  {
//下面雖然添加了暫停、繼續等請求的處理代碼,但沒有實際作用
  //這是為什麼呢?到了下面的KServiceMain函數裡面就明白了...
case SERVICE_CONTROL_PAUSE:
   servicestatus.dwCurrentState = SERVICE_PAUSE_PENDING;
   // TODO: add code to set dwCheckPoint & dwWaitHint
   // This value need to try a lot to confirm
   // ...
   ::SetServiceStatus(servicestatushandle, &servicestatus);
   // TODO: add code to pause the service
   // not called in this service
   // ...
   servicestatus.dwCurrentState = SERVICE_PAUSED;
   // TODO: add code to set dwCheckPoint & dwWaitHint to 0
   break;
  case SERVICE_CONTROL_CONTINUE:
   servicestatus.dwCurrentState = SERVICE_CONTINUE_PENDING;
   // TODO: add code to set dwCheckPoint & dwWaitHint
   ::SetServiceStatus(servicestatushandle, &servicestatus);
   // TODO: add code to unpause the service
   // not called in this service
   // ...
   servicestatus.dwCurrentState = SERVICE_RUNNING;
   // TODO: add code to set dwCheckPoint & dwWaitHint to 0
   break;
  case SERVICE_CONTROL_STOP:
   servicestatus.dwCurrentState = SERVICE_STOP_PENDING;
   // TODO: add code to set dwCheckPoint & dwWaitHint
   ::SetServiceStatus(servicestatushandle, &servicestatus);
   // TODO: add code to stop the service
   Stop();
   servicestatus.dwCurrentState = SERVICE_STOPPED;
   // TODO: add code to set dwCheckPoint & dwWaitHint to 0
   break;
  case SERVICE_CONTROL_SHUTDOWN:
   // TODO: add code for system shutdown
   // as quick as possible
   break;
  case SERVICE_CONTROL_INTERROGATE:
   // TODO: add code to set the service status
   // ...
   servicestatus.dwCurrentState = SERVICE_RUNNING;
   break;
  }
  ::SetServiceStatus(servicestatushandle, &servicestatus);
}
void WINAPI KServiceMain(DWORD argc, LPTSTR * argv)
{
  servicestatus.dwServiceType = SERVICE_WIN32;
  servicestatus.dwCurrentState = SERVICE_START_PENDING;
  servicestatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;//上面的問題的答案就在這裡
  servicestatus.dwWin32ExitCode = 0;
  servicestatus.dwServiceSpecificExitCode = 0;
  servicestatus.dwCheckPoint = 0;
  servicestatus.dwWaitHint = 0;
  servicestatushandle =
  ::RegisterServiceCtrlHandler("KService", ServiceCtrlHandler);
  if (servicestatushandle == (SERVICE_STATUS_HANDLE)0)
  {
   return;
  }
  bool bInitialized = false;
  // Initialize the service
  // ...
  Start();
  bInitialized = true;
  servicestatus.dwCheckPoint = 0;
  servicestatus.dwWaitHint = 0;
  if (!bInitialized)
  {
   servicestatus.dwCurrentState = SERVICE_STOPPED;
   servicestatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
   servicestatus.dwServiceSpecificExitCode = 1;
  }
  else
  {
   servicestatus.dwCurrentState = SERVICE_RUNNING;
  }
  ::SetServiceStatus(servicestatushandle, &servicestatus);
  return;
}
void Start()
{
  LogEvent("Service Starting...");
}
void LogEvent(LPCTSTR pFormat, ...)
{
  TCHAR chMsg[256];
  HANDLE hEventSource;
  LPTSTR lpszStrings[1];
  va_list pArg;
  va_start(pArg, pFormat);
  _vstprintf(chMsg, pFormat, pArg);
  va_end(pArg);
  lpszStrings[0] = chMsg;
  if (1)
  {
   // Get a handle to use with ReportEvent().
   hEventSource = RegisterEventSource(NULL, "KService");
   if (hEventSource != NULL)
   {
   // Write to event log.
    ReportEvent(hEventSource, EVENTLOG_INFORMATION_TYPE, 0, 0, NULL, 1, 0, (LPCTSTR*) &lpszStrings[0], NULL);
    DeregisterEventSource(hEventSource);
   }
  }
  else
  {
   // As we are not running as a service, just write the error to the console.
   _putts(chMsg);
  }
}
void Stop()
{
  LogEvent("Service Stoped.");
}

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved