获取后台进程的退出代码
1. 概述
在 Linux 系统中,当一个进程终止时,它会返回一个代码。例如,我们可以使用此代码来了解流程执行过程中是否出现错误。约定是如果执行成功则返回 0,如果出现错误则返回非零。
在本教程中,我们将讨论如何获取在后台运行的进程的退出代码。为此,我们将使用 Bash 内置命令wait 。
首先,我们将只关注后台的一个进程。然后,我们将看到如何同时监控多个进程。
2. 使用wait获取退出码
我们可以使用wait获取在后台运行的进程的退出代码:
$ wait <pid>
执行wait时,它接收进程 ID 作为参数并等待直到该进程终止。然后,等待本身返回进程返回的原始退出代码。
Bash 提供了两个我们可以使用的特殊变量:
- *$!*是最后发送到后台的进程的PID
- *$?*是最后一个进程的退出代码
让我们编写一个函数 来打印作为第一个参数传递的 PID 的退出代码:
$ wait_and_echo() {
PID=$1
echo Waiting for PID $PID to terminate
wait $PID
CODE=$?
echo PID $PID terminated with exit code $CODE
return $CODE
}
请注意,我们的函数还返回退出代码。
现在,我们可以等待后台运行的进程并获取其退出代码。让我们运行一个以代码42退出的子shell ,然后我们在它的 PID 上使用我们的函数wait_and_echo():
$ (sleep 20; exit 42) &
$ wait_and_echo $!
Waiting for PID 13160 to terminate
PID 13160 terminated with exit code 42
$ echo $?
42
3. 监控循环内的进程
有时,我们不想阻塞执行等待进程。在这种情况下,我们可以使用循环来检查进程是否仍在每次迭代中运行。这允许我们在等待时做其他事情。
当一个进程终止时,它的退出代码被保存。因此,我们可以使用wait来获取已经完成的进程的退出代码。
确定进程是否仍在运行的一种方法是检查文件夹/proc/是否存在。
让我们编写一个执行非阻塞等待的函数:
$ non_blocking_wait() {
PID=$1
if [ ! -d "/proc/$PID" ]; then
wait $PID
CODE=$?
else
CODE=127
fi
return $CODE
}
在这个函数中,我们只在目录*/proc/$PID不存在时调用wait* ,这意味着进程没有运行。如果它仍在运行,我们返回值127。当出现错误时, wait命令使用相同的值,因此我们的函数的行为类似于 wait 命令本身。
现在,我们可以在后台运行一个进程并获得它的退出代码而不会阻塞。让我们编写一个名为wait_in_loop.sh的脚本:
#!/bin/bash
non_blocking_wait() {
PID=$1
if [ ! -d "/proc/$PID" ]; then
wait $PID
CODE=$?
else
CODE=127
fi
return $CODE
}
(sleep 5; exit 42) &
PID=$!
while /bin/true; do
date
non_blocking_wait $PID
CODE=$?
if [ $CODE -ne 127 ]; then
echo "PID $PID terminated with exit code $CODE"
break
fi
sleep 2
done
最后,让我们运行它:
$ ./wait_in_loop.sh
Tue May 4 15:11:17 2021
Tue May 4 15:11:19 2021
Tue May 4 15:11:21 2021
Tue May 4 15:11:23 2021
PID 20664 terminated with exit code 42
正如我们在上一节中所做的那样,后台进程以代码42退出。但是,使用这种方法,我们仍然可以在等待进程终止的同时运行date命令。
4. 处理 SIGCHLD
另一种知道进程已终止的方法是处理 SIGCHLD 信号。如果作为当前 shell 的子进程的任何进程终止,则此信号将到达。
然后,我们可以在信号到达时执行等待,不需要阻塞执行,也不需要不断检查进程是否已经终止。
让我们编写一个函数作为 SIGCHLD 的处理程序:
$ handle_sigchld() {
if [ -n "$PID" -a ! -d "/proc/$PID" ]; then
wait $PID
CODE=$?
echo PID $PID terminated with exit code $CODE
unset PID
fi
}
**由于 SIGCHLD 是在任何子进程终止时传递的,所以我们应该首先保存我们需要监控的进程的 PID。*所以,在这个处理程序中,当变量$PID有值和文件夹/proc/$PID不存在时,我们都会调用wait 。*这意味着有一个进程需要检查,并且它已经终止。
一旦我们得到退出代码,我们将取消设置*$PID变量,因此处理程序将忽略 SIGCHLD,直到有新的 PID 需要监控。 现在,当信号 SIGCHLD 到达时,我们使用陷阱handle_sigchld SIGCHLD调用handle_sigchld()*。
让我们编写一个名为wait_in_sigchld.sh的脚本来在后台启动一个进程,同时执行另一个任务:
#!/bin/bash
handle_sigchld() {
if [ -n "$PID" -a ! -d "/proc/$PID" ]; then
wait $PID
CODE=$?
echo PID $PID terminated with exit code $CODE
unset PID
fi
}
unset PID
trap handle_sigchld SIGCHLD
(sleep 5; exit 42) &
PID=$!
echo Starting background process with PID $PID
echo Starting dd
timeout 7s dd if=/dev/zero of=/dev/null
echo dd terminated
在这个例子中,我们使用timeout命令只执行dd 7 秒。 现在,让我们运行它:
$ ./wait_in_sigchld.sh
Starting background process with PID 28173
Starting dd
PID 28173 terminated with exit code 42
dd terminated
正如我们所见,我们得到了退出代码,而无需阻止执行。此外,我们在前台运行dd而不需要循环。
**然而,这种方法有一些缺点。**由于处理程序需要知道子进程的 PID,因此进程终止的速度可能比设置PID=$! . 此外,如果有一个进程在前台运行(在我们的示例中为dd),脚本将不会处理该信号,直到前台中的进程终止。
5. 如何同时监控多个进程
至此,我们讨论了当后台只有一个进程时如何获取退出码。如果我们想同时监控多个进程怎么办?
**为此,我们可以使用一个数组 来存储我们需要监控的所有 PID。**然后,我们可以在每个 PID 上使用wait 。
后台进程终止后,我们需要从数组中删除 PID。我们将使用数组索引来存储 PID。这样,很容易使用*$ unset PIDS[$PID] 删除 PID。*
5.1. 使用等待
我们一直在使用只有一个参数的等待,但我们可以将更多的 PID 传递给它来等待。要知道哪个进程终止了,哪个是它的退出代码,我们需要使用参数-n和-p。**
使用*-n*,我们告诉wait在任何 PID 终止时返回,而不是等待所有这些。使用*-p*,我们指定存储 PID 的变量名称。
让我们修改第 2 节中的示例,在函数wait_and_echo中添加对多进程的支持:
$ wait_and_echo() {
PIDS=()
for PID in [[email protected]](/cdn_cgi/l/email_protection); do
PIDS[$PID]=1
done
while [ ${#PIDS[@]} -ne 0 ]; do
wait -n -p PID ${!PIDS[@]}
CODE=$?
echo PID $PID terminated with exit code $CODE
unset PIDS[$PID]
done
}
让我们尝试监控 3 个后台进程:
$ (sleep 20; exit 42) &
$ PID1=$!
$ (sleep 22; exit 43) &
$ PID2=$!
$ (sleep 24; exit 44) &
$ PID3=$!
$ wait_and_echo $PID1 $PID2 $PID3
Waiting for PID 31759 to terminate
PID 31759 terminated with exit code 42
Waiting for PID 31902 to terminate
PID 31902 terminated with exit code 43
Waiting for PID 32014 to terminate
PID 32014 terminated with exit code 44
5.2. 在循环中使用我们的非阻塞等待
为了监控多个进程,我们将遍历 PID 数组,检查它们是否已终止。让我们重写我们的wait_in_loop.sh脚本:
#!/bin/bash
non_blocking_wait() {
PID=$1
if [ ! -d "/proc/$PID" ]; then
wait $PID
CODE=$?
else
CODE=127
fi
return $CODE
}
PIDS=()
(sleep 5; exit 42) &
PIDS[$!]=1
(sleep 7; exit 43) &
PIDS[$!]=1
(sleep 9; exit 44) &
PIDS[$!]=1
while [ ${#PIDS[@]} -ne 0 ]; do
date
for PID in ${!PIDS[@]}; do
non_blocking_wait $PID
CODE=$?
if [ $CODE -ne 127 ]; then
echo "PID $PID terminated with exit code $CODE"
unset PIDS[$PID]
fi
done
sleep 2
done
正如我们所看到的,我们将在后台仍有 PID 运行时调用date 。当它们中的任何一个终止时,我们将其从 PIDS 数组中删除。 让我们看看它是如何工作的:
$ ./wait_in_loop.sh
Mon May 4 17:40:39 2021
Mon May 4 17:40:41 2021
Mon May 4 17:40:43 2021
Mon May 4 17:40:45 2021
PID 12018 terminated with exit code 42
Mon May 4 17:40:47 2021
PID 12019 terminated with exit code 43
Mon May 4 17:40:49 2021
PID 12020 terminated with exit code 44
5.3. 使用 SIGCHLD 处理程序
最后,我们可以在脚本wait_in_sigchld.sh中修改我们的 SIGCHLD 处理程序以支持多个 PID:
#!/bin/bash
handle_sigchld() {
for PID in ${!PIDS[@]}; do
if [ ! -d "/proc/$PID" ]; then
wait $PID
CODE=$?
echo PID $PID terminated with exit code $CODE
unset PIDS[$PID]
fi
done
}
PIDS=()
trap handle_sigchld SIGCHLD
(sleep 9; exit 44) &
PIDS[$!]=1
(sleep 7; exit 43) &
PIDS[$!]=1
(sleep 5; exit 42) &
PIDS[$!]=1
echo Starting background processes with PIDS ${!PIDS[@]}
echo Starting dd
timeout 15s dd if=/dev/zero of=/dev/null
echo dd terminated
在这个脚本中,当*handle_sigchld()*被调用时,我们遍历所有的 PID 来检查它们是否已经退出。 让我们看看它是如何工作的:
$ ./wait_in_sigchld.sh
Starting background process with PIDS 31491 31492 31493
Starting dd
PID 31491 terminated with exit code 44
PID 31492 terminated with exit code 43
PID 31493 terminated with exit code 42
dd terminated