跨平台场景下主进程异常终止时如何终止所有子进程及孙进程?
跨平台实现主进程退出时终止子进程及孙进程
核心思路
跨平台方案需要针对不同操作系统的进程管理机制做适配,核心是在主进程退出(正常/异常)时,递归终止所有关联的子进程、孙进程。以下是分平台的具体实现和统一封装方式:
1. Windows 平台
优先使用Job Object(最可靠,自动覆盖所有子进程/孙进程):
- 创建Job对象,将主进程和所有启动的子进程加入Job,设置
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE标记,主进程退出时Job自动关闭,所有关联进程会被强制终止。 - 若需兼容旧系统,也可通过Windows API或WMI遍历进程树,递归查找父进程链指向主进程的所有进程并调用
Kill()。
2. Linux 平台
- 父进程退出信号绑定:在启动子进程前调用
prctl(PR_SET_PDEATHSIG, SIGTERM),让子进程在主进程退出时自动收到终止信号;孙进程需由子进程同样设置该信号,才能实现连锁终止。 - /proc文件系统遍历:主进程退出前,递归遍历
/proc目录,通过/proc/<pid>/status中的PPid字段识别所有属于当前进程树的进程,发送SIGTERM或SIGKILL信号。
3. macOS 平台
- 类似Linux,可通过
ps命令结合父进程ID遍历进程树,筛选出所有关联进程后发送SIGTERM信号;也可利用launchd特性,但进程树遍历实现更简单直接。
统一封装示例代码
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.IO; public class ProcessGroupManager { private readonly List<Process> _childProcesses = new List<Process>(); private IntPtr _jobHandle = IntPtr.Zero; public Process StartChildProcess(string fileName, string arguments) { var startInfo = new ProcessStartInfo(fileName, arguments) { UseShellExecute = false }; var process = Process.Start(startInfo); if (process == null) return null; _childProcesses.Add(process); // 分平台处理 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { InitializeJobObject(); AddProcessToJob(_jobHandle, process.Handle); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { BindParentExitSignal(); } return process; } // 主进程退出时调用 public void TerminateAllChildProcesses() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _jobHandle != IntPtr.Zero) { CloseHandle(_jobHandle); _jobHandle = IntPtr.Zero; } else { foreach (var process in _childProcesses) { if (!process.HasExited) { TerminateProcessTree(process.Id); } } } _childProcesses.Clear(); } #region Windows Job Object 相关API private void InitializeJobObject() { if (_jobHandle != IntPtr.Zero) return; _jobHandle = CreateJobObject(IntPtr.Zero, null); var info = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION { BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION { LimitFlags = 0x2000 // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE } }; int length = Marshal.SizeOf(info); IntPtr ptr = Marshal.AllocHGlobal(length); Marshal.StructureToPtr(info, ptr, false); SetInformationJobObject(_jobHandle, JobObjectInfoClass.ExtendedLimitInformation, ptr, (uint)length); Marshal.FreeHGlobal(ptr); } [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoClass infoClass, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr hObject); private void AddProcessToJob(IntPtr jobHandle, IntPtr processHandle) { AssignProcessToJobObject(jobHandle, processHandle); } private enum JobObjectInfoClass { ExtendedLimitInformation = 9 } [StructLayout(LayoutKind.Sequential)] private struct JOBOBJECT_BASIC_LIMIT_INFORMATION { public long PerProcessUserTimeLimit; public long PerJobUserTimeLimit; public uint LimitFlags; public UIntPtr MinimumWorkingSetSize; public UIntPtr MaximumWorkingSetSize; public uint ActiveProcessLimit; public UIntPtr Affinity; public uint PriorityClass; public uint SchedulingClass; } [StructLayout(LayoutKind.Sequential)] private struct IO_COUNTERS { public ulong ReadOperationCount; public ulong WriteOperationCount; public ulong OtherOperationCount; public ulong ReadTransferCount; public ulong WriteTransferCount; public ulong OtherTransferCount; } [StructLayout(LayoutKind.Sequential)] private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION { public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; public IO_COUNTERS IoInfo; public UIntPtr ProcessMemoryLimit; public UIntPtr JobMemoryLimit; public UIntPtr PeakProcessMemoryUsed; public UIntPtr PeakJobMemoryUsed; } #endregion #region Linux Prctl API [DllImport("libc.so.6")] private static extern int prctl(int option, int arg2, int arg3, int arg4, int arg5); private const int PR_SET_PDEATHSIG = 1; private const int SIGTERM = 15; private void BindParentExitSignal() { prctl(PR_SET_PDEATHSIG, SIGTERM, 0, 0, 0); } #endregion #region 跨平台进程树遍历终止 private void TerminateProcessTree(int parentId) { var childPids = GetChildProcessIds(parentId); foreach (var pid in childPids) { TerminateProcessTree(pid); } try { var process = Process.GetProcessById(parentId); if (!process.HasExited) { process.Kill(); process.WaitForExit(1000); } } catch (ArgumentException) { // 进程已退出,忽略 } } private List<int> GetChildProcessIds(int parentId) { var childPids = new List<int>(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { var procDirs = Directory.GetDirectories("/proc"); foreach (var dir in procDirs) { if (int.TryParse(Path.GetFileName(dir), out int pid)) { try { var statusPath = Path.Combine(dir, "status"); var statusContent = File.ReadAllText(statusPath); var ppidLine = statusContent.Split('\n').FirstOrDefault(l => l.StartsWith("PPid:")); if (ppidLine != null && int.TryParse(ppidLine.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries)[1], out int ppid) && ppid == parentId) { childPids.Add(pid); } } catch { // 进程已退出,忽略 } } } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { var process = Process.Start(new ProcessStartInfo("ps", $"-o pid,ppid -ax") { RedirectStandardOutput = true, UseShellExecute = false }); if (process != null) { var output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); var lines = output.Split('\n').Skip(1); foreach (var line in lines) { var parts = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2 && int.TryParse(parts[0], out int pid) && int.TryParse(parts[1], out int ppid) && ppid == parentId) { childPids.Add(pid); } } } } return childPids; } #endregion } // 使用示例 class Program { private static readonly ProcessGroupManager _processManager = new ProcessGroupManager(); static void Main(string[] args) { // 注册主进程退出事件 AppDomain.CurrentDomain.ProcessExit += (s, e) => _processManager.TerminateAllChildProcesses(); Console.CancelKeyPress += (s, e) => _processManager.TerminateAllChildProcesses(); // 启动子进程 _processManager.StartChildProcess("echo", "hello world"); // 可启动更多子进程... Console.ReadLine(); } }
注意事项
- 异常终止场景:Windows的Job Object在主进程被强制kill时依然有效;Linux/macOS下
prctl方案更可靠,进程树遍历可能因主进程来不及执行而失效。 - 权限问题:Linux/macOS下终止进程需要足够权限,确保主进程拥有终止子进程的权限。
- 孙进程处理:
prctl方案需要子进程同样设置退出信号,才能让孙进程连锁终止;Job Object则自动覆盖所有后代进程。
内容的提问来源于stack exchange,提问作者Lai Yu-Hsuan
相关产品推荐
相关产品推荐

