程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
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
 
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved