(二)对服务的深入讨论之上

 
 上一章其实只是概括性的介绍,下面开始才是真正的细节所在。在进入点函数里面要完成ServiceMain的初始化,准确点说是初始化一个
SERVICE_TABLE_ENTRY结构数组,这个结构记录了这个服务程序里面所包含的所有服务的名称和服务的进入点函数,下面是一个
SERVICE_TABLE_ENTRY的例子:

SERVICE_TABLE_ENTRY service_table_entry[] =
{
  { "MyFTPd" , FtpdMain },
  { "MyHttpd", Httpserv},
  { NULL, NULL },
};

  第一个成员代表服务的名字,第二个成员是ServiceMain回调函数的地址,上面的服务程序因为拥有两个服务,所以有三个SERVICE_TABLE_ENTRY元素,前两个用于服务,最后的NULL指明数组的结束。

  接下来这个数组的地址被传递到StartServiceCtrlDispatcher函数:

BOOL StartServiceCtrlDispatcher(
LPSERVICE_TABLE_ENTRY lpServiceStartTable
)

 
 这个Win32函数表明可执行文件的进程怎样通知SCM包含在这个进程中的服务。就像上一章中讲的那
样,StartServiceCtrlDispatcher为每一个传递到它的数组中的非空元素产生一个新的线程,每一个进程开始执行由数组元素中的
lpServiceStartTable指明的ServiceMain函数。

  SCM启动一个服务程序之后,它会等待该程序的主线程去
调StartServiceCtrlDispatcher。如果那个函数在两分钟内没有被调用,SCM将会认为这个服务有问题,并调用
TerminateProcess去杀死这个进程。这就要求你的主线程要尽可能快的调用StartServiceCtrlDispatcher。

 
 StartServiceCtrlDispatcher函数则并不立即返回,相反它会驻留在一个循环内。当在该循环内
时,StartServiceCtrlDispatcher悬挂起自己,等待下面两个事件中的一个发生。第一,如果SCM要去送一个控制通知给运行在这个
进程内一个服务的时候,这个线程就会激活。当控制通知到达后,线程激活并调用相应服务的CtrlHandler函数。CtrlHandler函数处理这个
服务控制通知,并返回到StartServiceCtrlDispatcher。StartServiceCtrlDispatcher循环回去后再一次
悬挂自己。

  第二,如果服务线程中的一个服务中止,这个线程也将激活。在这种情况下,该进程将运行在它里面的服务数减一。如果服务数为
零,StartServiceCtrlDispatcher就会返回到入口点函数,以便能够执行任何与进程有关的清除工作并结束进程。如果还有服务在运
行,哪怕只是一个服务,StartServiceCtrlDispatcher也会继续循环下去,继续等待其它的控制通知或者剩下的服务线程中止。

 
 上面的内容是关于入口点函数的,下面的内容则是关于ServiceMain函数的。还记得以前讲过的ServiceMain函数的的原型吗?但实际上一
个ServiceMain函数通常忽略传递给它的两个参数,因为服务一般不怎么传递参数。设置一个服务最好的方法就是设置注册表,一般服务在
HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServiceServiceNameParameters

键下存放自己的设置,这里的ServiceName是服务的名字。事实上,可能要写一个客户应用程序去进行服务的背景设置,这个客户应用程序将这些信息存
在注册表中,以便服务读取。当一个外部应用程序已经改变了某个正在运行中的服务的设置数据的时候,这个服务能够用
RegNotifyChangeKeyValue函数去接受一个通知,这样就允许服务快速的重新设置自己。

  前面讲到
StartServiceCtrlDispatcher为每一个传递到它的数组中的非空元素产生一个新的线程。接下来,一个ServiceMain要做些
什么呢?MSDN里面的原文是这样说的:The ServiceMain function should immediately call the
RegisterServiceCtrlHandler function to specify a Handler function to
handle control requests. Next, it should call the SetServiceStatus
function to send status information to the service control manager.
为什么呢?因为发出启动服务请求之后,如果在一定时间之内无法完成服务的初始化,SCM会认为服务的启动已经失败了,这个时间的长度在Win NT
4.0中是80秒,Win2000中不详...

  基于上面的理由,ServiceMain要迅速完成自身工作,首先是必不可少的两项工作,第一项是调用RegisterServiceCtrlHandler函数去通知SCM它的CtrlHandler回调函数的地址:

SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler(
LPCTSTR lpServiceName, //服务的名字
LPHANDLER_FUNCTION lpHandlerProc //CtrlHandler函数地址
)

 
 第一个参数指明你正在建立的CtrlHandler是为哪一个服务所用,第二个参数是CtrlHandler函数的地址。lpServiceName必
须和在SERVICE_TABLE_ENTRY里面被初始化的服务的名字相匹配。RegisterServiceCtrlHandler返回一个
SERVICE_STATUS_HANDLE,这是一个32位的句柄。SCM用它来唯一确定这个服务。当这个服务需要把它当时的状态报告给SCM的时候,
就必须把这个句柄传给需要它的Win32函数。注意:这个句柄和其他大多数的句柄不同,你无需关闭它。

  SCM要求
ServiceMain函数的线程在一秒钟内调用RegisterServiceCtrlHandler函数,否则SCM会认为服务已经失败。但在这种情
况下,SCM不会终止服务,不过在NT 4中将无法启动这个服务,同时会返回一个不正确的错误信息,这一点在Windows 2000中得到了修正。

  在RegisterServiceCtrlHandler函数返回后,ServiceMain线程要立即告诉SCM服务正在继续初始化。具体的方法是通过调用SetServiceStatus函数传递SERVICE_STATUS数据结构。

BOOL SetServiceStatus(
SERVICE_STATUS_HANDLE hService, //服务的句柄
SERVICE_STATUS lpServiceStatus //SERVICE_STATUS结构的地址
)

  这个函数要求传递给它指明服务的句柄(刚刚通过调用RegisterServiceCtrlHandler得到),和一个初始化的SERVICE_STATUS结构的地址:

typedef struct _SERVICE_STATUS
{
DWORD dwServiceType;
DWORD dwCurrentState;
DWORD dwControlsAccepted;
DWORD dwWin32ExitCode;
DWORD dwServiceSpecificExitCode;
DWORD dwCheckPoint;
DWORD dwWaitHint;
} SERVICE_STATUS, *LPSERVICE_STATUS;

  SERVICE_STATUS结构含有七个成员,它们反映服务的现行状态。所有这些成员必须在这个结构被传递到SetServiceStatus之前正确的设置。

 
 成员dwServiceType指明服务可执行文件的类型。如果你的可执行文件中只有一个单独的服务,就把这个成员设置成
SERVICE_WIN32_OWN_PROCESS;如果拥有多个服务的话,就设置成SERVICE_WIN32_SHARE_PROCESS。除了这
两个标志之外,如果你的服务需要和桌面发生交互(当然不推荐这样做),就要用“OR”运算符附加上
SERVICE_INTERACTIVE_PROCESS。这个成员的值在你的服务的生存期内绝对不应该改变。

  成员dwCurrentState是这个结构中最重要的成员,它将告诉SCM你的服务的现行状态。为了报告服务仍在初始化,应该把这个成员设置成SERVICE_START_PENDING。在以后具体讲述CtrlHandler函数的时候具体解释其它可能的值。

 
 成员dwControlsAccepted指明服务愿意接受什么样的控制通知。如果你允许一个SCP去暂停/继续服务,就把它设成
SERVICE_ACCEPT_PAUSE_CONTINUE。很多服务不支持暂停或继续,就必须自己决定在服务中它是否可用。如果你允许一个SCP去停
止服务,就要设置它为SERVICE_ACCEPT_STOP。如果服务要在操作系统关闭的时候得到通知,设置它为
SERVICE_ACCEPT_SHUTDOWN可以收到预期的结果。这些标志可以用“OR”运算符组合。

  成员
