玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理
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();
return -;
} 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"))==)
{
*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"))==)
{
*pnRetCode = pT->UnregisterServer(TRUE);
if (SUCCEEDED(*pnRetCode))
*pnRetCode = pT->UnregisterAppId();
return false;
} if (WordCmpI(lpszToken, _T("RegServer"))==)
{
*pnRetCode = pT->RegisterAppId();
if (SUCCEEDED(*pnRetCode))
*pnRetCode = pT->RegisterServer(TRUE);
return false;
} if (WordCmpI(lpszToken, _T("UnregServerPerUser"))==)
{
*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"))==)
{
*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"))==)
{
*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 + , MAX_PATH); // Quote the FilePath before calling CreateService
szFilePath[] = _T('\"');
szFilePath[dwFLen + ] = _T('\"');
szFilePath[dwFLen + ] = ; ::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) == )
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服务启动超时时间
玩转Windows服务系列——使用Boost.Application快速构建Windows服务
玩转Windows服务系列——给Windows服务添加COM接口
玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理的更多相关文章
- 玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理
原文:玩转Windows服务系列——Debug.Release版本的注册和卸载,及其原理 Windows服务Debug版本 注册 Services.exe -regserver 卸载 Services ...
- 玩转Windows服务系列汇总
玩转Windows服务系列汇总 创建Windows服务 Debug.Release版本的注册和卸载及其原理 无COM接口Windows服务启动失败原因及解决方案 服务运行.停止流程浅析 Windows ...
- 玩转Windows服务系列——给Windows服务添加COM接口
当我们运行一个Windows服务的时候,一般情况下,我们会选择以非窗口或者非控制台的方式运行,这样,它就只是一个后台程序,没有界面供我们进行交互. 那么当我们想与Windows服务进行实时交互的时候, ...
- 玩转Windows服务系列——使用Boost.Application快速构建Windows服务
玩转Windows服务系列——创建Windows服务一文中,介绍了如何快速使用VS构建一个Windows服务.Debug.Release版本的注册和卸载,及其原理和服务运行.停止流程浅析分别介绍了Wi ...
- 玩转Windows服务系列——创建Windows服务
创建Windows服务的项目 新建项目->C++语言->ATL->ATL项目->服务(EXE) 这样就创建了一个Windows服务项目. 生成的解决方案包含两个项目:Servi ...
- 玩转Windows服务系列——无COM接口Windows服务启动失败原因及解决方案
将VS创建的Windows服务项目编译生成的程序,通过命令行 “服务.exe -Service”注册为Windows服务后,就可以通过服务管理器进行管理了. 问题 通过服务管理器进行启动的时候,发现服 ...
- 玩转Windows服务系列——服务运行、停止流程浅析
通过研究Windows服务注册卸载的原理,感觉它并没有什么特别复杂的东西,Windows服务正在一步步退去它那神秘的面纱,至于是不是美女,大家可要睁大眼睛看清楚了. 接下来研究一下Windows服务的 ...
- 玩转Windows服务系列——Windows服务小技巧
伴随着研究Windows服务,逐渐掌握了一些小技巧,现在与大家分享一下. 将Windows服务转变为控制台程序 由于默认的Windows服务程序,编译后为Win32的窗口程序.我们在程序启动或运行过程 ...
- 玩转Windows服务系列——命令行管理Windows服务
说到Windows服务的管理就不得不说通过命令行的方式管理Windows服务,因为无论是系统管理员,还是通过编程的方式调用cmd命令,命令行都是非常方便以及强大的工具. 接下来就看一下如何通过cmd命 ...
随机推荐
- javascript中的操作符详解1
好久没有写点什么了,根据博主的技术,仍然写一点javascript新手入门文章,接下来我们一起来探讨javascript的操作符. 一.前言 javascript中有许多操作符,但是许多初学者并不理解 ...
- 引人瞩目的 CSS 变量(CSS Variable)
这是一个令人激动的革新. CSS 变量,顾名思义,也就是由网页的作者或用户定义的实体,用来指定文档中的特定变量. 更准确的说法,应该称之为 CSS 自定义属性 ,不过下文为了好理解都称之为 CSS 变 ...
- 移动端访问PC站点时自动跳转至移动站点
方法一: 百度Site APP的uaredirect.js 实现手机访问,自动跳转 <script src="http://siteapp.baidu.com/static/webap ...
- 独立开发 一个社交 APP 的架构分享 (已实现)
(本博客为原创:http://www.cnblogs.com/linguanh/) My BananaCloud Android Application 前言: 这算是我的第一个 完完全全 由自 ...
- var和dynamic的区别
1.var 1.均是声明动态类型的变量. 2.在编译阶段已经确定类型,在初始化的时候必须提供初始化的值. 3.无法作为方法参数类型,也无法作为返回值类型. 2.dynamic 1.均是声明动态类型的变 ...
- .NET应用和AEAI CAS集成详解
1 概述 数通畅联某综合SOA集成项目的统一身份认证工作,需要第三方系统配合进行单点登录的配置改造,在项目中有需要进行单点登录配置的.NET应用系统,本文专门记录.NET应用和AEAI CAS的集成过 ...
- [原创]关于Hibernate中的级联操作以及懒加载
Hibernate: 级联操作 一.简单的介绍 cascade和inverse (Employee – Department) Casade用来说明当对主对象进行某种操作时是否对其关联的从对象也作类似 ...
- 【算法】(查找你附近的人) GeoHash核心原理解析及代码实现
本文地址 原文地址 分享提纲: 0. 引子 1. 感性认识GeoHash 2. GeoHash算法的步骤 3. GeoHash Base32编码长度与精度 4. GeoHash算法 5. 使用注意点( ...
- Function.prototype.toString 的使用技巧
Function.prototype.toString这个原型方法可以帮助你获得函数的源代码, 比如: function hello ( msg ){ console.log("hello& ...
- 用SecureCRT连接虚拟机中的Linux系统(Ubuntu)
今天突然练习linux命令行的时候,想在window中联系linux命令行.经过一番dudu找到了一个不错的的工具(SecureCRT--意思安全)就是用SSH链接linux主机.推荐大家使用.毕竟w ...