每個作業系統都需要有在後台執行任務的方法,無論是誰正在使用這部機器,這些任務都可以繼續運行,背景工作可以處理各種重要的服務,包括系統的或者使用者的。例如,一個信差服務可以監控網路,並且在接收到另一台機子的資訊時,可以顯示一個對話方塊。一個發送和接收傳真的應用需要在啟動的時候運行,並且不斷地監控負責傳真的modem,看有沒有傳真進來。一個家庭的或者辦公室的安全程式,用來控制一件檢測裝置時,它需要不時地查詢感應器,並且在適當的時候響應它。所有這些任務都需要CPU時間來執行它們,不過由於它們需要的CPU時間很少,因此可以放在後台而不影響使用者使用系統。
在MS-DOS中,背景任務是通過TSR(Terminate and Stay Resident)程式來處理的。這些程式經由autoexec.bat檔案開始。在UNIX中,背景工作是通過Daemons來處理的。在每次啟動UNIX的過程中,你都可以看到作業系統啟動一些任務,例如定時的程式(Cron)和Finger的daemons,然後才可以讓首個使用者登入。在Windows NT中,背景任務被稱為服務。服務可在每次NT啟動的時候運行,並且不管是誰登陸,都會一直運行下去。
Windows NT的服務都是通過一般的可執行程式實現的,不同的是,它遵循內部的一個特定協議來設計,以便它們能夠與服務控制管理員(SCM,Service Control Manager)進行正確的互動。在這篇文章中,你將學習到如何在Windows NT中建立和安裝簡單的Win32服務。一旦你懂得了這個簡單的服務,你要建立自己的服務也不難了,因為所有的服務,不論是如何地複雜,都必須包含有同樣基本的SCM介面代碼。只要符合SCM的要求,其實為服務設計的可執行檔和一般的程式並沒有多少的區別。
無論是對於編程者或者系統管理員,瞭解NT的服務如何工作都是很重要的。編程者就不必說了,因為他們要建立自己的服務,而對於系統管理員,也是同樣重要的。因為背景任務可以是很危險的。MS-DOS和Macintosh系統都是一個病毒的溫床,因為它們在安全性方面先天不足,它們都可以允許任何人或者程式在任何時間建立背景任務。Windows NT和UNIX系統是較安全的,因為只有系統管理員才可以為系統增加背景任務,不過,如果系統管理員加入了一個破壞性的背景程式,就它就可以為所欲為了。因此系統管理員要瞭解Windows NT服務的技巧和使用權限設定,就可以避免加入有潛在危險的背景工作。
基本的概念
服務有兩種不同的形式。磁碟機服務使用磁碟機協議,讓NT可以與特定的硬體進行通訊。另一個是Win32服務,通過一般的Win32 API來實現背景工作。這篇文章的重點是談Win32服務,因為它們更為常見,而且建立起來也很容易。任何的NT編程者通過使用一般的NT SDK(或者Visual C++),並且可以用管理員的身份訪問一台NT機器,都可以實現和安裝自己的Win32服務。如果你想建立一些在Windows NT啟動時就啟動並執行程式,並且要求它會在系統中一直運行,你就要使用Win32的服務。
在NT中,服務通過控制台進行管理。在控制台中,你會發現有一個服務的表徵圖,開啟它你會看到所有Win32服務的清單。在那裡你可以開始、停止、暫停和繼續某個服務。你按下其中的啟動按鈕後,就會出現一個對話方塊,你可以修改啟動操作以及服務使用的預設帳號。一個服務可以在系統啟動的時候自動運行,也可以被完全禁止。或者設定為手動執行。在手動的時候,使用者還可以設定啟動的參數。要對服務中的項目作修改的話,你需要以一個管理員或者超級使用者的身份登入。
Windows NT內建有一些預裝的任務,用來處理諸如網路信差服務的操作或者使用“at”命令定時執行的操作,以及分布的RPC命名。在你建立自己的服務時,你必須執行一個獨立的安裝步驟,以將服務的資訊插入到服務管理工具的列表中,這些資訊包括有新服務的名字、執行檔案的名字和啟動的類型等,都會寫入到註冊表中,這樣在機器下次啟動的時候,SCM就會得到新服務的相關資訊
建立一個新的服務
執行服務的程式也是一個EXE檔案,不過它必須符合某些特定的規範,以便可以與SCM進行正確的互動。微軟很細緻地設計了函數調用的流程,你必須遵循這些流程,否則你的服務就不能工作。具體的規定如下所列。你可以在Win32編程者的參考指南中找到以下涉及的函數,這些資料在SDK的Win32線上協助或者Visual C++都有:
.服務的代碼必須要有一個一般的main或者WinMain函數。這個函數應該會馬上調用StartServiceCrtlDispatcher函數。通過調用這個函數,你可以讓SCM得到ServiceMain函數的指標,這樣在SCM要啟動該服務時,就可以調用它
.在SCM要啟動服務的時候,就會調用ServiceMain函數。例如,如果管理員在服務管理員中按下啟動的按鈕,SCM就會在一個獨立的線程中執行ServiceMain函數。ServiceMain應該調用RegisterServiceCtrlHandler函數,這樣可以註冊一個Handler函數,以便SCM對服務進行控制。Handler函數的名字可以是任意的,不過它會在Handler下的文檔中列出來。RegisterServiceCtrlHandler函數會返回一個控制代碼,在服務需要發送狀態資訊給SCM時,可以通過該控制代碼進行。
.ServiceMain函數也必須啟動做該服務實際工作的線程。在服務停止前,ServiceMain函數是不應該有返回的。當它返回的時候,服務已經停止了。
.Handler函數包含了一個switch語句,用來分析由SCM傳送過來的請求。預設的情況,SCM可以發送以下任何的的控制常數:
SERVICE_CONTROL_STOP - 要服務停止
SERVICE_CONTROL_PAUSE - 要服務暫停
SERVICE_CONTROL_CONTINUE - 要服務繼續
SERVICE_CONTROL_INTERROGATE - 要服務馬上報告它的狀態
SERVICE_CONTROL_SHUTDOWN - 告訴服務即將關機
也可以建立自訂的常數(值在128到255之間),並且通過SCM發送給服務。
如果你建立的EXE包括有以上提到的main、ServiceMain和Handler函數,以及執行服務自身任務的線程函數,那麼你的服務程式設計就完成了。以下的圖總結了這些不同的函數和SCM之間的互動:
****************圖一***********************
在本文的最後還有幾段程式的列表,其中列表一為我們展示了一個可能是最簡單的服務。該服務只發出"嘟"的響聲。預設的狀態下,它每兩秒響一次。你可以通過啟動的參數來修改發聲的間隔。這個服務挺完整,它可以正確響應SCM傳來的每個控制訊號。因此,這個程式可作為你建立自己服務的一個很好的模板。
main函數通過調用StartServiceCtrlDispatcher來註冊ServiceMain函數。註冊的操作使用了一個SERVICE_TABLE_ENTRY結構的數組。在這個例子中,該程式只包含了一個服務,因此在表中只會有一個項目。不過,對於一個EXE檔案,可以建立幾個任務,這樣在表中就會有幾項,以識別不同的ServiceMain函數。在調用StartServiceCtrlDispatcher之前,可在main函數中放入初始化的代碼,不過這些代碼必須在少於30秒內完成,否則,SCM會認為某些地方出錯而終止服務。
在自動或者手動啟動服務時,將會調用ServiceMain函數。ServiceMain函數將包含有以下的步驟:
1.它馬上調用RegisterServiceCtrlHandler來註冊Handler函數,作為SCM控制該服務的Handler函數
2.然後它將調用SendStatusToSCM函數,將當前的進程通報給SCM。第四個參數是一個“click count”值,在程式每次更新狀態時,它的值就會增加。SCM和其它的程式可以根據click count的值來知道初始化期間進行的處理。最後的參數是"wait hint",是用來告訴SCM在click count下次更新前,它需要等待的時間(以毫秒計算)。
3.ServiceMain接著會建立一個事情,該事件在函數的底部使用,可讓它一直運行,直到SCM發出一個STOP的請求。
4.接著,ServiceMain檢查啟動的參數。參數可在使用者手動啟動服務時,通過服務管理工具中的啟動參數傳送過來。這些參數以一個argv形式的數組進入ServiceMain函數。
5.如果你的服務需要處理其它初始化的任務,它們應該放在這一步,在調用InitService之前。
6.ServiceMain函數接著調用InitService函數,這將啟動線程並做服務的真正工作。如果該調用成功,SverviceMain將會通知SCM服務已經成功啟動。
7.ServiceMain將調用WaitForSingleObject,用來等待terminateEvent事件對象被設定。這個對象通過Handler函數設定,一旦它被設定,ServiceMain將調用終止的函數來做清除的工作,然後就返回並停止服務。
你從上面可以看到,在這個函數中並沒有多少靈活的地方。除了第5步外,你必須按步執行以上提到的任務,否則服務將不可以正確啟動。
終止函數清除所有開啟的控制代碼,並且發送一個狀態的資訊給SCM,告訴它服務現已停止。
在SCM要暫停、繼續、詢問或者停止服務時,它就會調用Handler函數。要停止服務,Handler設定terminateEvent,這樣做會導致ServiceMain(它會以一個獨立線程的形式執行)終止並且返回。一旦ServiceMain返回,服務就停止了。
SendStatusToSCM 函數負責發送服務的目前狀態給SCM。
在需要啟動服務線程時,ServiceMain就會調用InitService函數。該函數調用CreateThread來為服務建立一個新的線程。
ServiceThread函數包含有該服務真正要做的工作。在這個例子中,該線程包含有一個無限的迴圈,以一個預定義的時間間隔發出響聲。在你建立自己的服務時,你可以在該線程中放入任何的代碼,調用Win32函數或者你自己的函數。
安裝和移除服務
為了使用上面提到的發響聲服務,你必須安裝它。安裝可讓SCM知道這個服務,並且讓SCM將它加入到控制台中的服務列表中。表單二的代碼展示了如何安裝一個服務。
表單2先通過OpenSCManager函數開啟一個到SCM的串連。在OpenSCManager的調用中,你必須指定你要做的東西,以便SCM可以驗證這個行為。如果你登入的帳號沒有足夠的許可權,該調用將返回NULL。
CreateService的調用是真正用來裝入新服務的。它使用OpenSCManager返回至SCM的指標、名字、標籤和命令列中的EXE檔案,還有一些標準的參數值。使用SERVICE_WIN32_OWN_PROCESS表明該服務的EXE檔案只包含有一個服務,而SERVICE_DEMAND_START則表明該服務是手動而不是自動啟動的。使用命令列安裝程式的一個典型格式如下:
install BeepService "Beeper" c:/winnt/beep.exe
第一個參數是SCM內部使用的服務名字。這個名字也用在以後移除服務。第二個參數是一個標識符,即服務管理中顯示的名字。第三個參數指出該服務的執行檔案的路徑。在你安裝服務後,可在控制台的服務管理中啟動它。如果有錯,可在Win32 API的線上文檔中查出錯誤碼的含義。
要移除服務,你要按列表3的步驟進行。它首先開啟一個到SCM的串連,然後使用OpenService函數開啟一個與服務的串連。列表3中接著查詢服務來看它是否現已停止了。如果不是的話,就會停止它。DeleteService 用來從控制台中移除服務,移除操作的典型格式如下:
remove BeepService
需要的話,你也可以在馬上重新安裝服務。
結論
服務對於Windows NT是很重要的一部分,因為它可讓你對作業系統進行擴充。使用列表1的代碼作為一個模板,你將會發現要建立一個自己的新服務是非常簡單的。
列表1
實現可能是最簡單NT服務的代碼
//***************************************************************
// From the book "Win32 System Services: The Heart of Windows NT"
// by Marshall Brain
// Published by Prentice Hall
file://
// This code implements the simplest possible service.
// It beeps every 2 seconds, or at a user specified interval.
file://***************************************************************
// beepserv.cpp
#include
#include
#include
#include
#define DEFAULT_BEEP_DELAY 2000
// Global variables
// The name of the service
char *SERVICE_NAME = "BeepService";
// Event used to hold ServiceMain from completing
HANDLE terminateEvent = NULL;
// Handle used to communicate status info with
// the SCM. Created by RegisterServiceCtrlHandler
SERVICE_STATUS_HANDLE serviceStatusHandle;
// The beep interval in ms.
int beepDelay = DEFAULT_BEEP_DELAY;
// Flags holding current state of service
BOOL pauseService = FALSE;
BOOL runningService = FALSE;
// Thread for the actual work
HANDLE threadHandle = 0;
void ErrorHandler(char *s, DWORD err)
{
cout << s << endl;
cout << "Error number: " << err << endl;
ExitProcess(err);
}
// This function consolidates the activities of
// updating the service status with
// SetServiceStatus
BOOL SendStatusToSCM (DWORD dwCurrentState,
DWORD dwWin32ExitCode,
DWORD dwServiceSpecificExitCode,
DWORD dwCheckPoint,
DWORD dwWaitHint)
{
BOOL success;
SERVICE_STATUS serviceStatus;
// Fill in all of the SERVICE_STATUS fields
serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
serviceStatus.dwCurrentState = dwCurrentState;
// If in the process of doing something, then accept
// no control events, else accept anything
if (dwCurrentState == SERVICE_START_PENDING)
serviceStatus.dwControlsAccepted = 0;
else
serviceStatus.dwControlsAccepted =
SERVICE_ACCEPT_STOP |
SERVICE_ACCEPT_PAUSE_CONTINUE |
SERVICE_ACCEPT_SHUTDOWN;
// if a specific exit code is defined, set up
// the win32 exit code properly
if (dwServiceSpecificExitCode == 0)
serviceStatus.dwWin32ExitCode = dwWin32ExitCode;
else
serviceStatus.dwWin32ExitCode =
ERROR_SERVICE_SPECIFIC_ERROR;
serviceStatus.dwServiceSpecificExitCode =
dwServiceSpecificExitCode;
serviceStatus.dwCheckPoint = dwCheckPoint;
serviceStatus.dwWaitHint = dwWaitHint;
// Pass the status record to the SCM
success = SetServiceStatus (serviceStatusHandle,
&serviceStatus);
return success;
}
DWORD ServiceThread(LPDWORD param)
{
while (1)
{
Beep(200,200);
Sleep(beepDelay);
}
return 0;
}
// Initializes the service by starting its thread
BOOL InitService()
{
DWORD id;
// Start the service's thread
threadHandle = CreateThread(0, 0,
(LPTHREAD_START_ROUTINE) ServiceThread,
0, 0, &id);
if (threadHandle==0)
return FALSE;
else
{
runningService = TRUE;
return TRUE;
}
}
// Dispatches events received from the SCM
VOID Handler (DWORD controlCode)
{
DWORD currentState = 0;
BOOL success;
switch(controlCode)
{
// There is no START option because
// ServiceMain gets called on a start
// Stop the service
case SERVICE_CONTROL_STOP:
// Tell the SCM what's happening
success = SendStatusToSCM(SERVICE_STOP_PENDING,
NO_ERROR, 0, 1, 5000);
runningService=FALSE;
// Set the event that is holding ServiceMain
// so that ServiceMain can return
SetEvent(terminateEvent);
return;
// Pause the service
case SERVICE_CONTROL_PAUSE:
if (runningService && !pauseService)
{
// Tell the SCM what's happening
success = SendStatusToSCM(
SERVICE_PAUSE_PENDING,
NO_ERROR, 0, 1, 1000);
pauseService = TRUE;
SuspendThread(threadHandle);
currentState = SERVICE_PAUSED;
}
break;
// Resume from a pause
case SERVICE_CONTROL_CONTINUE:
if (runningService && pauseService)
{
// Tell the SCM what's happening
success = SendStatusToSCM(
SERVICE_CONTINUE_PENDING,
NO_ERROR, 0, 1, 1000);
pauseService=FALSE;
ResumeThread(threadHandle);
currentState = SERVICE_RUNNING;
}
break;
// Update current status
case SERVICE_CONTROL_INTERROGATE:
// it will fall to bottom and send status
break;
// Do nothing in a shutdown. Could do cleanup
// here but it must be very quick.
case SERVICE_CONTROL_SHUTDOWN:
return;
default:
break;
}
SendStatusToSCM(currentState, NO_ERROR, 0, 0, 0);
}
// Handle an error from ServiceMain by cleaning up
// and telling SCM that the service didn't start.
VOID terminate(DWORD error)
{
// if terminateEvent has been created, close it.
if (terminateEvent) CloseHandle(terminateEvent);
// Send a message to the scm to tell about stopage
if (serviceStatusHandle)
SendStatusToSCM(SERVICE_STOPPED, error,
0, 0, 0);
// If the thread has started, kill it off
if (threadHandle) CloseHandle(threadHandle);
// Do not need to close serviceStatusHandle
}
// ServiceMain is called when the SCM wants to
// start the service. When it returns, the service
// has stopped. It therefore waits on an event
// just before the end of the function, and
// that event gets set when it is time to stop.
// It also returns on any error because the
// service cannot start if there is an eror.
VOID ServiceMain(DWORD argc, LPTSTR *argv)
{
BOOL success;
// immediately call Registration function
serviceStatusHandle =
RegisterServiceCtrlHandler(
SERVICE_NAME, (LPHANDLER_FUNCTION)Handler);
if (!serviceStatusHandle) {terminate(GetLastError()); return;}
// Notify SCM of progress
success = SendStatusToSCM(SERVICE_START_PENDING,
NO_ERROR, 0, 1, 5000);
if (!success) {terminate(GetLastError()); return;}
// create the termination event
terminateEvent = CreateEvent (0, TRUE, FALSE, 0);
if (!terminateEvent) {terminate(GetLastError()); return;}
// Notify SCM of progress
success = SendStatusToSCM(SERVICE_START_PENDING,
NO_ERROR, 0, 2, 1000);
if (!success) {terminate(GetLastError()); return;}
// Check for startup params
if (argc == 2)
{
int temp = atoi(argv[1]);
if (temp < 1000)
beepDelay = DEFAULT_BEEP_DELAY;
else
beepDelay = temp;
}
// Notify SCM of progress
success = SendStatusToSCM(SERVICE_START_PENDING,
NO_ERROR, 0, 3, 5000);
if (!success) {terminate(GetLastError()); return;}
// Start the service itself
success = InitService();
if (!success) {terminate(GetLastError()); return;}
// The service is now running.
// Notify SCM of progress
success = SendStatusToSCM(SERVICE_RUNNING,
NO_ERROR, 0, 0, 0);
if (!success) {terminate(GetLastError()); return;}
// Wait for stop signal, and then terminate
WaitForSingleObject (terminateEvent, INFINITE);
terminate(0);
}
VOID main(VOID)
{
SERVICE_TABLE_ENTRY serviceTable[] =
{
{ SERVICE_NAME,
(LPSERVICE_MAIN_FUNCTION) ServiceMain},
{ NULL, NULL }
};
BOOL success;
// Register with the SCM
success =
StartServiceCtrlDispatcher(serviceTable);
if (!success)
ErrorHandler("In StartServiceCtrlDispatcher",
GetLastError());
}
列表2
安裝NT服務的代碼
file://***************************************************************
// From the book "Win32 System Services: The Heart of Windows NT"
// by Marshall Brain
// Published by Prentice Hall
file://
// This code installs a service.
file://***************************************************************
// install.cpp
#include
#include
void ErrorHandler(char *s, DWORD err)
{
cout << s << endl;
cout << "Error number: " << err << endl;
ExitProcess(err);
}
void main(int argc, char *argv[])
{
SC_HANDLE newService, scm;
if (argc != 4)
{
cout << "Usage:/n";
cout << " install service_name /
service_label executable/n";
cout << " service_name is the /
name used internally by the SCM/n";
cout << " service_label is the /
name that appears in the Services applet/n";
cout << " (for multiple /
words, put them in double quotes)/n";
cout << " executable is the /
full path to the EXE/n/n";
return;
}
// open a connection to the SCM
scm = OpenSCManager(0, 0, SC_MANAGER_CREATE_SERVICE);
if (!scm) ErrorHandler("In OpenScManager",
GetLastError());
// Install the new service
newService = CreateService(
scm, argv[1], // eg "beep_srv"
argv[2], // eg "Beep Service"
SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
argv[3], // eg "c:/winnt/xxx.exe"
0, 0, 0, 0, 0);
if (!newService) ErrorHandler("In CreateService",
GetLastError());
else cout << "Service installed/n";
// clean up
CloseServiceHandle(newService);
CloseServiceHandle(scm);
}
列表3
移除NT服務的代碼
file://***************************************************************
// From the book "Win32 System Services: The Heart of Windows NT"
// by Marshall Brain
// Published by Prentice Hall
file://
// This code removes a service from the Services applet in the
// Control Panel.
file://***************************************************************
// remove.cpp
#include
#include
void ErrorHandler(char *s, DWORD err)
{
cout << s << endl;
cout << "Error number: " << err << endl;
ExitProcess(err);
}
void main(int argc, char *argv[])
{
SC_HANDLE service, scm;
BOOL success;
SERVICE_STATUS status;
if (argc != 2)
{
cout << "Usage:/n remove service_name/n";
return;
}
// Open a connection to the SCM
scm = OpenSCManager(0, 0, SC_MANAGER_CREATE_SERVICE);
if (!scm) ErrorHandler("In OpenScManager",
GetLastError());
// Get the service's handle
service = OpenService(scm, argv[1],
SERVICE_ALL_ACCESS | DELETE);
if (!service) ErrorHandler("In OpenService",
GetLastError());
// Stop the service if necessary
success = QueryServiceStatus(service, &status);
if (!success) ErrorHandler("In QueryServiceStatus",
GetLastError());
if (status.dwCurrentState != SERVICE_STOPPED)
{
cout << "Stopping service.../n";
success = ControlService(service,
SERVICE_CONTROL_STOP, &status);
if (!success) ErrorHandler("In ControlService",
GetLastError());
}
// Remove the service
success = DeleteService(service);
if (success) cout << "Service removed/n";
else ErrorHandler("In DeleteService",
GetLastError());
// Clean up
CloseServiceHandle(service);
CloseServiceHandle(scm);
}
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=665877