编写Windows服务程序

注意:windows上的服务通常是控制台应用程序,即程序一般是没有界面的。

编写服务程序主要关注四个点:

  • 状态反馈

  • 服务入口点(Service Entry Point)

  • 服务主函数(Service ServiceMain Function)

  • 服务控制函数(Service Control Handler Function)

按照我的理解,状态反馈即将程序当前的状态反馈给SCM。服务入口点即为服务从哪里开始。服务主函数即为服务启动后最先执行的函数。服务控制函数即为服务运行中接收各种命令的处理函数,比如响应停止,关闭等。

状态反馈

状态反馈是通过SetServiceStatus函数完成的,需要将程序连接到SCM后使用。连接SCM的操作通过StartServiceCtrlDispatcher函数完成。

当程序连接到SCM之后,需要使用RegisterServiceCtrlHandlerEx函数注册服务控制函数,同时该函数会返回当前程序的状态信息句柄。

SetServiceStatus函数需要输入两个参数,第一个就是当前程序的状态信息句柄(类型为SERVICE_STATUS_HANDLE),第二个就是程序最近的状态信息指针(类型为LPSERVICE_STATUS),注意这两个参数的区别。

服务入口点

当主线程启动后(main函数),应该在主线程中马上调用StartServiceCtrlDispatcher函数,这个函数会将主线程连接到SCM。注意相关初始化工作最好在服务主函数中进行。代码如下:

#include <Windows.h>

#define SERVICENAME L"Your Service Name"

void ServiceMain(int argc, char** argv)
{

}

int main()
{
    WCHAR ServiceName[] = SERVICENAME;
    SERVICE_TABLE_ENTRY DispatchTable[2];
    // 服务名字
    DispatchTable[0].lpServiceName = ServiceName;
    // 服务主函数
    DispatchTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;

    // 注意需要加上下面两行,因为StartServiceCtrlDispatcher规定最后一个元素必须为NULL
    DispatchTable[1].lpServiceName = NULL;
    DispatchTable[1].lpServiceProc = NULL;

    // 服务入口点
    StartServiceCtrlDispatcher(DispatchTable);
    return 0;
}

虽然ServiceMain作为服务主函数名不是唯一的,但是微软建议不要更改服务主函数名,即保持为ServiceMain。目前代码可以编译成功,但是还需要继续添加相关控制。

服务主函数

进入到服务主函数后,首先初始化全局变量,然后调用RegisterServiceCtrlHandler函数来注册服务控制函数,并获取当前程序的状态信息句柄,同时将程序的状态反馈给SCM。最后执行其他初始化,注意在状态未更改为SERVICE_RUNNING之前,初始化过程不要消耗太长时间,最好控制在1秒之内完成。代码如下:

#include <Windows.h>

#define SERVICENAME L"Your Service Name"

// 当前程序的状态信息句柄
SERVICE_STATUS_HANDLE gSvcStatusHandle;
// 程序最近的状态信息
SERVICE_STATUS gSvcStatus;


// 服务控制函数
void WINAPI ServiceControlHandler(DWORD request)
{

}

void ServiceMain(int argc, char** argv)
{
    // 下面填充当前服务的基本信息
    // 服务类型
    gSvcStatus.dwServiceType = SERVICE_WIN32;
    // 服务状态:正在启动中
    gSvcStatus.dwCurrentState = SERVICE_START_PENDING;
    // 当前服务接收的控制有哪些
    gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
    // 启动或停止时的服务错误码
    gSvcStatus.dwWin32ExitCode = 0;
    // 其它详细信息请查看文档: https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_status
    gSvcStatus.dwServiceSpecificExitCode = 0;
    gSvcStatus.dwCheckPoint = 0;
    gSvcStatus.dwWaitHint = 0;

    // 返回状态信息句柄
    gSvcStatusHandle = RegisterServiceCtrlHandler(SERVICENAME, ServiceControlHandler);
    if (gSvcStatusHandle == 0)
    {
        return;
    }

    // 将状态更改为running,即代表程序可以接收SCM发送的控制信息
    gSvcStatus.dwCurrentState = SERVICE_RUNNING;
    // 反馈状态信息给SCM
    SetServiceStatus(gSvcStatusHandle, &gSvcStatus);

    return;
}

int main()
{
    WCHAR ServiceName[] = SERVICENAME;
    SERVICE_TABLE_ENTRY DispatchTable[2];
    // 服务名字
    DispatchTable[0].lpServiceName = ServiceName;
    // 服务主函数
    DispatchTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;

    // 注意需要加上下面两行,因为StartServiceCtrlDispatcher规定最后一个元素必须为NULL
    DispatchTable[1].lpServiceName = NULL;
    DispatchTable[1].lpServiceProc = NULL;

    // 服务入口点
    StartServiceCtrlDispatcher(DispatchTable);
    return 0;
}

服务控制函数

在服务控制函数中编写针对SCM发送的各种请求进行处理即可。

