現在我們還剩下一個函數可以在細節上討論,那就是服務的CtrlHandler函數。
當調用RegisterServiceCtrlHandler函數時,SCM得到并保存這個回調函數的地址。一個SCP調一個告訴SCM如何去控制服務的Win32函數,現在已經有10個預定義的控制請求:
Requests the service to stop. The hService handle must have SERVICE_STOP access. Requests the service to pause. The hService handle must have SERVICE_PAUSE_CONTINUE access. Requests the paused service to resume. The hService handle must have SERVICE_PAUSE_CONTINUE access. Requests the service to update immediately its current status information to the service control manager. The hService handle must have SERVICE_INTERROGATE access. Requests the service to perform cleanup tasks, because the system is shutting down. For more information, see Remarks. Windows 2000: Requests the service to reread its startup parameters. The hService handle must have SERVICE_PAUSE_CONTINUE access. Windows 2000: Requests the service to update its network binding. The hService handle must have SERVICE_PAUSE_CONTINUE access. Windows 2000: Notifies a network service that a component for binding has been removed. The service should reread its binding information and unbind from the removed component. Windows 2000: Notifies a network service that a disabled binding has been enabled. The service should reread its binding information and add the new binding. Windows 2000: Notifies a network service that one of its bindings has been disabled. The service should reread its binding information and remove the binding.
上表中標有Windows 2000字樣的就是2000中新添加的控制代碼。除了這些代碼之外,服務也可以接受用戶定義的,范圍在128-255之間的代碼。
當CtrlHandler函數收到一個SERVICE_CONTROL_STOP、SERVICE_CONTROL_PAUSE、
SERVICE_CONTROL_CONTINUE控制代碼的時候,SetServiceStatus必須被調用去確認這個代碼,并指定你認為服務處理這個狀態變化所需要的時間。
例如:你的服務收到了停止請求,首先要把SERVICE_STATUS結構的dwCurrentState成員設置成SERVICE_STOP_PENDING,這樣可以使SCM確定你已經收到了控制代碼。當一個服務的暫停或停止操作正在執行的時候,必須指定你認為這種操作所需要的時間:這是因為一個服務也許不能立即改變它的狀態,它可能必須等待一個網絡請求被完成或者數據被刷新到一個驅動器上。指定時間的方法就像我上一章說的那樣,用成員dwCheckPoint和dwWaitHint來指明它完成狀態改變所需要的時間。如果需要,可以用增加dwCheckPoint成員的值和設置dwWaitHint成員的值去指明你期待的服務到達下一步的時間的方式周期性的報告進展情況。
當整個啟動的過程完成之后,要再一次調用SetServiceStatus。這時就要把SERVICE_STATUS結構的dwCurrentState成員設置成SERVICE_STOPPED,當報告狀態代碼的同時,一定要把成員dwCheckPoint和dwWaitHint設置為0,因為服務已經完成了它的狀態變化。暫停或繼續服務的時候方法也一樣。
當CtrlHandler函數收到一個SERVICE_CONTROL_INTERROGATE控制代碼的時候,服務將簡單的將dwCurrentState成員設置成服務當前的狀態,同時,把成員dwCheckPoint和dwWaitHint設置為0,然后再調用SetServiceStatus就可以了。
在操作系統關閉的時候,CtrlHandler函數收到一個SERVICE_CONTROL_SHUTDOWN控制代碼。服務根本無須回應這個代碼,因為系統即將關閉。它將執行保存數據所需要的最小行動集,這是為了確定機器能及時關閉。缺省時系統只給很少的時間去關閉所有的服務,MSDN里面說大概是20秒的時間,不過那可能是Windows NT 4的設置,在我的Windows 2000 Server里這個時間是10秒,你可以手動的修改這個數值,它被記錄在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control子鍵里面的WaitToKillServiceTimeout,單位是毫秒。
當CtrlHandler函數收到任何用戶定義的代碼時,它應該執行期望的用戶自定義行動。除非用戶自定義的行動要強制服務去暫停、繼續或停止,否則不調SetServiceStatus函數。如果用戶定義的行動強迫服務的狀態發生變化,SetServiceStatus將被調用去設置dwCurrentState、dwCheckPoint和dwWaitHint,具體控制代碼和前面說的一樣。
如果你的CtrlHandler函數需要很長的時間執行操作的話,千萬要注意:假如CtrlHandler函數在30秒內沒有返回的話,SCM將返回一個錯誤,這不是我們所期望的。所以如果出現上述情況,最好的辦法是再建立一個線程,讓它去繼續執行操作,以便使得CtrlHandler函數能夠迅速的返回。例如,當收到一個SERVICE_CONTROL_STOP請求的時候,就像上面說的一樣,服務可能正在等待一個網絡請求被完成或者數據被刷新到一個驅動器上,而這些操作所需要的時間是你不能估計的,那么就要建立一個新的線程等待操作完成后執行停止命令,CtrlHandler函數在返回之前仍然要報告SERVICE_STOP_PENDING狀態,當新的線程執行完操作之后,再由它將服務的狀態設置成SERVICE_STOPPED。如果當前操作的時間可以估計的到就不要這樣做,仍然使用前面交待的方法處理。
CtrlHandler函數我就先講這些,下面說說服務怎么安裝。一個服務程序可以使用CreateService函數將服務的信息添加到SCM的數據庫。
SC_HANDLE CreateService(
SC_HANDLE hSCManager, // handle to SCM database
LPCTSTR lpServiceName, // name of service to start
LPCTSTR lpDisplayName, // display name
DWORD dwDesiredAccess, // type of access to service
DWORD dwServiceType, // type of service
DWORD dwStartType, // when to start service
DWORD dwErrorControl, // severity of service failure
LPCTSTR lpBinaryPathName, // name of binary file
LPCTSTR lpLoadOrderGroup, // name of load ordering group
LPDWORD lpdwTagId, // tag identifier
LPCTSTR lpDependencies, // array of dependency names
LPCTSTR lpServiceStartName, // account name
LPCTSTR lpPassword // account password
);
hSCManager是一個標示SCM數據庫的句柄,可以簡單的通過調用OpenSCManager得到。
SC_HANDLE OpenSCManager(
LPCTSTR lpMachineName, // computer name
LPCTSTR lpDatabaseName, // SCM database name
DWORD dwDesiredAccess // access type
);
lpMachineName是目標機器的名字,還記得我在第一章里說過可以在其它的機器上面安裝服務嗎?這就是實現的方法。對方機器名字必須以“\\”開始。如果傳遞NULL或者一個空的字符串的話就默認是本機。
lpDatabaseName是目標機器上面SCM數據庫的名字,但MSDN里面說這個參數要默認的設置成SERVICES_ACTIVE_DATABASE,如果傳遞NULL,就默認的打開SERVICES_ACTIVE_DATABASE。所以我還沒有真的搞明白這個參數的存在意義,總之使用的時候傳遞NULL就行了。
dwDesiredAccess是SCM數據庫的訪問權限,具體值見下表:
SC_MANAGER_ALL_ACCESS:
Includes STANDARD_RIGHTS_REQUIRED, in addition to all of the access types listed in this table. Enables connecting to the service control manager. Enables calling of the CreateService function to create a service object and add it to the database. Enables calling of the EnumServicesStatus function to list the services that are in the database. Enables calling of the LockServiceDatabase function to acquire a lock on the database. Enables calling of the QueryServiceLockStatus function to retrieve the lock status information for the database.
想要獲得訪問權限的話,似乎沒那么復雜。MSDN里面說所有進程都被允許獲得對所有SCM數據庫的SC_MANAGER_CONNECT, SC_MANAGER_ENUMERATE_SERVICE, and SC_MANAGER_QUERY_LOCK_STATUS權限,這些權限使得你可以連接SCM數據庫,枚舉目標機器上安裝的服務和查詢目標數據庫是否已被鎖住。但如果要創建服務,首先你需要擁有目標機器的管理員權限,一般的傳遞SC_MANAGER_ALL_ACCESS就可以了。這個函數返回的句柄可以被CloseServiceHandle函數關閉。
lpServiceName是服務的名字,lpDisplayName是服務在“服務”管理工具里顯示的名字。
dwDesiredAccess也是訪問的權限,有一個比上面的還長的多的一個表,各位自己查MSDN吧。我們要安裝服務,仍然簡單的傳遞SC_MANAGER_ALL_ACCESS。
dwServiceType是指你的服務是否和其它的進程相關聯,一般是SERVICE_WIN32_OWN_PROCESS,表示不和任何進程相關聯。如果你確認你的服務需要和某些進程相關聯,就設置成SERVICE_WIN32_SHARE_PROCESS。當你的服務要和桌面相關聯的時候,需要設置成SERVICE_INTERACTIVE_PROCESS。
dwStartType是服務的啟動方式。服務有三種啟動方式,分別是“自動(SERVICE_AUTO_START)”“手動(SERVICE_DEMAND_START)”和“禁用(SERVICE_DISABLED)”。在MSDN里還有另外的兩種方式,不過是專為驅動程序設置的。
dwErrorControl決定服務如果在系統啟動的時候啟動失敗的話要怎么辦。
SERVICE_ERROR_IGNORE:
啟動程序記錄錯誤發生,但繼續啟動。 啟動程序記錄錯誤發生,并彈出一個消息框,但仍繼續啟動 啟動程序記錄錯誤發生,如果是以last-known-good configuration啟動的話,啟動會繼續。否則會以last-known-good configuration重新啟動計算機。 啟動程序記錄錯誤發生,如果可能的話。如果是以last-known-good configuration啟動的話,啟動會失敗。否則會以last-known-good configuration重新啟動計算機。好嚴重的錯誤啊。
lpBinaryPathName是服務程序的路徑。MSDN里面特別提到如果服務路徑里面有空格的話一定要將路徑用引號引起來。例如"d:\\my share\\myservice.exe"就一定要指定為"\"d:\\my share\\myservice.exe\""。
lpLoadOrderGroup的意義在于,如果有一組服務要按照一定的順序啟動的話,這個參數用于指定一個組名用于標志這個啟動順序組,不過我還沒有用過這個參數。你的服務如果不屬于任何啟動順序組,只要傳遞NULL或者一個空的字符串就行了。
lpdwTagId是應用了上面的參數之后要指定的值,專用于驅動程序,與本文內容無關。傳遞NULL。
lpDependencies標示一個字符串數組,用于指明一串服務的名字或者一個啟動順序組。當與一個啟動順序組建立關聯的時候,這個參數的含義就是只有你指定的啟動順序組里有至少一個經過對整個組里所有的成員已經全部嘗試過啟動后,有至少一個成員成功啟動,你的服務才能啟動。不需要建立依存關系的話,仍是傳遞NULL或者一個空的字符串。但如果你要指定啟動順序組的話,必須為組名加上SC_GROUP_IDENTIFIER前綴,因為組名和服務名是共享一個命名空間的。
lpServiceStartName是服務的啟動賬號,如果你設置你的服務的關聯類型是SERVICE_WIN32_OWN_PROCESS的話,你需要以DomainName\UserName的格式指定用戶名,如果這個賬戶在你本機的話,用.\UserName就可以指定。如果傳遞NULL的話,會以本地的系統賬戶登陸。如果是Win NT 4.0或更早的版本的話,如果你指定了SERVICE_WIN32_SHARE_PROCESS,就必須傳遞.\System指定服務使用本地的系統賬戶。最后,如果你指定了SERVICE_INTERACTIVE_PROCESS,你必須使服務運行在本機系統賬戶。
看名字就知道了,lpPassword是賬戶的密碼。如果指定系統賬戶的話,傳遞NULL。如果賬戶沒有密碼的話,傳遞空字符串。
總之服務的基本原理就是這樣子了,到了這里這篇文章似乎可以告一段落了,但實際上還有很多內容必須要討論,所以我還不能草草收筆,敬請關注下一章。
本文測試環境為Win2000 Server + SP2
Athlon XP 1700 + 256MB DDR
全部資料參考自MSDN OCT 2001