[转载]Windows服务编写原理及探讨(4)
(四)一些问题的讨论
前面几章的内容都是服务的一些通用的编写原理,但里面隐含着一些问题,编写简单的服务时看不出来,但遇到复杂的应用就会出现一些问题,所以本章就是用来分析、解决这些问题的,适用于高级应用的开发人员。我这一章的内容都是经过实验得到的,很有实际意义。
我在第一章里面就说过,是由一个服务的主线程执行CtrlHandler函数,它将收到各种控制命令,但是真正处理命令,执行操作的是
ServiceMain的线程。现在,当一个SERVICE_CONTROL_STOP到达之后,你作为一个开发者,要怎样停止这个服务?在我看过的一些
源代码里,大部分只是简单的调用TerminateThread函数去强行杀掉服务进程。但应该稍稍有点线程编程的常识就应该知道
TerminateThread函数是可用的调用中最为糟糕的一个,服务线程将得不到任何机会去做应该的清理工作,诸如清除内存、释放核心对象,Dlls
也得不到任何线程已经被毁的通知。
所以停止服务的适当方法是以某种方式激活服务线程,让它停止继续提供服务功能,然后执行完当前操作
和清除工作后返回。这就表示你必须在CtrlHandler线程和ServiceMain线程之间执行适当的线程通信。现在已知的最好的内部线程通信机制
是I/O Completion Port(I/O
完成端口),假如你编写的是一个大型的服务,需要同时处理为数众多的请求,并且运行在多处理器系统上面,这个模型就可以提供最佳的系统性能。但也正因为它
的复杂性较高,在小规模的应用上面不值得花费很多的时间和精力,这时作为开发者可以适当的选取其它的通信方式,诸如异步过程调用队列、套接字和窗口消息,
以适应实际情况。
开发服务时的另外一个重要问题就是调用SetServiceStatus函数时的所有状态报告问题。很多的服务开发
者为了在什么时候调用SetServiceStatus的问题而常常产生争论,一般推荐的方法就是:先调用SetServiceStatus函数,报告
SERVICE_STOP_PENDING状态,然后将控制代码传给服务线程或者再建立一个新的线程,让它去继续执行操作,当该线程即将执行完操作之前,
再由它将服务的状态设置成SERVICE_STOPPED,然后服务正好停止。
上面的主意从两个方面来讲还是很不错的。首先服务可以
立即确认收到了控制代码,并将在它认为适当的时候进行处理;然后就是因为前面说过的,执行CtrlHandler函数的是主线程,如果按照这种工作方
法,CtrlHandler函数可以迅速的返回,不会影响到其它服务可能收到的控制请求,对含有多个服务的程序来说,响应各个服务的控制代码的速度会大大
的提高。可是,随之而来的是问题―― race condition 即“竞争条件”的产生。
摆在下面的就是一个竞争条件的例子,我
花了一点时间来修改我的基本服务的代码,意图故意引发“竞争条件”的发生。我添加了一个线程,CtrlHandler函数的线程在收到请求后立刻作出反
应,将当前的服务状态设置成“请求正在被处理”即..._PENDING,然后由我添加的线程在睡眠了5秒之后再将服务状态设置成“请求已完成”状态――
以模拟服务正在处理一些不可中止的事件,只有处理完成后才会更改服务的状态。一切就绪之后,我尝试在短时间内连续发送两个“暂停”请求,如果“竞争条件”
不存在的话应该只有先发送的那个请求能够到达SCM,而另一个则应该返回请求发送失败的信息,天下太平。
事实上很不幸的,我成功了。当我在两个不同的“命令提示符”窗口分别同样的输入下面的命令:
net pause kservice
之后在“事件查看器”里面,我找到了我的服务在“应用程序日志”里添加的事件记录,结果是我得到了这样的事件列表:
SERVICE_PAUSE_PENDING
SERVICE_PAUSE_PENDING
SERVICE_PAUSED
SERVICE_PAUSED
看上去很奇怪是不是?因为服务处于正在暂停状态的时候,它不应该被再次暂停的。但事实摆在眼前,很多服务都曾明确的报告过上面的顺序状态。我曾经认为这
时SCM应该说些什么或做些什么,以阻止“竞争状态”的出现,但实验结果告诉我SCM似乎对此无能为力,因为它不能控制状态代码在什么时候被发送。当用户
使用“管理工具”里面的“服务”工具来管理服务的状态的时候,在一个“暂停”请求已经发出之后不能再次用这个工具向它发出“暂停”请求,如果正在暂停服
务,会有一个对话框出现,阻止你按下它后面的“服务”工具的工具栏上的任何按钮,如果已经暂停,“暂停“按钮将变成灰色。但是这时用命令行工具
net.exe
就可以很顺利地将暂停请求再次送到服务。证据就是我添加的其他事件记录里面记下了SetServiceStatus的调用全都成功了,这更进一步的说明了
我提交的两个暂停请求都经过SCM,然后到达了我的服务。
接下来我又进行了其它的测试,例如先发送“暂停”请求,后发送“停止”请
求,和先发送“停止”请求,再发送“暂停”或“停止”请求。前一种情况更加糟糕,先发送的“暂停”请求和后发送的“停止”请求都没有得到什么好下场,虽然
SCM老老实实的先暂停了服务,后停止了服务,但 net.exe
的两个实例的调用均告失败。不过在测试先发送停止“请求”的时候,所有的现象都表示这两个请求只有先发送的“停止”到达了SCM,这还算是个好消息...
为了解决这个问题,当服务得到一个“停止”“暂停”或“继续”请求的时候,应该首先检查服务是否已经在处理另外的一个请求,如果是,就依情况而定:是不
调用SetServiceStatus直接返回还是暂时忍耐直到前一个请求动作完成再调用SetServiceStatus,这是你作为一个开发者要自己
决定的。
如果说前面的问题已经足够麻烦了,下面的问题会令你觉得更加怪异。它其实是一种可以解决上面的问题的方法:当
CtrlHandler函数的线程收到SERVICE_PAUSE_PENDING请求之后,它调用SetServiceStatus报告服务正在暂停,
然后由它自己调用SuspendThread来暂停服务的线程,然后再由它自己调用SetServiceStatus报告服务已经被暂停。这样做的确避免
了“竞争条件”的出现,因为所有的工作都是由一个函数来做的。现在需要注意的不是“竞争条件”而是服务本身,挂起服务的线程会不会暂停服务呢?答案是会
的。但是暂停服务意味着什么呢?
假如我的服务是用来处理网络客户的请求,那么暂停对于我的服务来说应该是停止接受新的请求。如果我现
在正处在处理请求的过程中,那么我应该怎么办?也许我应该结束它,使客户不至于无限期悬挂。但如果我只是简单的调用SuspendThread,那么不排
除服务线程正处于孤立的中间状态的可能,或者正在调用malloc函数去尝试分配内存,如果运行在同一个进程中的另一个服务也调内存分配函数,那么它也会
被挂起,这肯定不是我期望的结果。
还有一个问题:用户认为自己可以被允许去停止一个已经被暂停了的服务吗?我认为是这样的,而且很明
显的,微软也这么认为。因为当我们在“服务”管理工具里面选中一个已暂停的服务之后,“停止”按钮是可以被按下的。但我要怎样停止一个由于线程被挂起才处
于暂停状态的服务呢?不,不要TerminateThread,请别跟我提起它。
解决这所有的混乱的最好方法,就是有一个能够把所有
事做好的线程,而且它应该是服务线程,而不是CtrlHandler线程。当CtrlHandler函数得到控制代码之后,它要迅速的将控制代码通过线程
内部通讯手段送到服务线程中排队,然后CtrlHandler函数就应该返回,它决不应该调SetServiceStatus。这样,服务可以随心所欲的
控制每件事情,因为没有什么比它更有发言权的了,没有“竞争条件”。服务决定暂停意味着什么,服务能够允许自己在已经暂停的情况下停止,服务决定什么内部
通讯机制是最好的――并且CtrlHandler函数必须简单的与这种机制相一致。
事情没有完美的,上面的方法也不例外,它仅有一个
小缺陷:就是假定当服务收到控制代码后,在较短的时间内就能做出应有的响应。如果服务线程正在忙于处理一个客户的请求,控制代码可能进入等待队列,而且
SetServiceStatus可能也无法迅速的被调用。如果真是这样的话,负责发送通知的SCP可能会认为你的服务已经失败,并向用户报告一个消息
框。事实上服务并没有失败,而且也不会被终止。
这种情况够糟糕了,没有用户会去责怪SCP――虽然SCP将他们引导到了错误的状态,他们只会责怪服务的作者――就是我或你...因此,在服务中怎么做才能防止这种问题发生呢?很简单,使服务快速有效的运行,并且总保持一个活动线程等待去处理控制代码。
说起来好像很容易,但实际做起来就被那么简单了,这也不是我能够向各位解释的了,只有认真的调试自己的服务,才能找出最为适合处理方法。所以我的文章也真的到了该结束的时候了,感谢各位的浏览。如果我有什么地方说的不对,请不吝赐教,谢谢。
下面是我写的一个服务的源代码,没什么功能,只能启动、停止和安装。
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <tchar.h>
#define SZAPPNAME "basicservice"
#define SZSERVICENAME "KService"
#define SZSERVICEDISPLAYNAME "KService"
#define SZDEPENDENCIES ""
void WINAPI KServiceMain(DWORD argc, LPTSTR * argv);
void InstallService(const char * szServiceName);
void LogEvent(LPCTSTR pFormat, ...);
void Start();
void Stop();
SERVICE_STATUS ssStatus;
SERVICE_STATUS_HANDLE sshStatusHandle;
int main(int argc, char * argv[])
{
if ((argc==2) && (::strcmp(argv[1]+1, "install")==0))
{
InstallService("KService");
return 0;
}
SERVICE_TABLE_ENTRY service_table_entry[] =
{
{ "KService", KServiceMain },
{ NULL, NULL }
};
::StartServiceCtrlDispatcher(service_table_entry);
return 0;
}
void InstallService(const char * szServiceName)
{
SC_HANDLE handle = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
char szFilename[256];
::GetModuleFileName(NULL, szFilename, 255);
SC_HANDLE hService = ::CreateService(handle, szServiceName,
szServiceName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, szFilename, NULL,
NULL, NULL, NULL, NULL);
::CloseServiceHandle(hService);
::CloseServiceHandle(handle);
}
SERVICE_STATUS servicestatus;
SERVICE_STATUS_HANDLE servicestatushandle;
void WINAPI ServiceCtrlHandler(DWORD dwControl)
{
switch (dwControl)
{
//下面虽然添加了暂停、继续等请求的处理代码,但没有实际作用
//这是为什么呢?到了下面的KServiceMain函数里面就明白了...
case SERVICE_CONTROL_PAUSE:
servicestatus.dwCurrentState = SERVICE_PAUSE_PENDING;
// TODO: add code to set dwCheckPoint & dwWaitHint
// This value need to try a lot to confirm
// ...
::SetServiceStatus(servicestatushandle, &servicestatus);
// TODO: add code to pause the service
// not called in this service
// ...
servicestatus.dwCurrentState = SERVICE_PAUSED;
// TODO: add code to set dwCheckPoint & dwWaitHint to 0
break;
case SERVICE_CONTROL_CONTINUE:
servicestatus.dwCurrentState = SERVICE_CONTINUE_PENDING;
// TODO: add code to set dwCheckPoint & dwWaitHint
::SetServiceStatus(servicestatushandle, &servicestatus);
// TODO: add code to unpause the service
// not called in this service
// ...
servicestatus.dwCurrentState = SERVICE_RUNNING;
// TODO: add code to set dwCheckPoint & dwWaitHint to 0
break;
case SERVICE_CONTROL_STOP:
servicestatus.dwCurrentState = SERVICE_STOP_PENDING;
// TODO: add code to set dwCheckPoint & dwWaitHint
::SetServiceStatus(servicestatushandle, &servicestatus);
// TODO: add code to stop the service
Stop();
servicestatus.dwCurrentState = SERVICE_STOPPED;
// TODO: add code to set dwCheckPoint & dwWaitHint to 0
break;
case SERVICE_CONTROL_SHUTDOWN:
// TODO: add code for system shutdown
// as quick as possible
break;
case SERVICE_CONTROL_INTERROGATE:
// TODO: add code to set the service status
// ...
servicestatus.dwCurrentState = SERVICE_RUNNING;
break;
}
::SetServiceStatus(servicestatushandle, &servicestatus);
}
void WINAPI KServiceMain(DWORD argc, LPTSTR * argv)
{
servicestatus.dwServiceType = SERVICE_WIN32;
servicestatus.dwCurrentState = SERVICE_START_PENDING;
servicestatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;//上面的问题的答案就在这里
servicestatus.dwWin32ExitCode = 0;
servicestatus.dwServiceSpecificExitCode = 0;
servicestatus.dwCheckPoint = 0;
servicestatus.dwWaitHint = 0;
servicestatushandle =
::RegisterServiceCtrlHandler("KService", ServiceCtrlHandler);
if (servicestatushandle == (SERVICE_STATUS_HANDLE)0)
{
return;
}
bool bInitialized = false;
// Initialize the service
// ...
Start();
bInitialized = true;
servicestatus.dwCheckPoint = 0;
servicestatus.dwWaitHint = 0;
if (!bInitialized)
{
servicestatus.dwCurrentState = SERVICE_STOPPED;
servicestatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
servicestatus.dwServiceSpecificExitCode = 1;
}
else
{
servicestatus.dwCurrentState = SERVICE_RUNNING;
}
::SetServiceStatus(servicestatushandle, &servicestatus);
return;
}
void Start()
{
LogEvent("Service Starting...");
}
void LogEvent(LPCTSTR pFormat, ...)
{
TCHAR chMsg[256];
HANDLE hEventSource;
LPTSTR lpszStrings[1];
va_list pArg;
va_start(pArg, pFormat);
_vstprintf(chMsg, pFormat, pArg);
va_end(pArg);
lpszStrings[0] = chMsg;
if (1)
{
// Get a handle to use with ReportEvent().
hEventSource = RegisterEventSource(NULL, "KService");
if (hEventSource != NULL)
{
// Write to event log.
ReportEvent(hEventSource, EVENTLOG_INFORMATION_TYPE, 0, 0, NULL, 1, 0, (LPCTSTR*) &lpszStrings[0], NULL);
DeregisterEventSource(hEventSource);
}
}
else
{
// As we are not running as a service, just write the error to the console.
_putts(chMsg);
}
}
void Stop()
{
LogEvent("Service Stoped.");
}
[转载]Windows服务编写原理及探讨(4)的更多相关文章
- [转载]Windows服务编写原理及探讨(1)
有那么一类应用程序,是能够为各种用户(包括本地用户和远程用户)所用的,拥有用户授权级进行管理的能力,并且不论用户是否物理的与正在运行该应用程序的计算机相连都能正常执行,这就是所谓的服务了. (一)服务 ...
- [转载]Windows服务编写原理及探讨(3)
(三)对服务的深入讨论之下 现在我们还剩下一个函数可以在细节上讨论,那就是服务的CtrlHandler函数. 当调用RegisterServiceCtrlHandler函数时,SCM得到并保存这个回调 ...
- [转载]Windows服务编写原理及探讨(2)
(二)对服务的深入讨论之上 上一章其实只是概括性的介绍,下面开始才是真正的细节所在.在进入点函数里面要完成ServiceMain的初始化,准确点说是初始化一个 SERVICE_TABLE_ENTRY结 ...
- C# windows 服务编写及安装
最近项目中用到window服务程序,以前没接触过,比较陌生,花了两天的时间学习了下,写了个简单的服务,但在制作安装程序的时候,参照网上很多资料,却都制作不成功,可能是开发环境或项目配置的不同,这里 ...
- windows服务编写和“以管理员运行”程序的方法
本文将首先解释如何 创建 一个定期查询可用物理内存并将结果写入某个文本文件的服务.然后指导你完成生成,安装和实现服务的整个过程. 第一步:主函数和全局定义 首先,包含所需的头文件.例子要调用 Win3 ...
- C#开发Windows服务 附简单实例实现禁止QQ运行
本实例主要实现下面三个基本功能 1.C#开发windows服务 2.禁止QQ等程序运行 3.为windows服务创建自动安装程序 下面针对这三个基本功能进行实现 一.C#开发windows服务 Win ...
- C#曲线分析平台的制作(六,Sqldependency+Signalr+windows 服务)
在经过五天的学习和资料收集后,终于初步实现了利用sqldependency进行数据库变化监控,signalr进行前后台交互,数据实时更新.下面将源代码贴出进行初步分析: 1.系统整体框架构成: 2.具 ...
- 玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理
Windows服务Debug版本 注册 Services.exe -regserver 卸载 Services.exe -unregserver Windows服务Release版本 注册 Servi ...
- 玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理
原文:玩转Windows服务系列——Debug.Release版本的注册和卸载,及其原理 Windows服务Debug版本 注册 Services.exe -regserver 卸载 Services ...
随机推荐
- variant conversion error for variable v23
excel数据导入到oracle数据库出现的问题 V23指的是excel列.,这列的数据长度超出或者类型与数据库表不一致导致的 解决方法,1.清空该列,再建个新列 2.用access SQL查出长度 ...
- BZOJ 2720: [Violet 5]列队春游
2720: [Violet 5]列队春游 Time Limit: 5 Sec Memory Limit: 128 MBSubmit: 189 Solved: 133[Submit][Status] ...
- android:onClick="xxx"
一般监听OnClickListener事件,我们都是通过Button button = (Button)findViewById(....); button.setOClickLisener....这 ...
- bzoj 4358: permu 莫队
第一步先莫队分块. 对于每一块l~r,初始右端点设为r+1,然后每个询问先将右端点往右移,然后处理询问在l~r之间的部分,最后用一个栈再把l~r的复原. 具体来说是维护两个数组now1和now2,一个 ...
- NIN (Network In Network)
Network In Network 论文Network In Network(Min Lin, ICLR2014). 传统CNN使用的线性滤波器是一种广义线性模型(Generalized linea ...
- python学习(24) 使用Xpath解析并抓取美女图片
Xpath最初用来处理XML解析,同样适用于HTML文档处理.相比正则表达式更方便一些 Xpath基本规则 nodename 表示选取nodename 节点的所有子节点 / 表示当前节点的直接子节点 ...
- aspx <%= %> 绑定变量问题及解决方法
aspx页面,前台页面需要用到后台变量字段的时候我们一般使用 <%= 变量 %>,多数情况下使用正常. 但是有的时候就会遇到变量解析被编码的情况,特别是是在head当中css引用lin ...
- Date时间格式比较大小
方法一: 两个Date类型的变量可以通过compareTo方法来比较.此方法的描述是这样的:如果参数 Date 等于此 Date,则返回值 0:如果此 Date 在 Date 参数之前,则返回小于 0 ...
- bzoj千题计划118:bzoj1028: [JSOI2007]麻将
http://www.lydsy.com/JudgeOnline/problem.php?id=1028 枚举等待牌 枚举对是哪个 判断 #include<cstdio> #include ...
- Linux部署node环境
# wget https://nodejs.org/dist/v8.11.4/node-v8.11.4-linux-x64.tar.xz# tar -axvf node-v8.11.4-linux-x ...