资讯动态

Qt操作Windows服务实战:封装SCM API实现服务启停与看门狗

发布时间:2026/10/17 16:55:41 来源:尧图企业网站定制
简介面向系统服务管理场景的Qt项目代码包适合需要在桌面工具中集成服务控制能力的开发者。代码演示了利用Qt框架对系统服务进行创建、删除、打开、关闭、启动、停止等操作并覆盖服务启动类型设置与信息查询同时强调管理员权限的必要性可作为开发系统管理工具的参考。压缩包共9个文件包含2个C源文件、2个头文件、1个界面文件、1个Qt工程文件及说明文档另有少量工程配置文件整体仅10KB结构紧凑便于快速阅读和复用。已有50人学习适合正在学习Qt与系统服务交互的初中级开发者。利用该代码可快速理解Qt调用系统接口控制服务的常见写法以及服务状态、启动类型等信息的获取途径说明文档中还梳理了权限检查和操作流程能够帮助开发者避开权限不足或服务句柄使用错误。1. 为什么我会把一套“QT操作Windows服务”的代码拆出来给你有一天运维同事丢给我一个需求内部工具链里有三个Windows服务启动、停止、看状态原来一直是手工在“服务”窗口里点既慢又容易点错。他问我能不能做个带界面的小工具一键拉起整套服务。我翻了翻手头的项目发现网上讲“Qt操作Windows服务”的资料七零八落要么是调sc命令的黑匣子要么是几十行C#示例跟Qt完全不搭边。后来我干脆用Qt重写了一套操作Windows服务控制管理器SCM的封装从枚举服务、查状态到启动、停止、重启全部走Windows API再把结果回传到Qt界面。这套东西不算大但够稳适合要写桌面运维工具、服务管理面板或者想把服务控制嵌入公司内部系统的朋友。下面我把实现思路、关键代码和踩过的坑一起摊开讲保证你拿走后能照着重现。2. 选型问题调sc.exe和直接调Windows API到底差在哪2.1 为什么QProcess调用sc命令在Qt里看着简单实际是个黑匣子很多刚上手的同行会直接QProcess::start(sc, {query, MyService})然后去parse输出。第一版跑通很快但接着就会撞上三堵墙第一sc.exe在不同Windows版本上输出格式不固定Win10和Server 2012的本地化语言还会让“STATE”关键字变成“状态”你的正则表达式得维护两个版本第二sc只能做单向查询你想一次性枚举全系统一百多个服务就得反复拉起进程慢到怀疑人生第三sc拿不到详细的错误码和依赖关系服务启动失败时你只能看到一个“service-specific error 1053”后面什么都没了。我一般不会把sc命令当主力但偶尔会在命令行调试时用它验证结果。真正要嵌入Qt程序的逻辑还是得走Windows的SCM API——OpenSCManager、OpenService、QueryServiceStatus、StartService、ControlService这套。它们是服务控制管理器直接暴露的接口稳定、快错误码是HRESULT可以翻译成可读信息。选API的代价是你要自己处理类型转换和缓冲区长度但这套数据结构的坑是有限的半小时就能摸清。2.2 用WinAPI封装服务控制器核心函数与权限模型Windows服务控制管理器本质上是一个系统级单例程序通过服务控制管理器句柄来获得所有权限操作。第一步是调用OpenSCManager这一步要指定权限常见的是SC_MANAGER_CONNECT连上查列表和SC_MANAGER_ALL_ACCESS建服务和改配置但一般我们不用最高权限免得被安全软件盯上。第二步是打开具体服务句柄SERVICE_START、SERVICE_STOP、SERVICE_QUERY_STATUS这些权限位要按需要组合。第三步才是执行操作。这个权限模型值得多说一句如果你只想启动一个服务需要的是SERVICE_START想改服务配置要SERVICE_CHANGE_CONFIG。把这些权限位组合传进OpenService就能避免每次都以管理员身份跑。实际上要操作除自己进程之外的服务程序本身基本都要提权到管理员否则OpenSCManager会返回ERROR_ACCESS_DENIED错误码5。这里有个常见的误用是虽然你的程序里写对了权限位但UAC挡住了进程代码再对也没辙后面我会讲manifest提权。2.3 最小可编译的枚举与状态查询代码我先给一个下载这套代码里最核心的枚举服务片段它是最小可编译的把SCM连接、枚举、断开关联都走通了。现贴枚举服务状态的部分并说明每段的作用#include windows.h #include vector #include string struct ServiceInfo { std::wstring name; // 服务名注册名 std::wstring displayName; // 显示名 DWORD state; // 1停止, 4运行中,7正在停止 }; bool EnumAllServices(std::vectorServiceInfo outList) { SC_HANDLE scm OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ENUMERATE_SERVICE); if (!scm) { // 提权不够时这里返回ERROR_ACCESS_DENIED return false; } DWORD bytesNeeded 0; DWORD servicesReturned 0; DWORD resumeHandle 0; // 第一次调用用来拿缓冲区大小这是SCM枚举的标准套路 EnumServicesStatusExW( scm, SC_ENUM_PROCESS_INFO, SERVICE_WIN32, SERVICE_STATE_ALL, nullptr, 0, bytesNeeded, servicesReturned, resumeHandle, nullptr); std::vectorBYTE buffer(bytesNeeded); if (!EnumServicesStatusExW( scm, SC_ENUM_PROCESS_INFO, SERVICE_WIN32, SERVICE_STATE_ALL, buffer.data(), bytesNeeded, bytesNeeded, servicesReturned, resumeHandle, nullptr)) { CloseServiceHandle(scm); return false; } auto* services reinterpret_castENUM_SERVICE_STATUS_PROCESSW*(buffer.data()); for (DWORD i 0; i servicesReturned; i) { ServiceInfo info; info.name services[i].lpServiceName; info.displayName services[i].lpDisplayName; info.state services[i].ServiceStatusProcess.dwCurrentState; outList.push_back(std::move(info)); } CloseServiceHandle(scm); return true; }逻辑说明先用第一次调用拿到byteNeeded再按这个大小分配缓冲区第二次调用才真正拿服务列表。这里参数SERVICE_STATE_ALL表示不过滤状态所有服务都返回SERVICE_WIN32过滤出Win32服务避免把内核驱动设备服务也枚举出来。servicesReturned会在第二次调用后告诉你实际拿到多少个。如果你把缓冲区设得太小EnumServicesStatusExW会返回ERROR_MORE_DATA234所以用两次调用的方式最稳妥。参数说明SERVICE_ENUM_PROCESS_INFO表示返回结构里带进程状态比如PID和状态码比不带这个参数的版本多拿一个ServiceStatusProcess方便后面做界面显示。resumeHandle在服务数量超过单次返回上限时会用到这里一个窗口页能拿完一般不用分页。3. 把启动、停止、重启写成稳定函数阻塞与非阻塞两种模式3.1 阻塞式控制StartService与ControlService的参数细节枚举服务只是开头真正要操作服务得写启动和停止。先给阻塞式的版本同步等待操作完成适合做命令行工具或者后台任务。启动服务本质上就是把服务路径和参数传进去然后调用StartService它告诉SCM“该去拉进程了”但不等服务真的进入RUNNING才返回。所以你需要自己轮询QueryServiceStatus。我把代码写成这样注意启动超时和状态判断的处理#include windows.h bool StartServiceBlocking(const std::wstring serviceName, DWORD timeoutMs) { SC_HANDLE scm OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); if (!scm) return false; SC_HANDLE service OpenServiceW(scm, serviceName.c_str(), SERVICE_START); if (!service) { CloseServiceHandle(scm); return false; } BOOL ok ::StartServiceW(service, 0, nullptr); // argc/argv这里不传启动参数 if (!ok) { // 错误码1058是服务已被禁用1072是标记删除 DWORD err GetLastError(); CloseServiceHandle(service); CloseServiceHandle(scm); SetLastError(err); return false; } // 轮询状态直到RUNNING或超时 SERVICE_STATUS status{}; DWORD elapsed 0; while (elapsed timeoutMs) { if (!QueryServiceStatus(service, status)) break; if (status.dwCurrentState SERVICE_RUNNING) { CloseServiceHandle(service); CloseServiceHandle(scm); return true; } Sleep(250); elapsed 250; } CloseServiceHandle(service); CloseServiceHandle(scm); return false; }逻辑说明StartServiceW拉到的是SERVICE_START权限虽然启动后要轮询QueryServiceStatus但你此时已经打开了服务句柄QueryServiceStatus不需要单独再开一次。这里Sleep(250)是为了避免空转耗CPU250毫秒在人工点界面时感觉不到延迟又不会把CPU打满。timeoutMs建议给510秒因为某些服务启动阶段要做网络等待短了会误判失败。参数说明StartServiceW的第二个参数和第三个参数是启动参数数组普通服务传0和nullptr表示用服务配置里的ImagePath启动不带额外参数。如果你的工具要支持传参就要把命令行拆成argv数组传进去注意Windows服务默认不允许交互式传参传了也会被忽略。3.2 异步化改造把服务操作推给线程避免卡死UI直接在Qt主线程里调上面的阻塞函数界面会僵住尤其是服务启动慢的时候。血泪经验是服务启动不是瞬间完成的有些服务要跑30多秒才算起来界面卡30秒等于被判死刑。所以我在Qt项目里做了异步封装用QtConcurrent::run丢给线程池。#include QtConcurrent #include QFutureWatcher #include QObject QFutureWatcherbool* SetupAsyncStart(const std::wstring serviceName, QObject* receiver, const std::functionvoid(bool) callback) { auto* watcher new QFutureWatcherbool(receiver); QObject::connect(watcher, QFutureWatcherbool::finished, receiver, [watcher, callback]() { callback(watcher-result()); watcher-deleteLater(); }); watcher-setFuture(QtConcurrent::run([serviceName]() { return StartServiceBlocking(serviceName, 15000); })); return watcher; }逻辑说明这段把StartServiceBlocking放到全局线程池里跑跑完通过QFutureWatcher回到调用线程执行callback。不要在lambda里直接操作Qt界面部件只能返回结果再由回调去更新界面避免线程竞争。代码里用了std::function做泛型回调这样可以在绑定按钮时直接把UI更新逻辑通过lambda捕获进来。参数说明receiver可以是按钮或主窗口的this指针这样watcher会在receiver销毁时自动取消连接省掉手动断开的麻烦。15000毫秒是启动超时适合大多数应用服务如果系统负载高可以再拉长。3.3 供Qt界面直接使用的C封装类为了让这套代码能被界面直接调用我把所有操作收敛到一个ServiceController类里。现在给你一个摘掉无关部分的最小类结构#include QObject #include QMutex #include QPendingFuture // 仅作示意实际用QFuture class ServiceController : public QObject { Q_OBJECT public: explicit ServiceController(QObject* parent nullptr); Q_INVOKABLE bool enumAll(); // 调用后发出serviceListChanged Q_INVOKABLE void startAsync(const QString name); Q_INVOKABLE void stopAsync(const QString name); Q_INVOKABLE void restartAsync(const QString name); signals: void serviceListChanged(); void opFinished(const QString serviceName, bool ok, int errorCode); private: MsvQObjectForward // 示意占位真正实现要写私有工具函数 };这里我没有把全部实现贴出来重点说设计套路类内部维护一个服务名到当前状态的缓存界面通过listView显示时可以直接读缓存。startAsync内部调用3.2节的异步启动stopAsync同理但要调用ControlService。restartAsync则先停后启注意不能同时发两个异步操作否则会互相干扰所以我内部用QMutex串行化。为什么要单独建一个类而不是把所有代码放在窗口里因为服务控制涉及权限和状态变化分开后方便做单元测试也方便多个窗口共用同一套服务连接。参数上所有公开方法都建议显式注明明线程安全问题比如startAsync和stopAsync不能同时对一个服务调用否则SCM会返回错误码1051服务控制请求有冲突。4. 在Qt界面里落地信号槽、日志与UAC提权4.1 界面侧怎么接用QFutureWatcher还是自建QThread3.2节里我用QtConcurrent::run实现了异步这里再展开说另一种做法如果你不想引入线程池调度可以直接在QThread里跑一个循环队列。我两种都试过最后固定用QtConcurrent——它按CPU核心自适应调度对界面工具这种短任务更合适。自建QThread的问题是你只能跑一个任务但服务启动/停止是离散操作每次开一个新线程太浪费。界面的接入很简单按钮点击时调用startAsync然后监听opFinished信号connect(ctrl, ServiceController::opFinished, this, [this](const QString name, bool ok, int errorCode) { if (!ok) { statusBar()-showMessage(name QStringLiteral( 操作失败: ) errorStr(errorCode), 5000); return; } refreshServiceStatus(name); }); statusBar()-showMessage是单行短提示别在这里写复杂弹窗用户连续点几个按钮时会烦。参数说明errorStr()是一个把GetLastError错误码转成中文提示的工具函数错误码末尾的1053、1079、1068依赖未启动会直接影响用户怎么处理比直接显示数字友好很多。4.2 UAC提权的两种做法与manifest配置操作别人服务必须提权。这里说两条路二选一。第一条最简单在程序exe同目录放一个manifest声明requireAdministrator。用Qt Creator开发时在.pro里加一行配置QMAKE_MANIFEST $$PWD/app.manifestmanifest文件内容只写最关键的一段?xml version1.0 encodingUTF-8 standaloneyes? assembly xmlnsurn:schemas-microsoft-com:asm.v1 manifestVersion1.0 trustInfo xmlnsurn:schemas-microsoft-com:asm.v3 security requestedPrivileges requestedExecutionLevel levelrequireAdministrator uiAccessfalse/ /requestedPrivileges /security /trustInfo /assembly这样exe运行时系统会自动弹UAC提权框。另一条路是程序启动时用ShellExecuteW以runas方式重新启动自己把原来的进程退掉。第二条适合你希望低权限时也能浏览基本信息只在操作服务时再提权。但改代码麻烦还要处理参数传递。我的习惯是直接用manifest稳定简单。4.3 服务状态轮询与变更通知怎么做服务状态变化比如服务崩溃后自动退出界面怎么知道最准确的方式是调用NotifyServiceStatusChangeW它可以在服务状态变化时发出事件。但传两个大头第一它需要SERVICE_QUERY_STATUS权限而且回调机制在Qt里很容易和事件循环撞车第二它一次只能监听一个服务监听多个就要多开线程。所以在实际工具里我宁可做3秒一次的轻量轮询把所有服务的state存进缓存对比前后两次值再更新界面。轮询成本很低QueryServiceStatusW一次调用也就几微秒一百个服务才半毫秒。界面侧我建议用QTimer超时后调枚举函数然后差异化刷新modelQTimer* timer new QTimer(this); connect(timer, QTimer::timeout, this, [this]() { std::vectorServiceInfo currentList; if (!EnumAllServices(currentList)) return; compareAndUpdateModel(currentList); // 只发增量变化信号 }); timer-start(3000);逻辑说明compareAndUpdateModel负责把新的currentList和服务列表里旧数据对比只更新状态变化的那一列。千万别直接清空model再重建否则界面会闪烁用户也会觉得工具很“脆”。参数上3秒是个折中选择太短浪费CPU太长服务崩溃了要等半天才反应过来。5. 排查服务操作最常见的六个坑下面这六个问题是我在这套工具从零到能上线的过程中实际翻车过的每个都按“现象→原因→解决”写。5.1 现象Qt打包后在别人机器上双击运行报“qt.qpa.plugin: could not find the Qt platform plugin ‘windows’”原因Qt程序发布时没有把plugins/platforms/qwindows.dll目录拷到exe旁边。这不是服务代码的问题但一旦出现你再怎么写服务操作都跑不起来。解决把Qt安装目录下plugins/platforms整个文件夹复制到exe同级的platforms目录里保证结构是exe所在目录/platforms/qwindows.dll。另外把qt的bin目录下的所有dll放进exe目录最简单的办法是用windeployqt工具自动部署。从那以后我每次打包都会顺手跑一遍windeployqt不再手抖漏文件。5.2 现象调用StartServiceW返回错误码1079但同样的服务在“服务”窗口里能手动启动原因1079是“为这个服务指定的账户与服务于其他进程指定的账户不同”。通常是因为服务登录账户填的是某个域账号或本地账号但服务控制器无法用该账户启动。你写程序触发启动时程序自己的权限和服务账户的权限是两回事前者是“谁能发命令”后者是“服务进程以谁的身份跑”。解决先用服务管理器打开该服务的“登录”选项卡确认“此账户”里的用户名和密码没被改。程序侧要确保OpenService时带SERVICE_START权限且程序已提权到管理员。如果服务是“本地系统账户”那没问题如果是自定义账户就要检查密码是否过期。另外命令行里用sc qc 服务名查看服务对应二进制路径是否还在如果PathName指向的文件被删了也有类似现象。5.3 现象QueryServiceStatus返回的state是SERVICE_RUNNING但服务实际啥也不干原因服务启动分两步SCM把进程拉起来然后服务自己报告“已就绪”。如果服务进程启动后在后台初始化失败没调用SetServiceStatus(SERVICE_RUNNING)那query只会看到START_PENDING值2或一直停留在START_PENDING。但有些服务进程自己会超时前强制填成RUNNING导致界面显示“正在运行”实际内部线程已经废了。解决程序侧不要只信dwCurrentState再用QueryServiceConfig2W查询服务的启动类型和可执行路径确认是不是被替换过。另外用任务管理器看服务进程PID是否活着如果PID存在但CPU为0说明卡在死循环。我在工具里给status列加了一个“PID”字段能直观区分“真运行”和“假运行”。5.4 现象EnumServicesStatusExW查不到某些驱动服务原因枚举时用了SERVICE_WIN32过滤器它只返回Win32服务类型为0x30驱动服务和文件系统过滤驱动类型0x10或0x20被过滤掉了。如果你要管理的是设备驱动类服务比如打印驱动、网络过滤器就会漏。解决把枚举过滤器改成SERVICE_DRIVER或者直接全部枚举用SERVICE_STATE_ALL过滤状态类型用混合位掩码。一般管理工具不用管驱动但如果你这款工具的目标用户是IT运维记得加一个“显示驱动服务”开关。5.5 现象getlastError返回错误5拒绝访问但程序明明已经提权原因程序提权的只是主进程但你的代码里可能用OpenSCManagerW时请求了SC_MANAGER_ALL_ACCESS权限这个权限要求进程“以管理员令牌运行”但对某些系统服务组策略仍会拒绝。另外32位程序在64位系统上跑会触发WOW64重定向导致部分注册表和服务配置读取的是32位视图。解决不要一上来就请求ALL_ACCESS按最小权限原则查询用SC_MANAGER_CONNECT启停服务用SERVICE_START/SERVICE_STOP。如果确实要改服务配置再动态提权。还有注意编译架构建议用64位编译发布或同时输出两个架构避免WOW64带来的额外麻烦。5.6 现象修改了服务操作的代码重新编译后启动工具却还是旧的逻辑在跑原因Qt的debug和release执行文件混了或者exe实际是从别的目录启动了旧版本。还有一个隐蔽原因服务操作的dll/库被系统服务缓存锁定重新复制dll时失败。解决先用任务管理器确认正在跑的exe完整路径。再把exe旁的旧dll全部删干净重新部署。如果发现服务控制管理器句柄被某个后台线程占住不释放也会导致新进程连不上这时候只能杀掉僵尸进程或重启开发机。别问我怎么知道的我因为没清干净旧dll连续排查了一个下午。6. 进阶让服务管理工具带上“依赖链一键启动”和“看门狗”6.1 用QueryServiceConfig2读依赖关系按序启动单一服务操作只是起点生产环境里服务之间经常有依赖链比如数据库服务没起来应用服务起不来。我在这套工具里加了一个“依赖启动”功能先枚举目标服务的依赖服务递归查询把依赖树按依赖关系排成序列然后逐个启动。核心做法是枚举服务配置里的lpDependencies字段这个字段的每一项是依赖的服务名。你可以调用QueryServiceConfigW拿到DEPENDENCY_SERVICE_DESCRIPTOR结构再层层递归。实际代码里要注意递归深度限制某些服务依赖关系很乱比如A依赖BB依赖A会形成环。我直接加了一个访问次数上限访问超过15次就放弃并提示“检测到循环依赖”。6.2 看门狗服务意外退出后自动拉起服务进程如果自己崩溃退出SCM会有崩溃记录但不会自动恢复。我利用5.3节里说的PID跟踪写了个看门狗线程每一轮检查已知服务名对应PID是否还活着如果死了且该服务启动类型是自动就自动调用StartServiceBlocking重新拉起。看门狗工作参数建议每次检测间隔5秒最多连续重启3次3次后发送通知避免服务反复崩溃导致系统资源耗尽。这个功能对内部工具特别贴心至少能保证你凌晨三点不需要爬起来手动重启。判断服务是否还活着光靠QueryServiceStatus不够万一服务状态卡在RUNNING但进程没了状态还是RUNNING。所以我在看门狗里直接用OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid)检查进程句柄如果返回错误码ERROR_INVALID_PARAMETER或ERROR_INVALID_HANDLE说明进程已经不在了。注意不要打服务状态本身很多时候状态和真实进程是脱节的。这套QT操作Windows服务的代码我清理了好几轮最终稳定版就保留前面讲到的枚举查询、异步启停、UAC提权、看门狗这几个模块。从那以后我每次做桌面运维工具都强制自己先走一遍“权限最小化线程不阻塞界面服务状态用PID验证”这三板斧踩过的坑明显少了很多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