如何捕获STDOUT、STDERR与进程PID且避免产生僵尸进程
Terraform包装脚本遗留僵尸进程问题排查与解决
问题背景
我正在为Terraform二进制文件编写包装脚本,作为企业解决方案的一部分,需满足以下需求:
- 文件日志捕获:分别捕获STDOUT、STDERR用于后续分析
- 实时日志捕获:在Jenkins任务中运行时,保持STDOUT和STDERR的实时输出与分离
- PID捕获:让Terraform进程后台运行,后续将添加SIGTERM处理逻辑
当前脚本核心代码如下:
#!/bin/bash ... terraform "$@" > >(tee "${STDOUT_LOG}") 2> >(tee "${STDERR_LOG}" >&2) & TF_PID="$!" wait "$TF_PID" EXIT_CODE="$?" ... wait exit "$EXIT_CODE"
在容器环境中调用该脚本数百次后,发现系统中遗留了大量执行tee命令的shell僵尸进程。即使在脚本退出前添加了通用wait命令,也无法让shell等待这些子进程完成并收割它们。
补充进程状态信息
执行ps aux --forest的输出:
> ps aux --forest 11295 ? S 0:00 | \_ /bin/bash /home/jenkins/workspace/build-1629@tmp/terraform.sh init 11376 ? Sl 0:01 | | \_ terraform init 11377 ? S 0:00 | | \_ /bin/bash /home/jenkins/workspace/build-1629@tmp/terraform.sh init 11379 ? S 0:00 | | | \_ /usr/bin/coreutils --coreutils-prog-shebang=tee /usr/bin/tee -a /home/jenkins/workspace/build-1629/src/vnet-01/stdout.log 11378 ? S 0:00 | | \_ /bin/bash /home/jenkins/workspace/build-1629@tmp/terraform.sh init 11380 ? S 0:00 | | \_ /usr/bin/coreutils --coreutils-prog-shebang=tee /usr/bin/tee -a /home/jenkins/workspace/build-1629/src/vnet-01/stderr.log
数分钟后的ps aux输出:
> ps aux ... 11377 ? Z 0:00 [terraform.sh] <defunct> 11378 ? Z 0:00 [terraform.sh] <defunct> ...
问题根源
问题出在bash进程替换的子shell管理机制上:
>(tee ...)这种进程替换语法会创建独立的子shell来执行tee命令,这些子shell是包装脚本的直接子进程,和Terraform进程属于同级关系,而非Terraform的子进程。- 原脚本仅
wait "$TF_PID"等待Terraform进程退出,并未等待这些执行tee的子shell。 - 额外添加的
wait命令默认仅等待脚本的直接子进程,但bash对进程替换创建的子shell会特殊处理——当父脚本准备退出时,不会主动等待这些子shell完成,导致它们的退出状态无人收割,最终变成僵尸进程(Z状态)。
解决方案
方法1:用命名管道替代进程替换(兼容所有bash版本)
通过手动创建命名管道,显式管理tee进程的生命周期,避免进程替换的子shell问题:
#!/bin/bash # 创建临时命名管道 mkfifo stdout_pipe stderr_pipe # 启动tee进程并记录PID,后台运行 tee "${STDOUT_LOG}" < stdout_pipe & STDOUT_TEE_PID=$! tee "${STDERR_LOG}" < stderr_pipe >&2 & STDERR_TEE_PID=$! # 运行Terraform,将输出重定向到管道 terraform "$@" > stdout_pipe 2> stderr_pipe & TF_PID=$! # 等待Terraform进程退出 wait "$TF_PID" EXIT_CODE=$? # 删除管道,触发tee进程自动退出(管道关闭后tee会终止) rm stdout_pipe stderr_pipe # 等待两个tee进程完全退出 wait "$STDOUT_TEE_PID" "$STDERR_TEE_PID" exit "$EXIT_CODE"
方法2:利用进程组等待(bash 4.3+适用)
启用作业控制,让Terraform和进程替换的子shell归属同一个进程组,然后等待整个进程组:
#!/bin/bash # 启用作业控制,使后台进程形成独立进程组 set -m # 运行Terraform,进程替换的子shell会与它同属一个进程组 terraform "$@" > >(tee "${STDOUT_LOG}") 2> >(tee "${STDERR_LOG}" >&2) & TF_PID=$! # 等待整个进程组退出,而非单个PID wait -p "$TF_PID" EXIT_CODE=$? # 关闭作业控制 set +m exit "$EXIT_CODE"
如果你的bash版本低于4.3,可替换为以下逻辑:
#!/bin/bash set -m terraform "$@" > >(tee "${STDOUT_LOG}") 2> >(tee "${STDERR_LOG}" >&2) & TF_PID=$! # 等待Terraform进程退出 wait "$TF_PID" EXIT_CODE=$? # 向整个进程组发送终止信号,确保子shell退出 kill -TERM -- -$TF_PID 2>/dev/null # 等待所有子进程退出 wait set +m exit "$EXIT_CODE"
内容的提问来源于stack exchange,提问作者Bernard Halas
相关产品推荐
相关产品推荐

