nginx 做成 Windows 服务:Win8 稳跑两年,Win7 三天两头起不来,都说 WinSW 不支持 Win7 ------ 拆开 exe 和日志后真相是另一个
摘要: 同一套 nginx 服务化方案,在 Win8 上稳定跑了两年零失败,换到 Win7 就频繁起不来。网上流传的解释是"WinSW 不支持 Win7"。本文拆开 exe 的 PE 头、翻完 WinSW 的 wrapper.log、对照官方 XML 规范后,给出一个恰好相反的结论:WinSW 支持 Win7,而且现场那个 exe 就是 Win7 能跑的那个构建。真正坑人的是"下错构建 / 缺运行时 / 一个被抄了十年的 XML 陷阱"。
一、现场:两台机器,两种命运
甲方环境里有两台 Windows 加 nginx,用的都是"WinSW 改名成 nginx-service.exe + 一个同名 XML 配置"这套流(网上流传最广的那套教程):
| 机器 A | 机器 B | |
|---|---|---|
| 系统 | Windows 8 | Windows 7 SP1 64 位 |
| nginx | 1.21.6(D:\nginx-1.21.6) |
1.22.1(E:\nginx-1.22.1) |
| 表现 | 服务自启一直正常 | 频繁起不来,手动双击 exe 还会弹框 |
Win7 上双击那个 nginx-service.exe,弹的是这个:

