前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Windows服务编程

Windows服务编程

作者头像
欧阳大哥2013
发布2018-10-25 15:00:02
1.7K0
发布2018-10-25 15:00:02
举报

一、服务(Service) 服务程序是NT系统支持的一种可执行文件,通常服务程序不与用户进行交互,在系统启动时会自动启动服务程序。所有的服务程序都由SCM进行管理,每个服务程序必须符合SCM所定义的规范。

二、SCM(Service control manager) 服务控制管理器会在系统启动时运行.它维护着一个数据库,这个数据库中记录中系统所安装的所有的服务和驱动服务。SCM提供了一个统一和安全的机制来控制所有的服务。它控制着每个服务的建立,删除,启动,停止,控制着与服务进行通信等功能。SCM通过提供一组接口来操作服务。因此可以将SCM看成是一个组件,所有对SCM操作的函数,在内部都是通过RPC来与SCM通信。SCM的接口可以实现三类程序:

Service programs: 服务程序本身,服务程序的建立方法和运行必须符合SCM的规定

Service configuration program: 服务配置程序通过SCM提供的API来将操作SCM所维护的数据库,它可以实现将一个服务加入到数据库,将服务 从数据库中删除,修改数据库中的服务信息,查询数据库的服务信息

Service control program: 服务控制程序通过SCM提供的一组API,通过这组API来让SCM控制服务的启动,暂停,继续等功能。

三、服务和驱动服务数据库 数据库放在注册表的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services的部分,系统所安装的每个服务和驱动服务都在这个键中有一个子键来记录服务的信息.子键名就是服务的服务名。每个服务的安装通过CreateService来实现。每个服务在注册表中包含的信息如下:

1.服务类型 2.启动类型 3.错误控制级别 4.执行文件的全路径,若为服务则是EXE文件,若为驱动服务则为SYS文件 5.可选的服务相关性信息 6.可选的帐号名,密码 7.可选的设备驱动对象名

四、服务程序接口

1.服务的启动是由SCM来完成的,启动时SCM通过获取服务在注册表中的可执行文件名,来建立一个新的进程。为了能调度进程中的服务,使其与SCM进程通信,因此在进程建立时,需要将进程的主线程作为服务的调度器。因此需要在进程的主程序入口处调用函数StartServiceCtrlDispatcher这个函数定义如下:

代码语言:javascript
复制
BOOL StartServiceCtrlDispatcher(
  CONST LPSERVICE_TABLE_ENTRY lpServiceTable   // service table
);

这个函数将进程的主线程与SCM建立连接关系,使得进程的主线程成为进程的服务控制调度线程。所谓服务控制调度线程就是控制着服务线程启动和结束善后的线程。它在服务启动时负责与SCM进行通信。这个函数接受一个SERVICE_TABLE_ENTRY结构数组,这个结构定义如下:

代码语言:javascript
复制
typedef struct _SERVICE_TABLE_ENTRY { 
  LPTSTR lpServiceName;    //服务的名称
  LPSERVICE_MAIN_FUNCTION lpServiceProc;   //服务的入口函数
} SERVICE_TABLE_ENTRY, *LPSERVICE_TABLE_ENTRY; 

在传递这个数组给函数时,要求最后一个元素的两个成员的值都为NULL。这个数组用来表示进程将要启动的服务,每个服务的入口点函数是由lpServiceProc函数指定的。通过上面描述可以得出服务的启动步骤

(1).当某个服务控制程序调用StartService函数来启动一个服务时,SCM通过服务在注册表中的可执行文件来建立一个进程,并执行主线程代码 (2).主线程代码通过调用StartServiceCtrlDispatcher函数来控制服务的启动,它接受一个服务入口表。然后通过启动的服务名检查应该启动该进程的那个服务。确定了后将为那个要启动的服务建立一个新的线程,然后让线程执行指定的服务的入口函数lpServiceProc,并将StartService函数所指定的两个服务入口参数传递给服务的入口点函数lpServiceProc,然后主线程进入等待一直到服务的线程结束后,StartServiceCtrlDispatcher函数才返回。函数若正确返回TRUE,失败返回FALSE。

