简介:NSSM(Non-Sucking Service Manager)是Windows平台上将任意可执行程序封装为后台服务的知名开源工具,相比系统自带的srvany工具更易用,且支持环境变量、错误日志记录等高级特性。这份标识为nssm-2.24-101-g897c7ad的资源包面向系统管理员、运维工程师和Windows服务开发者,可解决exe程序无法直接注册为系统服务、需要开机自启且在无用户登录时持续运行的场景。压缩包共40个文件、376KB,内含15个.h头文件与14个.cpp源文件组成的完整工程代码,win32/win64两个版本的nssm.exe可执行文件,配套Visual Studio解决方案(sln/vcproj)、命令行编译脚本version.cmd,以及README和ChangeLog说明文档,兼顾源码阅读与实际使用。源码覆盖GUI(gui.cpp)、控制台(console.cpp)与核心服务管理(service.cpp)等模块,方便对照学习界面操作和底层实现,也能帮助排查服务启动异常或参数配置问题。已有429人学习,适合想深入理解服务封装机制、研究服务注册与配置细节的读者,也可基于源码定制功能或二次编译,对自研服务管理工具具有很好的参考价值。
1. nssm-2.24-101-g897c7ad:把任意程序变成 Windows 服务的工具
凌晨一点,你部署了一个监控脚本,用任务计划程序设定每小时运行一次。脚本在调试窗口里跑得好好的,挂到后台就出各种幺蛾子:窗口被注销会话收走、日志找不到、崩溃后没人拉起来。你想起那台机器的服务管理器里躺着十几个注册表里配置的服务,再看看手里这个无名进程,决定给它一个“身份”。nssm-2.24-101-g897c7ad 就是干这个的:把任何程序变成 Windows 服务。它是 2.24 版之后第 101 次提交的构建快照,解决了不少老版本在日志轮转和优雅停止上的边角问题。这篇笔记记录我反复用下来的完整路径,从注册命令到参数调优到反直觉的事故现场,新手能照着跑通,熟手能直接拿走参数和坑位清单。
2. nssm 的工作原理与选型:服务注册时发生了什么
2.1 服务控制管理器、包装进程与应用进程的三方关系
要把普通 exe 变成 Windows 服务,先要理解 Windows 服务的运行模型。服务控制管理器(SCM)启动一个服务时,会创建该服务对应的进程,并向它发送启动指令;进程必须实现 ServiceMain 回调,在初始化完成后上报“启动成功”,随后一直在运行状态等待停止请求。绝大多数普通程序的 exe 根本不认识这套协议,它们不知道什么是 ServiceMain,也不知道怎么向 SCM 汇报状态。nssm 做的事情就是在两者之间插一层翻译官:它本身注册为一个合法服务,由 SCM 直接拉起来,然后这个包装进程再去创建你的应用进程。
启动链路是这样的。系统启动时 SCM 根据注册表里的服务配置创建 nssm 包装进程,包装进程通过 CreateProcess 把你要跑的程序作为子进程拉起,同时把 stdout 和 stderr 通过管道接出来重定向到日志文件。此后 SCM 发出的停止指令会先到达包装进程,nssm 再按顺序向应用进程发送各种关闭信号,最终把子进程回收干净。这个“SCM 只跟包装进程说话、包装进程再跟应用说话”的模型,就是 nssm 的立足点。
注册表里存着完整的配置。nssm 的服务参数写在 HKLM\SYSTEM\CurrentControlSet\Services<服务名> 下面,键值包括 ImagePath、ObjectName 和一台自定义参数。与直接用注册表写 ServiceMain 不同,nssm 把这些自定义配置用 DisplayName、AppDirectory、AppParameters 等键维护,修改时用 nssm edit 或命令行 set,不需要手改注册表。服务后台到底发生了什么,对很多人来说是黑匣子;但只要把注册表目录和日志文件打开,链条就很清楚了。
nssm 还承担了“看门狗”职责。应用进程意外崩溃时,包装进程捕获到退出事件,可以在你设置的延迟后重新拉起它。这个“崩溃重启”能力是裸应用和任务计划程序都给不了你的,也是很多值班系统选择 nssm 的核心原因。注意它默认是否自动重启取决于服务本身的恢复设置,nssm 的 AppExit 参数和系统 SCM 的恢复选项各有分工,后面参数章会细说。
2.2 与 sc.exe、任务计划程序的边界:为什么选 nssm
Windows 自带的 sc.exe 也能创建服务,但它的设计目标完全不同。sc create 是把一个程序注册成服务,要求这个程序自己能实现并响应 SCM 协议;对 Python 脚本、批处理、Java 应用这类普通进程,sc.exe 建出来的服务经常出现启动超时、状态错乱、无法停止的问题。因为它根本没有服务协议层,SCM 觉得这个进程已经“启动成功”了,实际脚本才刚跑第一行。
任务计划程序同样不是等价替代品。它适合“定时触发一个任务”,而服务是“开机拉起并长期驻留“;计划任务的进程生命周期和登录会话绑定,服务则跟 SCM 绑定。你可以在任务计划里设置“不管用户是否登录都要运行”和“失败后重新启动”,但遇到需要按服务状态查询、依赖服务排序、多实例共存这类场景,它就力不从心了。
我用一张表把三者边界框清楚:
| 能力维度 | nssm | sc.exe | 任务计划程序 |
|---|---|---|---|
| 注册任意应用为服务 | 支持,自动包装 | 仅支持符合服务协议的程序 | 不支持服务模型 |
| 崩溃后自动重启 | 支持,可配延迟 | 依赖系统恢复选项 | 支持有限重试 |
| stdout/stderr 日志重定向 | 内置,支持轮转 | 不支持 | 不支持 |
| 优雅停止应用 | 多级停止信号 | 无法干预停止方式 | 只结束任务 |
| 开机时按依赖启动 | 支持服务依赖 | 支持 | 不支持 |
| 命令行批量管理 | nssm start/stop/restart | sc start/stop | schtasks 命令 |
真正让我一直留在 nssm 的是日志和停止行为。sc.exe 建的服务往往在进程崩溃前没有留下任何输出;任务计划程序虽然能重定向输出,但轮转、句柄释放、退出码判断全都要自己写脚本。而这些正是 nssm 这个版本号里隐含的迭代重点:日志轮转和进程停止这两个最难缠的部分,在 2.24 之后的滚动提交里持续打磨。所以当你拿到 nssm-2.24-101-g897c7ad 这种 git describe 风格版本号时,可以直接把它当成比稳定版 2.24 更接近主干的快照来用。
3. 安装与最小跑通:从命令行注册一个 hello 服务
3.1 注册 hello.py 服务的完整命令与验证步骤
拿到 nssm 的压缩包后,解压到 C:\tools\nssm 这类路径,建议让 nssm.exe 所在目录进入 PATH,或者后面所有命令都写全路径。注意 64 位系统优先用 64 位版本的 nssm.exe,后面会讲为什么。注册服务首先要有一个管理员权限的命令行窗口,右键“以管理员身份运行”,否则注册时会被权限挡回来。
下面我用一个 Python 脚本 demo 演示最小注册,换成任意 exe、bat、jar 都能套用:
nssm install HelloSvc "C:\Python39\python.exe" "D:\apps\demo\hello.py"这条命令的参数顺序是:固定 install 动词,然后是服务名 HelloSvc,接着是应用路径和参数。nssm 会把这两个路径分别存成 Application 和 AppParameters。服务名有约束:不要带空格、斜杠和反斜杠,否则 SCM 解析会出问题,这也是为什么我习惯把所有服务名都写成无空格的驼峰形式。
注册成功后会弹出 nssm 的图形配置窗口。第一次用可以先直接点 “Install service” 完成注册,后面用命令行补参数。确认服务是否注册成功,我用两个命令交叉验证:
sc query HelloSvc nssm status HelloSvcsc query 输出显示服务状态是 STOPPED 或 START_PENDING 都算正常;如果报“指定的服务未安装”,说明注册没生效。接下来设置工作目录。脚本最常见的问题是 import 找不到模块或读不到相对路径配置文件,根源就是 nssm 启动进程时默认工作目录不是脚本所在目录。这里单独设置 AppDirectory:
nssm set HelloSvc AppDirectory "D:\apps\demo" nssm start HelloSvc启动后验证两种手段:看 sc query 状态是不是 RUNNING,再用日志输出确认程序真正跑起来了。我给这个最小服务配置了 stdout 重定向,方便排查:
nssm set HelloSvc AppStdout "D:\apps\demo\logs\stdout.log" nssm set HelloSvc AppStderr "D:\apps\demo\logs\stderr.log" nssm restart HelloSvc设置日志路径后必须重启服务才会拉起新参数对应的输出管道。注意在设置 AppStdout 之前启动过的服务不要去直接读这个日志文件,因为旧的输出句柄还握在进程里。这一套下来,最小服务就算跑通了。
3.2 start、stop、restart、edit、remove:日常管理命令
跑通之后日常操作其实只有五个高频动作。逐个看:
nssm stop HelloSvc nssm restart HelloSvc nssm edit HelloSvc nssm status HelloSvc nssm remove HelloSvc confirmstop 会触发 nssm 的停止顺序,对应用进程发送关闭信号并等待它退出;restart 是 stop 和 start 的连续操作,适合修改完参数后快速刷新服务状态。edit 会打开图形配置窗口,所有参数面板都在这里,省得记命令行参数名;status 输出里能看到 START_PENDING、RUNNING、STOP_PENDING 这类中间状态,判断卡在启动还是退出阶段很有用。
remove 是最后一步销毁操作,加上 confirm 参数会跳过二次确认,适合写脚本批量清理。要注意 remove 默认不会删除日志文件,只销注册表里的服务项;如果你加了 AppStdout 路径但在 remove 前忘记关服务,日志文件锁会一直挂在已经停止的 nssm 进程上,导致删除失败。
还有两个容易被忽略的命令是 nssm get 和 nssm set。get 可以取出任意参数的当前值,例如:
nssm get HelloSvc AppDirectory nssm get HelloSvc AppParameters这两个命令在排查“别人配过但没人记得怎么配的”服务时是救命稻草。我一般会先把 AppDirectory、AppParameters、AppExit 这几个关键参数拉出来存档,再做任何修改。后面讲批量部署时,你还会看到所有参数都可以用 set 一条条写成可重复的脚本,这也是 nssm 相比图形界面最大的优势。
4. 核心参数与日志行为:让服务可观测、可轮转、可停止
4.1 三个必填参数:Application、AppDirectory、AppParameters
很多人在 nssm 注册服务时习惯把完整命令行一股脑填进 Application 栏,例如"C:\Python39\python.exe D:\apps\demo\hello.py",这是常见误区。nssm 把“程序本体”和“传给程序的参数”分开存储:Application 是进程路径,AppParameters 是整个参数字符串。把参数拼进 Application 后,路径解析偶尔能工作,但一旦参数里包含引号嵌套,命令行解析就成玄学现场。
正确设置方式是两个字段分别维护:
nssm set Demo Application "C:\Python39\python.exe" nssm set Demo AppParameters "D:\apps\demo\hello.py --env=prod --port=8080"AppDirectory 是那个最容易被忽略、却直接决定脚本成败的参数。脚本运行时的工作目录取决于创建进程时传入的目录,不是你双击运行时所在的目录。Python 脚本里的相对路径open("config.json")会相对当前工作目录找文件,不是相对 .py 文件找。所以凡是部署目录里包含配置文件、数据文件、SQLite 库、静态资源的应用,必须把 AppDirectory 设置到程序包根目录,否则启动当前就“秒退”,日志里还看不到完整异常堆栈。
启动一个 Java 应用时,参数更容易踩坑。例如 Windows 下-Duser.dir决定了相对路径基准,但很多程序根本不看它,工作目录错了照样读不到配置。我建议养成一个习惯:注册服务时无论应用是什么类型,先无条件 set AppDirectory,和 Application、AppParameters 并列成“到底三件套”,少一件都要回头查。
4.2 日志输出与轮转参数:AppStdout / AppStderr / AppRotateFiles
nssm 对日志的处理方式是把应用进程的 stdout 和 stderr 分别重定向到两个文件。如果不设置 AppStdout 和 AppStderr,输出会被直接丢弃,这通常是“服务状态 RUNNING 但行为诡异”的原因。把输出落盘是第一步:
nssm set Demo AppStdout "D:\logs\demo.out.log" nssm set Demo AppStderr "D:\logs\demo.err.log" nssm set Demo AppRotateFiles 1 nssm set Demo AppRotateBytes 10485760 nssm restart DemoAppRotateFiles 置 1 启用轮转,AppRotateBytes 设置触发轮转的字节数阈值,10485760 就是 10MB。nssm 轮转的逻辑是:当前输出文件达到阈值后,把现有文件重命名为 .log.old 之类的时间戳名,再新建空文件继续写。这个机制避免日志无限制膨胀把磁盘打满。
还有两个经常一起配的轮转参数:AppRotateOnline 和 AppRotateBytesHigh。AppRotateOnline 置 1 表示在线轮转,即应用进程还持有日志文件的写句柄时,nssm 也尝试轮转,办法是尽量把句柄切换过去;老版本里“日志文件被占用导致轮转失败”的坑多半发生在这个环节。2.24 以后的滚动提交对这类在线轮转的处理强化了不少,我遇到 Python logging 这种长期持句柄的场景会优先选用这类快照版本而不是死守稳定版。
AppRotateBytesHigh 设置一个更高的触发阈值,nssm 会优先在低阈值触发,若低阈值轮转失败才等到高阈值强轮。简单参数配置如下:
nssm set Demo AppRotateOnline 1 nssm set Demo AppRotateBytes 10485760 nssm set Demo AppRotateBytesHigh 52428800日志参数配完之后有一个验证习惯很值得试:写一个会持续输出的脚本,故意让服务跑 20 分钟,看轮转时间是否稳定。很多应用的崩溃卡在 stdout 管道缓冲区和句柄没有及时 flush,这是你在本地调试窗口里永远复现不出来的,只能靠服务端真实日志验证。
4.3 停止顺序参数:优雅终止 vs 超时强杀
nssm 真正拉开与 sc.exe 差距的地方在停止过程。停止服务时,nssm 会按顺序执行一系列动作,每个动作都等待一段可配置的时间,直到应用进程退出;如果所有动作都做完进程还没消失,最后才用 TerminateProcess 强杀。这套流程由 AppStopMethodConsole、AppStopMethodWindow、AppStopMethodThreads、AppStopMethodKill 四个参数控制,单位是毫秒,值为 0 表示跳过该步骤。
默认停止序列大致是:先尝试向应用进程的控制台发 Ctrl+C 信号,等一段时间;再尝试向进程的顶层窗口发送 WM_CLOSE,等一段时间;然后尝试枚举并终止进程的线程;最后超时强杀。对后台服务来说,Ctrl+C 和 WM_CLOSE 往往没有实际意义,但要给应用留出处理数据库连接池、文件写入缓冲的收尾时间。
我常用的一组优雅停止参数长这样:
nssm set Demo AppStopMethodConsole 3000 nssm set Demo AppStopMethodWindow 3000 nssm set Demo AppStopMethodThreads 1000 nssm set Demo AppStopMethodKill 5000含义是:Ctrl+C 等 3 秒,WM_CLOSE 等 3 秒,线程终止等 1 秒,强杀前的总兜底超时 5 秒。如果你的应用里有重启协程、保存状态这类操作,可以把前两个值调大,例如 Console 5000、Window 5000、Kill 10000。这样的服务在nssm stop时不会瞬间消失,但它给了程序收拾残局的窗口。
值得注意的是,nssm 的停止序列只作用于它直接创建的那个应用进程。如果你的应用又派生出了孙进程,nssm 不会递归管理孙进程的退出,这就是常见的“服务停了,但孤儿进程还在”的来源。后面避坑章我会专门展开怎么处理这种情况。
5. 踩坑排查:服务起不来、停不掉、日志不滚动
5.1 现象:路径带空格时服务启动失败
现象是 Application 路径里包含空格目录,例如C:\My Apps\app.exe,注册后启动立即报错误 1067,进程根本没起来;或者起来后 AppParameters 被拆成几段,应用收到一堆残破参数。
原因是 nssm 内部构造 CreateProcess 命令行时,需要正确处理路径和参数的引号包裹;2.24 这个版本之后虽然整体稳定,但 Application 字段里塞入带空格的完整命令行仍然容易解析错位。
解决办法是严格拆分,Application 只写 exe 路径并用引号包围,参数全部放 AppParameters。注册完成后用nssm get Demo Application检查路径解析结果,再启动服务。对带空格的目录,我还会额外在 AppDirectory 里显式指定工作目录,避免启动进程时默认目录解析错位。
5.2 现象:服务启动后秒退,事件查看器里却没有记录
现象是 sc query 状态瞬间从 START_PENDING 跳成 STOPPED,应用日志目录空空如也,事件查看器里也没有进程崩溃记录,看起来就像服务根本没启动过。
这个坑绝大多数时候是工作目录造成的。脚本启动后马上 import 一个本地模块或打开相对路径配置,失败后直接抛异常退出,而因为启动瞬间输出还没有来得及写入重定向管道,日志文件自然是空的。
解决路径是先设 AppDirectory 再启动。确认工作目录没写错之后,如果还秒退,就把 AppStdout 和 AppStderr 先指向一个临时文件,启动后马上看内容;大多数异常堆栈都会在这一步露出原形。如果连临时文件都没有任何字节,考虑应用是否在 import 阶段依赖没有配置好的环境变量,用nssm set Demo AppEnvironmentExtra补充。
5.3 现象:日志文件不轮转,磁盘被打满
现象是 AppRotateFiles 已经设成 1,日志文件大小超过 AppRotateBytes 很多倍仍然不轮转,或者轮转后旧日志没有被正确归档,磁盘空间被疯狂消耗。
原因通常是应用自身长期持有日志文件的写入句柄,尤其是 Python 的 logging 模块和 Java 的 logback 默认在启动时就打开了输出流,nssm 在线轮转时无法重命名旧文件。老版本里这是常见案例;2.24 之后滚动提交持续改进的正是这部分逻辑,但不可能覆盖所有持句柄行为。
解决方法是把 AppRotateOnline 置 1,让 nssm 尝试不关闭进程句柄的在线轮转;同时提高 AppRotateBytesHigh 作为兜底。如果你看到“轮转失败”记录且应用不支持释放句柄,就只能在应用侧做按天滚动,并写定期清理脚本。还有一条经验:不要用 nssm 重定向同时让应用自己又写一份日志,双写日志会让句柄竞争和文件锁问题翻倍。
5.4 现象:停止服务后应用进程仍然存活
现象是执行 nssm stop 后 sc query 显示服务已停止,但任务管理器里应用进程还在跑,端口还被占用,重启后旧进程与新进程抢占资源。
原因是 nssm 停止序列只盯着它自己创建的直接子进程,应用 fork 出来的孙进程不在管理范围内。如果你的应用带 worker 子进程、守护进程、Java 启动器派生的 JVM,停止时那一串孙进程会脱离服务生命周期。
解决思路是在应用层处理好退出传播:主进程收到退出信号时主动终止它派生的子进程。对 Java 应用可以用 shutdown hook;对 Python 可以用 atexit 或信号处理器;实在改不了应用,就写一个包装脚本来管理孙进程生命周期。服务端建议把 AppStopMethodConsole 和 AppStopMethodWindow 的时间调长,给主进程足够时间去收编孙进程再关闭自身。
5.5 现象:服务用普通账户运行,写不了日志目录
现象是服务能启动,但日志目录一直不生成文件,或应用写数据时报权限错误,服务状态却显示 RUNNING。
原因是 nssm 默认以 LocalSystem 账户运行,权限很大;如果你用nssm set Demo ObjectName 某域\某用户改为普通账户,这个账户未必对日志目录有写权限。注意 AppDirectory 如果指向程序安装目录下的子目录,普通账户往往只对安装目录有读权限。
解决方法是先确认服务进程实际身份:
nssm get Demo ObjectName然后给日志目录和部署目录的写权限,或者新建专属服务账户并授予最小权限。还有一条 Windows 特有的限制:普通账户没有“作为服务登录”权限时,服务会启动失败并报特定的错误码,需要在本地安全策略里给账户授予“作为服务登录”权限,这个权限问题比文件权限更隐蔽,排查时记得一起检查。
6. 进阶技巧:用 nssm 命令行与导出脚本批量交付服务
当你需要把服务从测试机搬到生产机,或者在一个新环境批量部署几十个服务时,nssm 的图形界面就不够用了。我的做法是全程命令行加导出脚本。nssm 自带的 export 命令可以把一个服务的所有配置生成一个批处理脚本,这个脚本里包含了从零创建该服务的全部命令:
nssm export Demo D:\scripts\deploy_demo.cmd然后在新机器上直接执行这个导出的 cmd,服务就已经注册完成且带着原机器的全部参数。我每次调整参数后都会重新导出一份脚本并提交到部署仓库,这样生产环境出了问题,只要比对这份脚本和线上环境的差异,就能快速定位是哪台机器的配置漂移了。
批量部署时我会写一个循环脚本,从一个服务清单文件里读取服务名和路径,逐条调用 nssm install 和 set。清单同时包含日志轮转参数和停止参数,确保每个服务都默认带上优雅停止的配置:
for /f "tokens=1,2,3 delims=," %i in (services.csv) do ( nssm install %i "%j" %k nssm set %i AppDirectory "%j\.." nssm set %i AppRotateFiles 1 nssm set %i AppRotateBytes 10485760 nssm set %i AppStopMethodWindow 3000 )这里 tokens 第一列是服务名,第二列是程序路径,第三列是启动参数。注意 AppDirectory 用了%j\..,含义是程序路径的上一层目录,前提是部署目录结构统一。如果目录结构不一致,还是老老实实在清单文件里写全 AppDirectory,不要耍这个小聪明。
验证批量部署是否成功,我习惯一次性把所有服务状态拉出来:
nssm status All如果某些服务没有出现在输出列表里,说明注册时权限或服务名出了问题。最后一个值得养成的好习惯是:任何服务上线前,都要在测试环境执行一次完整的 stop/start 循环,确认停止后没有残留进程、启动后日志文件在轮转。我用这套流程交付过很多次服务,几乎没有在凌晨被叫起来处理“服务停不掉”的情况。
我现在的习惯是,每个新项目接入 nssm 前,先用默认配置跑一个最小可运行脚本,确认这个应用进程在无交互会话下能正常存活,再开始加参数、加轮转、加停止逻辑。这个习惯帮我避开了很多“在服务器上打不开调试窗口”的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取