Windows 服务启动失败:无法从命令行或调试程序启动服务。必须首先安装 Windows 服务(使用 installutil.exe),然后用 ServerExplorer、Windows 服务管理工具或 NET START 命令启动它。
(这台机器的系统信息 ------ Win7 专业版 SP1 64 位,一台 2011 年前后的台式机 ------ 见 ./images/02-win7-system-info.png;Framework64 下有 v4.0.30319,说明 .NET 4.x 是装了的,见 ./images/03-framework64-dir.png。)
于是"WinSW 不支持 Win7"这个说法就在项目里传开了。但它错了,而且错得挺有代表性。
二、先把"WinSW 不支持 Win7"这句拆开
2.1 官方声明:WinSW 明确支持 Win7
WinSW v2.12.0(稳定版,2023-01)的 README 原文:
WinSW offers executables for .NET Framework 2.0, 4.0 and 4.6.1. It can run on Windows platforms which have these versions of .NET Framework installed. For systems without .NET Framework, the project provides native 64-bit and 32-bit executables which are based on .NET Core 3.1.
结论:.NET Framework 2.0 / 4.0 / 4.6.1 三个构建 ,只要系统装了对应运行时就能跑。而 .NET Framework 4.6.1 的官方系统要求是"可在 Windows 7 SP1 和 Windows Server 2008 R2 SP1 上安装"。
再看 WinSW 3.x(默认分支,预发布)的 README:
WinSW 3 can run on Windows platforms with .NET Framework 4.6.1 or later versions installed. For systems without .NET Framework, the project provides native 64-bit and 32-bit executables based on .NET 7 . .NET 7 system requirements: Supported since Windows 10, version 1607, Windows Server (Core) 2012 R2 and Nano Server, version 1809.
看清楚这里的区别 ------ 同一个 release 页面上的两个 exe,最低系统要求差了十万八千里(下一节展开)。
2.2 实测:现场那个 exe,恰恰就是 Win7 能跑的那个
我把现场目录截图里的文件尺寸和 WinSW v2.12.0 官方 release 的资产清单做了比对(尺寸数据来自 GitHub Releases API):
文件名里的 "833 KB" 就是指纹。 Windows 资源管理器显示的 833 KB = 852,480 字节 ÷ 1024 = 832.5 KB ------ 和 WinSW.NET4.exe 一个字节都不差。
我把这个 exe 下下来,拆了 PE 头和导入表(自写 Python 解析 OptionalHeader + Import Directory):
| 检查项 | WinSW.NET4.exe(v2.12.0) |
结论 |
|---|---|---|
| 文件大小 | 852,480 字节 = 832.5 KB → 资源管理器显示 833 KB | 与现场 exe 完全一致 |
| 架构 | x86 (0x014c) | 32 位 .NET 程序,跑在 64 位系统上没问题 |
| PE SubsystemVersion | 4.0(Win7 = 6.1) | 远低于 Win7,加载器不会拒绝 |
| 是否托管程序 | 是(CLR 目录存在) | 内容其实是 IL 代码,不是原生机器码 |
| 导入 DLL | 只有 mscoree.dll |
这是 .NET Framework 的引导壳。它自己不实现任何逻辑,全靠系统里的 .NET Framework 运行时把代码跑起来 |
| 内嵌 CLR 版本串 | v4.0.30319 |
目标运行时 = .NET Framework 4.0 |
也就是说:现场这份 833 KB 的 nginx-service.exe 就是 WinSW 的 .NET Framework 4.0 构建 ------ 对 Win7 最友好的那个版本。它不需要 .NET Core、不需要 .NET 7、PE 头也不是"仅支持 Win10"的设定。
2.3 日志实证:它在 Win8 上跑了两年,一次没失败
拿到机器 A(Win8)的 nginx-service.wrapper.log 后,一切都对上了(节选):
yaml
2023-04-25 23:51:27,093 INFO - Installing service 'nginx (nginx)'...
2023-04-25 23:51:27,116 INFO - Service 'nginx (nginx)' was installed successfully.
2023-04-25 23:52:51,056 DEBUG - Starting WinSW in service mode
2023-04-25 23:52:51,102 INFO - Starting D:\nginx-1.21.6\nginx.exe
2023-04-25 23:52:51,136 INFO - Started process 12524
...
2023-10-02 10:11:26,333 INFO - Started process 6668
2024-07-04 00:47:35,003 INFO - Stopping nginx
2024-07-04 00:47:35,004 DEBUG - ProcessKill 6668
2024-07-04 00:47:35,026 DEBUG - Process 7064 canceled with code -1073741510.
...
2025-08-20 18:35:20,220 INFO - Started process 6332
逐条读出来的结论:
| 日志现象 | 含义 |
|---|---|
9 次 Starting WinSW in service mode → 9 次 Started process NNNN,零失败、零异常退出 |
服务一旦被 SCM 拉起,nginx 都能在 0.5--1.8 秒内正常启动。nginx 本体、路径、工作目录、80 端口全部没问题 |
| 启动条目稀疏:2023-10-02、2024-08-18、2024-10-29、2025-08-20 各一次 | 这些正是开机自启留下的记录 → 自启在这两年里一直是有效的 |
4 次停止全是 ProcessKill 直杀,worker 子进程 canceled with code -1073741510(0xC000013A) |
服务从来没被"优雅停止"过(原因见第五节,这是个配置陷阱,不是 Win7 的锅) |
| 2024-07-04 出现 8 条密集停/起(间隔约 2 分钟) | 人工调配置后重启服务,可忽略 |
所以:不是 WinSW 不支持 Win7,也不是 nginx 起不来。 问题要么在"选了哪个 exe",要么在配置和服务注册层,要么就是 Win7 特有的依赖缺失 ------ 而 Win7 那台现在缺的,恰恰就是一份能说话的日志。
三、WinSW 在 Win7 上真正会失败的 5 类原因
按"命中概率 × 排查成本"排序,逐条给判断方法。
① 下错了构建(最常见,也最容易避免)
WinSW 每个 release 里都有 5 个 exe,运行前提完全不同(v2.12.0 实测尺寸):
| 构建 | 大小 | 运行时 | Win7 SP1 能不能跑 |
|---|---|---|---|
WinSW.NET2.exe |
860,672 B (841 KB) | .NET Framework 2.0/3.5 | ✅ Win7 自带 3.5.1 |
WinSW.NET4.exe |
852,480 B (833 KB) | .NET Framework 4.0 | ✅ 需装 .NET 4.x(现场用的就是这个) |
WinSW.NET461.exe |
655,872 B (641 KB) | .NET Framework 4.6.1 | ✅ 需装 4.6.1+,且 Win7 要先打 SHA-2 补丁 |
WinSW-x86.exe |
17,253,337 B (≈16.5 MB) | 自包含(v2 是 .NET Core 3.1) | ⚠️ 需 UCRT(KB2999226) |
WinSW-x64.exe |
18,243,033 B (≈17.4 MB) | 自包含(v2 是 .NET Core 3.1;v3 是 .NET 7) | ⚠️ v2 勉强可(需 UCRT);v3 必失败(.NET 7 只支持 Win10 1607+) |
网上大量"WinSW 在 Win7 上跑不起来"的帖子,根源就在这:GitHub Releases 页面默认展示的是最新的 3.x 预发布版,而它那个 18 MB 的
WinSW-x64.exe是基于 .NET 7 的,官方只支持 Windows 10 1607+ 。照着"最新版"下,在 Win7 上必然失败。正确做法是回到 2.12.0 稳定版,选WinSW.NET4.exe(833 KB)。
3 秒判断你手上是哪个:
bash
dir D:\nginx-1.21.6\nginx-service.exe
对照上表的字节数即可。833 KB 那一档就是 .NET Framework 4.0 构建,Win7 可用。
② .NET Framework 缺失或版本不足
.NET Framework 构建的 WinSW 是纯托管程序 (实测导入表里只有 mscoree.dll)------ 目标机没装对应版本的 .NET Framework,它连第一行代码都执行不了。三个硬事实:
- 干净的 Win7 SP1 只自带 .NET Framework 3.5.1(Framework64 下只有 v2.0.50727 / v3.0 / v3.5);
- 有
v4.0.30319这个目录不等于 装了 .NET 4.x(.NET 4.5+ 是就地升级,目录名仍叫 v4.0.30319)。正确判断 :HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full下的Release值,或dir C:\Windows\Microsoft.NET\Framework64\v4.0.30319\里有没有大量 dll(干净系统里这个目录基本是空的); - Win7 上装 .NET Framework 4.6.1 及以上 ,必须先装 SHA-2 代码签名支持补丁:KB4474419 + KB4490628,否则安装程序直接报错 ------ 这是 Win7 特有的坎,Win8 及以后没这个问题。
③ 架构选错
x86 版跑在 64 位 Windows 上一般没问题(现场就是 x86 版跑在 64 位 Win7/Win8 上);反过来则必失败 :64 位版丢到 32 位系统上会报"不是有效的 Win32 应用程序"(0xC000007B)。
④ 杀软/安全软件拦截
国内现场的高频项。WinSW 要写服务注册表、要创建子进程、要常驻 ------ 很容易被拦。表现包括:install 报错、服务装上但起不来、exe 被杀软"修复"后体积变了。如果 exe 的字节数和官方对不上,先怀疑这个。
⑤ 与系统版本无关的配置层问题(最容易被误算到"系统不支持"头上)
- 启动类型没写成自动 (
sc qc nginx看START_TYPE,2=AUTO_START / 3=DEMAND_START / 4=DISABLED); - 服务根本没装成功 (
sc query nginx报 1060);XML 里<id>变了但服务没重装; - 服务账户(默认 LocalSystem,一般没问题;改成域账号后密码过期 → 事件 7000 + 1069);
- 开机时 80 端口被抢 (
http.sys、IIS、Skype、某些安全软件都会占;Win7 上这个特别常见)→ 事件 7000/7009/7031/7034; - 依赖顺序:nginx 反代的后端没起来导致启动慢,被 SCM 判超时(30 秒)→ 事件 7009。
四、Win7 那台(机器 B)现在该抓什么证据
只要三条命令,就能把它分流到上面五类原因里的某一条:
bash
REM 1) 服务注册状态:看存不存在、启动类型对不对
sc query nginx
sc qc nginx
REM 2) 日志里有没有"最近这次开机"的痕迹(把日期换成你最后一次重启的年份/月份)
findstr /c:"2026" E:\nginx-1.22.1\nginx-service.wrapper.log
findstr /c:"2025" E:\nginx-1.22.1\nginx-service.wrapper.log
REM 3) 事件查看器里按服务名过滤(导出为文本,方便回传)
wevtutil qe System /q:"*[System[Provider[@Name='Service Control Manager']]]" /c:40 /rd:true /f:text | findstr /i "nginx 7000 7009 7031 7034"
分流表:
findstr 结果 |
sc qc 结果 |
结论 |
|---|---|---|
有当次开机记录,且 Started process 之后无异常 |
START_TYPE=2 | 服务起来了 → 问题在"起来了但端口/后端不通",查 nginx 日志与端口 |
| 无当次开机记录 | START_TYPE=3 / 4 | SCM 根本没启动它 → 改自动启动即可(本文方案 A) |
| 无记录 | 报 1060/1056(服务不存在) | 服务注册丢了 → 重新 install |
有 Starting WinSW 但没有 Started process |
任意 | WinSW 起来了但拉不起 nginx → 看 .err.log / XML 路径 / 权限 |
另外两个小细节值得同时确认:
- 机器 B 的目录里到底有没有
nginx-service.xml?弹窗提到installutil.exe,说明那个nginx-service.exe是用 .NETServiceBase写的服务程序(这是ServiceBase.Run的标准报错文案,WinSW 双击时不会这么说)。如果机器 B 目录里只有 exe 和xxx.exe.config、没有 XML,那它用的就是另一套包装器,修法完全不同(往下看第五节末尾)。 nginx.pid有没有残留 :dir E:\nginx-1.22.1\logs\nginx.pid------ 强杀过的 nginx 会留下它。
五、XML 里那个被抄了十年的陷阱:stopexecutable
这一条与 Win7/Win8 无关,但必须说 ------ 因为它看起来像配好了,实际从未生效,属于典型的"沉默故障"。
网上那套教程模板里写的是:
xml
<stopexecutable>D:\nginx-1.21.6\nginx.exe -s stop</stopexecutable>
对照官方 v2.12.0 的 XML 规范(doc/xmlConfigFile.md):
stopargument/stopexecutable However, if
<stoparguments>element is present, winsw will instead launch another process of<executable>(or<stopexecutable>if that's specified) with the specified arguments... When you use the<stoparguments>, you must use<startarguments>instead of<arguments>.
<stopexecutable> 只接受可执行文件路径 ,停止参数必须写在 <stoparguments> (复数、单个元素)里。模板把 -s stop 拼进路径后,WinSW 会去找一个字面叫 nginx.exe -s stop 的文件 ------ 找不到,于是静默退回强杀 。这就是机器 A 日志里只有 ProcessKill、从来没有优雅停止的原因。
正确写法:
xml
<executable>D:\nginx-1.21.6\nginx.exe</executable>
<workingdirectory>D:\nginx-1.21.6</workingdirectory>
<stopexecutable>D:\nginx-1.21.6\nginx.exe</stopexecutable>
<stoparguments>-s stop</stoparguments>
顺手记住三条 WinSW 的官方默认值/语义,能省很多猜:
<startmode>默认就是Automatic(可选 Boot / System / Automatic / Manual),所以"开机不自启"通常不是漏了这一项,而是服务压根没被 SCM 拉到;- 停止流程 :
<stoptimeout>默认为 15 秒(先尝试 Ctrl+C,超时才TerminateProcess);配好<stoparguments>后,WinSW 会改为先跑停止命令并等它退出,这才是优雅停止; - 失败恢复 用
<onfailure action="restart" delay="10 sec"/>+<resetfailure>1 hour</resetfailure>(等价于sc failure,但写在 XML 里更不容易丢)。
关于机器 B 那个 installutil 弹窗
「必须首先安装 Windows 服务(使用 installutil.exe)」是 .NET ServiceBase 程序被直接运行时的标准提示 ------ 只有服务 exe 在服务模式下由 SCM 启动 ,ServiceBase.Run 才会成功。所以:
- 如果双击时弹它 → 正常现象,不代表 exe 坏了,但说明这台机器的服务很可能从来没被正确安装成 Windows 服务(这本身就能解释"频繁起不来");
- 如果开机时自动弹它 → 说明有人把这个 exe 塞进了「启动」文件夹或计划任务里 ------ 这是交付雷区:nginx 会以登录用户身份运行,还会和服务方式双开冲突。请务必检查
msconfig的启动项和任务计划程序。
六、我给你的三个现场文件(怎么用、能修什么)
这一节是实操部分。三个文件都是纯 ASCII + CRLF 换行,Win7 的 GBK 控制台不会乱码。
6.1 check-winsw-evidence.bat ------ 只读取证(先跑这个)
双击即可(需要管理员)。它不改任何东西 ,在脚本同目录生成 winsw-evidence.txt,一次收齐 13 项现场信息:
| 项 | 为什么需要 |
|---|---|
sc qc / sc query nginx |
服务存不存在、启动类型是 2/3/4 ------ 直击"开机不自启" |
wmic os get lastbootuptime |
最后开机时间 ------ 用来和日志时间线对账 |
wrapper 日志全文 + findstr "2026" 过滤 + 命中计数 |
判断"这次开机 WinSW 到底有没有被拉起来" |
nginx-service.xml 全文 |
检查 stopexecutable 陷阱、路径、服务名 |
tasklist / netstat :80 :8080 |
nginx 是否在跑、80 端口被谁占 |
logs\nginx.pid 是否存在 |
判断有没有强杀残留 |
nginx.exe -t |
配置语法校验(不影响线上,只是解析) |
| nssm.exe 是否存在 | 最终确认包装器归属,避免再套错命令 |
为什么坚持"先取证、后修复":如果把 nginx 手动启动一次,服务"启动失败"的状态就被冲掉了,事件日志和日志时间线的线索也随之消失。远程排障最怕的就是这个。
6.2 nginx-service.fixed.xml ------ 修好配置里的三个坑
在原始 XML 基础上改了四处(都写了注释):
stopexecutable改为纯路径 ,补上<stoparguments>-s stop</stoparguments>;- 补
<workingdirectory>------ 让 nginx 无论被谁启动,conf/、logs/都相对自己的目录解析(等价于 nssm 的AppDirectory,这是 nginx 服务化最高频的坑); <startmode>Automatic</startmode>显式写出;<depend>依赖项留好位置(MySQL / Tomcat 的服务名,用sc query state= all | findstr /i "mysql tomcat"查出来后取消注释)。
用法:备份原文件 → 用这份内容覆盖 → 保持文件名仍是 nginx-service.xml → nginx-service.exe restart(或重启机器)。文件必须和 exe 同名同目录,这是 WinSW 发现配置的方式。
6.3 fix-nginx-winsw.bat ------ 一键修复(改配置,需管理员)
默认目标 D:\nginx-1.21.6(改顶部 NGINX_DIR 即可)。顺序是:
ini
[0] 打印当前状态 sc query / sc qc
[1] 打印 wrapper 日志 ← 修复前先留证据
[2] nginx-service.exe uninstall → install ← 用 WinSW 自己的命令重装服务
[3] sc config nginx start= auto + sc failure ← Win7 上兜底,防止 install 写入不生效
[4] net start + 状态验证
[5] 80 端口监听检查
第 3 步为什么要 sc 兜底:<startmode> 默认虽然是 Automatic,但"服务属性写进去了"和"启动类型真的是自动"是两件事,用 sc qc 复核一遍最稳(注意 start= 后面必须有空格 ,这是 sc 的老规矩)。
跑完必须重启机器复验 sc query nginx = STATE : 4 RUNNING ------ 只验证"手动能启动"毫无意义。
七、三句话总结
- "WinSW 不支持 Win7"是误传。 官方三个 .NET Framework 构建都支持 Win7 SP1;现场那个 833 KB 的
nginx-service.exe(=WinSW.NET4.exe,852,480 字节)就是在 Win8 上跑了两年零失败的同一个构建。会翻车的是"照着最新版下了 3.x 的 .NET 7 自包含构建(17.4 MB)"这种选型错误。 - Win7 上要盯的是依赖而不是兼容性:干净的 Win7 只有 .NET 3.5.1、装 4.6.1+ 要 SHA-2 补丁、自包含构建要 UCRT(KB2999226)、架构只能 x86←x64 不能反过来。
- 别让配置悄悄失效。 这次的
stopexecutable就是"看着配了、其实一直在强杀"。判断标准很简单:日志里有没有真的执行停止命令,以及服务启动失败时有没有一份落盘的输出。
最后一句留给同样在做交付的同学:"开机不启动"这类问题,先别急着换组件,先让日志说话。 一份 wrapper.log 能同时否掉好几个错误方向,比换三套方案都快。
本文所有二进制尺寸、PE 头、SubsystemVersion、导入表数据均为实测(WinSW v2.12.0 取自 GitHub Releases,nssm 2.24 取自 Chocolatey 官方包);WinSW 的 XML 语义均引用官方 v2.12.0 文档原文。