调用StartServiceCtrlDispatcher函数一般是在主线程的开始就执行,并且函数返回后主线程也执行,但在编程时可以灵活而定,例如可以在调用前进行一些初始化操作,也可以在调用后执行一些代码等。

2.服务入口函数. 每个服务必须有一个服务入口函数,服务入口函数就是服务的执行体。其实每个服务在运行时都是一个新的线程。而服务入口点函数由自己定义。它的格式如下:

代码语言:javascript
复制
VOID WINAPI ServiceMain(
  DWORD dwArgc,     // number of arguments
  LPTSTR *lpszArgv  // array of arguments
);

其中函数的这两个参数时由调用StartService函数传递的。

3.服务控制请求的处理机制 当服务运行时,SCM可以控制服务的启动和暂停,和继续运行等,因此服务中必须有一种方法来处理SCM中的上面的控制请求。以完成相应的操作。这可以通过调用函数:

代码语言:javascript
复制
SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler(
  LPCTSTR lpServiceName,             // service name
  LPHANDLER_FUNCTION lpHandlerProc   // handler function
);

上面的函数将为每个服务指定一个控制请求处理函数,这个函数是用户自定义的函数,它用来处理当SCM发送某个控制请求给服务时,应该做的相应的操作。这个函数的格式如下:

代码语言:javascript
复制
VOID WINAPI Handler(
  DWORD fdwControl   // requested control code
);

fdwControl:是SCM发送给服务的控制请求。这个值可以为如下: SERVICE_CONTROL_STOP : 请求这个服务停止 SERVICE_CONTROL_PAUSE : 请求这个服务暂停 SERVICE_CONTROL_CONTINUE :请求这个服务继续执行 SERVICE_CONTROL_INTERROGATE : 请求服务立即更新其在SCM中的状态信息 SERVICE_CONTROL_SHUTDOWN : 请求执行一个清理任务,计算机正在关机时会发出这个请求,这个请求将等待20秒,若20秒未处理完则关闭计算机 SERVICE_CONTROL_PARAMCHANGE Windows 2000: 通知服务,传递给服务入口的启动参数已经改变, SERVICE_CONTROL_NETBINDADD Windows 2000: SERVICE_CONTROL_NETBINDREMOVE Windows 2000: SERVICE_CONTROL_NETBINDENABLE Windows 2000: SERVICE_CONTROL_NETBINDDISABLE Windows 2000: 128--255: 服务可以向SCM注册自己特定的请求.

函数RegisterServiceCtrlHandler函数应该在每个服务的入口函数的最开始处进行调用。每当SCM一个请求到达时,进程的服务控制调度线程就会将相应的控制代码发送给某个服务所注册的请求处理函数中去。每当服务的请求控制处理函数处理了一个请求后,都应该将服务的当前状态告诉SCM这可以通过调用SetServiceStatus函数来实现。RegisterServiceCtrlHandler函数返回一个句柄,这个句炳用户不要去删除它,可以不管它,函数调用失败返回0

RegisterServiceCtrlHandler的扩展函数为:

代码语言:javascript
复制
SERVICE_STATUS_HANDLE RegisterServiceCtrlHandlerEx(
  LPCTSTR lpServiceName,                // name of service
  LPHANDLER_FUNCTION_EX lpHandlerProc,  // handler function
  LPVOID lpContext                      // user data
);

请求控制处理函数的格式为:

代码语言:javascript
复制
DWORD WINAPI HandlerEx(
  DWORD dwControl,     // requested control code
  DWORD dwEventType,   // event type
  LPVOID lpEventData,  // event data
  LPVOID lpContext     // user-defined context data
);

4.服务的状态信息 每当一个服务启动时SCM都会为服务保存一条记录,这条记录包括的内容如下:

