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服务启动超时时间