// 服务控制函数
void WINAPI ServiceControlHandler(DWORD request)
{
    switch (request)
    {
        // 服务停止
    case SERVICE_CONTROL_STOP:
        gSvcStatus.dwWin32ExitCode = 0;
        gSvcStatus.dwCurrentState = SERVICE_STOPPED;
        break;
        // 系统关机
    case SERVICE_CONTROL_SHUTDOWN:
        gSvcStatus.dwWin32ExitCode = 0;
        gSvcStatus.dwCurrentState = SERVICE_STOPPED;
        break;
    default:
        break;
    }
    SetServiceStatus(gSvcStatusHandle, &gSvcStatus);
}

最后完善代码

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

#define SERVICENAME L"ServiceDemo"

// 当前程序的状态信息句柄
SERVICE_STATUS_HANDLE gSvcStatusHandle;
// 程序最近的状态信息
SERVICE_STATUS gSvcStatus;
// 停止事件
HANDLE  ghSvcStopEvent = NULL;

void WriteLog(const char* str)
{
    FILE* fp;
    fopen_s(&fp, "E:\\ServiceOutFile.txt", "a+");
    if (fp == NULL)
        return;
    fprintf(fp, "%s\n", str);
    fclose(fp);
}

// 服务控制函数
void WINAPI ServiceControlHandler(DWORD request)
{
    switch (request)
    {
        // 服务停止
    case SERVICE_CONTROL_STOP:
        gSvcStatus.dwWin32ExitCode = 0;
        gSvcStatus.dwCurrentState = SERVICE_STOPPED;
        WriteLog("stop!");
        SetEvent(ghSvcStopEvent);
        break;
        // 系统关机
    case SERVICE_CONTROL_SHUTDOWN:
        gSvcStatus.dwWin32ExitCode = 0;
        gSvcStatus.dwCurrentState = SERVICE_STOPPED;
        WriteLog("shutdown!");
        SetEvent(ghSvcStopEvent);
        break;
    default:
        break;
    }
    SetServiceStatus(gSvcStatusHandle, &gSvcStatus);
}

void ServiceMain(int argc, char** argv)
{
    // 下面填充当前服务的基本信息
    // 服务类型
    gSvcStatus.dwServiceType = SERVICE_WIN32;
    // 服务状态:正在启动中
    gSvcStatus.dwCurrentState = SERVICE_START_PENDING;
    // 当前服务接收的控制有哪些
    gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
    // 启动或停止时的服务错误码
    gSvcStatus.dwWin32ExitCode = 0;
    // 其它详细信息请查看文档: https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_status
    gSvcStatus.dwServiceSpecificExitCode = 0;
    gSvcStatus.dwCheckPoint = 0;
    gSvcStatus.dwWaitHint = 0;

    // 返回状态信息句柄
    gSvcStatusHandle = RegisterServiceCtrlHandler(SERVICENAME, ServiceControlHandler);
    if (gSvcStatusHandle == 0)
    {
        return;
    }

    // 将状态更改为running,即代表程序可以接收SCM发送的控制信息
    gSvcStatus.dwCurrentState = SERVICE_RUNNING;
    // 反馈状态信息给SCM
    SetServiceStatus(gSvcStatusHandle, &gSvcStatus);

    ghSvcStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    if (ghSvcStopEvent == 0)
    {
        return;
    }

    WriteLog("enter while\n");
    while (WaitForSingleObject(ghSvcStopEvent, 0) == WAIT_TIMEOUT)
    {
        WriteLog("while....\n");
        Sleep(500);
    }

    return;
}

int main()
{
    WCHAR ServiceName[] = SERVICENAME;
    SERVICE_TABLE_ENTRY DispatchTable[2];
    // 服务名字
    DispatchTable[0].lpServiceName = ServiceName;
    // 服务主函数
    DispatchTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;

    // 注意需要加上下面两行,因为StartServiceCtrlDispatcher规定最后一个元素必须为NULL
    DispatchTable[1].lpServiceName = NULL;
    DispatchTable[1].lpServiceProc = NULL;

    // 服务入口点
    StartServiceCtrlDispatcher(DispatchTable);
    return 0;
}

安装和删除服务

程序编译完成之后会生成exe文件,由于没有在代码中加入安装操作,所以需要借助其它工具来将这个exe文件安装到服务中,这个工具就是sc.exe,已经内置到系统中了。

第一步:首先使用管理员权限打开命令提示符。

第二步:安装服务,运行命令sc create ServiceDemo binpath=D:\c++\ServiceProgramDemo\x64\Debug\ServiceProgramDemo.exe。注意替换自己的路径。

第三步:查看自己的服务并配置启动项。搜索框搜索"运行",输入services.msc,然后找到自己的服务名字ServiceDemo,右键点击启动,服务就会启动了,并在E盘下输出相关日志信息。

删除服务很简单,先停止我们安装的服务,然后运行命令sc delete ServiceDemo即可。

参考文献

https://docs.microsoft.com/en-us/windows/win32/services/service-programs

https://docs.microsoft.com/en-us/windows/win32/services/configuring-a-service-using-sc

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容