(1).服务名 (2).服务启动的类型(自动,手动,禁用) (3).SERVICE_STATUS结构,用来描述服务的状态 (4).服务所依赖的属性列表

尤其是SERVICE_STATUS结构,SCM可以通过这个结构来描述服务的当前的一些状态。因此在服务运行的时候为了能让SCM有效的管理服务,需要将服务的当前的状态随时反映给SCM,这些服务运行的状态例如: 正在启动,已经启动,正在运行,正在关闭,已经关闭等等。因此在服务的运行过程中需要随时的向SCM报告当前的状态。这可以通过函数:

代码语言:javascript
复制
BOOL SetServiceStatus(
  SERVICE_STATUS_HANDLE hServiceStatus, // service status handle
  LPSERVICE_STATUS lpServiceStatus      // status buffer
);

hServiceStatus: 由函数RegisterServiceCtrlHandler调用获取。

lpServiceStatus:一个SERVICE_STATUS结构,这个结构定义如下:

代码语言:javascript
复制
typedef struct _SERVICE_STATUS { 
  DWORD dwServiceType;   //服务的类型
  DWORD dwCurrentState;  //
  DWORD dwControlsAccepted; 
  DWORD dwWin32ExitCode; 
  DWORD dwServiceSpecificExitCode; 
  DWORD dwCheckPoint; 
  DWORD dwWaitHint; 
} SERVICE_STATUS, *LPSERVICE_STATUS; 

dwCurrentState:服务的当前状态,可以如下一个:

SERVICE_STOPPED: 服务已经停止了 SERVICE_START_PENDING: 服务正在启动 SERVICE_STOP_PENDING: 服务正在停止 SERVICE_RUNNING: 服务正在运行 SERVICE_CONTINUE_PENDING : 服务正在继续运行,这主要是针对服务被暂停的情况 SERVICE_PAUSE_PENDING :服务正在暂停 SERVICE_PAUSED :服务已经暂停

dwControlsAccepted:表明服务可以接受的请求控制的类型,这个值可以联合如下:

SERVICE_ACCEPT_STOP: 服务可以接受到SERVICE_CONTROL_STOP SERVICE_ACCEPT_PAUSE_CONTINUE: 服务可以接受SERVICE_CONTROL_PAUSE 和SERVICE_CONTROL_CONTINUE SERVICE_ACCEPT_SHUTDOWN: 可以接受SERVICE_CONTROL_SHUTDOWN SERVICE_ACCEPT_PARAMCHANGE: 可以接受SERVICE_CONTROL_PARAMCHANGE SERVICE_ACCEPT_NETBINDCHANGE: SERVICE_ACCEPT_POWEREVENT: 可以接受SERVICE_CONTROL_POWEREVENT SERVICE_ACCEPT_NETBINDCHANGE:可以接受网络组件变化的所有请求.

dwWin32ExitCode: 错误代码,是WIN32的错误代码,表示服务在状态改变过程中出现一个错误,当没有错误可以传递NO_ERROR,当这个值为: ERROR_SERVICE_SPECIFIC_ERROR表示这是一个服务特定的代码,具体代码值可以由下面成员指定.

dwServiceSpecificExitCode: 服务特定的错误代码

dwCheckPoint: dwWaitHint:

要获取服务的状态信息可以调用函数:

代码语言:javascript
复制
BOOL QueryServiceStatus(
  SC_HANDLE hService,               // handle to service
  LPSERVICE_STATUS lpServiceStatus  //[OUT] service status
);


BOOL QueryServiceStatusEx(
  SC_HANDLE hService,       // handle to service
  SC_STATUS_TYPE InfoLevel, // information level,枚举量目前为SC_STATUS_PROCESS_INF
  LPBYTE lpBuffer,          //[OUT] buffer ,目前可以为SERVICE_STATUS_PROCESS结构指针.
  DWORD cbBufSize,          // size of buffer
  LPDWORD pcbBytesNeeded    //[OUT] bytes needed
);


