玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理

长平狐 发布于 2013/12/26 11:19
阅读 883
收藏 0

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服务项目

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方法的大致流程为

RegisterId流程图

由于调用方法时传入的参数是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安装服务,这样就可以通过控制面板的服务管理器查看和管理此服务了。

Service参数注册后_服务管理器查看

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;}

流程图如下

Uninstall流程图

程序执行完毕后,服务管理器中就看不到此服务了,这样此服务就被卸载掉了。

 

新的问题

之前的问题消除了,但是新的问题又产生了。

既然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服务系列——创建Windows服务

玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理

玩转Windows服务系列——无COM接口Windows服务启动失败原因及解决方案

玩转Windows服务系列——服务运行、停止流程浅析

玩转Windows服务系列——Windows服务小技巧


原文链接:http://www.cnblogs.com/hbccdf/p/3486565.html
加载中
返回顶部
顶部