Windows服務Debug版本
注冊
Services.exe -regserver
卸載
Services.exe -unregserver
Windows服務Release版本
注冊
Services.exe -service
卸載
Services.exe -unregserver
原理
Windows服務的Debug、Release版本的注冊和卸載方式均已明確。但是為什么要這么做呢。
最初我在第一次編寫Windows服務的程序時,並不清楚Windows服務的注冊方式。於是從谷歌搜索后得知,原來是這樣注冊的。
當按照谷歌提供的注冊方式注冊后,我就在想,這些注冊方式是不是Windows操作系統所支持的。后來一想不對,這明明是通過執行編寫的Windows服務程序+命令行參數的方式。
既然是命令行的方式,那么就是說編寫的Services程序,是支持 –regserver、-service 這些命令行參數的。
通過VS模板生成Windows服務項目后,並未寫一句代碼,那么它是如何支持這些命令行的呢,我決定一探究竟。
模板生成后的Windows服務項目概覽
VS2012下生成的Windows服務項目
其中主代碼文件為Services.cpp,“生成的文件”文件夾中的文件為COM模型編譯時生成的文件。
由此圖可見,程序的命令行解析應該就在Services.cpp文件中。
下面是Services.cpp文件的代碼
// Services.cpp : WinMain 的實現 #include "stdafx.h" #include "resource.h" #include "Services_i.h" using namespace ATL; #include <stdio.h> class CServicesModule : public ATL::CAtlServiceModuleT< CServicesModule, IDS_SERVICENAME > { public : DECLARE_LIBID(LIBID_ServicesLib) DECLARE_REGISTRY_APPID_RESOURCEID(IDR_SERVICES, "{0794CF96-5CC5-432E-8C1D-52B980ACBE0F}") HRESULT InitializeSecurity() throw() { // TODO : 調用 CoInitializeSecurity 並為服務提供適當的安全設置 // 建議 - PKT 級別的身份驗證、 // RPC_C_IMP_LEVEL_IDENTIFY 的模擬級別 // 以及適當的非 NULL 安全描述符。 return S_OK; } }; CServicesModule _AtlModule; // extern "C" int WINAPI _tWinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, LPTSTR /*lpCmdLine*/, int nShowCmd) { return _AtlModule.WinMain(nShowCmd); }
只有40行左右的代碼,那么命令行解析在哪里,針對不同的命令,又是做了什么操作?至少在這里我是得不到答案了。
既然程序能正確執行,那么我只要從程序的入口點跟蹤就行了。
Windows程序的四個入口函數是
WinMain //Win32程序 wWinMain //Unicode版本Win32程序 Main //控制台程序 Wmain //Unicode版本控制台程序
編譯后生成的Servers.exe明顯不是控制台程序,再結合代碼來看,那么服務程序的入口點就定位到了這里
extern "C" int WINAPI _tWinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, LPTSTR /*lpCmdLine*/, int nShowCmd) { return _AtlModule.WinMain(nShowCmd); }
_tWinMain函數中直接調用了 _AtlModule.WinMain方法。
那么_AtlModule又是什么呢?
於是我看到了
class CServicesModule : public ATL::CAtlServiceModuleT< CServicesModule, IDS_SERVICENAME > CServicesModule _AtlModule;
_AtlModule是CServicesModule類的一個實例,而CServicesModule類中沒有實現WinMain方法,實際上就是調用的父類public ATL::CAtlServiceModuleT< CServicesModule, IDS_SERVICENAME >的WinMain方法。
CAtlServiceModuleT類詳解
下面來看一下CAtlServiceModuleT的WinMain方法
int WinMain(_In_ int nShowCmd) throw() { if (CAtlBaseModule::m_bInitFailed) { ATLASSERT(0); return -1; } T* pT = static_cast<T*>(this); HRESULT hr = S_OK; LPTSTR lpCmdLine = GetCommandLine(); if (pT->ParseCommandLine(lpCmdLine, &hr) == true) hr = pT->Start(nShowCmd); return hr; }
可以看到方法中通過調用GetCommandLine方法取得當前程序的命令行,然后通過調用ParseCommandLine方法進行命令行的解析。
// Parses the command line and registers/unregisters the rgs file if necessary bool ParseCommandLine( _In_z_ LPCTSTR lpCmdLine, _Out_ HRESULT* pnRetCode) throw() { if (!CAtlExeModuleT<T>::ParseCommandLine(lpCmdLine, pnRetCode)) return false; TCHAR szTokens[] = _T("-/"); *pnRetCode = S_OK; T* pT = static_cast<T*>(this); LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens); while (lpszToken != NULL) { if (WordCmpI(lpszToken, _T("Service"))==0) { *pnRetCode = pT->RegisterAppId(true); if (SUCCEEDED(*pnRetCode)) *pnRetCode = pT->RegisterServer(TRUE); return false; } lpszToken = FindOneOf(lpszToken, szTokens); } return true; }
從代碼中可以看出首先調用父類CAtlExeModuleT的ParseCommandLine方法,那么CAtlExeModule中又做了些神馬呢。
bool ParseCommandLine( _In_z_ LPCTSTR lpCmdLine, _Out_ HRESULT* pnRetCode) throw() { *pnRetCode = S_OK; TCHAR szTokens[] = _T("-/"); T* pT = static_cast<T*>(this); LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens); while (lpszToken != NULL) { if (WordCmpI(lpszToken, _T("UnregServer"))==0) { *pnRetCode = pT->UnregisterServer(TRUE); if (SUCCEEDED(*pnRetCode)) *pnRetCode = pT->UnregisterAppId(); return false; } if (WordCmpI(lpszToken, _T("RegServer"))==0) { *pnRetCode = pT->RegisterAppId(); if (SUCCEEDED(*pnRetCode)) *pnRetCode = pT->RegisterServer(TRUE); return false; } if (WordCmpI(lpszToken, _T("UnregServerPerUser"))==0) { *pnRetCode = AtlSetPerUserRegistration(true); if (FAILED(*pnRetCode)) { return false; } *pnRetCode = pT->UnregisterServer(TRUE); if (SUCCEEDED(*pnRetCode)) *pnRetCode = pT->UnregisterAppId(); return false; } if (WordCmpI(lpszToken, _T("RegServerPerUser"))==0) { *pnRetCode = AtlSetPerUserRegistration(true); if (FAILED(*pnRetCode)) { return false; } *pnRetCode = pT->RegisterAppId(); if (SUCCEEDED(*pnRetCode)) *pnRetCode = pT->RegisterServer(TRUE); return false; } lpszToken = FindOneOf(lpszToken, szTokens); } return true; }
從代碼中可以找到,程序一共對四個參數進行了解析和執行,分別是UnregServer、RegServer、UnregServerPerUser、RegServerPerUser。由WordCmpI可知,參數是大小寫無關的。當執行某個參數后,會返回false,當參數不是這四個其中之一時,方法的返回值是true。
由之前看到的子類方法中
if (!CAtlExeModuleT<T>::ParseCommandLine(lpCmdLine, pnRetCode)) return false;
所以當命令行參數為UnregServer、RegServer、UnregServerPerUser、RegServerPerUser其中之一時,子類CServiceModuleT中的ParseCommandLine方法便不再執行。那么當參數不是四個之一的時候,子類CServiceModuleT中的ParseCommandLine方法會執行這樣的操作
if (WordCmpI(lpszToken, _T("Service"))==0) { *pnRetCode = pT->RegisterAppId(true); if (SUCCEEDED(*pnRetCode)) *pnRetCode = pT->RegisterServer(TRUE); return false; }
這里看到了Service參數。於是開篇中介紹的注冊和卸載所使用的參數regserver、unregserver、service就都找到了。至此明白了是底層的ATL框架中的CServiceModuleT為我們完成了注冊和卸載服務所必須的命令行參數的解析。
同時我又充滿了疑惑,為什么Debug、Release模式下注冊服務所用的參數不同,而卸載服務所用參數又相同了呢,不同模式下的命令參數又做了些什么操作呢。帶着這些問題,我又開始了探索。
RegServer參數
RegServer參數是Debug模式下用於注冊服務的參數,它做了哪些操作呢。
*pnRetCode = pT->RegisterAppId(); if (SUCCEEDED(*pnRetCode)) *pnRetCode = pT->RegisterServer(TRUE); return false;
根據前面的代碼,看到,傳入RegServer參數時,執行了兩個方法RegisterAppId、RegisterServer兩個方法,分別來看一下。
RegisterAppId
inline HRESULT RegisterAppId(_In_ bool bService = false) throw() { if (!Uninstall()) return E_FAIL; HRESULT hr = T::UpdateRegistryAppId(TRUE); if (FAILED(hr)) return hr; CRegKey keyAppID; LONG lRes = keyAppID.Open(HKEY_CLASSES_ROOT, _T("AppID"), KEY_WRITE); if (lRes != ERROR_SUCCESS) return AtlHresultFromWin32(lRes); CRegKey key; lRes = key.Create(keyAppID, T::GetAppIdT()); if (lRes != ERROR_SUCCESS) return AtlHresultFromWin32(lRes); key.DeleteValue(_T("LocalService")); if (!bService) return S_OK; key.SetStringValue(_T("LocalService"), m_szServiceName); // Create service if (!Install()) return E_FAIL; return S_OK; }
RegisterAppId方法的大致流程為
由於調用方法時傳入的參數是false,即bService為false,所以跳過了安裝服務Install的部分。所以RegisterId主要的操作為創建注冊表信息,Uninstall與注冊表信息后面會詳述。
RegisterServer
// RegisterServer walks the ATL Autogenerated object map and registers each object in the map // If pCLSID is not NULL then only the object referred to by pCLSID is registered (The default case) // otherwise all the objects are registered HRESULT RegisterServer( _In_ BOOL bRegTypeLib = FALSE, _In_opt_ const CLSID* pCLSID = NULL) { return AtlComModuleRegisterServer(this, bRegTypeLib, pCLSID); }
RegisterServer又會調用AtlComModuleRegisterServer方法,此方法主要是做一些和Com有關的操作,加之對Com的知識不是很清楚,所以就不在繼續跟蹤下去。
回到WinMain方法
if (pT->ParseCommandLine(lpCmdLine, &hr) == true) hr = pT->Start(nShowCmd); return hr;
由前面跟蹤時可知,方法執行完RegServer參數的操作后,會返回false,所以此處WinMain方法並不會調用Start方法,至此WinMain方法執行解析,這就是通過命令行參數RegServer注冊服務的過程。
總結
通過命令行參數RegServer注冊服務的過程,主要的操作是卸載服務、創建注冊表信息。由於並沒有安裝服務,所以此時通過控制面板中的服務管理器是看不到這個服務的。
Service參數
下面是命令行Service參數時,程序執行的操作
*pnRetCode = pT->RegisterAppId(true); if (SUCCEEDED(*pnRetCode)) *pnRetCode = pT->RegisterServer(TRUE); return false;
由代碼來看,程序執行的操作與RegServer參數並無差異,但仔細觀察可以看出,調用RegisterAppId方法時傳入的參數值是不一樣的。
RegServer參數時,傳入的值是false;而Service參數時,傳入的值是true。
根據前面的RegisterAppId方法的流程圖可知,當傳入的值為true時,會執行安裝服務Install的操作,其實這也就是RegServer參數與Service參數最主要的區別。
那么Install方法又做了些什么呢。
BOOL Install() throw() { if (IsInstalled()) return TRUE; // Get the executable file path TCHAR szFilePath[MAX_PATH + _ATL_QUOTES_SPACE]; ::GetModuleFileName(NULL, szFilePath + 1, MAX_PATH); // Quote the FilePath before calling CreateService szFilePath[0] = _T('\"'); szFilePath[dwFLen + 1] = _T('\"'); szFilePath[dwFLen + 2] = 0; ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); ::CreateService( hSCM, m_szServiceName, m_szServiceName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, szFilePath, NULL, NULL, _T("RPCSS\0"), NULL, NULL); ::CloseServiceHandle(hService); ::CloseServiceHandle(hSCM); return TRUE; }
這段代碼是Install方法中去掉錯誤處理的代碼。由此可以看出,創建服務所需的三個API為 OpenSCManger、CreateService、CloseServiceHandle。對這三個方法不熟的可以查一下MSDN。
同樣,做完這些操作后,程序就會退出。
總結
通過命令行參數service注冊服務的過程,主要的操作是卸載服務、創建注冊表信息,通過OpenSCManger、CreateService等Windows API安裝服務,這樣就可以通過控制面板的服務管理器查看和管理此服務了。
UnregServer參數
下面是命令行UnregServer參數時,程序執行的操作
*pnRetCode = pT->UnregisterServer(TRUE); if (SUCCEEDED(*pnRetCode)) *pnRetCode = pT->UnregisterAppId(); return false;
由注冊過程可以猜想,UnregisterServer方法主要是處理Com相關的東西,不再研究。而UnregisterAppId則應該是卸載服務、刪除注冊表信息等操作。下面來看一下。
HRESULT UnregisterAppId() throw() { if (!Uninstall()) return E_FAIL; // First remove entries not in the RGS file. CRegKey keyAppID; keyAppID.Open(HKEY_CLASSES_ROOT, _T("AppID"), KEY_WRITE); CRegKey key; key.Open(keyAppID, T::GetAppIdT(), KEY_WRITE); key.DeleteValue(_T("LocalService")); return T::UpdateRegistryAppId(FALSE); }
上面仍然是去掉了錯誤處理的代碼。由此可以驗證剛才的猜想是對的,接下來繼續查看Uninstall方法,去掉錯誤處理后的代碼如下
BOOL Uninstall() throw() { if (!IsInstalled()) return TRUE; ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); ::OpenService(hSCM, m_szServiceName, SERVICE_STOP | DELETE); SERVICE_STATUS status; ::ControlService(hService, SERVICE_CONTROL_STOP, &status); ::DeleteService(hService); ::CloseServiceHandle(hService); ::CloseServiceHandle(hSCM); return TRUE; }
流程圖如下
程序執行完畢后,服務管理器中就看不到此服務了,這樣此服務就被卸載掉了。
新的問題
之前的問題消除了,但是新的問題又產生了。
既然Debug模式下通過RegServer參數注冊服務,實際上只是向注冊表中添加了一些信息,並沒有安裝服務,而且Debug版為了方便調試,運行的時候也是通過啟動exe的方式運行,那么為什么還要通過RegServer方式注冊服務呢,編譯后直接運行exe程序不行嗎?
那么接下來開始繼續研究。
通過VS新建一個服務后,編譯稱為exe,然后直接運行exe,由於此處的服務是無窗口的,所以要通過任務管理器查看exe是否在運行。發現任務管理器中並沒有此服務的進程。
回到WinMain函數
if (pT->ParseCommandLine(lpCmdLine, &hr) == true) hr = pT->Start(nShowCmd);
由於直接啟動exe時,ParseCommandLine會返回true,所以接下來會執行Start方法,下面是Start方法的代碼。
HRESULT Start(_In_ int nShowCmd) throw() { T* pT = static_cast<T*>(this); // Are we Service or Local Server CRegKey keyAppID; LONG lRes = keyAppID.Open(HKEY_CLASSES_ROOT, _T("AppID"), KEY_READ); if (lRes != ERROR_SUCCESS) { m_status.dwWin32ExitCode = lRes; return m_status.dwWin32ExitCode; } CRegKey key; lRes = key.Open(keyAppID, pT->GetAppIdT(), KEY_READ); if (lRes != ERROR_SUCCESS) { m_status.dwWin32ExitCode = lRes; return m_status.dwWin32ExitCode; } TCHAR szValue[MAX_PATH]; DWORD dwLen = MAX_PATH; lRes = key.QueryStringValue(_T("LocalService"), szValue, &dwLen); m_bService = FALSE; if (lRes == ERROR_SUCCESS) m_bService = TRUE; if (m_bService) { SERVICE_TABLE_ENTRY st[] = { { m_szServiceName, _ServiceMain }, { NULL, NULL } }; if (::StartServiceCtrlDispatcher(st) == 0) m_status.dwWin32ExitCode = GetLastError(); return m_status.dwWin32ExitCode; } // local server - call Run() directly, rather than // from ServiceMain() #ifndef _ATL_NO_COM_SUPPORT HRESULT hr = T::InitializeCom(); if (FAILED(hr)) { // Ignore RPC_E_CHANGED_MODE if CLR is loaded. Error is due to CLR initializing // COM and InitializeCOM trying to initialize COM with different flags. if (hr != RPC_E_CHANGED_MODE || GetModuleHandle(_T("Mscoree.dll")) == NULL) { return hr; } } else { m_bComInitialized = true; } #endif //_ATL_NO_COM_SUPPORT m_status.dwWin32ExitCode = pT->Run(nShowCmd); return m_status.dwWin32ExitCode; }
從代碼中可以看到,Start方法會首先讀取注冊服務時創建的注冊表信息,如果注冊表信息不存在,Start方法便會立即返回,然后WinMain方法執行結束,這樣程序就會結束、進程退出。
所以雖然Debug模式下的服務程序不需要使用服務管理器進行管理,但是如果不通過RegServer參數進行注冊的話,程序是無法正常運行的。
當然,也可以通過實現自己的Start方法,來避免Debug模式下必須注冊才能運行的問題。
全文總結
Debug版本的程序可以通過命令行參數RegServer來注冊服務,這樣方便調試。
Release版本的程序通過命令行參數Service來注冊服務,方便通過服務管理器進行管理。
相關的Windows API
//打開服務控制管理器句柄 OpenSCManager //創建服務 CreateService //打開服務句柄 OpenService //控制服務的狀態 ControlService //刪除服務 DeleteService //關閉服務或者服務管理器的句柄 CloseServiceHandle
系列鏈接
玩轉Windows服務系列——Debug、Release版本的注冊和卸載,及其原理
玩轉Windows服務系列——無COM接口Windows服務啟動失敗原因及解決方案
玩轉Windows服務系列——Windows服務啟動超時時間