dwWin32ExitCode和dwServiceSpecificExitCode是允许服务报告错误的关键,如果希望服务去报告一个Win32错误
代码(预定义在WinError.h中),它就设置dwWin32ExitCode为需要的代码。一个服务也可以报告它本身特有的、没有映射到一个预定义
的Win32错误代码中的错误。为了这一点,要把dwWin32ExitCode设置为ERROR_SERVICE_SPECIFIC_ERROR,然后
还要设置成员dwServiceSpecificExitCode为服务特有的错误代码。当服务运行正常,没有错误可以报告的时候,就设置成员
dwWin32ExitCode为NO_ERROR。

  最后的两个成员dwCheckPoint和dwWaitHint是一个服务用来
报告它当前的事件进展情况的。当成员dwCurrentState被设置成SERVICE_START_PENDING的时候,应该把
dwCheckPoint设成0,dwWaitHint设成一个经过多次尝试后确定比较合适的数,这样服务才能高效运行。一旦服务被完全初始化,就应该重
新初始化SERVICE_STATUS结构的成员,更改dwCurrentState为SERVICE_RUNNING,然后把dwCheckPoint
和dwWaitHint都改为0。

  dwCheckPoint成员的存在对用户是有益的,它允许一个服务报告它处于进程的哪一步。每一
次调用SetServiceStatus时,可以增加它到一个能指明服务已经执行到哪一步的数字,它可以帮助用户决定多长时间报告一次服务的进展情况。如
果决定要报告服务的初始化进程的每一步,就应该设置dwWaitHint为你认为到达下一步所需的毫秒数,而不是服务完成它的进程所需的毫秒数。

 
 在服务的所有初始化都完成之后,服务调用SetServiceStatus指明SERVICE_RUNNING,在那一刻服务已经开始运行。通常一个服
务是把自己放在一个循环之中来运行的。在循环的内部这个服务进程悬挂自己,等待指明它下一步是应该暂停、继续或停止之类的网络请求或通知。当一个请求到达
的时候,服务线程激活并处理这个请求,然后再循环回去等待下一个请求/通知。

  如果一个服务由于一个通知而激活,它会先处理这个通知,
除非这个服务得到的是停止或关闭的通知。如果真的是停止或关闭的通知,服务线程将退出循环,执行必要的清除操作,然后从这个线程返回。当
ServiceMain线程返回并中止时,引起在StartServiceCtrlDispatcher内睡眠的线程激活,并像在前面解释过的那样,减少
它运行的服务的计数。

[转载]Windows服务编写原理及探讨(2)的更多相关文章

  1. [转载]Windows服务编写原理及探讨(4)

    (四)一些问题的讨论 前面几章的内容都是服务的一些通用的编写原理,但里面隐含着一些问题,编写简单的服务时看不出来,但遇到复杂的应用就会出现一些问题,所以本章就是用来分析.解决这些问题的,适用于高级应用 ...

  2. [转载]Windows服务编写原理及探讨(1)

    有那么一类应用程序,是能够为各种用户(包括本地用户和远程用户)所用的,拥有用户授权级进行管理的能力,并且不论用户是否物理的与正在运行该应用程序的计算机相连都能正常执行,这就是所谓的服务了. (一)服务 ...

  3. [转载]Windows服务编写原理及探讨(3)

    (三)对服务的深入讨论之下 现在我们还剩下一个函数可以在细节上讨论,那就是服务的CtrlHandler函数. 当调用RegisterServiceCtrlHandler函数时,SCM得到并保存这个回调 ...

  4. C# windows 服务编写及安装

      最近项目中用到window服务程序,以前没接触过,比较陌生,花了两天的时间学习了下,写了个简单的服务,但在制作安装程序的时候,参照网上很多资料,却都制作不成功,可能是开发环境或项目配置的不同,这里 ...

  5. windows服务编写和“以管理员运行”程序的方法

    本文将首先解释如何 创建 一个定期查询可用物理内存并将结果写入某个文本文件的服务.然后指导你完成生成,安装和实现服务的整个过程. 第一步:主函数和全局定义 首先,包含所需的头文件.例子要调用 Win3 ...

  6. C#开发Windows服务 附简单实例实现禁止QQ运行

    本实例主要实现下面三个基本功能 1.C#开发windows服务 2.禁止QQ等程序运行 3.为windows服务创建自动安装程序 下面针对这三个基本功能进行实现 一.C#开发windows服务 Win ...

  7. C#曲线分析平台的制作(六,Sqldependency+Signalr+windows 服务)

    在经过五天的学习和资料收集后,终于初步实现了利用sqldependency进行数据库变化监控,signalr进行前后台交互,数据实时更新.下面将源代码贴出进行初步分析: 1.系统整体框架构成: 2.具 ...

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

    Windows服务Debug版本 注册 Services.exe -regserver 卸载 Services.exe -unregserver Windows服务Release版本 注册 Servi ...

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

    原文:玩转Windows服务系列——Debug.Release版本的注册和卸载,及其原理 Windows服务Debug版本 注册 Services.exe -regserver 卸载 Services ...

