Windows服務(wù)編寫原理及探討(2)
2019-09-06 23:33:37
供稿:網(wǎng)友
(二)對服務(wù)的深入討論之上
上一章其實(shí)只是概括性的介紹,下面開始才是真正的細(xì)節(jié)所在。在進(jìn)入點(diǎn)函數(shù)里面要完成ServiceMain的初始化,準(zhǔn)確點(diǎn)說是初始化一個SERVICE_TABLE_ENTRY結(jié)構(gòu)數(shù)組,這個結(jié)構(gòu)記錄了這個服務(wù)程序里面所包含的所有服務(wù)的名稱和服務(wù)的進(jìn)入點(diǎn)函數(shù),下面是一個SERVICE_TABLE_ENTRY的例子:
SERVICE_TABLE_ENTRY service_table_entry[] =
{
{ "MyFTPd" , FtpdMain },
{ "MyHttpd", Httpserv},
{ NULL, NULL },
};
第一個成員代表服務(wù)的名字,第二個成員是ServiceMain回調(diào)函數(shù)的地址,上面的服務(wù)程序因?yàn)閾碛袃蓚€服務(wù),所以有三個SERVICE_TABLE_ENTRY元素,前兩個用于服務(wù),最后的NULL指明數(shù)組的結(jié)束。
接下來這個數(shù)組的地址被傳遞到StartServiceCtrlDispatcher函數(shù):
BOOL StartServiceCtrlDispatcher(
LPSERVICE_TABLE_ENTRY lpServiceStartTable
)
這個Win32函數(shù)表明可執(zhí)行文件的進(jìn)程怎樣通知SCM包含在這個進(jìn)程中的服務(wù)。就像上一章中講的那樣,StartServiceCtrlDispatcher為每一個傳遞到它的數(shù)組中的非空元素產(chǎn)生一個新的線程,每一個進(jìn)程開始執(zhí)行由數(shù)組元素中的lpServiceStartTable指明的ServiceMain函數(shù)。
SCM啟動一個服務(wù)程序之后,它會等待該程序的主線程去調(diào)StartServiceCtrlDispatcher。如果那個函數(shù)在兩分鐘內(nèi)沒有被調(diào)用,SCM將會認(rèn)為這個服務(wù)有問題,并調(diào)用TerminateProcess去殺死這個進(jìn)程。這就要求你的主線程要盡可能快的調(diào)用StartServiceCtrlDispatcher。
StartServiceCtrlDispatcher函數(shù)則并不立即返回,相反它會駐留在一個循環(huán)內(nèi)。當(dāng)在該循環(huán)內(nèi)時,StartServiceCtrlDispatcher懸掛起自己,等待下面兩個事件中的一個發(fā)生。第一,如果SCM要去送一個控制通知給運(yùn)行在這個進(jìn)程內(nèi)一個服務(wù)的時候,這個線程就會激活。當(dāng)控制通知到達(dá)后,線程激活并調(diào)用相應(yīng)服務(wù)的CtrlHandler函數(shù)。CtrlHandler函數(shù)處理這個服務(wù)控制通知,并返回到StartServiceCtrlDispatcher。StartServiceCtrlDispatcher循環(huán)回去后再一次懸掛自己。
第二,如果服務(wù)線程中的一個服務(wù)中止,這個線程也將激活。在這種情況下,該進(jìn)程將運(yùn)行在它里面的服務(wù)數(shù)減一。如果服務(wù)數(shù)為零,StartServiceCtrlDispatcher就會返回到入口點(diǎn)函數(shù),以便能夠執(zhí)行任何與進(jìn)程有關(guān)的清除工作并結(jié)束進(jìn)程。如果還有服務(wù)在運(yùn)行,哪怕只是一個服務(wù),StartServiceCtrlDispatcher也會繼續(xù)循環(huán)下去,繼續(xù)等待其它的控制通知或者剩下的服務(wù)線程中止。
上面的內(nèi)容是關(guān)于入口點(diǎn)函數(shù)的,下面的內(nèi)容則是關(guān)于ServiceMain函數(shù)的。還記得以前講過的ServiceMain函數(shù)的的原型嗎?但實(shí)際上一個ServiceMain函數(shù)通常忽略傳遞給它的兩個參數(shù),因?yàn)榉?wù)一般不怎么傳遞參數(shù)。設(shè)置一個服務(wù)最好的方法就是設(shè)置注冊表,一般服務(wù)在
HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServiceServiceNameParameters
子鍵下存放自己的設(shè)置,這里的ServiceName是服務(wù)的名字。事實(shí)上,可能要寫一個客戶應(yīng)用程序去進(jìn)行服務(wù)的背景設(shè)置,這個客戶應(yīng)用程序?qū)⑦@些信息存在注冊表中,以便服務(wù)讀取。當(dāng)一個外部應(yīng)用程序已經(jīng)改變了某個正在運(yùn)行中的服務(wù)的設(shè)置數(shù)據(jù)的時候,這個服務(wù)能夠用RegNotifyChangeKeyValue函數(shù)去接受一個通知,這樣就允許服務(wù)快速的重新設(shè)置自己。
前面講到StartServiceCtrlDispatcher為每一個傳遞到它的數(shù)組中的非空元素產(chǎn)生一個新的線程。接下來,一個ServiceMain要做些什么呢?MSDN里面的原文是這樣說的:The ServiceMain function should immediately call the RegisterServiceCtrlHandler function to specify a Handler function to handle control requests. Next, it should call the SetServiceStatus function to send status information to the service control manager. 為什么呢?因?yàn)榘l(fā)出啟動服務(wù)請求之后,如果在一定時間之內(nèi)無法完成服務(wù)的初始化,SCM會認(rèn)為服務(wù)的啟動已經(jīng)失敗了,這個時間的長度在Win NT 4.0中是80秒,Win2000中不詳...
基于上面的理由,ServiceMain要迅速完成自身工作,首先是必不可少的兩項(xiàng)工作,第一項(xiàng)是調(diào)用RegisterServiceCtrlHandler函數(shù)去通知SCM它的CtrlHandler回調(diào)函數(shù)的地址:
SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler(
LPCTSTR lpServiceName, //服務(wù)的名字
LPHANDLER_FUNCTION lpHandlerProc //CtrlHandler函數(shù)地址
)
第一個參數(shù)指明你正在建立的CtrlHandler是為哪一個服務(wù)所用,第二個參數(shù)是CtrlHandler函數(shù)的地址。lpServiceName必須和在SERVICE_TABLE_ENTRY里面被初始化的服務(wù)的名字相匹配。RegisterServiceCtrlHandler返回一個SERVICE_STATUS_HANDLE,這是一個32位的句柄。SCM用它來唯一確定這個服務(wù)。當(dāng)這個服務(wù)需要把它當(dāng)時的狀態(tài)報(bào)告給SCM的時候,就必須把這個句柄傳給需要它的Win32函數(shù)。注意:這個句柄和其他大多數(shù)的句柄不同,你無需關(guān)閉它。
SCM要求ServiceMain函數(shù)的線程在一秒鐘內(nèi)調(diào)用RegisterServiceCtrlHandler函數(shù),否則SCM會認(rèn)為服務(wù)已經(jīng)失敗。但在這種情況下,SCM不會終止服務(wù),不過在NT 4中將無法啟動這個服務(wù),同時會返回一個不正確的錯誤信息,這一點(diǎn)在Windows 2000中得到了修正。
在RegisterServiceCtrlHandler函數(shù)返回后,ServiceMain線程要立即告訴SCM服務(wù)正在繼續(xù)初始化。具體的方法是通過調(diào)用SetServiceStatus函數(shù)傳遞SERVICE_STATUS數(shù)據(jù)結(jié)構(gòu)。
BOOL SetServiceStatus(
SERVICE_STATUS_HANDLE hService, //服務(wù)的句柄
SERVICE_STATUS lpServiceStatus //SERVICE_STATUS結(jié)構(gòu)的地址
)
這個函數(shù)要求傳遞給它指明服務(wù)的句柄(剛剛通過調(diào)用RegisterServiceCtrlHandler得到),和一個初始化的SERVICE_STATUS結(jié)構(gòu)的地址:
typedef struct _SERVICE_STATUS
{
DWORD dwServiceType;
DWORD dwCurrentState;
DWORD dwControlsAccepted;
DWORD dwWin32ExitCode;
DWORD dwServiceSpecificExitCode;
DWORD dwCheckPoint;
DWORD dwWaitHint;
} SERVICE_STATUS, *LPSERVICE_STATUS;
SERVICE_STATUS結(jié)構(gòu)含有七個成員,它們反映服務(wù)的現(xiàn)行狀態(tài)。所有這些成員必須在這個結(jié)構(gòu)被傳遞到SetServiceStatus之前正確的設(shè)置。
成員dwServiceType指明服務(wù)可執(zhí)行文件的類型。如果你的可執(zhí)行文件中只有一個單獨(dú)的服務(wù),就把這個成員設(shè)置成SERVICE_WIN32_OWN_PROCESS;如果擁有多個服務(wù)的話,就設(shè)置成SERVICE_WIN32_SHARE_PROCESS。除了這兩個標(biāo)志之外,如果你的服務(wù)需要和桌面發(fā)生交互(當(dāng)然不推薦這樣做),就要用“OR”運(yùn)算符附加上SERVICE_INTERACTIVE_PROCESS。這個成員的值在你的服務(wù)的生存期內(nèi)絕對不應(yīng)該改變。
成員dwCurrentState是這個結(jié)構(gòu)中最重要的成員,它將告訴SCM你的服務(wù)的現(xiàn)行狀態(tài)。為了報(bào)告服務(wù)仍在初始化,應(yīng)該把這個成員設(shè)置成SERVICE_START_PENDING。在以后具體講述CtrlHandler函數(shù)的時候具體解釋其它可能的值。
成員dwControlsAccepted指明服務(wù)愿意接受什么樣的控制通知。如果你允許一個SCP去暫停/繼續(xù)服務(wù),就把它設(shè)成SERVICE_ACCEPT_PAUSE_CONTINUE。很多服務(wù)不支持暫停或繼續(xù),就必須自己決定在服務(wù)中它是否可用。如果你允許一個SCP去停止服務(wù),就要設(shè)置它為SERVICE_ACCEPT_STOP。如果服務(wù)要在操作系統(tǒng)關(guān)閉的時候得到通知,設(shè)置它為SERVICE_ACCEPT_SHUTDOWN可以收到預(yù)期的結(jié)果。這些標(biāo)志可以用“OR”運(yùn)算符組合。
成員dwWin32ExitCode和dwServiceSpecificExitCode是允許服務(wù)報(bào)告錯誤的關(guān)鍵,如果希望服務(wù)去報(bào)告一個Win32錯誤代碼(預(yù)定義在WinError.h中),它就設(shè)置dwWin32ExitCode為需要的代碼。一個服務(wù)也可以報(bào)告它本身特有的、沒有映射到一個預(yù)定義的Win32錯誤代碼中的錯誤。為了這一點(diǎn),要把dwWin32ExitCode設(shè)置為ERROR_SERVICE_SPECIFIC_ERROR,然后還要設(shè)置成員dwServiceSpecificExitCode為服務(wù)特有的錯誤代碼。當(dāng)服務(wù)運(yùn)行正常,沒有錯誤可以報(bào)告的時候,就設(shè)置成員dwWin32ExitCode為NO_ERROR。
最后的兩個成員dwCheckPoint和dwWaitHint是一個服務(wù)用來報(bào)告它當(dāng)前的事件進(jìn)展情況的。當(dāng)成員dwCurrentState被設(shè)置成SERVICE_START_PENDING的時候,應(yīng)該把dwCheckPoint設(shè)成0,dwWaitHint設(shè)成一個經(jīng)過多次嘗試后確定比較合適的數(shù),這樣服務(wù)才能高效運(yùn)行。一旦服務(wù)被完全初始化,就應(yīng)該重新初始化SERVICE_STATUS結(jié)構(gòu)的成員,更改dwCurrentState為SERVICE_RUNNING,然后把dwCheckPoint和dwWaitHint都改為0。
dwCheckPoint成員的存在對用戶是有益的,它允許一個服務(wù)報(bào)告它處于進(jìn)程的哪一步。每一次調(diào)用SetServiceStatus時,可以增加它到一個能指明服務(wù)已經(jīng)執(zhí)行到哪一步的數(shù)字,它可以幫助用戶決定多長時間報(bào)告一次服務(wù)的進(jìn)展情況。如果決定要報(bào)告服務(wù)的初始化進(jìn)程的每一步,就應(yīng)該設(shè)置dwWaitHint為你認(rèn)為到達(dá)下一步所需的毫秒數(shù),而不是服務(wù)完成它的進(jìn)程所需的毫秒數(shù)。
在服務(wù)的所有初始化都完成之后,服務(wù)調(diào)用SetServiceStatus指明SERVICE_RUNNING,在那一刻服務(wù)已經(jīng)開始運(yùn)行。通常一個服務(wù)是把自己放在一個循環(huán)之中來運(yùn)行的。在循環(huán)的內(nèi)部這個服務(wù)進(jìn)程懸掛自己,等待指明它下一步是應(yīng)該暫停、繼續(xù)或停止之類的網(wǎng)絡(luò)請求或通知。當(dāng)一個請求到達(dá)的時候,服務(wù)線程激活并處理這個請求,然后再循環(huán)回去等待下一個請求/通知。
如果一個服務(wù)由于一個通知而激活,它會先處理這個通知,除非這個服務(wù)得到的是停止或關(guān)閉的通知。如果真的是停止或關(guān)閉的通知,服務(wù)線程將退出循環(huán),執(zhí)行必要的清除操作,然后從這個線程返回。當(dāng)ServiceMain線程返回并中止時,引起在StartServiceCtrlDispatcher內(nèi)睡眠的線程激活,并像在前面解釋過的那樣,減少它運(yùn)行的服務(wù)的計(jì)數(shù)。