typedef struct _SERVICE_STATUS_PROCESS {
  DWORD  dwServiceType;
  DWORD  dwCurrentState;
  DWORD  dwControlsAccepted;
  DWORD  dwWin32ExitCode;
  DWORD  dwServiceSpecificExitCode;
  DWORD  dwCheckPoint;
  DWORD  dwWaitHint;
  DWORD  dwProcessId;            //进程ID
  DWORD  dwServiceFlags;       //SERVICE_RUNS_IN_SYSTEM_PROCESS表示这个服务属于系统进程,为0表示不是系统进程中的服务
} SERVICE_STATUS_PROCESS, *LPSERVICE_STATUS_PROCESS;

五、服务配置程序接口 服务必须由SCM来启动,而且服务也必须建立在数据库中。因此在使用服务时,首先必须将服务建立在数据库中。这可以通过函数:

(1). 服务的建立

代码语言:javascript
复制
SC_HANDLE CreateService(
  SC_HANDLE hSCManager,       // handle to SCM database 
  LPCTSTR lpServiceName,      // name of service to start
  LPCTSTR lpDisplayName,      // display name
  DWORD dwDesiredAccess,      // type of access to service
  DWORD dwServiceType,        // type of service
  DWORD dwStartType,          // when to start service
  DWORD dwErrorControl,       // severity of service failure
  LPCTSTR lpBinaryPathName,   // name of binary file
  LPCTSTR lpLoadOrderGroup,   // name of load ordering group,可为NULL
  LPDWORD lpdwTagId,          //[OUT] tag identifier,可为NULL
  LPCTSTR lpDependencies,     // array of dependency names ,可为NULL
  LPCTSTR lpServiceStartName, // account name, 可为NULL
  LPCTSTR lpPassword          // account password,可为NULL
);  //必须是系统管理员登陆,且具有SC_MANAGER_CREATE_SERVICE权限的SCM句柄

hSCManager: 这是一个SCM数据库句柄,由函数OpenSCManager来获取 lpServiceName: 服务名,每个服务必须有一个唯一的服务名,服务名使用不分大小写。最长为256个字符 lpDisplayName: 用于描述一个服务的显示名,这个名称主要用来在某个界面程序中,用做服务的显示的名称

dwDesiredAccess:访问安全设置

dwServiceType: 指定服务的类型可以为如下一个:

SERVICE_WIN32_OWN_PROCESS :表示这个程序只有一个服务 SERVICE_WIN32_SHARE_PROCESS : 表示一个程序中有多个服务 SERVICE_KERNEL_DRIVER:这是一个驱动服务 SERVICE_FILE_SYSTEM_DRIVER: 这是一个文件系统驱动服务 SERVICE_INTERACTIVE_PROCESS:表示服务是否可以在桌面上提供用户界面,并且无论谁登陆系统都能使用这个服务。这个选项可以联合上面的 1,2个选项,并且只能在帐号为LocalSystem的服务中使用

dwStartType:指定服务的启动类型可以如下一个:

SERVICE_BOOT_START :用于驱动服务 SERVICE_SYSTEM_START :用于驱动服务 SERVICE_AUTO_START : 在系统启动时自动启动 SERVICE_DEMAND_START: 手动启动 SERVICE_DISABLED : 禁止启动

dwErrorControl: 确定服务启动失败的严重性,并且确定服务启动失败时系统做出的响应,可以如下一个值:

SERVICE_ERROR_IGNORE: 启动失败时会将错误记录到一个日志中,并继续尝试启动 SERVICE_ERROR_NORMAL: 失败时弹出一个对话框,并继续尝试启动 SERVICE_ERROR_SEVERE: ?? SERVICE_ERROR_CRITICAL: ????

lpBinaryPathName: 这个服务所在的程序文件的路径,若程序文件的路径中有空格,则必须用""来引用程序

