selph
selph
Published on 2020-10-21 / 696 Visits
0
0

编写 Windows 服务程序

前言

想要了解服务程序的运行相关的基础知识,最好还是从最底层的API入手去动手实践一下,而不是用上层的封装,搭建一个简单的服务

由于某些原因,我需要编写一个服务程序,无奈没注释的示例代码看不懂,从往上零零散散的博客文章中找到了这么一个编写服务的练习,结合官方文档,终于,完成了简单的服务的搭建

本文用来记录一下服务程序是如何编写的

例子是:创建一个定期查询可用物理内存并将结果写入某个文本文件的服务。

服务程序介绍

服务程序主要有三部分构成:

  1. main程序:程序的入口函数,功能是创建分派表并启动控制分派机线程,启动控制分派机线程需要指定ServiceMain函数
  2. ServiceMain程序:负责初始化服务状态,注册指定控制处理器函数,更新报告服务状态,进入服务的功能部分
  3. 控制处理器程序:负责接收服务请求,进行处理,更新报告服务状态

编写服务程序

服务main函数

#include<Windows.h>
#include<stdio.h>

#define SLEEP_TIME 5000		//两次连续查询可用内存之间的毫秒间隔
#define LOGFILE "D:\\MyServicesLog.txt"		//日志文件的路径

//全局变量
SERVICE_STATUS ServiceStatus;	//服务状态
SERVICE_STATUS_HANDLE hStatus;

//服务程序main函数只用来创建分派表,和启动控制分派机
int main() {
	//创建分派表
	SERVICE_TABLE_ENTRY ServiceTable[2];
	ServiceTable[0].lpServiceName = (LPWSTR)L"MemoryStatus";
	ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;
	//这个结构体以空结尾
	ServiceTable[1].lpServiceName = NULL;
	ServiceTable[1].lpServiceProc = NULL;

	//启动控制分派机
	StartServiceCtrlDispatcher(ServiceTable);
}

服务主程序的功能非常简单,就做一件事。创建分派表并启动控制分派机线程

分派表是一个结构体数组SERVICE_TABLE_ENTRY,以空项作为结尾的标识

typedef struct _SERVICE_TABLE_ENTRYA {
  LPSTR                    lpServiceName;//服务名称,
  LPSERVICE_MAIN_FUNCTIONA lpServiceProc;//服务主函数
} SERVICE_TABLE_ENTRYA, *LPSERVICE_TABLE_ENTRYA;

https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_table_entrya

启动分派机函数:StartServiceCtrlDispatcherA

BOOL StartServiceCtrlDispatcherA(
  const SERVICE_TABLE_ENTRYA *lpServiceStartTable//指向SERVICE_TABLE_ENTRYA的指针,结构体最后一成员必须为空
);

功能是将服务进程主线程连接到服务控制管理器SCM,使该线程成为调用过程的服务控制调度程序线程,简单来说,就是创建一个新线程用来进行服务控制调度

当分派表中的所有服务之星完毕之后(服务为停止状态),或者发送运行时错误,该函数调用返回,进程终止

https://docs.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-startservicectrldispatchera

服务ServiceMain主函数

VOID ServiceMain(int argc, char** argv) {
	//设置服务状态
	//服务类型:创建Win32服务
	ServiceStatus.dwServiceType = SERVICE_WIN32;
	//服务当前状态,在这里的时候初始化未完成,所以pending,pending是啥???
	ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
	//通知SCM服务接收哪个域,处理控制请求后处理
	ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;//本例只接收系统关机和停止服务两种控制命令
	//终止服务并报告退出细节,初始化时不退出,进行置零
	ServiceStatus.dwWin32ExitCode = 0;
	ServiceStatus.dwServiceSpecificExitCode = 0;
	//表示初始化某个服务需要30s以上,因为这个服务很短,置零即可
	ServiceStatus.dwCheckPoint = 0;
	ServiceStatus.dwWaitHint = 0;
  
    ////////////////////////////////////////////////////////////////////////
  
	//为服务注册控制处理器SCM
	hStatus = RegisterServiceCtrlHandler(L"MemoryStatus", (LPHANDLER_FUNCTION)ControlHandler);
	if (!hStatus) {
		WriteToLog("RegisterServiceCtrlHandler Failed!\n");
		return;
	}
	WriteToLog("RegisterServiceCtrlHandler Success!\n");

	if (InitService() == FALSE) {
		ServiceStatus.dwCurrentState = SERVICE_STOPPED;
		ServiceStatus.dwWin32ExitCode = -1;
		SetServiceStatus(hStatus, &ServiceStatus); //向SCM报告服务出错的状态
		return;
	}

    //////////////////////////////////////////////////////////////////////////
    
	//向SCM报告运行服务状态
	ServiceStatus.dwCurrentState = SERVICE_RUNNING;
	BOOL bStatus = SetServiceStatus(hStatus, &ServiceStatus);
	if (!bStatus) {
		DWORD dwError = GetLastError();
		char szStr[100] = { 0 };
		sprintf_s(szStr,100,"SetServiceStatus Failed,The Error Value is %d",dwError);
		WriteToLog(szStr);
		return;
	}

	//启动任务循环,添加自己的代码

}

