什麼是windows服務
在NT/2000中,服務是一類受到操作系統優待的程序。一個服務首先是一個Win32可執行程序,如果要寫一個功能完備且強大的服務,需要熟悉動態連接庫(Dlls)、結構異常處理、內存映射文件、虛擬內存、設備I/O、線程及其同步、Unicode以及其他的由WinAPI函數提供的應用接口。服務器一般不需要界面,因此服務器程序的入口函數一般爲main而不是winmain。
Windows NT/2000提供了大量的管理工具,這些工具允許通過網絡上的其它計算機對某臺機器上面的服務進行管理。比如Windows 2000裏面的“控制檯”程序(mmc.exe),用它添加“管理單元”就可以管理本機或其他機器上的服務。
服務的安全性…
想要寫一個服務,就必須熟悉Win NT/2000的安全機制,在上述操作系統之中,所有安全都是基於用戶的。換句話說——進程、線程、文件、註冊表鍵、信號、事件等等等等都屬於一個用戶。當一個進程被產生的時候,它都是執行在一個用戶的上下文(context),這個用戶帳號可能在本機,也可能在網絡中的其他機器上,或者是在一個特殊的賬號:System Account——即系統帳號的上下文 如果一個進程正在一個用戶帳號下執行,那麼這個進程就同時擁有這個用戶所能擁有的一切訪問權限,不論是在本機還是網絡。系統帳號則是一個特殊的賬號,它用來標識系統本身,而且運行在這個帳號下的任何進程都擁有系統上的所有訪問權限,但是系統帳號不能在域上使用,無法訪問網絡資源…
服務也是Win32可執行程序,它也需要執行在一個context,通常服務都是在系統賬號下運行,但是也可以根據情況選擇讓它運行在一個用戶賬號下,也就會因此獲得相應的訪問資源的權限。
服務的三個組成部分
一個服務由三部分組成,第一部分是Service Control Manager(SCM)。每個Windows NT/2000系統都有一個SCM,SCM存在於Service.exe中,在Windows啓動的時候會自動運行,伴隨着操作系統的啓動和關閉而產生和終止。這個進程以系統特權運行,並且提供一個統一的、安全的手段去控制服務。它其實是一個RPCServer,因此我們可以遠程安裝和管理服務,不過這不在本文討論的範圍之內。SCM包含一個儲存着已安裝的服務和驅動程序的信息的數據庫,通過SCM可以統一的、安全的管理這些信息,因此一個服務程序的安裝過程就是將自身的信息寫入這個數據庫。
第二部分就是服務本身。一個服務擁有能從SCM收到信號和命令所必需的的特殊代碼,並且能夠在處理後將它的狀態回傳給SCM。
第三部分也就是最後一部分,是一個Service Control Dispatcher(SCP)。它是一個擁有用戶界面,允許用戶開始、停止、暫停、繼續,並且控制一個或多個安裝在計算機上服務的Win32應用程序。SCP的作用是與SCM通訊,Windows2000管理工具中的“服務”就是一個典型的SCP。
在這三個組成部分中,用戶最可能去寫服務本身,同時也可能不得不寫一個與其伴隨的客戶端程序作爲一個SCP去和SCM通訊,本文只討論去設計和實現一個服務,關於如何去實現一個SCP則在以後的其它文章中介紹。
怎樣開始設計服務
以main函數作爲入口點函數開始設計,如需和桌面用戶進行信息交互有時也可以winmain函數做入口(帶界面)。
入口函數負責初始化整個進程,由這個進程中的主線程來執行。這意味着它應用於這個可執行文件中的所有服務。要知道,一個可執行文件中能夠包含多個服務以使得執行更加有效。主進程通知SCM在可執行文件中含有幾個服務,並且給出每一個服務的ServiceMain回調(Call Back)函數的地址。一旦在可執行文件內的所有服務都已經停止運行,主線程就在進程終止前對整個進程進行清除。
第二個很重要的函數就是ServiceMain,我看過一些例子程序裏面對自己的服務的進入點函數都固定命名爲ServiceMain,任何的函數只要符合下列的形式都可以作爲服務的進入點函數。
VOID WINAPI ServiceMain(
DWORD dwArgc, // 參數個數
LPTSTR *lpszArgv // 參數串
);
這個函數由操作系統調用,並執行能完成服務的代碼。一個專用的線程執行每一個服務的ServiceMain函數,注意是服務而不是服務程序,這是因爲每個服務也都擁有與自己唯一對應的ServiceMain函數,關於這一點可以用“管理工具”裏的“服務”去察看Win2000裏面自帶的服務,就會發現其實很多服務都是由service.exe單獨提供的。當主線程調用Win32函數StartServiceCtrlDispatcher的時候,SCM爲這個進程中的每一個服務產生一個線程。這些線程中的每一個都和它的相應的服務的ServiceMain函數一起執行,這就是服務總是多線程的原因——一個僅有一個服務的可執行文件將有一個主線程,其它的線程執行服務本身。
第三個也就是最後的一個重要函數是CtrlHandler,它必須擁有下面的原型:
VOID WINAPI CtrlHandler(
DWORD fdwControl //控制命令
)
像ServiceMain一樣,CtrlHandler也是一個回調函數,用戶必須爲它的服務程序中每一個服務寫一個單獨的CtrlHandler函數,因此如果有一個程序含有兩個服務,那麼它至少要擁有5個不同的函數:作爲入口點的main()或WinMain(),用於第一個服務的ServiceMain函數和CtrlHandler函數,以及用於第二個服務的ServiceMain函數和CtrlHandler函數。
SCM調用一個服務的CtrlHandler函數去改變這個服務的狀態。例如,當某個管理員用管理工具裏的“服務”嘗試停止你的服務的時候,你的服務的CtrlHandler函數將收到一個SERVICE_CONTROL_STOP通知。CtrlHandler函數負責執行停止服務所需的一切代碼。由於是進程的主線程執行所有的CtrlHandler函數,因而必須儘量優化你的CtrlHandler函數的代碼,使它運行起來足夠快,以便相同進程中的其它服務的CtrlHandler函數能在適當的時間內收到屬於它們的通知。而且基於上述原因,你的CtrlHandler函數必須要能夠將想要傳達的狀態送到服務線程,這個傳遞過程沒有固定的方法,完全取決於你的服務的用途
對服務的深入討論
在進入點函數裏面要完成ServiceMain的初始化,準確點說是初始化一個SERVICE_TABLE_ENTRY結構數組,這個結構記錄了這個服務程序裏面所包含的所有服務的名稱和服務的進入點函數,下面是一個SERVICE_TABLE_ENTRY的例子:
<span style="font-size:12px;"> SERVICE_TABLE_ENTRY service_table_entry[] =
{
{ "MyFTPd" , FtpdMain },
{ "MyHttpd", Httpserv},
{ NULL, NULL },
};</span>
第一個成員代表服務的名字,第二個成員是ServiceMain回調函數的地址,上面的服務程序因爲擁有兩個服務,所以有三個SERVICE_TABLE_ENTRY元素,前兩個用於服務,最後的NULL指明數組的結束。
接下來這個數組的地址被傳遞到StartServiceCtrlDispatcher函數:
BOOL StartServiceCtrlDispatcher (LPSERVICE_TABLE_ENTRY lpServiceStartTable);
這個Win32函數表明可執行文件的進程怎樣通知SCM包含在這個進程中的服務。就像上一章中講的那樣,StartServiceCtrlDispatcher爲每一個傳遞到它的數組中的非空元素產生一個新的線程,每一個進程開始執行由數組元素中的lpServiceStartTable指明的ServiceMain函數。
SCM啓動一個服務程序之後,它會等待該程序的主線程去調StartServiceCtrlDispatcher。如果那個函數在兩分鐘內沒有被調用,SCM將會認爲這個服務有問題,並調用TerminateProcess去殺死這個進程。這就要求你的主線程要儘可能快的調用StartServiceCtrlDispatcher。
StartServiceCtrlDispatcher函數則並不立即返回,相反它會駐留在一個循環內。當在該循環內時,StartServiceCtrlDispatcher懸掛起自己,等待下面兩個事件中的一個發生:
第一,如果SCM要去送一個控制通知給運行在這個進程內一個服務的時候,這個線程就會激活。當控制通知到達後,線程激活並調用相應服務的CtrlHandler函數。CtrlHandler函數處理這個服務控制通知,並返回到StartServiceCtrlDispatcher。StartServiceCtrlDispatcher循環回去後再一次懸掛自己。
第二,如果服務線程中的一個服務中止,這個線程也將激活。在這種情況下,該進程將運行在它裏面的服務數減一。如果服務數爲零,StartServiceCtrlDispatcher就會返回到入口點函數,以便能夠執行任何與進程有關的清除工作並結束進程。如果還有服務在運行,哪怕只是一個服務,StartServiceCtrlDispatcher也會繼續循環下去,繼續等待其它的控制通知或者剩下的服務線程中止。
上面的內容是關於入口點函數的,下面的內容則是關於ServiceMain函數的。還記得以前講過的ServiceMain函數的的原型嗎?但實際上一個ServiceMain函數通常忽略傳遞給它的兩個參數,因爲服務一般不怎麼傳遞參數。設置一個服務最好的方法就是設置註冊表,一般服務在
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Service/ServiceName/Parameters子鍵下存放自己的設置,這裏的ServiceName是服務的名字。事實上,可能要寫一個客戶應用程序去進行服務的背景設置,這個客戶應用程序將這些信息存在註冊表中,以便服務讀取。當一個外部應用程序已經改變了某個正在運行中的服務的設置數據的時候,這個服務能夠用RegNotifyChangeKeyValue函數去接受一個通知,這樣就允許服務快速的重新設置自己。
前面講到StartServiceCtrlDispatcher爲每一個傳遞到它的數組中的非空元素產生一個新的線程。接下來,一個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. 爲什麼呢?因爲發出啓動服務請求之後,如果在一定時間之內無法完成服務的初始化,SCM會認爲服務的啓動已經失敗了,這個時間的長度在Win NT 4.0中是80秒
,Win2000中不詳…
基於上面的理由,ServiceMain要迅速完成自身工作,首先是必不可少的兩項工作,第一項是調用RegisterServiceCtrlHandler函數去通知SCM它的
CtrlHandler回調函數的地址:
<span style="font-size:12px;">SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler(
LPCTSTR lpServiceName, //服務的名字
LPHANDLER_FUNCTION lpHandlerProc //CtrlHandler函數地址
)</span>
第一個參數指明你正在建立的CtrlHandler是爲哪一個服務所用,第二個參數是CtrlHandler函數的地址。lpServiceName必須和在SERVICE_TABLE_ENTRY裏面被初始化的服務的名字相匹配。RegisterServiceCtrlHandler返回一個SERVICE_STATUS_HANDLE,這是一個32位的句柄。SCM用它來唯一確定這個服務。當這個服務需要把它當時的狀態報告給SCM的時候,就必須把這個句柄傳給需要它的Win32函數。注意:這個句柄和其他大多數的句柄不同,你無需關閉它。SCM要求ServiceMain函數的線程在一秒鐘內調用RegisterServiceCtrlHandler函數,否則SCM會認爲服務已經失敗。但在這種情況下,SCM不會終止服務,不過在NT 4中將無法啓動這個服務,同時會返回一個不正確的錯誤信息,這一點在Windows 2000中得到了修正。
在RegisterServiceCtrlHandler函數返回後,ServiceMain線程要立即告訴SCM服務正在繼續初始化。具體的方法是通過調用SetServiceStatus函數傳遞SERVICE_STATUS數據結構。
BOOL SetServiceStatus(
SERVICE_STATUS_HANDLE hService, //服務的句柄
SERVICE_STATUS lpServiceStatus //SERVICE_STATUS結構的地址
)
這個函數要求傳遞給它指明服務的句柄(剛剛通過調用RegisterServiceCtrlHandler得到),和一個初始化的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。除了這兩個標誌之外,如果你的服務需要和桌面發生交互(當然不推薦這樣做),就要用“OR”運算符附加上SERVICE_INTERACTIVE_PROCESS。這個成員的值在你的服務的生存期內絕對不應該改變。
成員dwCurrentState是這個結構中最重要的成員,它將告訴SCM你的服務的現行狀態。爲了報告服務仍在初始化,應該把這個成員設置成SERVICE_START_PENDING。在以後具體講述CtrlHandler函數的時候具體解釋其它可能的值。
成員dwControlsAccepted指明服務願意接受什麼樣的控制通知。如果你允許一個SCP去暫停/繼續服務,就把它設成SERVICE_ACCEPT_PAUSE_CONTINUE。很多服務不支持暫停或繼續,就必須自己決定在服務中它是否可用。如果你允許一個SCP去停止服務,就要設置它爲SERVICE_ACCEPT_STOP。如果服務要在操作系統關閉的時候得到通知,設置它爲SERVICE_ACCEPT_SHUTDOWN可以收到預期的結果。這些標誌可以用“OR”運算符組合。
成員dwWin32ExitCode和dwServiceSpecificExitCode是允許服務報告錯誤的關鍵,如果希望服務去報告一個Win32錯誤代碼(預定義在WinError.h中),它就設置dwWin32ExitCode爲需要的代碼。一個服務也可以報告它本身特有的、沒有映射到一個預定義的Win32錯誤代碼中的錯誤。爲了這一點,要把dwWin32ExitCode設置爲ERROR_SERVICE_SPECIFIC_ERROR,然後還要設置成員dwServiceSpecificExitCode爲服務特有的錯誤代碼。當服務運行正常,沒有錯誤可以報告的時候,就設置成員dwWin32ExitCode爲NO_ERROR。
最後的兩個成員dwCheckPoint和dwWaitHint是一個服務用來報告它當前的事件進展情況的。當成員dwCurrentState被設置成SERVICE_START_PENDING的時候,應該把dwCheckPoint設成0,dwWaitHint設成一個經過多次嘗試後確定比較合適的數,這樣服務才能高效運行。一旦服務被完全初始化,就應該重新初始化SERVICE_STATUS結構的成員,更改dwCurrentState爲SERVICE_RUNNING,然後把dwCheckPoint和dwWaitHint都改爲0。
dwCheckPoint成員的存在對用戶是有益的,它允許一個服務報告它處於進程的哪一步。每一次調用SetServiceStatus時,可以增加它到一個能指明服務已經執行到哪一步的數字,它可以幫助用戶決定多長時間報告一次服務的進展情況。如果決定要報告服務的初始化進程的每一步,就應該設置dwWaitHint爲你認爲到達下一步所需的毫秒數,而不是服務完成它的進程所需的毫秒數。
在服務的所有初始化都完成之後,服務調用SetServiceStatus指明SERVICE_RUNNING,在那一刻服務已經開始運行。通常一個服務是把自己放在一個循環之中來運行的。在循環的內部這個服務進程懸掛自己,等待指明它下一步是應該暫停、繼續或停止之類的網絡請求或通知。當一個請求到達的時候,服務線程激活並處理這個請求,然後再循環回去等待下一個請求/通知。
如果一個服務由於一個通知而激活,它會先處理這個通知,除非這個服務得到的是停止或關閉的通知。如果真的是停止或關閉的通知,服務線程將退出循環,執行必要的清除操作,然後從這個線程返回。當ServiceMain線程返回並中止時,引起在StartServiceCtrlDispatcher內睡眠的線程激活,並像在前面解釋過的那樣,減少它運行的服務的計數。
往後內容見:http://blog.csdn.net/plake/article/details/296886
windows 服務具體操作流程如下:(附實例代碼C++)
示例功能:其功能是查詢系統中可用物理內存數量,然後將結果寫入一個文本文件。(參考msdn文章“Creating a Simple Win32 Service in C++”)
由於用類對底層 Win32 函數調用進行了封裝,它不利於學習服務程序的基本知識。因此建議初學者使用c語言去實現。
第一步:主函數和全局定義
首先,包含所需的頭文件。例子要調用 Win32 函數(windows.h)和磁盤文件寫入(stdio.h):
接着,定義兩個常量:
#define SLEEP_TIME 5000 //指定兩次連續查詢可用內存之間的毫秒間隔。在第二步中編寫服務工作循環的時候要使用該常量。
#define LOGFILE "C:\\MyServices\\memstatus.txt"
LOGFILE 定義日誌文件的路徑,你將會用 WriteToLog 函數將內存查詢的結果輸出到該文件,WriteToLog 函數定義如下:
<span style="font-size:12px;">int WriteToLog(char* str)
{
FILE* log;
log = fopen(LOGFILE, "a+");
if (log == NULL)
return -1;
fprintf(log, "%s\n", str);
fclose(log);
return 0;
}</span>
聲明幾個全局變量,以便在程序的多個函數之間共享它們值。此外,做一個函數的前向定義:
<span style="font-size:12px;">SERVICE_STATUS ServiceStatus;
SERVICE_STATUS_HANDLE hStatus;
void ServiceMain(int argc, char** argv);
void ControlHandler(DWORD request);
int InitService();</span>
現在,準備工作已經就緒,你可以開始編碼了。服務程序控制臺程序的一個子集。因此,開始你可以定義一個 main 函數,它是程序的入口點。對於服務程序來說,main 的代碼及其簡單。因爲它只創建分派表並啓動控制分派機。
<span style="font-size:12px;">void main()
{
SERVICE_TABLE_ENTRY ServiceTable[2];
ServiceTable[0].lpServiceName = "MemoryStatus";
ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;
ServiceTable[1].lpServiceName = NULL;
ServiceTable[1].lpServiceProc = NULL;
// 啓動服務的控制分派機線程
StartServiceCtrlDispatcher(ServiceTable);
}</span>
一個程序可能包含若干個服務。每一個服務都必須列於專門的分派表中(爲此該程序定義了一個 ServiceTable 結構數組)。這個表中的每一項都要在 SERVICE_TABLE_ENTRY 結構之中。它有兩個域:
lpServiceName: 指向表示服務名稱字符串的指針;當定義了多個服務時,那麼這個域必須指定;
lpServiceProc: 指向服務主函數的指針(服務入口點);
分派表的最後一項必須是服務名和服務主函數域的 NULL 指針,文本例子程序中只宿主一個服務,所以服務名的定義是可選的。
服務控制管理器(SCM:Services Control Manager)是一個管理系統所有服務的進程。當 SCM 啓動某個服務時,它等待某個進程的主線程來調用 StartServiceCtrlDispatcher 函數。將分派表傳遞給 StartServiceCtrlDispatcher。這將把調用進程的主線程轉換爲控制分派器。該分派器啓動一個新線程,該線程運行分派表中每個服務的 ServiceMain 函數(本文例子中只有一個服務)分派器還監視程序中所有服務的執行情況。然後分派器將控制請求從 SCM
傳給服務。
注意:如果 StartServiceCtrlDispatcher 函數30秒沒有被調用,便會報錯,爲了避免這種情況,我們必須在 ServiceMain 函數中(參見本文例子)或在非主函數的單獨線程中初始化服務分派表。本文所描述的服務不需要防範這樣的情況。
分派表中所有的服務執行完之後(例如,用戶通過“服務”控制面板程序停止它們),或者發生錯誤時。StartServiceCtrlDispatcher 調用返回。然後主進程終止。
第二步:ServiceMain 函數
Listing 1 展示了 ServiceMain 的代碼。該函數是服務的入口點。它運行在一個單獨的線程當中,這個線程是由控制分派器創建的。ServiceMain 應該儘可能早地爲服務註冊控制處理器。這要通過調用 RegisterServiceCtrlHadler 函數來實現。
你要將兩個參數傳遞給此函數:服務名和指向 ControlHandlerfunction 的指針。它指示控制分派器調用 ControlHandler 函數處理 SCM 控制請求。註冊完控制處理器之後,獲得狀態句柄(hStatus)。通過調用 SetServiceStatus 函數,用 hStatus 向 SCM 報告服務的狀態。
Listing 1 展示瞭如何指定服務特徵和其當前狀態來初始化 ServiceStatus 結構,ServiceStatus 結構的每個域都有其用途:
dwServiceType:指示服務類型,創建 Win32 服務。賦值 SERVICE_WIN32;
dwCurrentState:指定服務的當前狀態。因爲服務的初始化在這裏沒有完成,所以這裏的狀態爲 SERVICE_START_PENDING;
dwControlsAccepted:這個域通知 SCM 服務接受哪個域。本文例子是允許 STOP 和 SHUTDOWN 請求。處理控制請求將在第三步討論;
dwWin32ExitCode 和 dwServiceSpecificExitCode:這兩個域在你終止服務並報告退出細節時很有用。初始化服務時並不退出,因此,它們的值爲 0;
dwCheckPoint 和 dwWaitHint:這兩個域表示初始化某個服務進程時要30秒以上。本文例子服務的初始化過程很短,所以這兩個域的值都爲 0。
調用 SetServiceStatus 函數向 SCM 報告服務的狀態時。要提供 hStatus 句柄和 ServiceStatus 結構。注意 ServiceStatus 一個全局變量,所以你可以跨多個函數使用它。ServiceMain 函數中,你給結構的幾個域賦值,它們在服務運行的整個過程中都保持不變,比如:dwServiceType。
在報告了服務狀態之後,你可以調用 InitService 函數來完成初始化。這個函數只是添加一個說明性字符串到日誌文件。如下面代碼所示:
<span style="font-size:12px;">// 服務初始化
int InitService()
{
int result;
result = WriteToLog("Monitoring started.");
return(result);
}
在 ServiceMain 中,檢查 InitService 函數的返回值。如果初始化有錯(因爲有可能寫日誌文件失敗),則將服務狀態置爲終止並退出 ServiceMain:
error = InitService();
if (error)
{
// 初始化失敗,終止服務
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
ServiceStatus.dwWin32ExitCode = -1;
SetServiceStatus(hStatus, &ServiceStatus);
// 退出 ServiceMain
return;
}</span>
如果初始化成功,則向 SCM 報告狀態:
// 向 SCM 報告運行狀態
ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus (hStatus, &ServiceStatus);
接着,啓動工作循環。每五秒鐘查詢一個可用物理內存並將結果寫入日誌文件。
如 Listing 1 所示,循環一直到服務的狀態爲 SERVICE_RUNNING 或日誌文件寫入出錯爲止。狀態可能在 ControlHandler 函數響應 SCM 控制請求時修改。
第三步:處理控制請求
在第二步中,你用 ServiceMain 函數註冊了控制處理器函數。控制處理器與處理各種 Windows 消息的窗口回調函數非常類似。它檢查 SCM 發送了什麼請求並採取相應行動。
每次你調用 SetServiceStatus 函數的時候,必須指定服務接收 STOP 和 SHUTDOWN 請求。Listing 2 示範瞭如何在 ControlHandler 函數中處理它們。
STOP 請求是 SCM 終止服務的時候發送的。例如,如果用戶在“服務”控制面板中手動終止服務。SHUTDOWN 請求是關閉機器時,由 SCM 發送給所有運行中服務的請求。兩種情況的處理方式相同:
寫日誌文件,監視停止;
向 SCM 報告 SERVICE_STOPPED 狀態;
由於 ServiceStatus 結構對於整個程序而言爲全局量,ServiceStatus 中的工作循環在當前狀態改變或服務終止後停止。其它的控制請求如:PAUSE 和 CONTINUE 在本文的例子沒有處理。
控制處理器函數必須報告服務狀態,即便 SCM 每次發送控制請求的時候狀態保持相同。因此,不管響應什麼請求,都要調用 SetServiceStatus。
第四步:安裝和配置服務
程序編好了,將之編譯成 exe 文件。本文例子創建的文件叫 MemoryStatus.exe,將它拷貝到 C:\MyServices 文件夾。爲了在機器上安裝這個服務,需要用 SC.EXE 可執行文件,它是 Win32
Platform SDK 中附帶的一個工具。(譯者注:Visaul Studio .NET 2003 IDE 環境中也有這個工具,具體存放位置在:C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\Tools\Bin\winnt)。使用這個實用工具可以安裝和移除服務。其它控制操作將通過服務控制面板來完成。以下是用命令行安裝 MemoryStatus 服務的方法:
sc create MemoryStatus binpath= c:\MyServices\MemoryStatus.exe
如果開啓服務失敗(錯誤代碼爲5:拒絕訪問),多爲無權限,可以嘗試修改以下註冊表的值:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\EnableLUA 值修改爲0
發出此創建命令。指定服務名和二進制文件的路徑(注意 binpath= 和路徑之間的那個空格)。安裝成功後,便可以用服務控制面板來控制這個服務。用控制面板的工具欄啓動和終止這個服務。
MemoryStatus 的啓動類型是手動,也就是說根據需要來啓動這個服務。右鍵單擊該服務,然後選擇上下文菜單中的“屬性”菜單項,此時顯示該服務的屬性窗口。在這裏可以修改啓動類型以及其它設置。你還可以從“常規”標籤中啓動/停止服務。
以下是從系統中移除服務的方法:
sc delete MemoryStatus
指定 “delete” 選項和服務名。此服務將被標記爲刪除,下次系統重啓後,該服務將被完全移除。
第五步:測試服務
從服務控制面板啓動 MemoryStatus 服務。如果初始化不出錯,表示啓動成功。過一會兒將服務停止。檢查一下 C:\MyServices 文件夾中 memstatus.txt 文件的服務輸出。在我的機器上輸出是這樣的:
Monitoring started.
273469440
273379328
273133568
273084416
Monitoring stopped.
爲了測試 MemoryStatus 服務在出錯情況下的行爲,可以將 memstatus.txt 文件設置成只讀。這樣一來,服務應該無法啓動。去掉只讀屬性,啓動服務,在將文件設成只讀。服務將停止執行,因爲此時日誌文件寫入失敗。如果你更新服務控制面板的內容,會發現服務狀態是已經停止。
以下是完整的服務代碼:
<span style="font-size:12px;">// NTService.cpp : Defines the entry point for the console application.
//
#include <windows.h>
#include <stdio.h>
#define SLEEP_TIME 5000
#define LOGFILE "C:/MyServices/memstatus.txt"
SERVICE_STATUS ServiceStatus;
SERVICE_STATUS_HANDLE hStatus;
void ServiceMain(int argc, char** argv);
void ControlHandler(DWORD request);
int InitService();
int WriteToLog(char* str);
int main(int argc, char* argv[])
{
SERVICE_TABLE_ENTRY ServiceTable[2];
ServiceTable[0].lpServiceName = "MemoryStatus";
ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;
ServiceTable[1].lpServiceName = NULL;
ServiceTable[1].lpServiceProc = NULL;
// 啓動服務的控制分派機線程
StartServiceCtrlDispatcher(ServiceTable);
}
void ServiceMain(int argc, char** argv)
{
int error;
ServiceStatus.dwServiceType =
SERVICE_WIN32;
ServiceStatus.dwCurrentState =
SERVICE_START_PENDING;
ServiceStatus.dwControlsAccepted =
SERVICE_ACCEPT_STOP |
SERVICE_ACCEPT_SHUTDOWN;
ServiceStatus.dwWin32ExitCode = 0;
ServiceStatus.dwServiceSpecificExitCode = 0;
ServiceStatus.dwCheckPoint = 0;
ServiceStatus.dwWaitHint = 0;
hStatus = RegisterServiceCtrlHandler(
"MemoryStatus",
(LPHANDLER_FUNCTION)ControlHandler);
if (hStatus == (SERVICE_STATUS_HANDLE)0)
{
// Registering Control Handler failed
return;
}
// Initialize Service
error = InitService();
if (!error)
{
// Initialization failed
ServiceStatus.dwCurrentState =
SERVICE_STOPPED;
ServiceStatus.dwWin32ExitCode = -1;
SetServiceStatus(hStatus, &ServiceStatus);
return;
}
// We report the running status to SCM.
ServiceStatus.dwCurrentState =
SERVICE_RUNNING;
SetServiceStatus (hStatus, &ServiceStatus);
MEMORYSTATUS memory;
// The worker loop of a service
while (ServiceStatus.dwCurrentState ==
SERVICE_RUNNING)
{
char buffer[16];
GlobalMemoryStatus(&memory);
sprintf(buffer, "%d", memory.dwAvailPhys);
int result = WriteToLog(buffer);
if (result)
{
ServiceStatus.dwCurrentState =
SERVICE_STOPPED;
ServiceStatus.dwWin32ExitCode = -1;
SetServiceStatus(hStatus,
&ServiceStatus);
return;
}
Sleep(SLEEP_TIME);
}
return;
}
void ControlHandler(DWORD request)
{
switch(request)
{
case SERVICE_CONTROL_STOP:
WriteToLog("Monitoring stopped.");
ServiceStatus.dwWin32ExitCode = 0;
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus (hStatus, &ServiceStatus);
return;
case SERVICE_CONTROL_SHUTDOWN:
WriteToLog("Monitoring stopped.");
ServiceStatus.dwWin32ExitCode = 0;
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus (hStatus, &ServiceStatus);
return;
default:
break;
}
// Report current status
SetServiceStatus (hStatus, &ServiceStatus);
return;
}
int WriteToLog(char* str)
{
FILE* log;
log = fopen(LOGFILE, "a+");
if (log == NULL)
return -1;
fprintf(log, "%s ", str);
fclose(log);
return 0;
}
int InitService(){
WriteToLog("Monitoring started.");
return true;
}</span>