lpLoadOrderGroup: 指定这个服务所属的组名,一般为驱动服务所用

lpdwTagId: 输出这个服务在这个组中的标号

lpDependencies: 指定这个服务所依赖的服务,这是一个符串,它的格式为:"servicename<null>servicename<null>servicename<null><null>

lpServiceStartName: 若为NULL则表示是本地帐号LocalSystem,对于共享进程服务来说必须为NULL,而对于独占进程的服务来说,可以指定一个 帐号,帐号的格式为: domainname\username 或者为.\username

lpPassword: 帐号的密码,LocalSystem帐号的密码必须为NULL

这个函数失败返回NULL,这个函数将会在注册表的HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services下面建立一个服务

(2).连接到SCM并且获取数据库句柄.

代码语言:javascript
复制
SC_HANDLE OpenSCManager(
  LPCTSTR lpMachineName,   // computer name
  LPCTSTR lpDatabaseName,  // SCM database name
  DWORD dwDesiredAccess    // access type
);

这个函数首先跟指定计算机上的SCM建立连接,所有对服务的操作之前都必须跟SCM建立连接。建立连接后返回SCM数据库的句柄。

lpMachineName:指定要连接的计算机名,若为NULL则为本机。指定目标计算机的格式为: "\computername" lpDatabaseName: 指定SCM数据库的名称,应该设置为SERVICES_ACTIVE_DATABASE,或NULL,而NULL就是代表SERVICES_ACTIVE_DATABASE dwDesiredAccess: 指定对SCM具有的访问权限,其中SC_MANAGER_CONNECT总是隐含的指定,这个值可以联合如下:

SC_MANAGER_ALL_ACCESS: 全部权限 SC_MANAGER_CONNECT: 连接到SCM的权限 SC_MANAGER_CREATE_SERVICE: 建立服务的权限 SC_MANAGER_ENUMERATE_SERVICE: 列举服务的权限 SC_MANAGER_LOCK: 具有锁定SCM数据库的权限 SC_MANAGER_QUERY_LOCK_STATUS: 查询数据库的锁定状态的权限

GENERIC_READ: STANDARD_RIGHTS_READ|SC_MANAGER_ENUMERATE_SERVICE|SC_MANAGER_QUERY_LOCK_STATUS. GENERIC_WRITE: STANDARD_RIGHTS_WRITE|SC_MANAGER_CREATE_SERVICE. GENERIC_EXECUTE: STANDARD_RIGHTS_EXECUTE|SC_MANAGER_CONNECT|SC_MANAGER_LOCK.

函数失败返回NULL,最后错误可能如下:

ERROR_ACCESS_DENIED : 访问禁止 ERROR_DATABASE_DOES_NOT_EXIST : 数据库不存在 ERROR_INVALID_PARAMETER:无效参数

(3).SC_HANDLE句柄的关闭 要关闭一个SCM数据库句柄或Serivce句柄需要调用:

代码语言:javascript
复制
BOOL CloseServiceHandle(
  SC_HANDLE hSCObject   // handle to service or SCM object
);

(4).从SCM数据库中打开服务

代码语言:javascript
复制
SC_HANDLE OpenService(
  SC_HANDLE hSCManager,  // handle to SCM database
  LPCTSTR lpServiceName, // service name
  DWORD dwDesiredAccess  // access
);

(5).从SCM数据库中删除服务

代码语言:javascript
复制
BOOL DeleteService(
  SC_HANDLE hService   // handle to service
);

(6).获取服务的显示名

代码语言:javascript
复制
BOOL GetServiceDisplayName(
  SC_HANDLE hSCManager,  // handle to SCM database
  LPCTSTR lpServiceName, // service name
  LPTSTR lpDisplayName,  // display name
  LPDWORD lpcchBuffer    //[IN/OUT] size of display name buffer
);

(7).获取服务的服务名

