服務是含有附加基礎,允許它根據作業系統而受到特別處理的一般Windows應用程式,例如,遠端管理的能力,允許管理者從一個遠端的機器來啟動或停止應用程式的執行。您將免費取得將您的伺服器應用程式轉成一個服務程式的方式以及其他相關的特性。本章將會解釋服務的內容、如何設計一個服務應用程式與作業系統為服務提供了什麼附加的工具等。
Windows內包含了一些服務。表3-1列出了一些服務,安裝在筆者執行Microsoft Windows 2000作業系統的機器上,而其可執行檔名稱包含了服務程式碼。
| 表3-1 服務與其相關的可執行檔案 |
| 服務名稱 | 說明 | 可執行檔案名稱 |
|---|---|---|
| Alerter | 當管理內容改變時,通知用戶和電腦。 | Services.exe |
| ClipBook | 允許使用遠端ClipBook工具來觀看記錄檔。 | ClipSrv.exe |
| Computer Browser | 維護一個位於您網路上的電腦表。 | Services.exe |
| DHCP | 客戶端登錄以及更新IP位址與DNS名稱。 | Services.exe |
| Distributed Transaction Coordinator |
協調跨越二個或多個分散式資料庫的異動、 訊息佇列、檔案系統或管理其他異動保護的 資源。 | MSDTC.exe |
| Event Log | 經用由程式與Windows來發佈事件記錄訊息。 | Services.exe |
| Messenger | 發送與接收由管理者或Alerter服務傳送出來的訊息。 | Services.exe |
| Net Logon | 支援通過網域內電腦登錄帳號事件的驗證。 | LSASS.exe |
| Plug and Play | 管理裝置的安裝及設定,以及將裝置改變的訊息通知給程式。 | Services.exe |
| Remote Procedure Call (RPC) | 提供點對對點以及許多其他的RPC服務。 | SvcHost.exe |
| Remote Procedure Call (RPC) Locator | 管理RPC名稱服務之資料庫。 | Locator.exe |
| Server | 提供支援RPC以及檔案、印表機與具名管道(named pipe)的分享。 | Services.exe |
| Task Scheduler | 使一個程式能在指定的時間內執行。 | MSTask.exe |
| Telephony | 提供Telephony API (TAPI) 的支援。 | SvcHost.exe |
| Uninterruptible Power Supply | 管理一個連接到電腦上的不斷電供電系統(Uninterruptible Power Supply, UPS)。 | UPS.exe |
| Windows Installer | 根據包含在 .msi檔案內的指令來做軟體的安裝、修復以及移除動作。 | MSIExec.exe |
| Windows Management Instrumentation |
提供系統管理資訊。 | WinMgmt.exe |
| Workstation | 提供網路連結與通訊。 | Services.exe |
Microsoft還提出了許多沒有列在表3-1中的服務,像是Microsoft Exchange Server、Microsoft Merchant Server以及Microsoft SQL Server等,所有的這些服務應用程式皆被實作成服務,並且是Microsoft BackOffice套件的一部份。
首先,最重要的是一個服務應程式只是一個32位元或64位元的可執行檔,所以任何您已知之有關DLLs、結構化例外處理、記憶體應對檔、虛擬記憶體、裝置I/O、本機執行緒儲存、執行緒同步、Unicode以及其他Windows工具皆可用於服務上。這表示對您來說,要轉換一個已存在的伺服應用程式到服務應用程式應該相當容易且簡單。
說明
在Microsoft Windows 2000 Resource KIT中包含了一個工具程式,叫做SrvAny.exe,它允許從遠端啟動一個已存在的應用程式,就像是一個真實的服務應用程式一般。然而,SrvAny並不允許以任何方式透過遠端來管理應用程式,因此應該僅僅當作一個短期的解決辦法使用。強烈地建議您將應用程式碼轉成一個成熟的服務應用程式並且不要使用SrvAny工具程式。
第二,您應該要知道一個服務完全沒有使用者介面的部份。大部份的服務鎖在一個隱密地方的伺服器上執行。所以您的服務若出現任何的使用者介面元件,像訊息對話方塊,此時不會有使用者在機器前面看見它並按下它的按鈕。就如您稍後會在本章看到的一樣,任何出現的視窗可能會展現在一個時常變換使用者的工作站或是桌面上,如此一來,這個訊息的顯示對使用者來說並沒有意義。因為服務不會有使用者介面,所以無論您選擇將您的服務實作成一個圖形使用者介面(Graphical User Interface, GUI)應用程式(以(w)WinMain做為程式的進入點)或是Console User Interface(CUI)應用程式(以(w)main做為程式的進入點)都沒有關係。
假如一個服務不會顯示任何的使用者介面,您要如何設定它呢?您要如何啟動與停止一個服務?要如何送出服務的警告或錯誤訊息呢?如何使服務將關於執行效能的統計資料報告出來?這些問題的答案即是:一個服務可以被遠端地管理。Windows提供一些管理的工具,這些工具能從其他連接到網路上的機器管理一個服務的執行,所以不需要為了某人而去實際地確認(甚至是實際去存取)執行服務的電腦。您可能早已熟悉以下這些工具:Microsoft Management Console(MMC)與其服務、事件檢視器與系統監視器之嵌入式管理單元、登錄編輯器與Net.exe命令列工具。
這些工具是Windows為了簡化服務應用程式撰寫者之開發所提供的工具,也能使一個管理者以一致的方式去管理本機與遠端的機器。請注意,這些工具並非專門用在服務中,任何應用程式(或裝置驅動程式)都可以利用它們。本書將在章節裡討論這些工具。
Windows服務通訊架構
建立服務的工作需要以下三種元件:
- 服務控制管理員(Service Control Manager,SCM,讀作scum) Windows 2000系統皆會包含服務控制管理員。這個元件存在Services.exe檔案中,當作業系統啟動以及系統關機時,它便會自動地被呼叫。SCM使用系統特權執行並提供一個統一與安全之控制服務應用程式的方法。SCM負責與許多服務溝通,告知它們開始啟動、停止、暫停、繼續等等。
- 服務應用程式 服務只是一個包含與SCM溝通所需基礎的應用程式,傳送命令給服務,告知啟動、停止、暫停、繼續或停止工作。服務也會呼叫一些特定的函式,以將它的狀態送回SCM。
- 服務控制程式(Service Control Program, SCP) 這是一個通常會顯示使用者介面,允許使用者去啟動、停止、暫停、繼續以及其他所有安裝在機器上之服務的控制。服務控制程式呼叫一些與SCM溝通的特定Windows函式。
圖3-1說明了那些元件間彼此溝通的方式。注意SCP應用程式並沒有直接與服務溝通;所有的通訊都會通過SCM。這裡明確地說明了建立通過SCP與服務應用程式之遠端管理的架構。實作一個使您的SCP應用程式直接與您的服務應用程式溝通的架構與通訊協定是可以的,但是您必須自己撰寫它們的訊程式碼。
| 圖3-1 Windows服務的通訊機制 |
在這三個部分中, 您絕對不會執行到SCM。Microsoft實作了SCM並將它封裝到每一個Windows 2000的版本中。您要實作的是服務與SCPs。本章會涵蓋您應該了解之設計與實作服務的內容,而在下一章則會說明撰寫SCP的細節。
包含在Windows中的服務控制程式(SCP)
在我們探討如何撰寫一個服務之前,您至少必需知道一個SCP可以控制一個服務。所以我會開始檢查一些包含在Windows中的SCP應用程式。
服務嵌入式管理單元
您應該要最熟悉的SCP應用程式是服務嵌入式管理單元,如圖3-2所示。這個嵌入式管理單元顯示了所有安裝在目的機器上的服務清單。在名稱與描述欄中可以發現每個服務的名稱並提供服務函式的說明資訊。狀態欄顯示著哪一個服務是啟動、暫停或停止(空白項表示「已停止」)。啟動類型欄顯示著什麼時候應該喚起服務,而登入身份欄則顯示了當服務執行時所使用的安全性內容。
這個資訊保存在SCM的資料庫中,並存在以下子機碼(Subkey)的登錄中:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services
您應該不會直接存取到這個子機碼;反之,SCP應該呼叫Windows函式(在下一章討論)並操作這個子機碼中的資料庫。直接地修改這個機碼內容會發生不可預料的結果。當您安裝一個包含服務的產品時,該產品的安裝程式即是一個增加服務資訊到SCM資料庫的SCP應用程式。
您可以在電腦管理主控台左邊窗格中選擇電腦管理節點,以及從執行功能表中選擇連線到另一台電腦,來觀看一個遠端SCM的服務資料庫。
| 圖3-2 服務嵌入式管理單元 |
說明
所有包含在Windows中的服務皆會以本機安全內容登入。這是一個享有高度特權的帳號,而且強烈建議您所撰寫的任何服務也要使用本機帳號。
現在您會開始看到服務嵌入式管理單元,您可能會懷疑是否能夠用它來完成所有的任務。這裡是一些普通的操作:
- 啟動一個服務 管理者可以從清單選擇並按下工具列上的啟動按鈕來啟動一個服務。只有啟動類型為自動或手動的服務可以被啟動,停用的服務則不行。停用一個服務在解決機器上的問題時是非常有用的。
- 停止一個服務 管理者要停止一個服務時,只需選擇一個服務並按下工具列上的停止按鈕即可。要注意有一些服務在它們啟動後並不允許它們自己停止。事件記錄服務即是一個例子,它只有在機器關機時才能停止。
- 暫停與繼績執行一個服務 當管理者選擇一個正在執行的服務並按下工具列上的暫停鈕後,即可暫停一個服務的執行。注意大部份的服務並不允許它們自己暫停執行。也要注意「暫停」並沒有明確的定義,對於一個服務,暫停表示在該服務完成了那些未解決的要求前不會再接受客戶端的要求;對另一個服務來說,暫停執行表示該服務不能再處理任何操作。暫停服務可以在按工具列上的啟動鈕時被繼續執行。
- 重新啟動一個服務 管理者可以透過選擇一個正在執行或被暫停的服務並且按下工具列上的重新啟動鈕來重新啟動一個服務。重新啟動一個服務會引起一個停止該服務然後啟動該服務的動作。這是一個簡單方便的特色,而且當您在對您的服務進行除錯時,這個功能會非常有幫助。
前面的清單說明了百分之九十九的管理者所做的服務嵌入式管理單元工作,但是嵌入式管理單元也可以被用來重新設定一個服務。要改變一個伺服器的設定時,您要選擇服務並且顯示出它的內容對話方塊。這個對話方塊包含了四個頁籤,每一個頁籤皆允許管理者去重新設定所選的服務。以下的章節中會討論更改設定的部份。
一般頁籤的內容
一般頁籤(如圖3-3所示)允許管理者去檢查與重新設定服務的一般資訊。您需要去了解的第一個真相是,每一個服務都有兩個字串名稱:一個內部名稱(使用於計劃性的目的)以及一個顯示名稱(一個顯示給管理者與使用者看的字串)。在被加入服務資料庫後,服務的內部名稱即不能改變,但是管理者可以修改該服務的顯示名稱與內容。一般頁籤中也顯示服務的執行程式路徑,而它並不允許管理者改變它(這是一個使用頁籤的限制,並非系統的)。管理者可以改變服務的啟動類型至以下所列之一:
- 自動 服務的其中一個特色即是SCM可以自動地啟動。假如該服務有一個設定為自動的啟動類型,則SCM會在作業系統啟動時產生該服務。請注意自動服務在任何使用者登入機器前即已執行。事實上,許多執行Windows的設定只執行服務─即沒有任何採互動式的登入情形。例如,某機器執行Windows與伺服器服務,並允許一個在網路上的客戶端機器存取子目錄、檔案以及印表機。
- 手動 一個設定為手動的服務即告訴SCM不要去啟動該服務。管理者可以使用SCP以手動的方式啟動一個服務。一個手動的服務也可以依據其他已啟動的服務來啟動。我們會在下一章說明依存關係的內容。
- 停用 一個停用的服務即告訴SCM在任何情況下皆不要啟動服務。當您手動地為您的機器指定一個IP位址,而非讓它動態地從DHCP Server上取得IP位址時,您即是停用了DHCP客戶端服務。停用一個服務解決系統問題時會非常有幫助。
| 圖3-3 Windows Installer服務的一般頁籤內容 |
登入頁籤內容
除了設定真實的服務之外,管理者可以在該服務之登入頁籤內重新設定安全內容,如圖3-4所示。安全內容可以為以下所列之一:
- 本機系統帳戶 在本機系統帳戶下執行的服務只能做任何與該電腦有關的操作:開啟任何檔案、將機器關機、修改系統時間等等。在本機系統帳戶下執行的服務被允許可以隨意地與桌面做互動。大部份的服務並不要求這個選項,然而我們卻要強烈地建議您使用它。
- 這個帳戶 服務也可以在一個特定的安全內容下執行(視使用者與密碼而定)。如此便可限制服務以特定的帳戶權限來存取資源。
在本章之〈服務議題〉一節中,會討論更多有關本機系統與使用者帳戶的部份。
登入頁籤也允許管理者具體指定一個硬體設定檔,以讓服務能在此設定環境下執行。硬體設定檔可讓您依據您的硬體設定來設定服務。例如,您想在膝上型電腦被放入船塢時執行Fax Service服務;反之,則不執行該服務。
| 圖3-4 Distributed Link Tracking服務之登入頁籤內容 |
修復頁籤內容
如圖3-5所示之修復頁籤內容,它允許管理者告訴SCM當服務發生異常中斷情形時,該執行什麼動作。異常中斷表示該服務沒有回報一個SERVICE_STOPPED的狀態即停止執行。在第一次、第二次以及後續的嘗試執行失敗時,SCM可以不執行動作、重新執行服務、執行檔案或重新啟動電腦。注意到如果執行該服務的帳戶沒有適當的存取權限或者許可,則在執行檔案和重新啟動電腦的動作時,該動作的執行會失敗。
| 圖3-5 Fax Service服務之修復頁籤內容 |
依存關係頁籤內容
如圖3-6所示之依存關係頁籤說明了被選擇的服務依存於何種服務或是什麼服務依存於該服務。在圖中,您會看到五個服務依存於Workstation服務。假如管理者試圖去停止Workstation服務且附屬於它的服務正在執行,則SCM會呼叫失敗。許多SCP程式會通知使用者該附屬服務正在執行,並可讓使用者選擇哪一個附屬的服務要一併停止。依存關係頁籤不允許管理者修改任何的依存關係(我會在下一章討論更多有關服務依存關係的內容)。
| 圖3-6 Workstation服務之依存關係頁籤內容 |
Net.exe與SC.exe
除了服務嵌入式管理單元外,Windows還附了一個稱為Net.exe的SCP命令列工具。這個工具限制您只能控制那些存在於本機上的服務。使用Net.exe,您可以使用下列的語法來啟動、停止、暫停以及繼續執行服務:
NET START servicename NET STOP servicename NET PAUSE servicename NET CONTINUE servicename
您也可以不需指定一個servicename,只需簡單地鍵入以下的指令即可顯示正在本機上執行的服務清單:
NET START
您可以將這些呼叫指令放入一個批次檔或其他的指令碼檔案中,所以Net.exe工具非常容易使用在偵錯上。
另一個Microsoft提供的SCP應用程式是一個稱做SC.exe的命令列工具程式。這個工具包含在Microsoft Windows 2000 Resource KIT中。當執行這個工具而沒有傳入任何參數時,它會顯示用法與語法,列示如下:
說明: SC是一個命令列程式,用在與NT服務控制器及服務溝通上。 用法: sc <server> [command] [service name] <option1> <option2>... 類型、方法、型式 <server> 選項的型式為 \\ ServerName 在鍵入「sc [command]」時可以獲得進一步的幫助。 命令: query-----------詢問一個服務狀態或列舉出多個服務類型的狀態。 queryex---------詢問一個服務的延伸狀態或列舉出多個服務類型 的狀態。 start-----------啟動一個服務。 pause-----------傳送一個PAUSE控制要求給服務。 interrogate-----傳送一個INTERROGATE控制要求給服務。 continue--------傳送一個CONTINUE控制要求給服務。 stop------------傳送一個STOP要求給服務。 config----------改變服務的永久設定。 description-----改變服務的說明。 failure---------改變服務失敗後的動作。 qc--------------詢問服務的說明資訊。 qdescription----詢問服務的內容。 qfailure--------詢問服務失則時所做的動作。 delete----------從登錄中刪除服務。 create----------建立一個服務(新增至登錄中)。 control---------傳送一個控制給服務 sdshow----------顯示服務的安全描述。 sdset-----------設定服務的安全描述。 GetDisplayName-取得服務的DisplayName。 GetKeyName------取得服務的ServiceKeyName。 EnumDepend------列舉服務的依存關係。 以下的命令不需要提供服務名稱: sc <server> <command> <option> boot------------(ok | bad)指出哪一個啟動記錄應該被儲存成 last-known-good之啟動設定。 Lock------------鎖定服務資料庫。 QueryLock-------要求SCManger資料庫之LockStatus。 範例:: sc start MyService 您想要看有關QUERY與QUERYEX的幫助說明嗎? [ y | n ]: y QUERY與QUERYEX OPTIONS: 假如在一個服務名稱後跟著query命令,則該服務的狀態會被回傳。其他的選 項在這個情形下不會起任何作用。如果query命令的後面跟隨著下面所列的選 項之一或是空白,所有服務皆會被列舉出來。 Type= 列舉服務的類型(所有的驅動程式、服務)。 (預設值 = service) state= 列舉服務的狀態(所有閒置的) (預設值 = active) bufsize= 列舉緩衝器的位元組大小。 (預設值 = 1024) ri= 開始此列舉之恢復索引號碼。 (預設值 = 0) group= 列舉服務群組。 (預設值 = all groups) 語法範例 sc query -列舉所有執行中的服務與驅動程式狀態。 sc query messenger -顯示Messenger服務的狀態。 sc queryex messenger -顯示Messenger服務的延伸狀態。 sc query type= driver -只列舉執行中之驅動程式。 sc query type= service -只列舉Win32服務。 sc query state= all -列舉所有的服務與驅動程式。 sc query bufsize= 50 -列舉一個50 byte大小的緩衝器。 sc query ri= 14 -列舉恢復索引號碼為14之內容。 sc queryex group= "" -列舉不在群組中之現行服務。 sc query type= service type= interact -列舉所有具人、機通信功能之服務。 sc query type= driver group= NDIS -列舉所有NDIS驅動程式。
由於這個工具對所有的服務控制選項提供了相當豐富的命令列介面且能簡單地使用在一個指令碼檔案中,所以當我們在開發或偵錯一個服務時,可以得到極大的幫助。
Windows服務應用程式架構
本節會解釋那些將伺服應用程式轉換成服務的附加基礎,從而允許您們的應用程式可以被遠端地控管。要了解Microsoft的服務架構有一些困難,這是由於每一個服務程序總是包含著至少二個執行緒,而那些執行緒在彼此間必須互相溝通所致。所以您必須要應付執行緒同步與執行緒間通訊的議題。
一個單一執行檔可以包含多個服務的情形是另一個您需要去考慮的議題。假如您回頭看
表3-1 的內容,您會發現有許多的服務皆被包含在Service.exe內。大部份的這些服務(如DHCP Client、Messenger以及Alerter)在實作上都相當地簡單。如果每一個服務皆要像一個分開的處理程序般執行,則會非常無效率,而且會花費一些位址空間與附加處理程序的成本。因為這個緣故,Microsoft允許一個單一執行檔可以包含多個服務。Service.exe檔案實際上包含20個不同的服務,包括剛才所述那三個。要設計一個可執行的服務時,您必須自己考慮以下的三個函式:
- 處理程序之函式進入點 這個函式即是您現在應該要非常熟悉的標準(w)main或是(w)WinMain函式。對一個服務來說,這個函式為一個整體初始化處理程序然後它會呼叫一個與本機系統之SCM連接的特別函式。由此點看來,SCM會取得您的主要執行緒之控制權。只有在可執行的所有服務停止執行時,您的程式碼才會取回控制權。
- 服務的ServiceMain函式 您必須為每個包含在您的可執行檔中的服務實作ServiceMain函式。執行一個服務時,SCM會產生一個在您處理程序中的執行緒,以執行您的ServiceMain函式。當該執行緒從ServiceMain函式中回傳時,SCM會考慮停止服務。注意這個函式不一定要稱為ServiceMain;您可以為它取一個任何您想要的名稱。
- 服務的HandlerEx函式 每一個服務必須擁有一個與它關聯的HandlerEx函式。SCM會呼叫服務的HandlerEx函式以與服務溝通。在HandlerEx函式中的程式碼會被您的主要處理程序執行緒執行。HandlerEx函式不是執行必要的動作就是必須傳達SCM指令給執行緒以透過使用一些執行緒內部通訊的形式來執行服務的ServiceMain函式。注意每一個服務可以擁有屬於它自己的HanderEx函式,或者多個服務可以共享一個HanderEx函式。傳遞給HanderEx函式的其中一個參數指出SCM希望與哪一些服務溝通。這個函式可以不叫做HanderEx,您可以為它取一個您想要的名稱。
圖3-7可以幫助您正確地了解這個架構。它說明了一個可執行之服務內存在的二個服務以及處理程序間通訊(ITC)與執行緒間通訊(IPC)。在未來的章節中,會仔細的檢視這三個函式以及說明它們的責任為何。建議您在閱讀時可以參考此圖。
| 圖3-7 Windows服務應用程式架構 |
處理程序之進入點函式:(w)main或(w)WinMain
當管理者想要啟動一個服務時,不管該執行檔案所包含的服務是否已執行,都由SCM來決定。如果它沒有被執行,則SCM會將該執行檔產生出來。此處理程序之主要執行緒的主要任務是執行整個程序的初始化(應該在服務之ServiceMain函式中完成初始化的動作)。
在初始化完成後,該進入點函式必須與SCM聯繫,以決定現在應該接管哪一個程序的控制。為了要與SCM接觸,首先進入點函式必須配置和初始化一個SERVICE_TABLE_ENTRY結構的陣列:
typedef struct _SERVICE_TABLE_ENTRY {
PTSTR lpServiceName; // 服務的內部名稱
LPSERVICE_MAIN_FUNCTION lpServiceProc; // 服務的ServiceMain
} SERVICE_TABLE_ENTRY, *LPSERVICE_TABLE_ENTRY;第一個成員表示內部、服務的計劃性名稱,第二個成員則是服務之ServiceMain回呼函式的位址。如果該執行檔內只包含了一個服務,則SERVICE_TABLE_ENTRY結構的陣列必須利用以下的方式初始化:
SERVICE_TABLE_ENTRY ServiceTable[] = {
{ TEXT("ServiceName1"), ServiceMain1 },
{ NULL, NULL} // Marks end of array
};假如您的執行檔內包含了三個服務,您必須使用以下的方法來初始化它,就像這樣:
SERVICE_TABLE_ENTRY ServiceTable[] = {
{ TEXT("ServiceName1"), ServiceMain1 },
{ TEXT("ServiceName2"), ServiceMain2 },
{ TEXT("ServiceName3"), ServiceMain3 },
{ NULL, NULL } // Marks end of array
};存在陣列中的最後一個結構必須將二個成員皆設定為NULL,以表示它為該陣列的結尾。現在該處理程序經由呼叫StartServiceCtrlDispatcher來與SCM連接:
BOOL StartServiceCtrlDispatcher( CONST SERVICE_TABLE_ENTRY* pServiceTable);
在可執行之處理程序中呼叫這個函式與傳遞服務陣列表內的位址可以指出哪些服務包含在該處理程序內。如此,SCM會知道哪些服務曾經嘗試啟動以及哪些服務會反覆地經由陣列來尋找它。一旦服務被發現,則會建立一個執行緒並執行該服務的ServiceMain函式(由SERVICE_TABLE_ENTRY陣列獲得位址者)。
說明
SCM保持著關閉的狀態頁籤,以表示服務正以何種方式執行。例如,當SCM產生一個可執行的服務,SCM會等待可執行之主要執行緒去呼叫StartServiceCtrlDispatcher,如果StartServiceCtrlDispatcher在30秒內沒有被呼叫,則SCM會認為該服務發生故障並且會呼叫TerminateProcess來強迫刪除該處理程序。由於此原因,如果您的處理程序需要超過30秒的時間來初始化,您必須產生另一個執行緒去處理該初始化動作,所以主要的執行緒可以快速地呼叫StartServiceCtrlDispatcher。注意此處我所提出的是於全程序範圍的初始化動作。個別的服務應該自己使用它們的ServiceMain函式來做初始化。
StartServiceCtrlDispatcher在所有服務執行結束前並不會返回。當至少一個服務正在執行時,SCM會控制該處理程序的主要執行緒。通常這個執行緒並沒有做什麼動作而且只是處於靜止狀態,所以不會浪費CPU時間。如果管理者試圖去啟動另一個實作在同一個執行檔內的服務時,SCM不會產生另一個可執行檔實例。反之,SCM會與可執行檔之主要執行緒通訊並且再次重複服務的清單,並尋找此服務。一旦找到,一個新的執行緒會被產生,它會執行適當服務的ServiceMain函式。
說明
當決定是否要產生一個新的服務程序或是一個已存在於服務處理程序的執行緒時,SCM會與一個服務執行路徑字串做詳細的比較。例如,在同一個可執行檔MyServices.exe內執行二個服務。使用一個可執行的路徑名稱「%windir%\System32\MyService.exe」將第一個服務加到SCM資料庫內,而第二個服務則是使用「C:\WinNT\System32\MyService.exe」而加到資料庫內。如果這二個服務被啟動,則SCM會產生二個分開的處理程序,二者皆執行同一個MyService.exe服務應用程式。在服務被加到SCM資料庫時使用相同的路徑名稱字串是為了確定SCM將所有在單一可執行檔內的服務用於一個處理程序上。
在系統內不會記錄在處理程序中的哪些服務正在執行。當每個服務離開時(通常是因為ServiceMain返回的緣故),系統會檢查並察看哪些服務仍然在執行。如果沒有服務正在執行,則只有當時執行進入點函式呼叫者會被返回。因為處理程序中斷執行的緣故,您的程式碼必須在全程序範圍內執行清除的動作,並將進入點函式回傳。注意您必須在30秒內或是在SCM刪除該處理程序前完成您的清除程式碼。
ServiceMain函式
每一個在執行檔內的服務必須擁有它們自己的ServiceMain函式:
VOID WINAPI ServiceMain( DWORD dwArgc, PTSTR* pszArgv);
SCM經由建立一個新的執行緒來啟動一個服務,此執行緒使用它的ServiceMain函式執行。像之前所提到的,呼叫這個特定的ServiceMain函式,卻可以將此函式命名為任何您想要的。您將此函式命名成任何名稱皆不重要,因為您傳遞的是存在SERVICE_TABLE_ENTRY內的lpServiceProc成員。然而您不能在一個單一執行檔內使用二個相同的ServiceMain函式;如果這樣的話,當您試著建立您的專案時,編譯器或連結器會產生錯誤。
ServiceMain函式需要傳遞二個參數,這些參數建立了一個允許管理者經由StartService函式使用一些命令列工具來啟動服務的機制(在下一章中討論)。就個人而言,我不知道有任何服務參考到這些參數並且建議您忽略它們。一個可經由讀取下列所示之登錄子機碼內容來自我設定的服務比使用傳遞參數到ServiceMain函式的方法要好。
HKEY_LOCAL_MACHINE \SYSTEM \CurrentControlSet \Services \ServiceName \Parameters
許多服務包含在允許管理者修改服務設定的客戶端應用程式中。該客戶端簡單地儲存了那些存在登錄子機碼中的設定。當服務啟動時,它會至登錄中檢查設定。
假如改變了一個正在執行之服務的架構時,可使用以下三個選項:
- 服務會忽略此架構之修改內容,直到下一次服務啟動為止。這是一個簡單的選擇,很多今天存在的服務皆會採取這個方法。
- 服務明確地被告知自己被重新設定。一個SCP會經由呼叫ServiceControl函式來傳遞SERVICE_CONTROL_PARAMCHANGE值。在第四章中我們會討論如何達到這個功能的內容。
- 當一個外部應用程式改變了它的登錄設定時,服務可以呼叫RegNotifyChangeKeyValue函式以取得通知。這允許一個服務在執行時重新設定它自己。第五章的RegNotify範例應用程式說明了如何完成這個任務。
ServiceMain必須完成的第一個任務是將服務的HandlerEx回呼函式的位址告訴SCM。它利用呼叫RegisterServiceCtrlHandlerEx來完成這個任務:
SERVICE_STATUS_HANDLE RegisterServiceCtrlHandlerEx( PCTSTR pszServiceName, // 服務的內部名稱 LPHANDLER_FUNCTION_EX pfnHandler, // 服務的HandlerEx函式 PVOID pvContext); // 使用者定義值
第一個參數表示您放置了一個HandlerEx函式的服務,而第二個參數是HandlerEx函式的位址。當SERVICE_TABLE_ENTRY陣列被初始化並被傳遞到StartService-CtrlDispatcher時,pszServiceName參數必須與符合所使用的名稱。最後一個參數pvContext是一個使用者定義的值,它會被傳遞到服務的HandlerEx函式中。下一節我們將會討論HandlerEx函式的內容。
RegisterServiceCtrlHandlerEx返回了一個唯一可以從服務到SCM識別的SERVICE_STATUS_HANDLE值。所有將來從服務到SCM的通訊皆會要求以這個Handle來取代服務的內部字串名稱。
說明
不像大部份在系統中的Handle一般,從RegisterServiceCtrlHandlerEx回傳的Handle不會為了您而關閉。
在RegisterServiceCtrlHandlerEx回傳後,ServiceMain執行緒應該立即地告知SCM該服務已開始執行初始化。這個工作由呼叫SetServiceStatus函式來完成:
BOOL SetServiceStatus( SERVICE_STATUS_HANDLE hService, LPSERVICE_STATUS pServiceStatus);
這個函式要求您傳遞該識別您的服務之Handle(那些已經從呼叫RegisterServiceCtrlHandlerEx返回者)以及一個已初始化之SERVICE_STATUS之結構的位址:
typedef struct _SERVICE_STATUS {
DWORD dwServiceType;
DWORD dwCurrentState;
DWORD dwControlsAccepted;
DWORD dwWin32ExITCode;
DWORD dwServiceSpecificExITCode;
DWORD dwCheckPoint;
DWORD dwWaITHint;
} SERVICE_STATUS, *LPSERVICE_STATUS;SERVICE_STATUS結構包含了影響服務之現行狀態的七個成員。這些成員定義在如下所示清單,在您傳遞此結構到SetServiceStatus前,它們必須被正確地設定。
- dwServiceType 這個成員表示您所執行的可執行服務類型。當您的可執行檔中存在一個單一的服務,可以將它設定為SERVICE_WIN32_OWN_ PROCESS,而當您的可執行檔包含了二個或多個服務時,則將它設定為SERVICE_WIN32_SHARE_PROCESS。除了這二個旗標之外,當您的服務需要與桌面互動時,您可以在SERVICE_INTERACTIVE_PROCESS旗標內做OR的動作(您應該儘量避免執行一個相互作用的服務)。在您的服務執行期間,絕對不要更改dwServiceType值。
- dwCurrentState 這個成員是SERVICE_STATUS結構內最重要的。它將您的服務之現行狀態告知SCM。若要在您的服務初始化期間報告現行狀態,則您應該將此成員設定成SERVICE_START_PENDING。〈要求描述狀態之程式碼〉一節會討論HandlerEx並解釋其他可能的值。
- dwControlsAccepted 這個成員表示哪些控制通執服務是可接受的。假如您允許一個服務控制程式去暫停與繼續執行您的服務,指定SERVICE_ACCEPT_PAUSE_CONTINUE即可。許多服務並不支援暫停與繼續執行的功能,然而若這個功能可用在您的服務上,您便必須去解決它。假如您允許一個服務控制程式停止您的服務,必須指定SERVICE_ACCEPT_STOP。如果您想讓您的服務在關機時收作業系統的通知,必須指定SERVICE_ACCEPT_SHUTDOWN。您也可以指定SERVICE_ACCEPT_ PARAMCHANGE、SERVICE_ACCEPT_HARDWAREPROFILECHANGE或SERVICE_ACCEPT_POWEREVENT,以決定您想要在參數被改變、硬體設定檔改變以及收到電源事件通知時接收它。
使用OR運算子去結合被要求之旗標組。注意當您的服務執行時可改變它接受的控制。例如,我寫了一個只要沒有客戶端連接至它時,便允許它們自己暫停的服務。 - dwWin32ExITCode與dwServiceSpecificExITCode 這二個成員允許服務回報錯誤碼。如果一個服務想要回報告一個Win32的錯誤碼(預設是在WinError.h中),則它設定dwWin32ExITCode成員到所需的程式碼中。服務也可以回報一個被指定到服務而且沒有對應到事先已定義之Win32錯誤碼。要使服務執行這個動作,您必須設定dwWin32ExITCode成員為ERROR_SERVICE_ SPECIFIC_ERROR,然後設定dwServiceSpecificExITCode為伺服器特性的錯誤碼。注意一個經自訂的SCP可能會要求回報這些錯誤碼。當服務已經正常地執行並且沒有回報錯誤時,請將dwWin32ExITCode成員設定為NO_ERROR。
- dwCheckPoint與dwWaITHint 這二個成員允許一個服務回報它的進度。當您將dwCurrentState設定為SERVICE_START_PENDING時,您應該將dwCheckPoint設定為1,並將dwWaITHint設定為服務需抵達下一個SetServiceStatus呼叫時所需達到的毫秒數。一旦該服務已完全地被初始化後,您應該對SERVICE_STATUS結構的成員重新初始化,所以dwCurrentState成員設為SERVICE_RUNNING,然後將dwCheckPoint與dwWaITHint二者設定為0。
由於dwCheckPoint成員的存在而使您擁有優勢。它允許一個服務回報它已經處理了多少進度。每一次您呼叫SetServiceStatus時,您應該增加dwCheckPoint到一個數字,以指示您的服務已經執行了什麼「步驟」。要多常去回報服務執行進度完全取決於您。如果您決定在您的服務初始化的每個?驟皆回報執行進度,則dwWaITHint成員應該被設定為指示您認為到達下一個步驟(檢查點(Checkpoint))所花的毫秒數,而不是指定到完成服務時所需的毫秒數。
說明
ServiceMain函式必須在啟動後80秒內或SCM認為該服務啟動失敗時呼叫SetServiceStatus。假如沒有其他服務正在服務處理程序內執行,則SCM會刪除該處理程序。
說明
在建立您的服務執行緒前,SCM會設定您的服務狀態到顯示一個START_PENDING的現行狀態、一個為0的檢查點以及一個等待2000毫秒的提示。如果您的ServiceMain函式要求超過2000毫秒的時間來做初始化的動作,則在您第一次呼叫SetServiceStatus時,應該指示一個SERVICE_START_PENDING之現行狀態、一個為1的檢查點以及一個被要求等待的提示。注意檢查點應該被設定為1,一個程式開發者常犯的一個非常普通的錯誤是在第一次呼叫SetServiceStatus時將檢查點設定為0,這會使管理者的SCP程式混淆,認為服務沒有正確地回應。如果您的服務要求更多的初始化動作,您可以繼續回報一個增加中的檢查點,並依需要設定等待提示時間之SERVICE_START_PENDING狀態。
在您的服務初始化完成後,您的服務會呼叫SetServiceStatus以指定為SERVICE_RUNNING(檢查點與等待提示時間皆設定為0)。現在您的服務正在執行。通常一個服務會將它自己放在一個迴圈內執行。在迴圈內,該服務執行緒會自己暫停執行,並等待一個網路要求或一個指定服務應該暫停的通知、繼續停止、關機等等。如果一個網路要求到來,該服務執行緒會醒來、處理該要求以及回到迴圈中等待下一個要求或通知。
假如服務被一個通知喚醒,則它會處理該通知。如果服務收到一個停止或關機的通知,則該迴圈會結束以及該服務的ServiceMain函式會返回、並刪除執行緒。若服務為處理程序中最後一個被執行,則該處理程序也會停止執行。
說明
SetServiceStatus函式會檢查SERVICE_STATUS結構的+成員。若此成員被設定為SERVICE_STOPPED,則SetServiceStatus會關閉該服務的Handle(第一個傳遞至SetServiceStatus的參數)。這就是為什麼您從來不必很明確地關閉從RegisterServiceCtrlHandlerEx回傳的Handle,以及更重要的是為什麼在呼叫它與一個SERVICE_STOPPED的現行狀態後不用呼叫SetServiceStatus的答案。當在除錯器之下執行服務時,試圖那麼做會引起一個無效的Handle例外。
HandlerEx函式
每一個在可執行檔案中的服務必須與一個HandlerEx關聯:
DWORD WINAPI HandlerEx( DWORD dwControl, DWORD dwEventType, PVOID pvEventData, PVOID pvContext);
雖然我稱呼這個函式為HandlerEx,然而您可以為此函式另外命名為任何您選擇的名稱。它的實際名稱為何並不重要,因為您將該函式之位址當成一個參數而傳遞到RegisterServiceCtrlHandlerEx函式中。不過,您不能在一個單一可執行檔內使用二個名稱相同的HandlerEx函式;如果您這麼做了,在建構您的專案時,編譯器或連結器會產生一個錯誤。
在大部份的時間裡,呼叫至StartServiceCtrlDispatcher內之處理程序的主要執行緒處於暫停狀態。當一個SCP程式想要去控制一個服務時,SCM會與該處理程序之主要執行緒通訊。執行緒會醒來並且呼叫適當服務的HandlerEx函式。例如,當一個管理者使用服務嵌入式管理單元去停止一個服務時,該嵌入式管理單元會與管理者要求的本機或遠端SCM溝通。接著SCM會喚醒該可執行服務之主要執行緒、呼叫服務的HandlerEx函式並傳遞一個SERVICE_CONTROL_STOP控制碼。
SCM也傳送裝置、硬體設定檔以及電源事件通知給服務之HandlerEx函式。這些通知允許服務適當地重新設定自己並且同意或拒絕加入系統的變更。
說明
因為處理程序的主要執行緒在每一個服務的HandlerEx函式執行,所以您應該要實作那些HandlerEx函式,以使它們快速地執行。不那麼做會妨礙另一個在同一個處理程序內的服務在一個合理的時間內取得它們所需之動作。
若主要執行緒執行了HandlerEx函式,而服務由另一個執行緒執行時,它可能會需要使HandlerEx與行動溝通或者通知服務執行緒。沒有標準方法可以完成這個通訊;此方法完全依據您如何實作服務而定。您可以排隊等候一個Asynchronous Procedure Call(APC)、宣告一個I/O完成通訊埠狀態、宣佈一個像剛才所提之避免將HandleEx執行緒與ServiceMain做同步化動作之需求機制。
HandlerEx函式傳遞了四個參數。第一個dwControl參數表示被要求的動作或通知。假如此參數指示為一個裝置、硬體設定檔或是電源事件通知,則dwEventType以及pvEventData參數會提供有關動作或通知的更多特殊資訊。pvContext參數是一個原來傳送至RegisterServiceCtrlHandlerEx函式中的使用者定義值。在使用這個值時,您可以建立一個在單一可執行檔內被所有服務使用之單一HandlerEx函式。pvContext值可被需要與之通訊的HandlerEx函式用來決定特定的服務。在下一節裡,我會討論如何處理這些控制碼以及通知的方法。
HandlerEx函式的回傳值允許一個服務的控制者回傳一些資訊至SCM。如果此函式沒有處理一個特定的控制碼,則回傳ERROR_CALL_NOT_IMPLEMENTED;若此函式處理了一個裝置、硬體設定檔或是電源事件要求,則回傳NO_ERROR。要拒絕一個要求,則回傳任何其他的Win32之錯誤碼。對任何其他的控制碼,HandlerEx應該回傳NO_ERROR。
控制碼與狀態回報
HandlerEx的責任是控制所有被要求之服務與所有通知的動作。它的第一個參數是指示一個動作要求或通知的控制碼。表3-2描述了指示一個動作要求的控制碼內容。一個動作要求告知服務去完成一些動作以改變執行狀態。
| 表3-2 指示一個動作要求的控制碼 |
| 控制碼 | 說明 |
|---|---|
| SERVICE_CONTROL_STOP | 要求服務停止。 |
| SERVICE_CONTROL_PAUSE | 要求服務暫停。 |
| SERVICE_CONTROL_CONTINUE | 要求被暫停的服務繼續執行。 |
| SERVICE_CONTROL_INTERROGATE | 要求服務立即地更新目前狀態資訊至SCM。這是所有服務必須回應的控制碼。 |
表3-3說明了指示為通知的控制碼內容。該通知會通報存在系統中令人注目之事件的服務。然而服務通常不會因為回應一個通知而改變它們的執行狀態(儘管一個服務可能會選擇改變它的執行狀態)。
| 表3-3 指示一個通知的控制碼 |
| 控制碼 | 說明 |
|---|---|
| SERVICE_CONTROL_PARAMCHANGE | 定義參數被改變時通告服務。當一個服務正在執 行時,可以忽略或重新設定自己。 |
| SERVICE_CONTROL_DEVICEEVENT | 通告一個裝置事件的服務。該服務必須呼叫RegisterDeviceNotification以取回這些通知訊息。 |
| SERVICE_CONTROL_HARDWAREPROFILECHANGE | 當一個硬體設定檔改變時通告服務。 |
| SERVICE_CONTROL_POWEREVENT | 通告一個電源事件給服務。 |
| 一個介於在128與255(包括)之間的數字 | 通告一個使用者定義事件給服務。 |
控制碼要求回報狀態
被HandlerEx函式執行的工作依據取回的控制碼而有所不同。尤其是,動作要求控制碼會要求在您的程式碼中得到特定的注意。當HandlerEx函式取回一個SERVICE_CONTROL_STOP、SERVICE_CONTROL_SHUTDOWN、SERVICE_ CONTROL_PAUSE或SERVICE_CONTROL_CONTINUE控制碼時,SetServiceStatus必須被呼叫以告知對方已收到該控制碼並且詳細說明服務需要使用多久的時間以考慮自己開始從事狀態改變之處理程序。例如,您告知經SERVICE_STATUS結構之dwCurrentState成員設定至SERVICE_STOP_PENDING、SERVICE_PAUSE_ PENDING或SERVICE_CONTINUE_PENDING的控制碼已被接收。另外,HandlerEx函式必須在30秒內回傳,或是SCP應用程式會再次認為該服務已經停止回應。如果SCM認為服務已停止回應,則它不會刪除該服務,它只會回傳一個失敗至SCP以對服務控制碼做初始化的動作。
當一個停止、關機、暫停或繼續的操作是懸而未決的,您必須也要指定您認為該操作會需要多久的時間來完成該工作。指定持續時間是有用的,因為一個服務可能無法立即地改變它自己的狀態─即它可能必須等待一個網路要求完成或是資料被注入一個裝置。您可以使用SERVICE_STATUS結構的dwCheckPoint與dwWaITHint成員來指示需要多久的時間以完成該狀態的改變,就像您回報一個服務之第一次啟動一樣。如果您想要的話,您可以回報一個被dwCheckPoint成員增加與dwWaITHint成員設定去指示您預期該服務需要多久才能開始從事下一個步驟之週期性的進度。
在服務完成了所有要求停止、關機、暫停或繼續的動作時,SetServiceStatus應該再次被呼叫。此時您會設定dwCurrentStatus成員至SERVICE_STOPPED、SERVICE_PAUSED或SERVICE_RUNNING之控制碼。當您回報了這三個狀態控制碼之一時,因為服務已經完成了它的狀態改變動作,所以dwCheckPoint與dwWaITHint成員應為0。
說明
在服務呼叫SetServiceStatus回報SERVICE_STOPPED時,SCM允許該服務可以執行30秒之久。如果服務在30秒後仍在執行中,若當時並沒有其他的服務正在該處理程序中執行,則SCM會終止該服務的處理程序。
當HandlerEx函式收到一個SERVICE_CONTROL_INTERROGATE控制碼時,該服務應該簡單地告知設定dwCurrentState至服務的當前狀態以及呼叫的SetServiceStatus已被接收(再一次說明,在建立SetServiceStatus呼叫前,設定dwCheckPoint與dwWaITHint為0)。
當系統關機時,HandlerEx會接收一個SERVICE_CONTROL_SHUTDOWN的通知控制碼。服務應該完成將任何資料儲存所必需之行動的最小限度,並且最後應該呼叫SetServiceStatus以回報SERVICE_STOPPED。為了及時地確認一個機器的關機動作,只要必須做的話,一個服務應該處理這個控制碼。在預設的情形下,系統只會給20秒的時間以關閉所有的服務。在20秒之後,SCM處理程序(Service.exe)會被刪除,而且機器會繼續完成關機的動作。這個20秒的期間是經由設定WaITToKillServiceTimeout值而來的,它包含在以下的登錄子機碼中:
HKEY_LOCAL_MACHINE \SYSTEM \CurrentControlSet \Control
說明
當系統正在關機時,SCM會通知所有服務接受SERVICE_CONTROL_SHUTDOWN通知控制碼。有一些服務可能會忽略這個控制碼,一些可能會儲存資料至磁碟中,另一些則可能會自己停止並終止執行。您必須非常小心地不去執行任何在您的服務中需要與其他服務關聯的動作。這些其他的服務可能會存在一個「不適當」的狀態或甚至必須終止執行。當在關機時,系統會完全地忽視服務的依存關係。Microsoft的目標是使系統能以儘可能快的速度關機。事實上,在您的服務收到它的通知前,其他的服務可能也會收到它們的關機通知。與這個關機通知順序的問題是,您的服務所依賴的服務可能會在任何時間停止,而且您的服務必須處理這個情形。
通知控制碼在之前已經列在表3-3中,您的HandlerEx函式應該處理該通知並且回傳。除非通報回應迫使服務改變它的實施狀態,否則不要呼叫SetServiceStatus函式。如果服務將要改變它的執行狀態,則SetServiceStatus被呼叫去設定dwCurrentState、dwCheckPoint與dwWaITHint成員為像之前所討論的適當值。
處理執行緒內部通訊議題
儘管主要執行緒執行了收到動作要求的HandlerEx函式,一個服務還是很難撰寫的,通常服務執行緒需要去做現實的工作以處理要求。例如,您也許想撰寫一個處理從具名管道上傳來之客戶端要求的服務。您的服務執行緒會自己暫停以等待一個客戶端連結。如果您的HandlerEx執行緒收到一個SERVICE_CONTROL_STOP控制碼,您該如何停止該服務?我曾經看過許多開發者只是簡單地從HandlerEx函式中呼叫TerminateThread來強迫刪除該服務執行緒。現在,您應該知道TerminateThread是您可能呼叫的函式中最糟的一個,因為執行緒不能取得一個清除的機會:執行緒的堆疊沒有被刪除、執行緒無法釋放任何必須被等待的核心物件、DLLs沒有收到執行緒已被刪除的通知等等。
使服務停止之適當方法是以某種方式醒來、察看哪個服務應被停止、正確地清除以及從它的ServiceMain函式回傳。為了建立一個服務執行它,您必須實作一些在您的HandlerEx與您的ServiceMain函式間的內部執行緒通訊。您可以使用任何您喜歡的要求內部執行緒通訊機制,包括APC佇列、插槽以及視窗訊息。我總是使用I/O完成通訊埠。
為了更新目前的狀態,一個服務必須頻繁地呼叫SetServiceStatus。所有的這一切狀態回報在服務編碼方面可能是很困難的。服務的實作通當會思考在哪裡放置呼叫SetServiceStatus的函式。以下是一些可能的方式:
- 使HandlerEx函式建立一個初始呼叫SetServiceStatus以回報未完成的動作,然後使用執行緒內部通訊去取得ServiceMain執行緒的控制碼。ServiceMain執行緒會做該工作並且使用執行緒內部通訊以使HandlerEx函式得知該動作已完成。由此得知,HandlerEx再次呼叫SetServiceStatus以回報服務的新執行狀態。
- 使HandlerEx函式使用執行緒內部通訊以取得ServiceMain執行緒的控制碼。ServiceMain執行緒會建立初始的呼叫SetServiceStatus以回報未完成的動作、執行這個工作,然後再次呼叫SetServiceStatus以回執服務的新執行狀態。
- 使HandlerEx函式建立初始呼叫至SetServiceStatus以回報未完成動作,然後使用執行緒內部通訊去取得ServiceMain執行緒的控制碼。ServiceMain執行緒會執行這個工作而且也會再次呼叫SetServiceStatus以回報服務的新執行狀態。
所有以上所述的情形有正面的也有反面的。我曾對這些可能的方案做過實驗並且大膽的建議使用最後的選項,理由是:
第一,SCP呼叫一個函式以控制一個服務,而且SCM傳遞此控制給服務。由於這點,SCP會暫停執行,等待服務呼叫SetServiceStatus以指示服務已經接收該控制碼。假如服務的HandlerEx函式沒有在30秒內回傳,SCM會允許SCP醒來並且SCP的函式會呼叫去控制服務回傳失敗。
第二,HandlerEx函式經由服務處理程序的主要執行緒而被執行(所有在一個單一處理程序中的服務皆擁有它們的HandlerEx函式,而且它們被主要執行緒執行)。如果HandlerEx在等候ServiceMain於回傳之前完成該動作,任何在同一個處理程序中的其他服務皆不能接收動作要求或通知。這可以使所有其他的服務出現沒有回應的情形,這是不被接受的。
所以我優先選擇第三種方法─HandlerEx函式建立初始呼叫至SetServiceStatus,執行緒內部通訊被用來取得ServiceMain執行緒的控制碼,而且由ServiceMain執行緒完成工作與呼叫SetServiceStatus以回報新的執行狀態。然而,這個方法也有一個問題:存在著一個潛在的?賽條件(race condITion)。想像一個服務的HandlerEx函式接收了一個SERVICE_CONTROL_PAUSE控制碼,並以SERVICE_PAUSE_PENDING回答,然後傳遞控制碼至ServiceMain執行緒。當ServiceMain執行緒開始處理該控制碼時,突然間,HandlerEx執行緒先取得ServiceMain執行緒並且接收一個SERVICE_CONTROL_STOP控制碼。HandlerEx函式現在回應一個SERVICE_STOP_PENDING控制碼並且新的控制碼佇列至ServiceMain執行緒中。當ServiceMain緒行緒再次取得CPU時間,它會完成自己的SERVICE_CONTROL_PAUSE控制碼過程並且回報SERVICE_PAUSED。然後執行緒會察看被佇列之SERVICE_CONTROL_STOP控制碼、停止服務以及回報SERVICE_STOPPED。在這些以後,SCM會接收以下的更新狀態:
SERVICE_PAUSE_PENDING SERVICE_STOP_PENDING SERVICE_PAUSED SERVICE_STOPPED
就像您看到的,這些更新毫無意義,只會讓管理者相當困惑。請注意,不管怎樣,服務還是執行得很好。您會對我曾經見過可以實際地回執這個序列的數量感到驚訝。這些服務的開發者從未修復這些問題,它是不太可能會發生的,因為一個管理者會快速地發佈動作要求至服務中─但是它還是可能發生。為了解決這個序列問題您必須使用一個同步執行緒機制。在本章後面的TimeService應用程式範例中使用了一個CGAte的C++ 類別來有效地解決這個問題。
當我開始使用服務時,認為SCM可能是預防發生競賽條件的原因。但是經驗告訴我SCM對確定一個適當地接受控制碼的服務來說是沒有幫助的。事實上,它真的沒有什麼幫助。意思是說:當一個服務已被暫停時,試著傳送給服務一個SERVICE_CONTROL_PAUSE控制碼。因為嵌入式管理單元一旦知道服務已被暫停即會使暫停按鈕失效,所您不能在服務嵌入式管理單元中使用它。但是如果您使用SC.exe命令列工具程式,任何傳送一個暫停控制碼至已經被暫停的服務並不會被停止。我曾預期SCM回報失敗至SC.exe工具程式中,但是SCM只會呼叫服務的HandlerEx函式,並傳送SERVICE_CONTROL_PAUSE控制碼。您的服務必須能夠小心地處理這些不正確的控制碼。
我曾經見過許多沒有對存在於一個資料列中被多次傳送至服務之相同控制碼的可能性做處理。例如,我知道當服務被閒置時,它關閉了具名管道的handle。這個服務接下來會建立另一個核心物件,碰巧的是,它取得了與原始具名管道同樣的handle值。然後服務會接收另一個暫停控制碼並且呼叫CloseHandle與傳遞舊管道的handle值。由於這個值剛好和另一個核心物件的handle相同,所以新的核心物件會被刪除,而其餘的服務則由於奇怪且不可思議的方法而失敗。我無法告訴您該如何愉快的調整這個混亂的情形。
為了要修復這個多重的停止、暫停或繼續執行控制碼的問題,第一件是即是察看您的哪一些服務已經位於需求狀態。如果它是的話,不要呼叫SetServiceStatus,也不要執行您的改變狀態之程式碼─只要回傳即可。這裡有一些我常用在服務的邏輯。當HandlerEx函式接收一個SERVICE_CONTROL_PAUSE控制碼時,HandlerEx函式會呼叫SetServiceStatus以回報SERVICE_PAUSE_ PENDING,呼叫SuspendThread以使服務的執行緒暫停執行,然後再次呼叫SetServiceStatus以回報SERVICE_PAUSED控制碼。這一系列的呼叫是為了避免競賽條件的產生,因為所有的工作被一個執行緒完成,但是這個控制碼做了什麼?閒置服務執行緒會使該服務暫停執行嗎?對於這些,我想必須回答「是的」。然而,對於服務來說,暫停它代表什麼意思?答案是依據服務而定。
如果我正在撰寫一個處理網路上之客戶端要求的服務,對我來說,暫停代表停止接受新的要求,但是要如何處理正在處理中的要求呢?也許我應該完成它以使客戶端不會被無限期地懸置。如果我的HandlerEx函式簡單地呼叫了SuspendThread,該服務執行緒可能會在任何的狀態中。也許該執行緒正呼叫至malloc,並試著去配置一些記憶體。假如另一個服務在也呼叫malloc的同一個處理程序中執行,這個服務也會被懸置(直到該存取動作被序列化為止)。這必定是我們不想要產生的情形。
看看這個如何:您認為您應該被允許去停止的一個已被暫停的服務嗎?我想是的,而且顯然Microsoft也是這樣想的,因為即使該服務已被暫停,服務嵌入式管理單元還是允許我去按下停止按鈕來停止它的執行。但是我要如何停止一個因為它的執行緒已被懸置之已被暫停的服務呢?請不要回答TerminateThread。
這些是關於向建立服務開發挑戰的議題。
關於服務的議題
當您第一次開發服務程式時,您將會注意到有一些程式並沒有照您的預期的狀況執行。服務是一個在一個特殊的作業環境中執行的野獸。本節將會討論一些您可能會遭遇的情形。然而,並不會花太多時間在它們上面,因為本書的各個章節中會對它們做更詳細的說明。在此只是先給您一個大略的概念而已。
本機帳戶與特定的使用者帳戶
本節會開始解釋在本機帳戶中與在一個特定的使用者帳戶中執行服務有什麼不同。本機帳戶是一個被作業系統給予的帳戶,作業系統不會對它限制資源的存取權限。在本機帳戶中執行一個服務時,可以存取任何的目錄或檔案、改變系統時間、啟動或停止任何的服務、關閉機器以及沒有任何的障礙即可以執行所有其他被限制的正常動作。一個本機服務被認為是系統之Trusted Computing Base(TCB)的一部份。
說明
這裡是我在http://nsi.org/Library/Compsec/compglos.txt找到對Trusted Computing Base(TCB)的定義內容:「一個電腦系統中的總體保護機制」─包括了硬體、韌體以及軟體─負責加強一個安全策略的結合。一個TCB由一個或多個在一個產品或系統上實施統一的安全策略之元件所組成。一個TCB的能力為正確地執行那些依據TCB以及由系統管理者所輸入與安全策略有關之正確參數的安全策略。(例如,清除一個使用者帳戶)。」
當然,所有的核心模式程式碼─硬體裝置、記憶體管理、檔案系統、安全控管、執行緒的工作排程等等,皆是系統之TCB的一部份。在TCB中執行的服務具有非常大的權力,這就是為什麼只有機器的管理者擁有安裝服務權利的緣故。
所以如果本機帳戶擁有很大的權力,為何您還會想讓服務在一個特定的使用者帳戶中執行呢?的確,本機服務擁有在本機上的所有權力,在預設的情形下,它們不能在整個網路中被使用。例如,因為本地端機器的本機帳戶無法被遠端機器驗證,所以本機服務不能存取另一台機器上被分享的目錄、檔案或印表機。在Windows 2000中,Microsoft對這個情況做了改善:當一台電腦位於網域中,您可以將它視為一個使用者帳戶並取得它的存取權限。
如果您正在執行服務的機器並不存在一個網域中,而且您的服務需要存取網路資源時,以下是您可以做的:
- 在一個已取得網路資源存取權之特定使用者帳戶下執行服務。注意如此做將會限制該服務所能在本機上做的工作。
- 使用一個不要求驗證的通訊協定來存取資源。例如,一個本機服務可以經由插槽、具名管道或郵件通訊協定。當然,要使這個通訊執行成功需要遠端機器支援這些通訊協定。這些連結被稱為NULL工作階段,並且可以經由設定位於以下所列之登錄機碼中的NullSessionPipes與NullSessionShares的值以加以控制:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services \LanmanServer\Parameters
您也能夠允許由所有NULL工作階段的連結而把資料值設置成0(位於相同的子機碼下)以存取機器上的所有管道與被分享的資料。雖然您可以這麼做,但您卻不應該做它,因為它會使系統上產生一個巨大的安全漏洞。
- 模擬一個特定使用者以存取資源。為了達到這個目的,您可以呼叫許多Windows所提供的模擬函式(我們將在本書第四篇的章節中討論模擬的部份)。一個服務也能經由使本機服務呼叫LogonUser函式來模擬一個特定的使用者,並傳遞一個網域、使用者名稱與密碼以被驗證。注意LogonUser函式需要經過TCB授權(也被視為作業系統特權的一部份),在預設的情形下,只有本機帳戶─多麼方便啊!
本機與特定使用者之登錄子機碼
登錄被分為二個主要的機碼。第一個為HKEY_LOCAL_MACHINE,它被用來儲存所有的系統設定。一個服務或是一個應用程式可以經常讀取任何位於此機碼中的設定值。
第二個機碼為HKEY_USERS,它被用來儲存每一個使用者之特定設定值。這個機碼會進一步分成二個類型之子機碼。第一個類型是一個特定的使用者子機碼。機器上的每一個使用者帳戶皆有一個在HKEY_USERS之下對應至子機碼之登錄設定集合。當特定的使用者登入並且成為一個互動的使用者時,常見的HKEY_CURRENT_USER機碼會對應至位於HKEY_USERS下的特定使用者之子機碼。
第二個位於HKEY_USERS中的子機碼稱為 .DEFAULT,它包含了一個使用者的內定設定值。當一個新的使用者帳戶在系統中被建立時,在HKEY_USERS之中也會建立一個新的子機碼而且該子機碼的設定值會與目前存放在 .DEFAULT子機碼中的設定相同。
就像位於HKEY_LOCAL_MACHINE中的設定一般,雖然一個服務並不需要存取它,但是存在HKEY_USERS\.DEFAULT中的設定值還是可以被服務與應用程式使用。一個存放於HKEY_USERS下之特定的使用者設定不能被使用,除非該使用者登入至系統中。由於服務通常會在本機帳戶下執行,所以服務不該試圖去存取任何儲存在HKEY_USERS中特定使用者帳戶內容。相同的一個本機帳戶服務也不該使用HKEY_CURRENT_USER來存取登錄之內容。關於登錄設定與使用者設定檔的更多資訊,請參閱
第五章 。核心物件之安全性
本節會介紹許多開發者皆會遇到的一個常見問題。即當他們的客戶端應用程式與服務在同一台機器上執行而且客戶端與服務嘗試分享一個核心物件時所發生的問題。
以下為解決方案。您的服務開始執行並且呼叫CreateFileMapping去建立一個檔案對應的物件。CreateFileMapping建立了一個核心物件,所以它的參數之一是一個SECURITY_ATTRIBUTES資料結構的位址。假如您像多數的程式開發者一樣的話,則您會傳遞NULL給此參數,因為核心物件會被以「預設的安全性」建立。請注意我所提的是「預設的安全性」,而不是「沒有安全性」。預設的安全性表示在物件被建立時經由安全性條件而定義之物件存取控制。
例如,一個核心物件會被本機服務所建立,在預設的情形下,它被允許可以完全的存取任何不在執行中的本機帳戶,並只允許可以讀取與執行本端管理者之成員群組。所以如沒有一個本機服務以預設的安全性建立了一個檔案對應的物件,則一個在本機管理員帳戶下執行的應用程式可以從讀取檔案對應物件,但不能以任何方式對它做寫入的動作。在任何其他帳戶下執行的應用程式不能完全存取檔案對應物件。
現在,我只是想要使您知道這個議題。有很多關於客戶端與服務之核心物件安全性的內容可以談,而且有很多方法可以處理它,但是必須是在您更了解Windows安全性的前提下。所以建議您閱讀本書第五篇中有關安全性的章節,以取得所有的細節。
互動式的服務、視窗配置與桌面
本節只討論有關服務如何影響視窗配置與桌面之內部操作的議題。有關視窗配置與桌面的更多資訊,請參閱 第十章 。
當您建立了一個核心物件時,您可以指定它應該被安全地經由一個SECURITY_ATTRIBUTES結構傳送。但是像視窗與功能表這種使用者物件要如何做呢?使用者物件使用了一個不同的型式;它們不會被開啟或關閉─只要在您需要的時候存取它們即可,如此可使程式碼容易撰寫以及增進效能。另外,使用存在於16位元之Windows作業系統中的物件並不以任何形式來支援安全性。如果Microsoft加入SECURITY_ATTRIBUTES結構至CreateWindow以及CreateMenu中,開發者在放置他們的16位元程式碼時會產生困難。
Microsoft需要一個在不影響已存在之函式以及不影響您使用物件的前提下,讓使用者物件變得安全的方式。這包括了所有關於視窗配置與桌面的部份。一個視窗配置為一個 邏輯 的鍵盤、滑鼠以及顯示器之集合體。「邏輯」一字表示裝置不一定真的存在。當系統被啟動時,它會建立互動式的視窗配置,稱為「WinSta0」;實體的鍵盤、滑鼠以及顯示器會與這個視窗配置關聯。一個視窗配置也可以包含它所擁有的記事本、一個通用元素的集合以及一個桌面物件群組。
一個桌面由一個邏輯的顯示外觀以及一個使用者物件的集合所組成:視窗、功能表以及攔截程序(Hook)。執行緒也會與桌面聯繫(請參閱Platform SDK文件中的SetThreadDesktop與GetThreadDesktop函式的內容)。假如一個已與桌面關聯的執行緒試圖傳送一個訊息至被另一個桌面建立的視窗時,執行緒不能在屬於另一個桌面之執行緒上安裝攔截程序。
就像視窗配置一樣,桌面會被它的字串名稱所確認。WinLogon.exe會建立三個桌面:
- WinLogon 事先配置登入對話方塊。在使用者登入後,WinLogon.exe會轉換至預設桌面。
- Default Explorer.exe與所有使用者的應用程式所顯示之視窗的位置。每當一個應用程式執行時,它會在桌面執行。
- Screen saver 當使用者閒置了一段時間後,執行系統的螢幕保護程式。
系統自己擁有本身的特定使用者帳戶。所以實際上有二個「使用者」曾經存取機器:即本機使用者及已登入的使用者。當然,如果這兩個使用者正在單一機器上執行應用程式,您不用等待所有應用程式之視窗在單一的顯示裝置中顯現,因為二個使用者皆會要求分配一個屬於自己的視窗配置。
由於這個原因,本機帳戶會被給予一個屬於它的視窗配置,稱為「Service-0x0-3e7$」(那些數字是服務的登入SID),以及屬於它的桌面,稱為Default。視窗配置為非互動式而且沒有一個實體的鍵盤、滑鼠與顯示器─即本機「使用者」不是一個真實的人類,因此不需要去打字、按下滑鼠或「察看」任何東西。任何正在此視窗配置下之桌面上執行的應用程式皆可以建立一個視窗,但是該視窗不會被顯示給已登錄的使用者看。這就是為什麼服務不應顯示一個使用者介面的原因:沒有人會看到它,而且執行緒會為了等待輸入而被懸置,因此可能永遠都無法被執行。
使用服務嵌入式管理單元,您可以顯示一個服務的內容。您可以回想一下登入身份頁籤內容包含了一個允許服務與桌面互動的核取方塊。當它被選擇時,這個選項會讓SCM以互動式視窗配置之預設桌面的方式啟動一個服務:「WinSta0\Default」。注意您只能在您的服務執行於本機帳戶的情形下才能選擇該選項。因為本機帳戶擁有較高的存取權限並且它能夠存取互動式的使用者視窗配置與桌面。
有一個與服務跟桌面互動有關的問題即是預設值。桌面不會經常被顯示。當螢幕保護程式執行或是使用者登出系統時,服務的使用者介面還保留在預設的桌面上,但是在下一個使用者登入前,該使用者介面不會被顯示。另一個問題是它使系統相當的不安全。例如,一個標準的應用程式可以傳送視窗訊息至服務的視窗中。該應用程式使用已登入使用者的安全條件執行,但是此應用程式現在正與一個經由一個不安全通道執行本機帳戶之處理程序互相通訊。一個被限制的使用者可以輕易地存取不應被他們取得的系統資源。
這裡有一個範例:一個服務正以本機帳戶執行而且它正顯示在互動式預設桌面的視窗中。正常的情形下,當已登入的使用者試圖使用工作管理員的處理程序頁籤去刪除一個服務時,會出現一個拒絕存取或類似的訊息而且該服務會繼續執行。這個功能當然是被需要的。您不會想要讓一個以Guest帳戶登入至系統上的使用者刪除一個伺服器的服務、妨礙位於網路上的其他使用者存取目錄、檔案與印表機(特別是當伺服器服務在Service.exe中執行時)。然而,假如本機服務建立了一個互動式的視窗,則已登入的使用者會在工作管理員的應用程式頁籤中看見它。此時選擇工作結束會傳送一個WM_CLOSE訊息至視窗中,使得一個服務能被刪除。
說明
由於上述的所有理由,Microsoft強烈地建議使用允許服務與桌面互動核取方塊。事實上,一個管理員可以經由與桌面互動的方式將以下所示之登錄子機碼NoInteractiveServices值設定為非0值以禁止服務執行:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Windows
所以在一個特定的使用者帳戶下執行服務如何?當SCM在一個特定的使用者帳戶下執行一個可執行的服務時,SCM會先對使用者做驗證,請使用者提供使用者名稱與密碼會成為服務設定資訊的一部份(使用者名稱被儲存在登錄中,而密碼則儲存在一個安全的LSA檔案中)。這個驗證過程建立了一個登入工作階段,表示哪一個取得自己擁有之非互動式視窗與桌面。可執行服務現在已包含了使用這個專用的視窗配置與桌面,其名稱大概是「UserAccountLogonSID\ Default」,「UserAccountLogonSID」是一個在驗證期間產生的唯一數字。
這個唯一的鑑定表示如果您擁有二個或多個設定在同一個使用者帳戶下執行的服務,則每一個皆會取得它自己所擁有的登入SID、視窗配置以及桌面(如果它們在不同的處理程序中)─位於這些可執行服務中的執行緒不能經由使用者物件而互相通訊。這個唯一的鑑定也表示如果一個正在目前已使用互動式登入的使用者帳戶下執行服務,則該服務的使用者介面不會被顯示出來。反之,本機帳戶只會被驗證一次,所以所有分享同一個視窗配置與桌面以及可以經由使用者物件通訊的許多可執行服務會在本機帳戶下執行。
Microsoft已經加入一些特定的服務特性至常見的MessageBox(Ex)函式中。第一,當您傳遞了MB_SERVICE_NOTIFICATION旗標時,不管哪一個桌面為WinLogon、預設或是正在執行螢幕保護程式,該函式皆會在互動式視窗配置之活動中桌面上顯示訊息方塊。這保證訊息方塊會顯示在顯示裝置中。
第二,MessageBox(Ex)支援MB_DEFAULT_DESKTOP_ONLY旗標。除了它只在互動式視窗配置的預設桌面上顯示訊息方塊外,此旗標與MB_SERVICE_ NOTIFICATION是很類似的:一個使用者必須登入才能看見該訊息方塊。注意除非一個使用者已經看見訊息方塊並且已按下按鈕使其離開,否則MessageBox(Ex)不會返回。順便一提,若要使一個服務不需在本機帳戶下執行,也沒有核取允許服務與桌面互動之核取方塊時,不是使用MB_DEFAULT_DESKTOP_ONLY就是使用MB_SERVICE_NOTIFICATION旗標來達到這個目的。系統會保證該訊息方塊會被顯示。
如果您一直謹慎地跟隨著我們的進度,則您應該會注意到在我使用為了創造互動式服務時所建立的每個方法時,使您的意志受挫了。如果您想製作一個提供使用者介面的服務時,應該如何做呢?這個回答很簡單:建立一個分開的、使用一些IPC機制(RPC、COM、具名管道、插槽、記憶體映對檔案等)與服務對談的客戶端應用程式。我知道有許多人不想這麼做,因為他們必須建立一個可以自我執行的全新專案,但是建立一個分開的應用程式是正確且可得到最多支援的方法。如果您這麼做的話,那麼Microsoft便無法簡單地打破您的架構。
客戶端應用程式應該在Windows 2000、Windows 98、UNIX或任何其他作業系統上執行。您可以建立一個以HTML為基礎的使用者介面以與使用ActiveX控制或Active Server Pages的服務溝通。您也應該為了MMC而考慮建立您的使用者介面之嵌入式管理單元。想想這個客戶端應用程式為了您而開啟的可能性,不是因為所有使用者介面被發佈而限制它們。
在移至這個主題之前,我想要多討論一些有關使用者介面的議題:一些Windows函式會產生硬體錯誤的訊息方塊。例如,如果您正從光碟執行一個應用程式並且移除了光碟片,此時系統會自動地顯示一個訊息方塊。如果系統沒有顯示訊息方塊,它的另一個選擇即是刪除該處理程序。
它有被系統修改的可能性,所以硬體錯誤會被記錄至事件記錄中並且不會產生訊息方塊。要改變系統的行為,您必須在以下所列之登錄子機碼中修改ErrorMode的值:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Windows
表3-4列出了ErrorMode的可能值。
| 表3-4 定義是否顯示硬體錯誤之訊息方塊的ErrorMode值 |
| 值 | 說明 |
|---|---|
| 0(預設值) | 系統會顯示錯誤訊息方塊。 |
| 1 | 對於非系統產生的錯誤,會顯示一個錯誤訊息方塊。若為系統產生的錯誤,則會在事件記錄中加入一個項目並且不會顯示錯 誤訊息方塊。 |
| 2 (一個沒有人看顧的伺服器之最佳選擇) | 不管為系統產生或非系統產生的錯誤,皆加入事件記錄中,並且不會顯示錯誤訊息方塊。 |
對服務偵錯
在對一個服務偵錯時比對一般應用程式的偵錯更難處理,原因有許多個。第一,除錯器無法啟動服務,必須由SCM啟動服務。第二,許多服務在使用者登入前即已啟動。由於這個原因,當您在對服務做偵錯時,將設定為自動啟動的服務改為手動啟動的方式是一個好方法。第三,服務在它們各自擁有的視窗配置與桌面中執行,而它們不會為了互動式使用者而顯示。
所以您要如何對服務偵錯呢?最好的方法是像服務那樣固定地執行一個固定的可執行程式。在您服務內的(w)main或(w)WinMain函式會為一個特定的命令列打開您所擁有的設計,如果該開關已被打開,則以直接地呼叫您服務之ServiceMain函式的方式取代呼叫StartServiceCtrlDispatcher。這個技術當然會有很多缺點:
- 如果您的可執行檔中包含了許多個服務,則在同一個時間內您只能對其中一個服務做偵錯的動作。
- 該執行檔正以您的帳戶執行而您的帳戶不能讓SCM使用。這也許會限制一個正常情形下應被允許的資源存取。
- 您不能傳送一個暫停、繼續執行、停止、關機或任何使用者已定義的通知至服務中,並且被禁止對執行路徑做測式。
剛才所提的方法可以使您較容易的對您的服務做偵錯,但是有一個較好的方法是當服務正在執行時,將偵錯器與服務連接起來。大部份的偵錯器皆提供連接至正在執行之處理程序的能力。如果您已經在系統上安裝了一個偵錯器,那麼您便可以開啟工作管理員,在服務處理程序的名稱上敲擊滑鼠右鍵,並且由在功能表中選擇偵錯選項。這個動作會使偵錯器連接至您的服務。現在您可以設定中斷點、對服務程式碼偵錯甚至可以測試您的程式如何回應控制碼的通知。以下為將偵錯器與服務連接所產生的幾個問題:
- 您所使用來登入系統的帳戶必須擁有偵錯的權限。在預設的情形下,這個權限只被指定給管理員。如果您以Power User的身份或一些其他的帳戶登入系統,則您必須擁有管理員指定給您的偵錯權限。
- 您不能對您的初始化程式碼做偵錯,因為偵錯器在服務執行後才與它連結。
如果您真的想要在使用這個方法來對您的服務之初始化程式碼做偵錯,只要在您的(w)main或(w)WinMain中增加一個對DebugBreak函式的呼叫即可以簡單地達到這個目的。然而這個技術只在您的服務以本機帳戶執行時才能產生作用。如果您在一個不同的使用者帳戶下執行您的服務時,此時您的偵錯器不會正常地執行,因為系統不允許它與互動式視窗配置及桌面溝通。
這裡有另一個您可以用來對服務偵錯的技巧:Windows提供了一個每當一個處理程序啟動時即喚起偵錯器的能力。要使系統達到這個功能,您必須先建立一個以下所列之登錄機碼中的子機碼:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\ CurrentVersion\Image File Execution Options
在這個機碼下建立一個與可執行之服務名稱相同的子機碼(不用加入路徑)。在該可執行名稱之子機碼中,加入一個名稱為Debugger的字串值,並且設定其值為您的偵錯器之完整的執行路徑(例如,「C:\Program Files\Microsoft Visual Studio\Common\MSDev98\Bin\msdev.exe」)。
一旦您將這些設定完成後,您便可以到服務嵌入式管理單元中啟動服務。此時SCM會啟動偵錯器而非可執行之服務。由此,您可以開啟您的服務程式碼檔案、設定中斷點然後讓服務執行。注意在SCM強迫終止偵錯器前,您有30秒的時間可以做您的偵錯工作(因為服務不會呼叫StartServiceCtrlDispatcher)。
如果所有的這些限制令您煩惱,而且您想要能夠在最自然的情形下對服務偵錯,不用與桌面互動或擔心使用者帳戶的問題,那麼您可以使用核心的偵錯器。
TimeService服務範例
在本節最後面的列表3-1中說明了TimeService服務範例(「03 TimeService.exe」),其中包括了所有建立一個服務所需的元件。該應用程式的程式碼與資源檔案皆存放在隨書光碟中的03-TimeService目錄中。這是一個非常簡單的服務,當一個客戶端連結至此服務時,它會回傳執行服務之機器的日期以及時間。此服務假設您已經稍微對具名管道與I/O完成通訊埠有所了解(在第二章中討論)。
如果您檢查_tWinMain函式,您將會看見這個服務擁有依據命令列參數所傳送之「-install」或「-remove」可以選擇從SCM的資料庫中安裝或移除它自己的能力。一旦您建立了服務並且第一次從命令列環境中執行它時,請傳遞「-install」參數給它。當您不想讓該服務保存在您的機器上時,請從命令列環境中執行它,並傳遞「-remove」參數給它。我將會在下一章中討論如何從SCM增加或移除一個服務之函式的細節。
_tWinMain之最重要的觀念是去通知一個被二個成員初始化的SERVICE_TABLE_ENTRY結構陣列:一個是服務,另一個是使用NULL項目來識別最後一個服務。那些為了服務而建立的執行緒之服務表格陣列位址會被傳遞到StartServiceCtrlDispatcher中。這個新的執行緒會與TimeServiceMain函式一起開始執行。注意除非TimeServiceMain函式已離開且它的執行緒已終止執行,否則StartServiceCtrlDispatcher不會返回至_tWinMain函式。
TimeServiceMain函式實作了真實的程式碼以處理客戶端的要求。經由建立一個I/O完成通訊埠可以啟動它。該服務執行緒會在一個等待要求以進入完成連接埠的迴圈中執行。有二種可能的要求類型:一個客戶端已連接至管道並且等待機器的日期與時間資訊;或是該服務需要去處理一個例如暫停、繼續或停止的動作要求。
一旦完成連接埠被建立後,我初始化了一個全域的CserviceStatus物件─g_ssTime。該CserviceStatusC++ 類別是我自己建立的,而且它只是簡單地回報服務的更新狀態。這個類別是來自Windows的SERVICE_STATUS結構而且基本上它使用了一點點的抽象化概念,用來放置被更新的成員變數之一些邏輯的狀態。CserviceStatus擁有一個被使用來讓TimeHandlerEx執行緒與TimeServiceMain達到同?以及允許ServiceMain執行緒在同一時間處理一個單一動作之CGate類別物件成員。
在CserviceStatus的InITialize方法內呼叫了RegisterServiceCtrlHandlerEx以通知HandlerEx函式(TimeHandlerEx)的SCM,而且它會傳遞C++ 類別物件之I/O完成通訊埠的位址iocp至它的pvContext中以控制函式。InITialize方法中也設定了dwServiceType成員,以表示在服務的執行期間中沒有被改變的內容。
接下來,我呼叫了AcceptControls方法,以設定dwControlsAccepted成員。當服務執行至接受或拒絕被要求的控制時,AcceptControls方法可以被週期性地呼叫。在執行期間TimeService接受停止、暫停、繼續以及關閉之控制碼。
您將會注意到TimeHandlerE x函式經由呼叫CIOCP的PostStatus方法來傳遞控制碼給服務執行緒,並經由使用I/O完成通訊埠的Handle來由內部呼叫PostQueuedCompletionStatus。一個代表完成的CK_SERVICECONTROL控制碼會被指定以指示ServiceMain已經因為一個服務之要求而醒來,然後TimeHandlerEx函式會儘快地返回。服務執行緒的責任為醒來,處理控制碼,然後等待更多的客戶端要求。
在TimeServiceMain函式中,一個do-while迴圈開始啟動。在該迴圈中,我檢查了CompKey變數的值察看服務下一個需要回應的動作。由於此變數被初始化到CK_SERVICECONTROL中,而dwControl變數被初始化至SERVICE_CONTROL_ CONTINUE中,所以此服務的第一件工作即是建立一個具名管道,然後客戶端應用程式會使用它來建立對服務的要求。接下來會使用一個CK_PIPE的完成控制碼來使這個管道與完成連接埠關聯,並且建立一個對ConnectNamedPipe的非同步呼叫。此服務現在會經由呼叫g_ssTime物件之ReportUltimateState方法中以呼叫內部的SetServiceStatus方式將SERVICE_RUNNING回報至SCM中。
服務呼叫iocp物件之GetStatus方法(內部呼叫GetQueuedCompletionStatus)。這導致服務執行緒進入睡眠狀態,直到一個事件在完成連接埠中出現為止。如果出現了一個服務控制碼(因為TimeHandlerEx已經呼叫PostQueuedCompletion Status),則服務執行緒會醒來,適當地處理控制碼以及再次將作業完成的狀態回報至SCM。請注意,TimeHandlerEx的責任是回報動作的懸置狀態,而TimeServiceMain的義務則是回報服務的最後執行狀態(即是我曾在之前的〈處理執行緒內部通訊議題〉一節中討論的第三個方法)。
當客戶端已經連結至管道,而服務執行緒因為GetQueuedCompletionStatus的返回而醒來時, CK_PIPE的完成控制碼會被回傳。由此,服務會取得系統時間並呼叫WrITeFile以傳送時間至客戶端。然後服務會中斷客戶端與發佈另一個對ConnectNamedPipe的非同步呼叫的連結,以讓其他的客戶端可以與它連接。
當服務執行緒因為SERVICE_CONTROL_STOP或SERVICE_CONTROL_ SHUTDOWN控制碼而醒來時,它會關閉管道並終止執行。這會導致完成連接埠關閉、TimeServiceMain函式返回、並且將該服務執行緒刪除。由此,StartServiceCtrlDispatcher會返回至_tWinMain中,而它也會返回並刪除該處理程序。
在您建立了服務後,您必須在命令列環境中傳遞「-install」以將服務安裝至SCM的資料庫中。因為在執行檔名稱中包含了空白字元,所以請確定在可執行檔名稱前後包括了引號,即「"03 TimeService.exe"」。而且,您會想要使用服務嵌入管理單元去啟動以及管理該「Programming Server-Side Applications Time」服務。
TimeService.cpp
/********************************************************************
/模組:TimeService.cpp
通告:Copyright (c)2000 Jeffrey Richter
********************************************************************/
#include "..\CmnHdr.h" /* 請參閱附錄A */
#include "..\ClassLib\IOCP.h" /* 請參閱附錄B */
#include "..\ClassLib\EnsureCleanup.h" /* 請參閱附錄B */
#define SERVICESTATUS_IMPL
#include "ServiceStatus.h"
//////////////////////////////////////////////////////////////////////////////
TCHAR g_szServiceName[] = TEXT("Programming Server-Side Applications Time");
CServiceStatus g_ssTime;
//////////////////////////////////////////////////////////////////////////////
// 完成連接埠因為以下二個原因之一而醒來
enum COMPKEY {
CK_SERVICECONTROL, // 一個服務控制碼
CK_PIPE // 一個客戶端連接至我們的管道
};
//////////////////////////////////////////////////////////////////////////////
DWORD WINAPI TimeHandlerEx(DWORD dwControl, DWORD dwEventType,
PVOID pvEventData, PVOID pvContext) {
DWORD dwReturn = ERROR_CALL_NOT_IMPLEMENTED;
BOOL fPostControlToServiceThread = FALSE;
swITch (dwControl) {
case SERVICE_CONTROL_STOP:
case SERVICE_CONTROL_SHUTDOWN:
g_ssTime.SetUltimateState(SERVICE_STOPPED, 2000);
fPostControlToServiceThread = TRUE;
break;
case SERVICE_CONTROL_PAUSE:
g_ssTime.SetUltimateState(SERVICE_PAUSED, 2000);
fPostControlToServiceThread = TRUE;
break;
case SERVICE_CONTROL_CONTINUE:
g_ssTime.SetUltimateState(SERVICE_RUNNING, 2000);
fPostControlToServiceThread = TRUE;
break;
case SERVICE_CONTROL_INTERROGATE:
g_ssTime.ReportStatus();
break;
case SERVICE_CONTROL_PARAMCHANGE:
break;
case SERVICE_CONTROL_DEVICEEVENT:
case SERVICE_CONTROL_HARDWAREPROFILECHANGE:
case SERVICE_CONTROL_POWEREVENT:
break;
case 128: // 一個測試用的使用者定義控制碼
// 注意:正常的情況下,一個服務不該顯示使用者介面
MessageBox(NULL, TEXT("In HandlerEx processing user-defined code."),
g_szServiceName, MB_OK |MB_SERVICE_NOTIFICATION);
break;
}
if (fPostControlToServiceThread) {
// Handler執行緒非常簡單而且執行的非常快速
// 它只傳遞控制碼以離開ServiceMain執行緒
CIOCP* piocp = (CIOCP*) pvContext;
piocp->PostStatus(CK_SERVICECONTROL, dwControl);
dwReturn = NO_ERROR;
}
return(dwReturn);
}
//////////////////////////////////////////////////////////////////////////////
void WINAPI TimeServiceMain(DWORD dwArgc, PTSTR* pszArgv) {
ULONG_PTR CompKey = CK_SERVICECONTROL;
DWORD dwControl = SERVICE_CONTROL_CONTINUE;
CEnsureCloseFile hpipe;
OVERLAPPED o, *po;
SYSTEMTIME st;
DWORD dwNumBytes;
// 建立完成通訊埠並儲存在整體變數中它的handle
//所以Handler函式可以存取它
CIOCP iocp(0);
g_ssTime.InITialize(g_szServiceName, TimeHandlerEx, (PVOID) &iocp, TRUE);
g_ssTime.AcceptControls(
SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE);
do {
swITch (CompKey) {
case CK_SERVICECONTROL:
// 我們取得一個控制碼
swITch (dwControl) {
case SERVICE_CONTROL_CONTINUE:
// 當它正在執行時,建立一個管道以讓客戶端連結
hpipe = CreateNamedPipe(TEXT("\\\\.\\pipe\\TimeService"),
PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE, 1, sizeof(st), sizeof(st), 1000, NULL);
// 建立管道與完成連接埠之關聯
iocp.AssociateDevice(hpipe, CK_PIPE);
// 相對於管道,此處懸置了一個非同步連接
ZeroMemory(&o, sizeof(o));
ConnectNamedPipe(hpipe, &o);
g_ssTime.ReportUltimateState();
break;
case SERVICE_CONTROL_PAUSE:
case SERVICE_CONTROL_STOP:
// 若沒有在執行中,則關閉管道以使客戶端無法與之連結
hpipe.Cleanup();
g_ssTime.ReportUltimateState();
break;
}
break;
case CK_PIPE:
if (hpipe.IsValid()) {
// 我們取得了一個客戶端要求:傳送我們的系統時間至客戶端
GetSystemTime(&st);
WrITeFile(hpipe, &st, sizeof(st), &dwNumBytes, NULL);
FlushFileBuffers(hpipe);
DisconnectNamedPipe(hpipe);
// 允許另一個客戶端連結
ZeroMemory(&o, sizeof(o));
ConnectNamedPipe(hpipe, &o);
} else {
// 當管道被關閉時,我們會執行到此處
}
}
if (g_ssTime != SERVICE_STOPPED) {
// 除非一個控制碼進來或是有一個客戶端連接上來,否則即處於睡眠狀態
iocp.GetStatus(&CompKey, &dwNumBytes, &po);
dwControl = dwNumBytes;
}
} while (g_ssTime != SERVICE_STOPPED);
}
/////////////////////////////////////////////////////////////////////////////////////////
void InstallService() {
// 在此機器上開啟SCM
CEnsureCloseServiceHandle hSCM =
OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
// 取得我們的路徑名稱
TCHAR szModulePathname[_MAX_PATH * 2];
GetModuleFileName(NULL, szModulePathname, chDIMOF(szModulePathname));
// 因為處理程序去執行一個服務,所以增加轉換swITch
lstrcat(szModulePathname, TEXT(" /service"));
// 將此服務加入SCM的資料庫中
CEnsureCloseServiceHandle hService =
CreateService(hSCM, g_szServiceName, g_szServiceName,
SERVICE_CHANGE_CONFIG, SERVICE_WIN32_OWN_PROCESS,
SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE,
szModulePathname, NULL, NULL, NULL, NULL, NULL);
SERVICE_DESCRIPTION sd = {
TEXT("Sample Time Service from")
TEXT("Programming Server-Side Applications for Microsoft Windows Book")
};
ChangeServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION, &sd);
}
//////////////////////////////////////////////////////////////////////////////
void RemoveService() {
// 在此機器上開啟SCM
CEnsureCloseServiceHandle hSCM =
OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
// 為了DELETE的存取而開啟這個服務
CEnsureCloseServiceHandle hService =
OpenService(hSCM, g_szServiceName, DELETE);
// 從SCM資料庫中移除此服務
DeleteService(hService);
}
//////////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) {
int nArgc = __argc;
#ifdef UNICODE
PCTSTR *ppArgv = (PCTSTR*) CommandLineToArgvW(GetCommandLine(), &nArgc);
#else
PCTSTR *ppArgv = (PCTSTR*) __argv;
#endif
if (nArgc < 2) {
MessageBox(NULL,
TEXT("Programming Server-Side Applications for Microsoft Windows:")
TEXT("Time Service Sample\n\n")
TEXT("Usage:TimeService.exe [/install] [/remove] [/debug] ")
TEXT("[/service]\n")
TEXT(" //install\t\tInstalls the service in the SCM's database.\n")
TEXT(" //remove\t\tRemoves the service from the SCM's database.\n")
TEXT(" //debug \t \tRuns the service as a normal process for ")
TEXT("debugging.\n ")
TEXT(" //service \t\tRuns the process as a service")
TEXT("(should only be set in the SCM's database)."),
g_szServiceName, MB_OK);
} else {
for (int i = 1; i < nArgc; i++) {
if ((ppArgv[i][0] == TEXT('-')) || (ppArgv[i][0] == TEXT('/'))) {
// 命令列開關
if (lstrcmpi(&ppArgv[i][1], TEXT("install")) == 0)
InstallService();
if (lstrcmpi(&ppArgv[i][1], TEXT("remove")) == 0)
RemoveService();
if (lstrcmpi(&ppArgv[i][1], TEXT("debug")) == 0) {
// 執行服務控制碼
TimeServiceMain(0, NULL);
}
if (lstrcmpi(&ppArgv[i][1], TEXT("service")) == 0) {
// 連接至服務控制分發器
SERVICE_TABLE_ENTRY ServiceTable [] == {
{ g_szServiceName, TimeServiceMain },
{ NULL, NULL } // End of list
};
chVERIFY(StartServiceCtrlDispatcher(ServiceTable));
}
}
}
}
#ifdef UNICODE
HeapFree(GetProcessHeap(), 0, (PVOID) ppArgv);
#endif
return(0);
}
///////////////////////////////// End of File ////////////////////////////////
ServiceStatus.h
/*******************************************************************
模組:ServiceStatus.h
注意:Copyright (c)2000 Jeffrey Richter
目的:這個類別封裝了一個SERVICE_STATUS結構以適當的使用
********************************************************************/
#pragma once
///////////////////////////////////////////////////////////////////////////////
#include "..\CmnHdr.h" /*See Appendix A. */
#include "Gate.h "
///////////////////////////////////////////////////////////////////////////////
class CServiceStatus : public SERVICE_STATUS {
public:
CServiceStatus();
BOOL InITialize(PCTSTR szServiceName, LPHANDLER_FUNCTION_EX pfnHandler,
PVOID pvContext, BOOL fOwnProcess, BOOL fInteractWIThDesktop = FALSE);
VOID AcceptControls(DWORD dwFlags, BOOL fAccept = TRUE);
BOOL ReportStatus();
BOOL SetUltimateState(DWORD dwUltimateState, DWORD dwWaITHint = 0);
BOOL AdvanceState(DWORD dwWaITHint, DWORD dwCheckPoint = 0);
BOOL ReportUltimateState();
BOOL ReportWin32Error(DWORD dwError);
BOOL ReportServiceSpecificError(DWORD dwError);
operator DWORD() const { return(dwCurrentState); }
private:
SERVICE_STATUS_HANDLE m_hss;
CGate m_gate;
};
///////////////////////////////////////////////////////////////////////////////
inline CServiceStatus::CServiceStatus() {
ZeroMemory(this, sizeof(SERVICE_STATUS));
m_hss = NULL;
}
///////////////////////////////////////////////////////////////////////////////
inline VOID CServiceStatus::AcceptControls(DWORD dwFlags, BOOL fAccept) {
if (fAccept) dwControlsAccepted |= dwFlags;
else dwControlsAccepted &= ~dwFlags;
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CServiceStatus::ReportStatus() {
BOOL fOk = ::SetServiceStatus(m_hss, this);
chASSERT(fOk);
return(fOk);
}
///////////////////////////////////////////////////////////////////////////////
inline BOOL CServiceStatus::ReportWin32Error(DWORD dwError) {
dwWin32ExITCode = dwError;
dwServiceSpecificExITCode = 0;
return(ReportStatus());
}
inline BOOL CServiceStatus::ReportServiceSpecificError(DWORD dwError) {
dwWin32ExITCode = ERROR_SERVICE_SPECIFIC_ERROR;
dwServiceSpecificExITCode = dwError;
return(ReportStatus());
}
///////////////////////////////////////////////////////////////////////////////
#ifdef SERVICESTATUS_IMPL
///////////////////////////////////////////////////////////////////////////////
BOOL CServiceStatus::InITialize(PCTSTR szServiceName,
LPHANDLER_FUNCTION_EX pfnHandler, PVOID pvContext,
BOOL fOwnProcess, BOOL fInteractWIThDesktop) {
m_hss = RegisterServiceCtrlHandlerEx(szServiceName, pfnHandler, pvCon