如何在Bash中追踪并终止脚本衍生的所有未知子进程?
我有一个名为pipeline_runner.sh的Bash包装脚本,它会启动一个复杂的建模脚本,该脚本又会自行启动多个子进程和脚本。我希望能追踪该建模脚本单次运行所衍生的所有进程,以便在满足特定条件时将它们全部终止。
示例pipeline_runner.sh脚本内容如下:
#!/bin/bash # Some set up of the script ... ./monitor_job.sh ... arguments TBD ... & script_path="path/to/bash/script" chmod u+x "$script_path" "$script_path" # ...
每次运行pipeline_runner.sh时,都会在后台启动一个monitor_job.sh实例,用于监控本次运行所启动的path/to/bash/script进程。当monitor_job.sh中定义的任意条件满足时,需要能够终止该次运行的path/to/bash/script进程及其直接或间接衍生的所有进程。
由path/to/bash/script运行所启动的其他进程数量众多且不固定,因此我需要找到一种方法将所有衍生脚本归入某个组或列表,以便在需要时全部终止。仅终止初始的$script_path进程是不够的,因为该脚本的所有子进程会继续运行。
重要的次要目标:
- 实现动态适配,不依赖于
$script_path指定的具体脚本,即不能硬编码特定命令名称进行查找; - 监控操作需在独立脚本
monitor_job.sh中执行,而非直接在pipeline_runner.sh中进行。
请问如何追踪建模脚本启动的所有进程,以便在需要时将它们全部终止?
方法1:利用进程组(Process Group)批量终止
这是Bash原生的轻量方案,无需额外工具,核心是让目标脚本及其所有子进程归入同一个进程组。
步骤1:修改pipeline_runner.sh传递进程组ID
修改启动逻辑,记录目标脚本的进程组ID并传递给监控脚本:
#!/bin/bash # Some set up of the script ... script_path="path/to/bash/script" chmod u+x "$script_path" # 启动目标脚本并获取其PID与进程组ID "$script_path" & TARGET_PID=$! # 提取进程组ID并去除空格 TARGET_PGID=$(ps -o pgid= "$TARGET_PID" | tr -d ' ') # 将进程组ID作为参数传入monitor_job.sh ./monitor_job.sh "$TARGET_PGID" & # 等待目标脚本执行完成 wait "$TARGET_PID"
步骤2:在monitor_job.sh中终止整个进程组
触发终止条件时,直接向进程组发送信号(-前缀表示目标是进程组而非单个PID):
#!/bin/bash TARGET_PGID="$1" # 示例监控逻辑:当触发文件存在时终止进程组 while true; do if [ -f "/tmp/terminate_trigger" ]; then # 先发送SIGTERM让进程优雅退出 kill -TERM -"$TARGET_PGID" # 5秒后强制终止未退出的进程 sleep 5 kill -KILL -"$TARGET_PGID" 2>/dev/null exit 0 fi sleep 1 done
注意:如果目标脚本主动调用setsid脱离进程组,需要调整脚本避免此操作,否则子进程会脱离监控范围。
方法2:使用cgroup(Linux专属,适合精细控制)
cgroup是Linux内核提供的进程分组管理机制,所有衍生进程会自动归入同一cgroup,适合需要同时管控资源的场景。
步骤1:修改pipeline_runner.sh创建临时cgroup
#!/bin/bash # 创建以当前脚本PID命名的临时cgroup(需root权限或配置用户cgroup权限) CGROUP_NAME="pipeline_$$" mkdir -p /sys/fs/cgroup/cpu/"$CGROUP_NAME" mkdir -p /sys/fs/cgroup/pids/"$CGROUP_NAME" # 将当前脚本(pipeline_runner.sh)加入cgroup,避免误杀自身 echo "$$" > /sys/fs/cgroup/cpu/"$CGROUP_NAME"/cgroup.procs echo "$$" > /sys/fs/cgroup/pids/"$CGROUP_NAME"/cgroup.procs script_path="path/to/bash/script" chmod u+x "$script_path" # 启动目标脚本并加入cgroup "$script_path" & TARGET_PID=$! echo "$TARGET_PID" > /sys/fs/cgroup/cpu/"$CGROUP_NAME"/cgroup.procs echo "$TARGET_PID" > /sys/fs/cgroup/pids/"$CGROUP_NAME"/cgroup.procs # 传递cgroup名称给监控脚本 ./monitor_job.sh "$CGROUP_NAME" & wait "$TARGET_PID" # 清理临时cgroup rmdir /sys/fs/cgroup/cpu/"$CGROUP_NAME" rmdir /sys/fs/cgroup/pids/"$CGROUP_NAME"
步骤2:在monitor_job.sh中通过cgroup终止进程
#!/bin/bash CGROUP_NAME="$1" while true; do if [ -f "/tmp/terminate_trigger" ]; then # 获取cgroup内所有进程PID并发送终止信号 ALL_PIDS=$(cat /sys/fs/cgroup/cpu/"$CGROUP_NAME"/cgroup.procs) kill -TERM $ALL_PIDS sleep 5 kill -KILL $ALL_PIDS 2>/dev/null exit 0 fi sleep 1 done
方法3:递归遍历进程树(无权限场景备选)
通过PID递归查找所有子进程,适合无法使用cgroup或进程组的场景,但存在微小的漏杀风险(遍历过程中新增的子进程可能未被捕获)。
步骤1:修改pipeline_runner.sh传递目标PID
#!/bin/bash script_path="path/to/bash/script" chmod u+x "$script_path" "$script_path" & TARGET_PID=$! # 传递目标PID给监控脚本 ./monitor_job.sh "$TARGET_PID" & wait "$TARGET_PID"
步骤2:在monitor_job.sh中实现进程树遍历与终止
#!/bin/bash TARGET_PID="$1" # 递归获取所有子进程PID get_child_pids() { local parent_pid="$1" # 获取父进程的直接子进程 local child_pids=$(ps -o pid= --ppid "$parent_pid" | tr -d ' ') echo "$child_pids" # 递归查找子进程的子进程 for pid in $child_pids; do get_child_pids "$pid" done } while true; do if [ -f "/tmp/terminate_trigger" ]; then # 收集目标PID及其所有子进程 ALL_PIDS="$TARGET_PID $(get_child_pids "$TARGET_PID")" kill -TERM $ALL_PIDS sleep 5 kill -KILL $ALL_PIDS 2>/dev/null exit 0 fi sleep 1 done
内容的提问来源于stack exchange,提问作者Chephen

