You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

跨平台场景下主进程异常终止时如何终止所有子进程及孙进程?

跨平台实现主进程退出时终止子进程及孙进程

核心思路

跨平台方案需要针对不同操作系统的进程管理机制做适配,核心是在主进程退出(正常/异常)时,递归终止所有关联的子进程、孙进程。以下是分平台的具体实现和统一封装方式:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 18:24:51