Windows Services 学习
一、学习点滴
1、本机服务查看:services.msc /s
2、服务手动安装(使用sc.exe):
sc create MemoryStatus binpath= c:MyServicesMemoryStatus.exe
sc delete MemoryStatus
3、把服务所在进程杀掉,服务也就停止了。
4、服务删除,错误信息:
D:>sc delete Sample_Srv
[SC] DeleteService FAILED 1072
D:>sc stop Sample_srv
[SC] ControlService FAILED 1052
解决:service中不能有MessageBox这样的UI框,对话框是不会弹出来的,但是程序会被阻塞,导致无法Stop、无法Delete服务,需要关机重启,服务才能被清除。另外,如果服务启动的进程是没有界面的,那么这个新进程的功能仍然是可以执行的。
5、如何使用DebugView调试
需要在Capture中选中Capture Global Win32
6、服务的界面。Interactive Services
最便捷的方式是使用 WTSSendMessage API 弹出一个对话框,无需任何额外工作。
如果是直接的 MessageBox API,或者是WinExec直接新启动一个GUI进程,需要在CreateService的时候Service类型参数带上SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS参数。但是,win7下任何UI都会导致显示“交互式服务检测”对话框。网络上的那段”打开用户的winsta0”而没有使用CreateProcessAsUser API 的代码在win7下一样有显示“交互服务监测”对话框的问题,是不靠谱的,估计是xp下的杰作。这个弹窗的问题是可以解决的,MSDN上给了解决方案——使用CreateProcessAsUser API 新启动一个进程做界面,服务进程跟界面进程可以使用命名管道做进程间通信。
如果没有设置上面说到的参数,则服务在启动子进程或者是孙进程的整个过程中,不能出现任何界面,不然服务就死了,而且还无法Stop、无法Delete服务,需要关机重启,服务才能被清除。
7、如果CreateProcessAsUser API使用了参数CREATE_NEW_CONSOLE,那么从Process Explorer就不会看到服务进程跟被启动进程的关联关系。
8、通过WTSSendMessage api显示对话框。
DWORD resp = 0;
WTSSendMessage(
WTS_CURRENT_SERVER_HANDLE,
WTSGetActiveConsoleSessionId(),
"Hello", 5,
"Hello_2", 7,
0, 0, &resp, false);
9、一个完善的例子,包括服务程序和界面程序,以及它们之间的通信。但是有些代码无法编译,看看思路就好。
Interaction between services and applications at user level in Windows Vista
http://www.codeproject.com/Articles/36581/Interaction-between-services-and-applications
10、非常完善的从服务启动GUI进程。
NT Services 2013
http://www.codeproject.com/Articles/640245/NT-Services-2013
10、服务开机启动:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms681957(v=vs.85).aspx
11、Sessions,Desktops and Windows Stations
http://blogs.technet.com/b/askperf/archive/2007/07/24/sessions-desktops-and-windows-stations.aspx
12、一个相当简单的服务程序:http://www.vckbase.com/index.php/wv/1193
13、一个包含了安装服务程序、服务程序的完整例子:http://blog.csdn.net/itcastcpp/article/details/7079574 (代码似乎是来自windows核心编程,或者是来自这里:http://bbs.pediy.com/showthread.php?t=114122)
14、一个监控子进程的服务,如果子进程有界面,则会显示出win7下的弹窗,这例子不太好,代码逻辑简单看看就行。
http://www.codeproject.com/Articles/16488/A-Windows-Service-Application
二、MSDN阅读
http://msdn.microsoft.com/en-us/library/ms685477(v=vs.85).aspx
服务入口点
服务程序一般写成控制台应用程序,main函数为入口函数,main函数的参数在CreateService函数中指定。当SCM启动一个服务程序时,SCM等待服务程序调用StartServiceCtrlDispatcher函数,如果服务进程没有及时调用该函数,则会导致启动服务失败(譬如,手动启动时,StartService函数失败)。对于StartServiceCtrlDispatcher函数调用的时机,有下面的规则:
1、如果服务程序是SERVICE_WIN32_OWN_PROCESS类型,服务程序必须在主线程立即调用StartServiceCtrlDispatcher,初始化的工作可以在服务启动之后做,也就是在服务主函数中做。
2、如果服务程序是SERVICE_WIN32_SHARE_PROCESS类型,并且初始化是属于本程序中所有的服务共有的,则可以在调用StartServiceCtrlDispatcher之前执行,但是最多只能执行30秒。可以考虑启动一个新线程去做公共的初始化任务。如果有服务独自的初始化任务,仍然需要在服务启动后在服务主函数中做。
StartServiceCtrlDispatcher函数的参数是SERVICE_TABLE_ENTRY结构体数组,结构体包含了:1、服务名称,这个名称只是在服务主函数中用上,如果服务是SERVICE_WIN32_OWN_PROCESS类型的,那么这个名称是可以忽略的;2、服务主函数指针。
如果StartServiceCtrlDispatcher函数执行成功,调用线程(也就是服务进程的主线程)不会返回,直到所有的服务进入到SERVICE_STOPPED状态。调用线程扮演着控制分发的角色,干这样的事情:1、在新的服务启动时启动新线程去调用服务主函数(主意:服务的任务是在新线程中做的);2、当服务有请求时(注意:请求是由SCM发给它的),调用它对应的处理函数(主意:这相当于主线程“陷入”了,它在等待控制消息并对消息做处理)。
demo代码所在:http://msdn.microsoft.com/en-us/library/windows/desktop/ms687416(v=vs.85).aspx
http://msdn.microsoft.com/en-us/library/ms685984(v=vs.85).aspx
服务主函数
当服务控制程序启动一个新的服务时,服务控制管理器(SCM)启动服务进程,然后服务进程发送开始请求到控制分配器,控制分配器创建一个新的线程去执行服务主函数。服务主函数需要做这样的事情:
1、初始化所有全局变量
2、调用RegisterServiceCtrlHandler API去注册服务的控制请求处理函数,函数的返回值是服务状态句柄,在通知SCM当前服务的状态时用上。
3、完成初始化。如果初始化时间很短(小于1秒),那么初始化可以直接在服务主函数中执行。否则,应该选择下边的一种方式做:
(1)调用SetServiceStatus API把服务状态设置为SERVICE_RUNNING,同时SERVICE_STATUS结构体中的dwControlsAccepted要设置为0,保证在初始化期间SCM不会发送控制请求给本服务,也让SCM可以去管理其他服务。推荐这种方式,以便提高性能,特别是自动运行服务。
(2)调用SetServiceStatus API把服务状态设置为SERVICE_START_PENDING,这时候不会接收控制请求,并且定义了一个超时时间。如果服务的初始化时间超过了预定义的等待时间,那么必须周期性的调用SetServiceStatus API重新定义超时时间,但要确保初始化确实是有进度的,因为此时SCM认为初始化是有进度的,它会阻塞其他服务的开启。如果超时了,而等待的进度信息未改变,SCM或者服务控制程序(可以是自己写的启动服务的程序)会认为发生了错误而需要终止服务(但是如果该服务与其他服务共享进程,则不会结束该服务所在进程)。使用这种方式初始化,可以根据实际进度周期性的调用SetServiceStatus API增加“检查点”的值,服务的启动者可以通过QueryServiceStatus或者QueryServiceStatusEx API去获得检查点的值,也就获得了服务初始化进度。
4、当初始化完成时,调用SetServiceStatus API进入到SERVICE_RUNNING状态、可以接收控制请求。
5、执行服务任务。如果当前没有任务(也就是服务主函数执行完了),控制权还给了调用者(也就是control dispatcher)。如果是单个服务调用 StartServiceCtrlDispatcher,服务主函数执行完了, StartServiceCtrlDispatcher还是不会返回的,要直到服务进入停止SERVICE_STOPPED状态才返回,进程才结束。一般情况下我们不会让出控制权,可以写个死循环去干活,反正主线程退出时会把服务主函数所在线程也结束掉的。另外,任何服务状态的改变都是通过SetServiceStatus做的。
6、如果在服务初始化、执行任务的过程中出现错误,服务在最后一个要被结束的线程中进入SERVICE_STOPPED状态,如果结束服务的清理工作时间很长,则需要进入 SERVICE_STOP_PENDDING状态,执行清理工作。可以通过SERVICE_STATUS结构体的dwServiceSpecificExitCode和dwWin32ExitCode参数告知外界错误信息。
demo代码所在:http://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx
http://msdn.microsoft.com/en-us/library/ms685149(v=vs.85).aspx
服务控制处理函数
每一个服务都有一个控制处理函数,当服务进程收到控制请求时,由控制分发器调用该函数,所以这个函数的是在控制分发器线程中执行的,也就是服务进程的主线程。当服务控制处理函数被调用时,可以通过SetServiceStatus API把服务设置进不同的状态(也就是通知SCM当前服务是什么状态)。
外界可以通过ControlService API 发送控制请求。所有的服务必须接收并处理SERVICE_CONTROL_INTERROGATE 控制码,可以通过SetServiceStatus API 设置接收哪些控制码,如果要接收SERVICE_CONTROL_DEVICEEVENT控制码,必须调用RegisterDeviceNotification API,服务也能接收用户自定义的控制码。
控制处理函数必须在30秒内返回(但是实际上我测试了Sleep40秒返回也不会导致SCM停止),当有长时间任务时,必须开始新线程去执行。譬如在停止的时候,应该让服务进入到SERVICE_STOP_PENDDING状态,开启新线程执行任务,让服务控制处理函数立即返回。
当系统关闭时,如果有设置了SERVICE_ACCEPT_PRESHUTDOWN,则控制处理函数能收到SERVICE_CONTROL_PRESHUTDOWN 控制码。SCM会等待所有服务停止,或者等待时间超过关机前通知超时值,超时值可以通过ChangeServiceConfig2 API 去设置。这个控制码使用时需要谨慎,避免阻塞关机。
在关机前通知执行完毕之后,所有设置了SERVICE_ACCEPT_SHUTDOWN的服务都能收到SERVICE_CONTROL_SHUTDOWN控制码,通知的顺序是它们安装服务时的顺序。一般情况下,一个服务在关机前有20秒钟的时间处理任务,超时后系统执行关机,无论服务是否完成任务。当系统处于关机状态时,服务仍然是在运行的。
如果一个服务需要更多的时间去做清理工作,它会发送STOP_PENDING状态并设置等待时间。但是,为了防止服务阻止关机,对超时有限制。
在关机期间,SCM发送关机消息时,不会考虑服务之间的依赖关系,所有一个服务可能会因为它所依赖的服务已经停止了而失败。可以手动或者通过API设置服务关闭的依赖关系。
demo代码所在:http://msdn.microsoft.com/en-us/library/windows/desktop/ms687413(v=vs.85).aspx
http://msdn.microsoft.com/en-us/library/ee126211(v=vs.85).aspx
服务状态转换
服务负责向SCM报告状态的改变。服务控制程序和系统只能从SCM获取到服务的状态。服务程序通过SetServiceStatus API设置服务状态。
服务的初始状态是SERVICE_STOPPED,当SCM启动服务后,服务状态进入SERVICE_START_PENDING状态、调用服务主函数,接着服务完成初始化、设置能接受的控制请求,接着通过SetServiceStatus通知SCM进入到SERVICE_RUNNING状态,如果进入的不是SERVICE_RUNNING状态,则SCM、服务监控工具会认为该服务启动失败。
SCM只会发送指定的控制请求给服务(除了SERVICE_CONTROL_INTERROGATE请求,它无需指定)。SERVICE_STATUS结构体的dwControlsAccepted成员指定了哪些控制请求是需求通知服务的。如果要收到设备事件,则需要通过RegisterDeviceNotification API设置。
通常是响应控制请求而去改变服务状态。会引起服务状态改变的控制请求包括:SERVICE_CONTROL_STOP、SERVICE_CONTROL_PAUSE、SERVICE_CONTROL_CONTINUE。如果服务响应时间很长,必须创建第二个线程去完成任务同时告诉SCM进入相应的pendding状态,完成任务之后再进入完成状态。为了更好的性能,vista以及之后版本的系统推荐使用thread pool。
服务状态有效转换图:
服务上报给SCM的状态决定了跟SCM的互动(也就是接下来SCM能发给服务的控制请求)。譬如,如果服务告诉SCM它进入了SERVICE_STOP_PENDDING状态,意味着这时候服务正在关闭,接下来服务只能是通知SCM进入SERVICE_STOPPED状态。
控制服务停止demo:http://msdn.microsoft.com/en-us/library/windows/desktop/ms686335(v=vs.85).aspx
http://msdn.microsoft.com/en-us/library/ms685145(v=vs.85).aspx
服务和注册表
服务不能访问HEKY_CURRENT_USER和HKEY_CLASSES_ROOT,应该使用RegOpenCurrentUser和RegOpenUserClassesRoot函数。
简结:
整个服务的流程可以理解为这样子:假设进程只有单个服务,SCM把服务进程拉起来,服务进程的主线程调用服务控制分发函数,这个函数直到服务停止或者服务开启失败才返回。服务控制分发函数启动新线程执行服务主函数,服务主函数会注册控制处理函数,这是个回调函数,由主线程去调用响应外界的通知,控制回调函数里根据通知把服务设置进不同的状态,这也是告诉SCM服务进入了另一个状态。当服务的状态进入到SERVICE_STOPPED状态时,服务控制分发函数返回,主线程结束,进程结束。
http://msdn.microsoft.com/en-us/library/windows/desktop/ms683502(v=vs.85).aspx
交互式服务
一般情况下,服务是一个没有界面的不需要交互的控制台程序。但是,一些服务有时候要求跟使用者有交互。
在vista系统中服务无法直接跟用户交互。
用户与服务间接交互。
在windows支持的所有版本上,可以使用下边的技术实现:
1、使用WTSSendMessage函数给用户显示对话框。
2、创建一个隐藏的GUI应用程序,使用CreateProcessAsUser API使应用程序在可以跟用户交互的环境下运行。GUI程序使用IPC跟服务程序做数据通信,如果通信使用的是命名管道,那么通过提供基于会话ID的管道名,服务能够区分多用户的进程。
使用交互式服务。
默认情况下服务使用不可交互的“窗口站”,无法跟用户交互。但是,一个可交互的服务能够显示用户界面和接收用户输入。
......
这种方式显示的窗口,是会弹窗提示的。没啥用。
最后文档也说了:所有的服务运行在终端服务session 0。因此,如果一个交互服务显示用户界面,只能是那些进入到session 0的用户才能看到,所有在win7下才会显示一个提示框,让用户进入到另一个界面,也就是session 0。
三、总结
注意到,从服务中启动GUI进程这个可以解决win7下程序要求有管理员权限时,UAC弹框的问题。而服务启动GUI进程,主要需要使用CreateProcessAsUser API。无法让服务进程弹出漂亮的窗口,最多只能直接弹出一个MessageBox,如果想要使用服务的高权限来做些事情,又想要有漂亮的用户界面,则可以让UI进程借服务进程干活,UI进程负责与用户交互,这需要涉及进程间通信(IPC),使用命名管道也很容易实现一个demo,这里就不说了。
服务demo代码所在:https://github.com/cswuyg/simple_win/tree/master/my_service_example,包括了:服务进程、服务中启动GUI进程、服务控制进程的代码。
2014.3 补充
1、服务编程:http://msdn.microsoft.com/en-us/library/windows/desktop/ms685969(v=vs.85).aspx
2、一个完整的服务demo:http://msdn.microsoft.com/en-us/library/bb540476(v=vs.85).aspx
3、服务控制回调函数的响应:
必须在Control Handle函数里正确、及时通知SCM本服务状态的改变。
用户通过服务面板将服务设置为某种状态之后,系统通过检查SCM里该服务的状态是否进入用户想要的状态,来确定操作是否有效,所以,对于服务来说,只需要在Control Handle按照通知码修改对应的状态,就行了,服务是否真的停止了,就看我们程序的意愿了。
如果用户操作了“停止”服务,但是我们的程序没有通知SCM本服务状态改变,就会导致出“Windows 无法停止....服务”的窗口,这种情况发生两次之后,服务进程退出。
4、如何修改服务的一些属性,包括Enabled、Disabled、开机运行服务(这个也可以在CreateService的时候设置SERVICE_AUTO_START)、描述信息:http://msdn.microsoft.com/en-us/windows/desktop/ms682006(v=vs.100).aspx
5、 在编写开机运行服务的时候,需要有自己的log,才能定位是哪块没有执行,windows本身也有日志,但信息太少,windows日志所在:命令行运行运行compmgmt.msc,定位到系统工具事件查看器Windows日志系统,可以看到服务启动相关的信息。在我编写的时候,出现过这种服务无法开机启动的错误:“等待 cswuyg_Svc 服务的连接超时(30000 毫秒)”。多次测试之后,发现把服务进程EXE从Debug版换成Release版本就解决,估计是Debug模式下的性能太差,导致service main函数线程无法及时启动(从log看出service main函数没有被执行到)。