在C#开发中,经常需要由主程序拉起别的exe、脚本或者命令行工具,并且在它们运行期间掌握状态,用完之后干净地关掉。.NET在System.Diagnostics命名空间下提供了Process类,它是对操作系统进程接口的托管封装,能够完成启动、传参、重定向输出、关闭以及监控等一整套操作。

一、使用Process启动外部进程
启动外部程序最核心的类型是Process和ProcessStartInfo。ProcessStartInfo用来描述“要运行什么、怎么运行”,例如可执行文件路径、命令行参数、工作目录,以及是否隐藏窗口、是否重定向标准输出等。配置好之后,调用Process.Start方法即可创建操作系统层面的进程。
下面示例演示启动一个控制台程序并读取它的标准输出。注意当UseShellExecute设为false时,才能对StandardOutput等流进行重定向;否则系统会使用Shell启动,托管程序拿不到这些流。
using System;
using System.Diagnostics;
class Program
{
static void Main()
{
ProcessStartInfo info = new ProcessStartInfo();
info.FileName = "ping.exe";
info.Arguments = "127.0.0.1 -n 3";
info.UseShellExecute = false;
info.RedirectStandardOutput = true;
info.CreateNoWindow = true;
using (Process proc = Process.Start(info))
{
string output = proc.StandardOutput.ReadToEnd();
proc.WaitForExit();
Console.WriteLine("退出码:" + proc.ExitCode);
Console.WriteLine(output);
}
}
}
上面的代码通过RedirectStandardOutput把子进程的输出捕获到托管字符串中,适合需要解析结果的场景。若只是单纯启动一个GUI程序,可以保持UseShellExecute为true,并省略输出重定向,这样进程会以用户桌面环境正常打开。
需要提醒的是,Process对象实现了IDisposable,使用using或者手动调用Dispose可以释放句柄资源。很多长期运行的服务如果频繁启动进程却忘记释放,会出现句柄数缓慢上涨的问题。
二、关闭和终止进程
正常关闭进程应当优先使用CloseMainWindow方法,它会向进程主窗口发送关闭消息,相当于用户点击右上角关闭按钮,给程序一个清理资源的机会。只有当程序没有界面或者失去响应时,才考虑调用Kill强制结束。
以下代码展示了先尝试优雅关闭、超时后再强制终止的模式:
using System;
using System.Diagnostics;
class ProcessKiller
{
public static void Stop(Process proc, int gracefulTimeoutMs)
{
if (proc == null || proc.HasExited)
return;
if (proc.MainWindowHandle != IntPtr.Zero)
{
proc.CloseMainWindow();
if (proc.WaitForExit(gracefulTimeoutMs))
return;
}
try
{
proc.Kill();
proc.WaitForExit();
}
catch (Exception ex)
{
Console.WriteLine("终止失败:" + ex.Message);
}
}
}
CloseMainWindow只对拥有消息循环的GUI进程有效。如果启动的是控制台程序且没有窗口,MainWindowHandle通常为零,这时只能依靠Kill。Kill会连同子进程树一起结束吗?默认不会,它只结束当前进程对象对应的那一个。若需要结束整棵进程树,需要借助WMI或者递归遍历同名父进程,这属于进阶内容。
另外,Kill是异步的,调用后进程不会立刻消失,所以后面跟一个WaitForExit能够确保资源回收完成,也避免后续代码误以为进程已经彻底退出。
三、监控进程状态
监控分为两种思路:轮询和事件通知。轮询适合主线程本来就有循环逻辑的服务,事件通知则更省CPU,适合事件驱动的程序。
3.1 轮询方式
通过HasExited属性和Responding属性可以定期判断进程是否还在运行、是否卡死。Responding仅对GUI进程有意义,它内部调用SendMessage超时检测,若返回false说明界面已无响应。
using System;
using System.Diagnostics;
using System.Threading;
class Poller
{
public static void Watch(Process proc)
{
while (!proc.HasExited)
{
bool alive = proc.Responding;
Console.WriteLine("运行中,响应状态:" + alive);
Thread.Sleep(2000);
}
Console.WriteLine("进程已退出,退出码:" + proc.ExitCode);
}
}
轮询写起来直观,但Sleep会阻塞线程,在UI程序里应当放到后台线程或者用Timer。它的优点是逻辑集中,不需要处理事件绑定生命周期。
3.2 事件方式
Process类暴露了Exited事件,开启EnableRaisingEvents后,进程结束时CLR会回调。这种方式不占用主逻辑,适合监控多个外部工具。
using System;
using System.Diagnostics;
class EventWatcher
{
public static void Attach(Process proc)
{
proc.EnableRaisingEvents = true;
proc.Exited += (sender, e) =>
{
Process p = (Process)sender;
Console.WriteLine("进程退出,码:" + p.ExitCode);
};
}
}
事件方式的缺点是如果进程在绑定事件之前就退出,可能漏掉通知,因此应在Start之后立即设置EnableRaisingEvents。此外,在多进程批量管理时,建议把每个Process放入字典,以Id为键,在Exited里移除并释放。
四、获取资源占用与综合实践
除了存活状态,有时还要知道进程吃了多少CPU和内存。Process自带WorkingSet64等属性可拿内存,但CPU占用率需要借助PerformanceCounter或者自己按时间差计算Process.TotalProcessorTime。
下面示例组合了启动、超时关闭与退出事件,形成一个最小可用的管理器片段:
using System;
using System.Diagnostics;
class Manager
{
public static Process LaunchAndGuard(string file, string args)
{
ProcessStartInfo si = new ProcessStartInfo(file, args);
si.UseShellExecute = false;
si.CreateNoWindow = true;
Process p = Process.Start(si);
p.EnableRaisingEvents = true;
p.Exited += (s, e) => Console.WriteLine("外部程序结束");
return p;
}
}
综合来看,C#通过Process类能以很少的代码完成进程管理。关键点在于:启动前想清楚要不要重定向输出,关闭时优先优雅退出再强制Kill,监控时根据程序类型选轮询或事件,并且始终注意Dispose释放句柄。把这些细节串起来,就能写出稳定可靠的外部进程调度模块。