在后台运行的.NET Core应用执行Bash命令时出现挂起问题
后台运行.NET应用时调用Bash命令挂起的问题解决
我们的应用是部署在Linux主机(基于定制Yocto Linux嵌入式镜像)上的服务,通过调用Bash命令修改系统配置(如日期、systemd网络配置等),实现了RunCommand方法。直接运行./Controller一切正常,但后台运行./Controller &时应用会永久挂起。
原实现代码:
public static Result<string> RunCommand(string command, bool wait = true) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return new Result<string>() { ResultObject = "done" }; Result<string> result = new Result<string>(); using (Process proc = new()) { proc.StartInfo.FileName = "/bin/bash"; proc.StartInfo.Arguments = "-c \" " + command + " \""; proc.StartInfo.UseShellExecute = true; proc.StartInfo.RedirectStandardOutput = true; proc.StartInfo.RedirectStandardError = true; proc.StartInfo.CreateNoWindow = true; proc.Start(); proc.OutputDataReceived += Proc_OutputDataReceived; result.ResultObject = proc.StandardOutput.ReadToEnd(); var error = proc.StandardError.ReadToEnd(); if (!string.IsNullOrWhiteSpace(error)) { result.HasError = true; result.ErrorMessage = error; result.ResultCode = Enums.ResultCode.UnspecifiedError; } if (wait) proc.WaitForExit(); proc.OutputDataReceived -= Proc_OutputDataReceived; } return result; }
问题根源
- UseShellExecute与重定向冲突
当设置UseShellExecute = true时,.NET会通过系统shell启动进程,但此时不支持重定向StandardOutput/StandardError。后台运行时,子进程的输出缓冲区无法正常被读取,满了之后会阻塞子进程,进而导致主进程挂起。 - 同步读取输出的死锁风险
先调用StandardOutput.ReadToEnd()再读StandardError.ReadToEnd(),如果子进程往stderr写入大量内容,stderr缓冲区满了之后子进程会阻塞,而主进程还在等待stdout读完,形成死锁。 - OutputDataReceived事件未生效
注册了事件但未调用proc.BeginOutputReadLine(),事件监听根本没启动,完全没起到作用。
修复后的代码
public static Result<string> RunCommand(string command, bool wait = true) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return new Result<string>() { ResultObject = "done" }; Result<string> result = new Result<string>(); using (Process proc = new Process()) { // 核心:禁用UseShellExecute,才能正确重定向输出 proc.StartInfo.FileName = "/bin/bash"; // 优化参数拼接,避免命令含空格时的引号问题 proc.StartInfo.Arguments = $"-c \"{command}\""; proc.StartInfo.UseShellExecute = false; proc.StartInfo.RedirectStandardOutput = true; proc.StartInfo.RedirectStandardError = true; proc.StartInfo.CreateNoWindow = true; // 用异步方式读取输出,彻底避免死锁 StringBuilder outputBuilder = new StringBuilder(); StringBuilder errorBuilder = new StringBuilder(); proc.OutputDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)) outputBuilder.AppendLine(e.Data); }; proc.ErrorDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)) errorBuilder.AppendLine(e.Data); }; proc.Start(); // 启动异步读取流程 proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); if (wait) { // 添加超时处理,防止命令执行卡住导致主进程挂起 if (!proc.WaitForExit(30000)) { proc.Kill(); result.HasError = true; result.ErrorMessage = "命令执行超时"; result.ResultCode = Enums.ResultCode.TimeoutError; return result; } } result.ResultObject = outputBuilder.ToString().Trim(); string errorOutput = errorBuilder.ToString().Trim(); if (!string.IsNullOrWhiteSpace(errorOutput)) { result.HasError = true; result.ErrorMessage = errorOutput; result.ResultCode = Enums.ResultCode.UnspecifiedError; } } return result; }
关键修复说明
- 禁用UseShellExecute:设置为
false是.NET中重定向进程输出的必要前提,确保主进程能直接控制子进程的输出流。 - 异步读取输出:通过
BeginOutputReadLine和BeginErrorReadLine配合事件监听,避免同步读取顺序导致的死锁。 - 参数拼接优化:用字符串插值处理bash命令参数,避免多余空格和引号解析错误。
- 超时控制:添加进程等待超时逻辑,防止命令执行异常时主进程无限挂起。
内容的提问来源于stack exchange,提问作者Marcomattia Mocellin
相关产品推荐
相关产品推荐