进入ServiceMain函数之后,首先需要设置服务的状态,也就是向SERVICE_STATUS结构体填充值

typedef struct _SERVICE_STATUS {
  DWORD dwServiceType;	//服务类型,SERVICE_WIN32表示创建Win32服务
  DWORD dwCurrentState;	//指定服务当前状态,初始化未完成时填SERVICE_START_PANDING
  DWORD dwControlsAccepted;	//这个通知SCM服务接收哪种控制
  DWORD dwWin32ExitCode;	//终止服务报告退出细节时用,不退出则为0
  DWORD dwServiceSpecificExitCode;//终止服务报告退出细节时用,不退出则为0
  DWORD dwCheckPoint;	//初始化要30s以上时候需要填,不然为0
  DWORD dwWaitHint;		//初始化要30s以上时候需要填,不然为0
} SERVICE_STATUS, *LPSERVICE_STATUS;

这里第一次初始化的时候每个值都要进行设置,初始化的时候主要就是设置前三个成员的值,后面全是0

然后之后更变服务状态的时候,主要就是改变退出码和服务状态然后报告给SCM

https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_status

初始化完服务的状态之后,需要为服务注册控制处理器,注册完之后返回状态句柄

SERVICE_STATUS_HANDLE RegisterServiceCtrlHandlerA(
  LPCSTR             lpServiceName,	//服务名称
  LPHANDLER_FUNCTION lpHandlerProc	//服务控制函数
);

https://docs.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-registerservicectrlhandlera

注册完控制处理器之后,需要向SCM进行报告

ServiceStatus.dwCurrentState = SERVICE_RUNNING;
BOOL bStatus = SetServiceStatus(hStatus, &ServiceStatus);

注册完控制处理器之后,服务状态更变为RUNNING,每次变更状态都需要向SCM进行报告

BOOL SetServiceStatus(
  SERVICE_STATUS_HANDLE hServiceStatus,//状态句柄
  LPSERVICE_STATUS      lpServiceStatus//服务状态结构体
);

https://docs.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-setservicestatus

报告完状态,没有问题的话,就可以开始写服务功能函数了

这里的WriteToLog函数是:

//将文本写入文件
BOOL WriteToLog(LPCSTR szText) {
	FILE* log = fopen(LOGFILE, "a+");//以向后添加的方式打开文件
	if (log == NULL) {
		return FALSE;
	}
	fprintf(log, "%s\n", szText);
	fclose(log);
	return TRUE;
}

服务功能是:

	//这里是每隔10s查询一次可用物理内存写入日志
	MEMORYSTATUS Memstatus;
	BOOL bRun = TRUE;
	while (bRun) {
		char szStr[100] = { 0 };
		GlobalMemoryStatus(&Memstatus);
		int iAvailMb = Memstatus.dwAvailPhys / 1024 / 1024;
		sprintf_s(szStr, 100, "Available Memory is %d MB", iAvailMb);
		WriteToLog(szStr);
		Sleep(SLEEP_TIME);
	}
	WriteToLog("Service Stopped");

服务初始化函数InitService:检查服务功能是否能够正常使用

//服务初始化
int InitService() {
	int result = WriteToLog("Monitoring Start");
	return result;
}

服务控制处理器

接下来就是服务控制处理器了

VOID ControlHandler(DWORD request) {
	//不管响应什么请求,都要报告状态
	switch (request) {
	case SERVICE_CONTROL_STOP: {//SCM终止服务的时候发送
		//写日志文件,停止监视,报告状态
		WriteToLog("Monitoring stopped.");

		ServiceStatus.dwCurrentState = SERVICE_STOPPED;
		ServiceStatus.dwWin32ExitCode = 0;
		SetServiceStatus(hStatus,&ServiceStatus);
		return;
		}
	case SERVICE_CONTROL_SHUTDOWN: {//电脑关机时发送的
		WriteToLog("Monitoring stopped.");

		ServiceStatus.dwCurrentState = SERVICE_STOPPED;
		ServiceStatus.dwWin32ExitCode = 0;
		SetServiceStatus(hStatus, &ServiceStatus);
		return;
	}
	default:break;
	}
	//不管响应什么请求,都要调用这个函数报告状态
	SetServiceStatus(hStatus, &ServiceStatus);
	return;
}

这个玩意有点像窗口过程(除此之外,还有调试循环也很像窗口过程)

函数获得请求信息,通过switch语句进行选择执行,执行不同的指令,设置不同的服务状态,并报告给SCM

这一块比较简单,用到的都是前面提过的结构体和API

生成服务&效果演示

到此,服务程序的代码已经写完了,接下来要让这个程序变成服务

通过cmd(要用管理员启动才行)的sc命令可以实现服务的创建与删除:

//服务的创建
sc create MemStatus binpath= D:\\MyFirstWindowsService.exe
//服务的删除
sc delete MemStatus
//服务的启动
sc start MemStatus
//服务的停止
sc stop MemStatus

image-20201021105110585

可通过运行services.msc来查看系统的服务

image-20201021104814435

通过Process Explorer也能查看到:

image-20201021105011096

这个SESSION 0是只有系统服务才有的

这个服务的功能是每10s查询一次可用物理内存并写入文件内:

image-20201021105159387

服务程序正常运行了!

参考资料


Comment