Windows 服务的创建与运维 ── 从任务计划程序的取舍到 BackgroundService 服务化
· 小村 豪 · Windows 服务, Windows, .NET, C#, BackgroundService, Generic Host, 常驻处理, 运维, 判断表, 技术咨询
「每 5 分钟跑一次的任务计划程序已经跟不上了」「想让常驻监听接收设备数据的监控常驻程序一直开着」「文件一放进文件夹就希望在几秒内处理完」。持续接触定期执行相关的咨询,需求发展到一定程度后,最后几乎都会走到这个话题。
本博客已经连续写了用 Power Automate 做业务自动化、任务计划程序的安全运维设计这两篇自动化相关文章。在任务计划程序那篇的第 8 章提到「一旦需要分钟级轮询或常驻监控,就是常驻进程的领域」这样的分界,但实际上该怎么创建 Windows 服务、又该怎么让它投入运维?如果在这里含糊地「先做成服务再说」,就会生出「停不下来的服务」「崩溃了也没人发现的服务」「用 LocalSystem 跑任何东西的服务」。
本文将按实务上决策的顺序,整理常驻处理是否该做成 Windows 服务的判断表、服务机制的最低限知识(SCM・启动类型・Session 0)、用 .NET 8 的 Worker Service 进行实现、用 sc.exe 注册与设置恢复选项、运行账户的选择,以及安全的停止处理。
1. 先说结论
- 取舍的判断轴很简单。间隔数分钟以上的定期处理,任务计划程序就够用;一旦需求里出现常驻监听(socket、命名管道、FileSystemWatcher)、秒级响应、崩溃后自动恢复,就该用 Windows 服务。
- 从 Windows Vista 开始,服务会在 Session 0 中运行,无法直接与用户交互(无法显示 UI)。如果需要 UI,就要把服务和 UI 应用程序拆开,靠进程间通信来对话。1
- .NET 的官方创建方式,是 Worker Service 模板加上
Microsoft.Extensions.Hosting.WindowsServices的AddWindowsService(如果是IHostBuilder系列,则是UseWindowsService)。可以直接当作控制台应用程序运行,让开发、调试比传统的服务开发轻松许多。2 - .NET 6 之后,
BackgroundService内未处理的异常,默认会让主机停止(BackgroundServiceExceptionBehavior.StopHost)。但这属于「正常停止」,所以 SCM 恢复选项带来的重启不会生效。想要重启的失败,要用非 0 的退出代码让进程终止。32 - 别图方便就把运行账户设成 LocalSystem。
sc.exe create的默认值就是 LocalSystem,不留意的话就会以最强权限开始运行。4 只在本机完成的处理,优先考虑 LocalService 或虚拟账户;需要在域中访问共享资源或数据库的,gMSA 才是正解。5 - 恢复选项(
sc.exe failure)和停止处理(HostOptions.ShutdownTimeout,默认 30 秒6)要在注册时就设计好。「对停止没有响应的服务」是运维现场最不受欢迎的存在。
2. 什么时候该做成服务 ── 任务计划程序・服务・常驻应用程序的判断表
出现类似常驻需求时,可选的方案有 3 种:任务计划程序、Windows 服务,以及「注册到启动项的常驻应用程序」(常驻在通知区域那种)。先把各自的特性列出来。
| 角度 | 任务计划程序 | Windows 服务 | 常驻应用程序(启动项) |
|---|---|---|---|
| 启动时机 | 依时间・事件触发每次启动 | OS 启动时(登录前)就常驻 | 用户登录时开始 |
| 是否需要登录 | 可以不需要(非交互执行) | 不需要 | 需要(注销就消失) |
| UI | 无法显示(非交互配置时) | 无法显示(Session 0) | 可以显示(通知区域・对话框) |
| 常驻或定期 | 定期(适合以小时到每天为单位) | 常驻 | 常驻(但局限在用户会话内) |
| 权限 | 每个任务各自指定运行账户 | 服务账户(第 6 章) | 登录用户的权限 |
| 监控・恢复 | 历史记录+自行通知 | SCM 的恢复选项(自动重启) | 没有(要自己做) |
| 部署的工作量 | 用 XML/PowerShell 注册 | sc.exe/安装程序 | 注册到 Run 注册表项等 |
判断的思路如下。
- 每天数次到以小时为单位的定期处理,且处理之间不需要保留状态,用任务计划程序就好。特意把它做成服务、自己管理定时器是过度设计,任务计划程序那篇文章里写的运维设计成本要低得多。
- 本质上是常驻监听──TCP 或管道的请求等待、FileSystemWatcher 文件夹监控、队列的逐一处理、秒级响应──就用 Windows 服务。如果开始用「每 5 分钟一个任务」把处理切碎去轮询,那就是在劣化重造常驻进程的信号。
- UI 才是主体(通知区域操作、向用户弹窗)就用常驻应用程序。但它必须登录才会运行,所以不适合服务器类用途。如果「后台处理需要常驻运行,但也需要 UI」,就按下一章所说,拆成服务和 UI 应用程序两个进程。
还有一个容易被忽略的判断依据是恢复。任务计划程序的任务即使失败,也只能放着等到下次调度;服务则有 SCM 帮忙处理自动重启(第 5 章)。「就算夜里崩溃了,也希望能在早上之前自行恢复」这种需求,本身就足以成为做成服务的理由。
3. 服务机制的最低限知识 ── SCM・启动类型・Session 0
3.1 SCM 与启动类型
Windows 服务背后的操盘手是服务控制管理器(SCM)。它管理服务的登记簿,中介启动、停止的请求,并在失败时执行恢复动作。服务进程必须按照约定与 SCM 对话,但这部分已经由 .NET 的库代劳(第 4 章),我们在设计上要决定的,只有「启动类型」「运行账户」「恢复」「停止」这 4 点。
启动类型(Startup type)有 4 种可选。4
| 启动类型 | 行为 | 适用场景 |
|---|---|---|
| 自动(auto) | OS 启动时开始 | 常驻运行服务的基本选项 |
| 自动(延迟启动,delayed-auto) | 在其他自动服务之后稍晚开始 | 业务服务优先选这个,避免启动刚完成时的拥堵与依赖对象尚未就位 |
| 手动(demand) | 只在被请求时才开始 | 由其他应用程序唤起的辅助服务 |
| 已禁用(disabled) | 无法启动 | 用于停用措施、封存 |
业务服务应该以延迟启动为默认值。OS 刚启动时,网络、数据库、其他服务都还没到位,如果用「自动」抢最快启动,很容易在第一次连接时就失败。用延迟启动拉开时间差,再靠后面提到的 ExecuteAsync 内部重试机制吸收连接失败,这种双重保障会更稳定。
另外还要知道启动超时这件事。SCM 不会无限期等待服务汇报启动完成。超过默认超时(ServicesPipeTimeout,30 秒)就会记录事件 7000/7011,并视为启动失败。7 也就是说,像数据库连接重试、构建大型缓存这类繁重的初始化,不该放在「启动处理」里执行。原则是让启动立刻完成,繁重的处理留给主体循环(ExecuteAsync)去做。
3.2 Session 0 隔离 ── 服务无法显示 UI
从 Windows Vista 开始,服务会在名为 Session 0 的隔离会话中运行,无法直接与用户交互。1 就算在服务里调用 MessageBox.Show 或显示表单,登录中用户的屏幕上也不会出现任何东西。更糟的是,它会一直等待没有人能点击的「确定」按钮,让整个处理卡住,这是经典的假死成因。把旧代码搬进服务时,一定要确认是否还残留着原本用来显示错误的消息框。
需要 UI 时的正解,是把服务(Session 0)和 UI 应用程序(用户会话)拆成两个进程,靠进程间通信(IPC)对话。这也是 Microsoft 官方建议的设计,用命名管道之类的 IPC,让 UI 端把结果回传给服务。1 该选哪种 IPC 手段,整理在同日发布的姊妹文章「进程间通信的判断表」里。把 UI 应用程序端也放到 Generic Host 上,就能让两个进程在 DI、日志、配置的架构上保持一致(参见「在桌面应用程序中使用 Generic Host 与 BackgroundService」)。
4. .NET 中的创建方式 ── Worker Service 与 AddWindowsService
4.1 模板与 Program.cs
.NET 的服务开发,要从 Worker Service 模板开始。它已内置在 SDK 中,用 dotnet new worker 就能生成雏形,接着只要添加 Microsoft.Extensions.Hosting.WindowsServices 包并调用 AddWindowsService,就准备好可以当 Windows 服务运行了。2 作为基础的 Generic Host 机制,在「什么是 .NET Generic Host」里有说明。
using App.MonitorService;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// 添加以 Windows 服务启动时与 SCM 对话用的生命周期。
// 若以控制台方式启动,则维持原本的 ConsoleLifetime 运行
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "KsMonitor";
});
builder.Services.AddSingleton<MeasurementQueue>();
builder.Services.AddHostedService<MonitorWorker>();
IHost host = builder.Build();
host.Run();
如果是使用 Host.CreateDefaultBuilder 的传统风格项目,就改成调用 IHostBuilder 扩展方法 UseWindowsService(),作用相同。
有一个和任务计划程序文章里「开始(可选)」问题同类型的陷阱。以服务方式启动时,当前工作目录是 C:\Windows\System32。如果 appsettings.json 之类的文件是以相对路径为前提去读取,就会出现「控制台能跑,服务却读不到配置」的状况。请让配置文件以可执行文件所在位置为基准来解析,或依官方教程的指示,在注册时的 binpath 中加上 --contentRoot,明确指定内容根目录。2
4.2 ExecuteAsync ── stoppingToken 与异常设计
主体要写在继承自 BackgroundService 的 ExecuteAsync 里。这里该决定的,只有「如何响应停止请求」和「如何处理异常」这两件事。
namespace App.MonitorService;
public sealed class MonitorWorker(
MeasurementQueue queue,
ILogger<MonitorWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 从队列取出一条进行处理。等待时务必一律传入 token
var item = await queue.DequeueAsync(stoppingToken);
await ProcessAsync(item, stoppingToken);
}
catch (Exception ex) when (ex is IOException or TimeoutException)
{
// 可以继续运行的失败:记录后间隔一段时间再重试
logger.LogError(ex, "处理失败,将于 30 秒后重试。");
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// 来自 SCM 的停止请求,属于正常流程,不做任何事。
// 用 when 子句限定为 stoppingToken 触发的原因,是为了避免内部处理
// 超时等状况抛出的取消被误判成「正常停止」,导致工作线程
// 悄悄停下、而主机却继续存活
}
catch (Exception ex)
{
// 未预期的失败:默认的 StopHost 会让主机「正常」停止,
// 导致 SCM 的恢复选项(自动重启)不会触发。
// 用非 0 的退出代码让进程终止,告诉 SCM「这是失败」
logger.LogCritical(ex, "发生无法恢复的错误,服务即将终止。");
Environment.Exit(1);
}
}
private Task ProcessAsync(Measurement item, CancellationToken token)
=> throw new NotImplementedException();
}
- 把
stoppingToken传给所有等待。Task.Delay、I/O、队列等待都要传。只要有一处忽略了这个 token 的长时间等待,那里就会变成「对停止没有响应的服务」的温床(第 7 章)。 - 把可以继续运行的失败和无法恢复的失败分开。 网络中断、临时性超时之类的状况,要在循环内捕获、记录并重试。只是吞掉异常再
continue的catch (Exception),只会做出一个悄悄空转的服务。该怎么分类的思路,整理在「遇到未预期异常时该终止还是继续的判断表」里。 - 要知道未处理异常的默认行为。 .NET 6 之前,从
ExecuteAsync泄漏出来的异常会凭空消失,服务看起来还在运行,实际上却什么都不做,变成「僵尸」。.NET 6 之后,默认行为变成BackgroundServiceExceptionBehavior.StopHost,异常会先记录到日志,再让主机停止。3 但这个停止属于「正常停止」,就算设置了恢复选项也不会重启。想靠恢复选项恢复的失败,就要像上面的代码那样,用Environment.Exit(1)明确地让它异常终止,这才是官方教程所示范的做法。2
4.3 可以直接以控制台运行 ── 开发变轻松的最大原因
AddWindowsService 只会在以 Windows 服务方式启动时,才切换成服务专用的生命周期。也就是说,同一个 exe 在 Visual Studio 按 F5 或用 dotnet run 时,会当作普通的控制台应用程序运行。断点照样能命中,按下 Ctrl+C 就能在本机验证从停止请求到 StopAsync 的整个流程。
不过,控制台上能跑,不代表服务也一定能跑。运行账户的差异(第 6 章)、Session 0、当前工作目录——只有这 3 点,一定要以服务身份在实机上验证。「控制台能跑、服务却不能跑」的原因,几乎都会收敛到这 3 点上。
5. 注册与运维 ── sc.exe・恢复选项・事件日志
5.1 用 sc.exe create 注册
把发布(dotnet publish,发布成单文件会更好处理)出来的 exe,用 sc.exe create 注册到 SCM。要在管理员权限的 PowerShell 中执行。
sc.exe create "KsMonitor" binpath= "C:\Services\KsMonitor\KsMonitor.exe" start= delayed-auto obj= "NT AUTHORITY\LocalService" displayname= "KS 测量数据导入服务"
sc.exe description "KsMonitor" "将测量数据导入并写入数据库的常驻服务(负责部门:信息系统部)"
先说一个经典陷阱。binpath= 或 start= 的等号后面必须有空格。等号之前是选项名,选项名和值之间的空格在语法上是必须的,这是 sc.exe 特有的规则(省略就会失败)。4 另外,省略 obj= 时的默认值是 LocalSystem。4 什么都不设置就直接注册,就会以最强权限开始运行,所以请在注册时就完成第 6 章提到的选择。
设置 description 看似不起眼却很重要。留下足够信息,让几年后打开 services.msc 的人能判断「这是什么服务、能不能停、该问谁」,之后的排查成本就能省下来。移除时要先停止服务,再执行 sc.exe delete "KsMonitor"。2
如果部署对象有多台机器,或是需要定期更新(替换文件),就尽早改用安装程序(MSI/WiX 的 ServiceInstall 等)把注册、更新、移除整套处理好,而不是靠 sc.exe 手动操作。手动注册的操作手册,一定会产生某台漏了一步的机器。
5.2 恢复选项 ── 「挂了就重启」交给 SCM 处理
服务化的一大优点,就是 SCM 内置的恢复选项。它让你可以用声明式的方式,设置进程异常终止时该做什么。2
sc.exe failure "KsMonitor" reset= 86400 actions= restart/60000/restart/60000/restart/300000
这个示例的意思是「第 1、第 2 次失败,60 秒后重启;第 3 次及以后,5 分钟后重启;失败计数器每 24 小时重置一次」。用图形界面(services.msc 属性 →「恢复」选项卡)也能做同样的设置。在自己动手打造「监视任务」或监控脚本之前,请先用好这个标准功能。
有两点要注意。第一,如前一章所说,进程必须以非 0 退出代码终止才会触发恢复。StopHost 造成的正常停止不在恢复范围内。第二,如果是启动后必定崩溃的状态(配置错误、数据库连不上),就会变成重启循环。这也是为什么第 3 次及以后的间隔要拉长,同时要提前做好「为什么会崩溃」能留在事件日志里(下一节)的准备。
5.3 事件日志与文件日志并用
Host.CreateApplicationBuilder 在 Windows 上会自动添加 EventLog 日志提供程序。而默认只有 Warning 及以上级别的消息才会发送到事件日志。2 很容易让人误以为「做成服务之后 Information 日志全都不见了」,但其实不是消失,而是被事件日志的默认过滤器挡住了。如果也想记录启动、停止之类的运维事件,就要在 appsettings.json 中明确设置 EventLog 提供程序的级别。
{
"Logging": {
"EventLog": {
"SourceName": "KsMonitor",
"LogLevel": {
"Default": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
}
有一点要注意。事件源(Event Source)如果没有事先注册就无法写入,而注册需要管理员权限。省略 SourceName 也逃不掉——因为 AddWindowsService 默认会用应用程序名作为源名称2,所以无论如何都是从「未注册的源」开始。像 LocalService 这种最小权限账户,在运行期无法创建源,一旦创建失败,事件日志就会什么都写不进去,运维却已经开始了。注册工作请交给安装程序(或具有管理员权限的配置流程)处理。这和任务计划程序文章第 6 章写的「把 CreateEventSource 分离到配置流程那一侧」是同一件事。
区分使用的原则是:事件日志只放「运维人员该看的东西」(启动、停止、失败、恢复),详细的处理追踪则交给自己维护的文件日志。自行维护文件日志时的最低要求(滚动、写入失败时的行为),整理在「自制日志组件的最低要求」;进程整个崩溃时该如何留下证据的设计,整理在「崩溃时留存日志与转储文件的设计」。因为在靠恢复选项自动重启的架构下,崩溃那一刻的记录有没有留在现场,会是唯一的线索。
6. 运行账户 ── 别图方便就选 LocalSystem
服务会在指定账户的安全上下文下运行,SCM 在启动时会用该账户登录,并把令牌附加到进程上。8 这就是任务计划程序文章第 3 章写的「以谁的身份运行」,换成服务版的说法。先把各个选项列出来。
| 账户 | 权限 | 在网络上的身份 | 密码管理 | 适用场景 |
|---|---|---|---|---|
| LocalSystem | 极强(相当于 OS 层级) | 计算机账户 | 不需要 | 原则上要避免。因为是 sc.exe create 的默认值,要特别注意 |
| LocalService | 最小限 | 匿名(无法访问共享资源) | 不需要 | 本机内完成处理的第一候选 |
| NetworkService | 最小限 | 计算机账户 | 不需要 | 需要访问域内资源的轻量处理 |
| 虚拟账户(NT SERVICE\服务名) | 只有被授予的部分 | 计算机账户 | 不需要 | 可按服务单位授予 ACL,对本机资源采用最小权限 |
| gMSA | 只有被授予的部分 | gMSA 本身 | 由 DC 自动管理 | 需要在域中访问共享资源/数据库的服务首选 |
判断的重点有 3 个。
- 不要因为「这样能跑」就选 LocalSystem。 服务的漏洞会直接等同于整台机器被接管。是否真的需要相当于管理员的权限,可以直接套用「什么时候才真的需要管理员权限」里的整理。多数情况下,需要的顶多是「写入特定文件夹」这种程度,只要把该文件夹的 ACL 授予虚拟账户(
NT SERVICE\KsMonitor)就够了。虚拟账户不需要创建,也不需要密码管理。5 - 网络共享的陷阱。 LocalService 在网络上会变成匿名身份,所以访问
\\server\share会失败。NetworkService、虚拟账户、LocalSystem 都会以计算机账户(DOMAIN\机器名$)的身份出现在网络上5,所以共享那一端要对这个计算机账户同时开放共享权限和 NTFS 权限。「明明给了用户权限,但只有服务读不到」的真相,多半就是这个。先决定好「要以谁的身份出现在网络上」,再去配置共享那一端。 - 要用普通用户账户运行,就要连密码运维一起考虑。 使用专用账户时,需要「作为服务登录」的权利,而且密码一旦过期,登录就会失败,服务也就无法启动。8 这是「密码变更让任务悄悄死掉」问题的服务版。如果是域环境,用密码由域端自动管理的 gMSA,把这整个问题一起消掉才是正解(和任务计划程序文章 3.2 节的结论一致)。
7. 安全停止 ── ShutdownTimeout 与在途处理
先掌握停止的流程。当 services.msc、sc.exe stop,或是 OS 关机让 SCM 发出停止请求时,.NET 的服务生命周期会把它转换成主机停止(StopApplication),stoppingToken 会被取消,ExecuteAsync 退出,接着调用各服务的 StopAsync。主机等待这一连串停止处理的时间,就是 HostOptions.ShutdownTimeout,默认是 30 秒。69 如果来不及完成,就会在后续收尾工作尚未完成的情况下强行停止。
如果 30 秒不足以写完一条在途处理,就从所需时间反推、延长这个超时。
builder.Services.Configure<HostOptions>(options =>
{
// 从写完一条在途处理所需的最长时间反推
options.ShutdownTimeout = TimeSpan.FromSeconds(90);
});
在此基础上,停止处理的设计要注意 3 点。
- 先决定好收到停止请求后的执行顺序。 停止接受新任务 → 让在途处理完成(或在安全的断点处中断,并以能够续跑的方式记录下来) → 清理连接和临时文件。没有决定这个顺序,就会走向两个极端:一看到 token 就把所有东西全丢掉,或是想全部做完而导致超时。
- 不要做出「对停止没有响应的服务」。 只要有一处忽略 token 的长循环或长时间等待,就会变成在 SCM 上卡在「正在停止」的服务。这会妨碍 Windows Update 的重启,并在每次服务器计划性停机时都需要手动 kill,是运维现场最不受欢迎的缺陷。如第 4 章所说,请在每个处理节点检查 token,并把
CancellationToken传给长时间的 I/O。用控制台方式运行的话,按 Ctrl+C 就能原样测试这个停止流程,建议把「按下 Ctrl+C 后几秒内结束」纳入发布前的测试项。 - 把「即使被 kill 也不会坏」的设计,看得比停止处理做得多细致更重要。 断电或强制终止,不管停止处理做得多完善都还是会发生。事务、原子性的文件写入、可重跑的处理单元——「就算中途死掉,下次启动时也能保持一致」才是根本,做得细致的停止处理,终究只是礼貌问题。
8. 总结
判断很简单:定期处理用任务计划程序;需要常驻监听、秒级响应、自动恢复就用 Windows 服务;UI 才是主体就用常驻应用程序。一旦决定做成服务,用 .NET 的 Worker Service 和 AddWindowsService,采取「以控制台开发、以服务部署」的形式,就是目前的标准做法。
在注册时需要作为设计决定的有 4 点——启动类型(业务服务用延迟启动)、运行账户(避开 LocalSystem,改用虚拟账户或 gMSA)、恢复选项(sc.exe failure 搭配 Environment.Exit(1))、停止(ShutdownTimeout 与在途处理的收尾)。只要掌握这 4 点,加上尊重 stoppingToken 的 ExecuteAsync 写法,Windows 服务就不再是「特别难做的东西」。反过来说,没有决定这 4 点就开始运行的服务,几年后往往会带着「停不掉、崩溃了也没人发现、权限太强不敢动」三重苦回来找你。要重整现有服务,从盘点这 4 点开始,也是最快的捷径。
相关文章
相关咨询领域
合同会社小村软件提供常驻处理的设计评审(包括从任务计划程序迁移的判断),以及「对停止没有响应」「不知不觉就崩溃」的 Windows 服务排查与重构相关咨询。
参考链接
-
Microsoft Learn,Interactive Services。说明 Windows Vista 之后服务无法直接与用户交互、服务会在 Session 0 中运行,以及需要 UI 时建议采用另一个 GUI 应用程序进程搭配 IPC 的设计。 ↩ ↩2 ↩3
-
Microsoft Learn,Create Windows Service using BackgroundService。把 Worker Service 做成 Windows 服务的官方教程,说明
AddWindowsService、EventLog 提供程序默认级别为 Warning、用sc.exe进行注册/恢复/删除,以及恢复选项需要非 0 退出代码。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
Microsoft Learn,.NET 6 breaking change: Exception handling in hosting。说明 .NET 6 之后,
BackgroundService.ExecuteAsync的未处理异常会先记录到日志,再依默认设置让主机停止(BackgroundServiceExceptionBehavior.StopHost)。 ↩ ↩2 -
Microsoft Learn,sc.exe create。说明
start=(auto/demand/delayed-auto 等)与obj=(默认为 LocalSystem)的规格,以及选项的等号后面必须有空格(省略会失败)。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn,Service Accounts in Windows Server。说明虚拟账户(
NT SERVICE\服务名)不需要密码管理,会用计算机账户的凭据访问网络,以及 gMSA 的密码由域端自动管理。 ↩ ↩2 ↩3 -
Microsoft Learn,.NET Generic Host。说明主机关闭处理的流程,以及停止时会等待可配置的关闭超时(默认 30 秒)。 ↩ ↩2
-
Microsoft Learn,A service does not start, and events 7000 and 7011 are logged。说明 SCM 只会等待
ServicesPipeTimeout指定的时间让服务启动,超过就会记录事件 7000/7011,以及调高默认超时的步骤。 ↩ -
Microsoft Learn,Service User Accounts。说明服务会在指定账户的安全上下文下运行,SCM 在启动时会进行登录,密码过期会导致服务无法启动,以及 LocalService/NetworkService/LocalSystem 这几个特殊账户。 ↩ ↩2
-
Microsoft Learn,HostOptions.ShutdownTimeout Property。说明用来控制
StopAsync默认超时的属性定义。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
如何理解 Windows 的会话隔离 ── Session 0・RDP・多用户同时运行
整理 Windows 应用程序开发者容易混淆的「会话」概念。说明服务无法显示 UI 的 Session 0 隔离原因、RDP 连接时会话的行为、命名对象的会话隔离,以及共享 PC・RDS 环境中常见的设计失误,从实务角度解说。
业务应用的日期时间与时区 ── 从 DateTime 的陷阱到 UTC 存储原则、测试设计
服务器迁移后时间整整差了 9 个小时、只有海外分支机构的日期会变成前一天——本文从 DateTime 的 Kind 与隐式转换梳理日期时间事故的根本原因。内容涵盖与 DateTimeOffset 的取舍、UTC 存储与 ISO 8601 原则、TimeZoneInfo 与夏...
Windows 的进程间通信该怎么选 ── 命名管道 / TCP / gRPC / 共享内存 / COM 判断表
整理 Windows 应用程序之间应该如何选择通信方式。用判断表整理命名管道、本地 TCP、gRPC、共享内存、文件对接、COM 各自的优势与陷阱,并从实务角度说明 UI+服务分离・32bit/64bit 桥接・权限边界等常见架构,以及命名管道的实现示例。
在 C# 业务应用中使用 SQLite ── WAL 模式、排他控制、防损坏对策、与 EF Core 的取舍
本文整理在 Microsoft.Data.Sqlite 中把 SQLite 嵌入业务 Windows 应用的实务知识:连接字符串与连接池、WAL 模式的原理、SQLITE_BUSY 与写入归并、通过事务提速、类型映射的陷阱、用 VACUUM INTO 备份,以及该在什么场合...
Windows应用的数据存储位置怎么选 ── SQLite / JSON / 注册表 / Access 判断表
Windows桌面应用的数据该存在哪里、用什么格式保存?本文整理AppData/ProgramData的使用区分,以及SQLite、JSON文件、注册表、Access(.accdb)各自的优势与陷阱,并附判断表,从实务角度讲解防止数据损坏与位数问题等注意事项。
常见问题
汇总了咨询这一主题时常见的问题。
- 任务计划程序和 Windows 服务该怎么取舍?
- 如果是间隔数分钟以上的定期处理,且处理之间不需要保留状态,用任务计划程序就够了。一旦需求里出现 TCP 或管道的常驻监听、FileSystemWatcher 文件夹监控、秒级响应、崩溃后自动恢复,就该用 Windows 服务。如果开始用「每 5 分钟一个任务」把处理切碎去轮询,这就是在劣化重造常驻进程的信号。如果 UI 才是主体(比如通过通知区域操作),那么只在登录期间运行的常驻应用程序,就是第三个选项。
- Windows 服务可以显示 UI(界面)吗?
- 不可以。从 Windows Vista 开始,服务会在称为 Session 0 的隔离会话中运行,无法直接与用户交互。就算在服务里调用 MessageBox.Show,用户的屏幕上也不会出现任何东西,反而会因为一直等待没有人能点击的「确定」按钮而让处理卡住,造成假死。如果需要 UI,正确做法是把服务和 UI 应用程序拆成两个进程,通过命名管道之类的进程间通信来对话。把旧代码搬到服务里时,一定要检查是否还残留着用来显示错误的消息框。
- .NET 要怎么创建 Windows 服务?
- 官方做法是先从 Worker Service 模板(dotnet new worker)开始,添加 Microsoft.Extensions.Hosting.WindowsServices 包后调用 AddWindowsService。同一个 exe 在 Visual Studio 按 F5 或用 dotnet run 运行时,会当作普通的控制台应用程序运行,开发、调试因此轻松许多。注册要用 sc.exe create,但要注意它独特的规则:binpath= 或 start= 的等号后面必须有空格,而且省略 obj= 时默认会是 LocalSystem(最强权限)。服务启动时的当前工作目录是 C:\Windows\System32,所以配置文件要以可执行文件所在位置为基准来解析。
- BackgroundService 发生异常时,服务会怎么样?
- .NET 6 之后,从 ExecuteAsync 泄漏出来的未处理异常,默认行为(BackgroundServiceExceptionBehavior.StopHost)是先记录到日志,再让主机停止。但这会被视为「正常停止」,所以不会触发 SCM 的恢复选项(自动重启)。如果希望恢复选项能重启服务,就必须像 Environment.Exit(1) 那样,用非 0 的退出代码让进程明确地异常终止。像网络中断这类可以继续运行的失败,应该在循环内捕获、记录并重试,并在设计上与无法恢复的失败区分开来。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。