環境:Windows 2003,VC 6.0
摘要:從建立一個COM服務程序入手,然後將一個MFC項目改造成服務程序,最後讓這一程序在啟動時可以顯示圖形界面。
關鍵字:windows服務程序 COM服務程序 開機前啟動 NT服務 與桌面交互
一、什麼是windows的服務程序?
可以使用下面的幾種方法看到它。
打開控制面板,然後是管理工具,裡面有一個“服務”,雙擊後打開;
或者是通過輸入命令的方式,打開開始菜單,點擊運行,輸入mmc services.msc(mmc可省略),也可打開;
我們會在打開的頁面中看到一個大的列表,標題欄上包含有名稱、描述、狀態、啟動類型、登錄身份等項。其中在狀態一欄中顯示為“已啟動”的是系統中已經啟動了的服務。我們先看一下服務的屬性。舉個例子,找到Print Spooler這一名稱,然後用右鍵在上面點擊,選擇“屬性”,可以看到它所執行的命令行是C:\WINDOWS\system32\spoolsv.exe,按下停止後,任務管理器中spoolsv.exe進程退出。我們所見到的這個列表就是服務程序的集中地,每一項就是一個服務程序。
上面這些標為自啟動的服務程序隨系統一起啟動。它與一些修改注冊表:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
項,及類似注冊表項的程序不同的是,即使用戶沒有登錄到系統中,它們也是會運行的,或者說它們在系統登錄前運行。
二、怎麼建立自己的服務程序?
每一個服務程序對應注冊表項HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services下的一個子項。因此我們可以通過增加注冊表項的方式增加服務程序。比如,我現在要增加一個test1服務程序,對應的可執行文件是c:\test1.exe。那麼我要增加如下注冊表項:
1.[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\]下增加子項test1;
2.test1下增加:
字符串型:"Description"="測試服務1"
字符串型:"DisplayName"="test1-displayname"
DWORD型:"ErrorControl"=dword:00000001
可擴充字符串值(即文件所在路徑):
"ImagePath"=hex(2):43,00,3a,00,5c,00,74,00,65,00,73,00,74,00,31,00,2e,00,65,00,\
78,00,65,00,00,00
字符串型:"ObjectName"="LocalSystem"
DWORD型,值為3表示是手動:"Start"=dword:00000003
DWORD型:"Type"=dword:00000020
3.test1下增加子項:
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\test1\Security]
裡面的鍵值從其他的服務程序注冊表值中復制。
如果test1這一程序只是一個普通的win32程序,那麼這樣做了之後還是不行,服務程序有它自己的一些結構特點。那麼怎麼編寫這些服務程序?
建立一個服務程序的最簡單的方法是用VC中的ATL COM向導。主菜單中選擇新建,然後選Projects中的ATL COM AppWizard,輸入一個項目名,選擇了所在目錄後,點OK按鈕,在出現的對話框中選擇Service(EXE),點Finish即可。然後編譯生成test1.exe。
運行test1.exe /regserver可以注冊程序為服務,test1.exe /unregserver是取消注冊。test1.exe運行時的參數是在:
Project->Settings->Debug->Program arguments中設置。
三、怎麼在建立的服務程序中加入自己的代碼?
我們看一下剛才生成的test1項目的結構。
我們看到test1有一個類CServiceModule和一些Globals的內容。Globals包括一個_tWinMain函數,也就是程序的入口,其中使用了FindOneOf這一與分析命令行有關的函數,還剩下一個全局變量_Module。
_tWinMain函數中,_Module初始化並設置m_bService為TRUE,在一些分析命令行和判斷是否為服務的代碼之後,使用_Module.Start()進入主要的執行部分。CServiceModule::Start()中,結構體SERVICE_TABLE_ENTRY建立了服務名與相應處理函數的映射。在這裡,如果m_bService為TRUE,則調用StartServiceCtrlDispatcher進入一種類似win32程序的消息處理的過程,用SERVICE_TABLE_ENTRY中的處理函數讓程序執行下去。如果m_bService不為TRUE,則直接執行Run()函數。
在SERVICE_TABLE_ENTRY中,我們看到服務處理函數為_ServiceMain,繼續跟蹤下去,發現是ServiceMain函數。在ServiceMain中又調用RegisterServiceCtrlHandler為服務增加了一個_Handler函數。對服務程序來說,我們可以在前面打開的服務列表中對它們進行“啟動”,“停止”,“暫停”,“恢復”等操作。這實際上是由_Handler來處理不同的信號。_Handler內部調用Handler,在Handler中,對傳入的dwOpcode參數作出處理。比如如果是SERVICE_CONTROL_STOP,也就是我們“停止”服務時,將使用PostThreadMessage對主線程發出一個退出的信號。回到ServiceMain函數,在裡面同樣是在調用Run()函數。也就是說程序以服務身份和非服務身份運行時,區別在於以服務身份運行時多了一個Handler函數,處理用戶對服務程序發出的一些信號。
需要注意的是,這個程序注冊為服務時並不是直接寫注冊表,而是在Install中使用了OpenSCManager,CreateService等函數來完成的任務。顯然,這比直接寫注冊表要好一些,因為有時候我們並不太清楚要怎麼去修改注冊表項的值來適應不同的服務程序配置,而這些函數有參數可以做到。
說到這裡,就涉及到我們自己編寫的代碼了。
比如現在我們已經建立了一個MFC的程序,想讓它成為一個服務程序,那要怎麼做呢?
我現在建立一個MFC EXE的項目mfc1,基於對話框。那麼把它變為一個服務程序的最簡單的方法就是把CServiceModule給拿過來使用。因為我們已經看到CServiceModule類已經把安裝服務,卸載服務,運行服務這些操作封裝得很好。
打開test1的stdafx.h文件,復制CServiceModule的聲明及相關頭文件和變量到mfc1的stdafx.h中。
然後是把test1的test1.cpp中對CServiceModule類的實現,復制到mfc1中的mfc1.cpp中。
在stdafx.h中CServiceModule類聲明前加上#include <winsvc.h>,它裡面是對結構體SERVICE_STATUS_HANDLE的聲明。
編譯後出現以下類似錯誤:
D:\vc6_test\mfc1\mfc1.cpp(52) : error C2065: ''IDR_Test1'' : undeclared identifier
D:\vc6_test\mfc1\mfc1.cpp(336) : error C2065: ''CoInitializeSecurity'' : undeclared identifier
D:\vc6_test\mfc1\mfc1.cpp(337) : error C2065: ''EOAC_NONE'' : undeclared identifier
D:\vc6_test\mfc1\mfc1.cpp(362) : error C2065: ''IDS_SERVICENAME'' : undeclared identifier
D:\vc6_test\mfc1\mfc1.cpp(362) : error C2065: ''LIBID_TEST1Lib'' : undeclared identifier
我們可以在test1中找到IDR_Test1的聲明,放到mfc1中,解決第一條錯誤。但我們也可以去掉CServiceModule中與COM有關的一些代碼。這裡我們刪除RegisterServer,UnregisterServer兩個函數,並讓Run函數成為
void CServiceModule::Run()
{
_Module.dwThreadID = GetCurrentThreadId();
LogEvent(_T("Service started"));
if (m_bService)
SetServiceStatus(SERVICE_RUNNING);
MSG msg;
while (GetMessage(&msg, 0, 0, 0))
DispatchMessage(&msg);
}
增加資源IDS_SERVICENAME為“mfc1”。
注釋掉CServiceModule::Init中“CComModule::Init(p, h, plibid);”一行。
注釋_tWinMain函數(技巧:用#if 0和#endif注釋)。
現在編譯程序,應該沒有錯誤了,但加入的CServiceModule還沒有起到作用。
在mfc1中的IDD_MFC1_DIALOG上加入兩個按鈕,分別是“安裝服務”,“卸載服務”。增加的單擊事件代碼為:
“安裝服務”按鈕:void CMfc1Dlg::OnButton1() { _Module.Install(); }
“卸載服務”按鈕:void CMfc1Dlg::OnButton2() { _Module.Uninstall(); }
下面在CMfc1App::InitInstance()中加入一些代碼:
_Module.Init(ObjectMap, this->m_hInstance, IDS_SERVICENAME, NULL);
_Module.m_bService = TRUE;
_Module.Start();
地點是在原來產生對話框的代碼的地方。而原有的生成對話框的代碼轉移到Run()中,位置是在使用了SetServiceStatus函數設置服務狀態之後,並注釋掉其後的消息處理代碼,因對話框自身有消息處理機制。
編譯時若出現如下錯誤,將Install()和Uninstall()前的inline參數去掉即可:
mfc1Dlg.obj : error LNK2001: unresolved external symbol "public: int __thiscall CServiceModule::Install(void)" (?Install@CServiceModule@@QAEHXZ)
mfc1Dlg.obj : error LNK2001: unresolved external symbol "public: int __thiscall CServiceModule::Uninstall(void)" (?Uninstall@CServiceModule@@QAEHXZ)
現在可以編譯運行了。然後點擊“安裝服務”,就可以在服務列表中看到mfc1了。
四、這一服務程序運行時沒有圖形界面?
不錯,剛才直接運行mfc1.exe時我們看到了圖形界面,但在服務列表中用右鍵菜單中的“啟動”時卻看不到任何界面。這該怎麼辦?
我們還需要在使用CreateService函數時(Install()中),加上一個參數,這樣才能允許程序與桌面交互,也就是可以顯示界面。這個參數是SERVICE_INTERACTIVE_PROCESS。
填加後的CreateService:
SC_HANDLE hService = ::CreateService(
hSCM, m_szServiceName, m_szServiceName,
SERVICE_ALL_ACCESS,
SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS,
SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
szFilePath, NULL, NULL, _T("RPCSS\0"), NULL, NULL);
再次編譯mfc1,卸載服務後,安裝服務。我們可以看到,通過服務列表啟動mfc1,原有的對話框出現了。
如需將服務設為自動啟動,則將 SERVICE_DEMAND_START 改為 SERVICE_AUTO_START。