随机推荐

  1. Fibsieve`s Fantabulous Birthday LightOJ - 1008(找规律。。)

    Description 某只同学在生日宴上得到了一个N×N玻璃棋盘,每个单元格都有灯.每一秒钟棋盘会有一个单元格被点亮然后熄灭.棋盘中的单元格将以图中所示的顺序点亮.每个单元格上标记的是它在第几秒被点 ...

  2. Proving Equivalences UVALive - 4287(强连通分量 水题)

    就是统计入度为0 的点 和 出度为0 的点  输出 大的那一个,, 若图中只有一个强连通分量 则输出0即可 和https://www.cnblogs.com/WTSRUVF/p/9301096.htm ...

  3. 【bzoj3295】[Cqoi2011]动态逆序对 树套树 线段树套替罪羊树

    这个东西,关于某个数的逆序对数为在他之前比他大的在他之后比他小的数的数目的和,所以删除之前先把这个减去就好了 人傻自带超大常数,中间由于内存池开小了所以运行错误. #include<cstdio ...

  4. JS中数组和字符串具有的方法,以及substring,substr和slice的用法与区别

     String 对象属性 属性 描述 constructor 对创建该对象的函数的引用 length 字符串的长度 prototype 允许您向对象添加属性和方法 String 对象方法 方法 描述 ...

  5. 【刷题】洛谷 P4716 【模板】最小树形图

    题目背景 这是一道模板题. 题目描述 给定包含 \(n\) 个结点, \(m\) 条有向边的一个图.试求一棵以结点 \(r\) 为根的最小树形图,并输出最小树形图每条边的权值之和,如果没有以 \(r\ ...

  6. C内存对齐问题-bus error!总线错误!其实是 字符串字面量修改问题!

    最近写个小程序,出现bus error! int main(void) { /** * char :1个字节 * char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也 ...

  7. 【bzoj2759】一个动态树好题

    Portal -->bzoj2759 Solution 哇我感觉这题真的qwq是很好的一题呀qwq 很神qwq反正我真的是自己想怎么想都想不到就是了qwq 首先先考虑一下简化版的问题应该怎么解决 ...

  8. windows环境下封装条件wait和signal

    linux 环境有提供好的pthread_cond_wait() 和 phread_signal().pthread_broadcast() windows需要自己封装,利用semophore控制线程 ...

  9. redis 字符串的管理的一点理解

    redis字符串可以实现通过地址偏移找到所在结构体的首地址,struct sdshdr *sh = (void *)(s - (sizeof(struct sdshdr))) 也就是通过buf地址可以 ...

  10. 3.UiObejct API 详细介绍

    一.点击与长按 1.组件区域位置关系: Rect 对象代表一个矩形区域:[left,Top][ARight,Bottom](即左上角图标到右下角图标) 2.点击与长按相关API: 返回值 API 说明 ...