代码语言:javascript
复制
BOOL GetServiceKeyName(
  SC_HANDLE hSCManager,  // handle to SCM database
  LPCTSTR lpDisplayName, // display name,大小写不敏感
  LPTSTR lpServiceName,  //[OUT] service name
  LPDWORD lpcchBuffer    //[IN/OUT] size of service name buffer
);

(8).获取设置服务在注册表中的配置信息

代码语言:javascript
复制
BOOL QueryServiceConfig(
  SC_HANDLE hService,                     // handle to service,必须有SERVICE_QUERY_CONFIG权限
  LPQUERY_SERVICE_CONFIG lpServiceConfig, //[OUT] buffer
  DWORD cbBufSize,                        // size of buffer
  LPDWORD pcbBytesNeeded                  //[OUT] bytes needed,实际输出的字节数
);


typedef struct _QUERY_SERVICE_CONFIG { 
  DWORD dwServiceType; 
  DWORD dwStartType; 
  DWORD dwErrorControl; 
  LPTSTR lpBinaryPathName; 
  LPTSTR lpLoadOrderGroup; 
  DWORD dwTagId; 
  LPTSTR lpDependencies; 
  LPTSTR lpServiceStartName; 
  LPTSTR lpDisplayName; 
} QUERY_SERVICE_CONFIG, *LPQUERY_SERVICE_CONFIG;    //这个结构保存的是建立时所指定的各种数据

要更改服务在注册表中的配置可以调用函数:

代码语言:javascript
复制
BOOL ChangeServiceConfig(
  SC_HANDLE hService          // handle to service
  DWORD dwServiceType,        // type of service
  DWORD dwStartType,          // when to start service
  DWORD dwErrorControl,       // severity of start failure
  LPCTSTR lpBinaryPathName,   // service binary file name
  LPCTSTR lpLoadOrderGroup,   // load ordering group name
  LPDWORD lpdwTagId,          // tag identifier
  LPCTSTR lpDependencies,     // array of dependency names
  LPCTSTR lpServiceStartName, // account name
  LPCTSTR lpPassword,         // account password
  LPCTSTR lpDisplayName       // display name
);  //若在更改时服务正在运行,则除了显示名能够马上更改之外,其他的更改要等到服务停止后才能产生效果

(9).获取和设置服务在注册表中的可选扩展信息.

代码语言:javascript
复制
BOOL ChangeServiceConfig2(
  SC_HANDLE hService,  // handle to service,需有SERVICE_CHANGE_CONFIG权限
  DWORD dwInfoLevel,   // information level
  LPVOID lpInfo        // new data
);

dwInfoLevel决定信息的内容,也就是lpInfo的内容。它可以如下:

SERVICE_CONFIG_DESCRIPTION: 为服务添加的描述信息,描述信息用来描述服务的特性。这是lpInfo为一个

代码语言:javascript
复制
typedef struct _SERVICE_DESCRIPTION {
  LPTSTR       lpDescription;  //描述字符串,不能超过1024个字符
} SERVICE_DESCRIPTION, *LPSERVICE_DESCRIPTION;  //结构的指针

SERVICE_CONFIG_FAILURE_ACTIONS: 用来指定当服务启动失败时所进行的活动。这时lpInfo是一个SERVICE_FAILURE_ACTIONS结构指针,定义:

代码语言:javascript
复制
typedef struct _SERVICE_FAILURE_ACTIONS {
  DWORD       dwResetPeriod;
  LPTSTR       lpRebootMsg;
  LPTSTR       lpCommand;
  DWORD       cActions;
  SC_ACTION * lpsaActions;
} SERVICE_FAILURE_ACTIONS, *LPSERVICE_FAILURE_ACTIONS;
代码语言:javascript
复制
BOOL QueryServiceConfig2(
  SC_HANDLE hService,     // handle to service
  DWORD dwInfoLevel,      // information level
  LPBYTE lpBuffer,        // buffer
  DWORD cbBufSize,        // size of buffer
  LPDWORD pcbBytesNeeded  // bytes needed
);
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.10.17 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档