Получить код завершения фонового процесса
У меня есть команда CMD, вызванная из моего основного сценария оболочки Bourne, которая выполняется вечно.
Я хочу изменить скрипт следующим образом:
- Запустите команду CMD параллельно в качестве фонового процесса ($CMD &).
- В основном сценарии есть цикл для отслеживания порожденной команды каждые несколько секунд. Цикл также выводит некоторые сообщения на стандартный вывод, указывающие на ход выполнения скрипта.
- Выйдите из цикла, когда порожденная команда завершится.
- Захватите и сообщите код выхода порожденного процесса.
Может кто-нибудь дать мне указатели для достижения этой цели?
14 ответов
1: в bash, $!
содержит PID последнего фонового процесса, который был выполнен. Это все равно скажет вам, какой процесс контролировать.
4: wait <n>
ожидает завершения процесса с идентификатором (он будет блокироваться до завершения процесса, поэтому вы можете не вызывать его до тех пор, пока не убедитесь, что процесс завершен). После wait
возвращается, код выхода процесса возвращается в переменной $?
2, 3: ps
или же ps | grep " $! "
может сказать вам, работает ли этот процесс. Вам решать, как понять результат и решить, насколько он близок к завершению. (ps | grep
не является идиотом Если у вас есть время, вы можете придумать более надежный способ определить, работает ли этот процесс).
Вот скелетный скрипт:
# simulate a long process that will have an identifiable exit code
(sleep 15 ; /bin/false) &
my_pid=$!
while ps | grep " $my_pid " # might also need | grep -v grep here
do
echo $my_pid is still in the ps output. Must still be running.
sleep 3
done
echo Oh, it looks like the process is done.
wait $my_pid
my_status=$?
echo The exit status of the process was $my_status
Вот как я решил это, когда у меня была похожая потребность:
# Some function that takes a long time to process
longprocess() {
# Sleep up to 14 seconds
sleep $((RANDOM % 15))
# Randomly exit with 0 or 1
exit $((RANDOM % 2))
}
pids=""
# Run five concurrent processes
for i in {1..5}; do
( longprocess ) &
# store PID of process
pids+=" $!"
done
# Wait for all processes to finnish, will take max 14s
for p in $pids; do
if wait $p; then
echo "Process $p success"
else
echo "Process $p fail"
fi
done
Pid фонового дочернего процесса хранится в $!, Вы можете хранить pids всех дочерних процессов в массиве, например, PIDS [].
wait [-n] [jobspec or pid …]
Дождитесь завершения дочернего процесса, заданного каждым идентификатором процесса pid или спецификацией задания, и верните состояние завершения последней ожидаемой команды. Если задана спецификация задания, все процессы в задании ожидаются. Если аргументы не указаны, ожидаются все активные в данный момент дочерние процессы, а статус возврата равен нулю. Если указана опция -n, wait ожидает завершения любого задания и возвращает статус завершения. Если ни jobspec, ни pid не указывают активный дочерний процесс оболочки, возвращаемое состояние - 127.
С помощью команды wait вы можете дождаться завершения всех дочерних процессов, в то время как вы можете получить состояние завершения каждого дочернего процесса через $? и сохраните статус в STATUS []. Затем вы можете сделать что-то в зависимости от статуса.
Я попробовал следующие 2 решения, и они работают хорошо. Решение 01 более краткое, а решение 02 немного сложнее.
solution01
#!/bin/bash
# start 3 child processes concurrently, and store each pid into array PIDS[].
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
./${app} &
PIDS+=($!)
done
# wait for all processes to finish, and store each process's exit code into array STATUS[].
for pid in ${PIDS[@]}; do
echo "pid=${pid}"
wait ${pid}
STATUS+=($?)
done
# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
if [[ ${st} -ne 0 ]]; then
echo "$i failed"
else
echo "$i finish"
fi
((i+=1))
done
solution02
#!/bin/bash
# start 3 child processes concurrently, and store each pid into array PIDS[].
i=0
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
./${app} &
pid=$!
PIDS[$i]=${pid}
((i+=1))
done
# wait for all processes to finish, and store each process's exit code into array STATUS[].
i=0
for pid in ${PIDS[@]}; do
echo "pid=${pid}"
wait ${pid}
STATUS[$i]=$?
((i+=1))
done
# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
if [[ ${st} -ne 0 ]]; then
echo "$i failed"
else
echo "$i finish"
fi
((i+=1))
done
Как я вижу, почти все ответы используют внешние утилиты (в основном ps
) опросить состояние фонового процесса. Существует более универсальное решение, перехватывающее сигнал SIGCHLD. В обработчике сигнала необходимо проверить, какой дочерний процесс был остановлен. Это может быть сделано kill -0 <PID>
встроенный (универсальный) или проверка наличия /proc/<PID>
каталог (специфичный для Linux) или используя jobs
встроенный (специфичный для bash. jobs -l
также сообщает пид. В этом случае 3-е поле вывода может быть остановлено |Running|Done|Exit .).
Вот мой пример.
Запущенный процесс называется loop.sh
, Принимает -x
или число в качестве аргумента. За -x
это выходы с кодом выхода 1. Для числа он ждет num*5 секунд. Каждые 5 секунд он печатает свой PID.
Процесс запуска называется launch.sh
:
#!/bin/bash
handle_chld() {
local tmp=()
for((i=0;i<${#pids[@]};++i)); do
if [ ! -d /proc/${pids[i]} ]; then
wait ${pids[i]}
echo "Stopped ${pids[i]}; exit code: $?"
else tmp+=(${pids[i]})
fi
done
pids=(${tmp[@]})
}
set -o monitor
trap "handle_chld" CHLD
# Start background processes
./loop.sh 3 &
pids+=($!)
./loop.sh 2 &
pids+=($!)
./loop.sh -x &
pids+=($!)
# Wait until all background processes are stopped
while [ ${#pids[@]} -gt 0 ]; do echo "WAITING FOR: ${pids[@]}"; sleep 2; done
echo STOPPED
Подробнее об этом см.: Запуск процесса из скрипта bash завершился неудачно
#/bin/bash
#pgm to monitor
tail -f /var/log/messages >> /tmp/log&
# background cmd pid
pid=$!
# loop to monitor running background cmd
while :
do
ps ax | grep $pid | grep -v grep
ret=$?
if test "$ret" != "0"
then
echo "Monitored pid ended"
break
fi
sleep 5
done
wait $pid
echo $?
Я бы немного изменил ваш подход. Вместо того, чтобы проверять каждые несколько секунд, если команда все еще активна, и сообщать о сообщении, создайте другой процесс, который каждые несколько секунд сообщает, что команда все еще выполняется, а затем завершите этот процесс, когда команда завершится. Например:
#! / Bin/ ш cmd() {сон 5; выход 24; } cmd & # Запустить длительный процесс PID =$! # Записать пид # Создайте процесс, который постоянно сообщает, что команда все еще выполняется в то время как echo "$(date): $pid все еще работает"; спать 1; сделанный & echoer = $! # Установите ловушку, чтобы убить репортера, когда процесс завершится trap 'kill $ echoer' 0 # Дождаться окончания процесса если ждать $pid; затем echo "cmd успешно" еще echo "cmd FAILED!! (вернул $?)" фи
У нашей команды была такая же потребность в удаленном скрипте, выполняемом по SSH, который истекал через 25 минут бездействия. Вот решение с циклом мониторинга, проверяющим фоновый процесс каждую секунду, но печатающим только каждые 10 минут, чтобы подавить таймаут неактивности.
long_running.sh &
pid=$!
# Wait on a background job completion. Query status every 10 minutes.
declare -i elapsed=0
# `ps -p ${pid}` works on macOS and CentOS. On both OSes `ps ${pid}` works as well.
while ps -p ${pid} >/dev/null; do
sleep 1
if ((++elapsed % 600 == 0)); then
echo "Waiting for the completion of the main script. $((elapsed / 60))m and counting ..."
fi
done
# Return the exit code of the terminated background process. This works in Bash 4.4 despite what Bash docs say:
# "If neither jobspec nor pid specifies an active child process of the shell, the return status is 127."
wait ${pid}
Простой пример, аналогичный решениям выше. Это не требует мониторинга какого-либо вывода процесса. В следующем примере используется tail, чтобы следовать выводу.
$ echo '#!/bin/bash' > tmp.sh
$ echo 'sleep 30; exit 5' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh &
[1] 7454
$ pid=$!
$ wait $pid
[1]+ Exit 5 ./tmp.sh
$ echo $?
5
Используйте tail для отслеживания вывода процесса и выхода, когда процесс завершен.
$ echo '#!/bin/bash' > tmp.sh
$ echo 'i=0; while let "$i < 10"; do sleep 5; echo "$i"; let i=$i+1; done; exit 5;' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh
0
1
2
^C
$ ./tmp.sh > /tmp/tmp.log 2>&1 &
[1] 7673
$ pid=$!
$ tail -f --pid $pid /tmp/tmp.log
0
1
2
3
4
5
6
7
8
9
[1]+ Exit 5 ./tmp.sh > /tmp/tmp.log 2>&1
$ wait $pid
$ echo $?
5
Другим решением является мониторинг процессов через файловую систему proc (безопаснее, чем комбо ps/grep); когда вы запускаете процесс, у него есть соответствующая папка в /proc/$pid, поэтому решение может быть
#!/bin/bash
....
doSomething &
local pid=$!
while [ -d /proc/$pid ]; do # While directory exists, the process is running
doSomethingElse
....
else # when directory is removed from /proc, process has ended
wait $pid
local exit_status=$?
done
....
Теперь вы можете использовать переменную $exit_status так, как вам нравится.
При использовании этого метода вашему сценарию не нужно ждать фонового процесса, вам нужно будет только отслеживать временный файл на предмет состояния выхода.
FUNCmyCmd() { sleep 3;return 6; };
export retFile=$(mktemp);
FUNCexecAndWait() { FUNCmyCmd;echo $? >$retFile; };
FUNCexecAndWait&
теперь ваш скрипт может делать что-то еще, в то время как вы просто должны следить за содержимым retFile (он также может содержать любую другую информацию, которую вы хотите, например, время выхода).
PS: кстати, я закодировал мышление в Bash
как насчет ...
# run your stuff
unset PID
for process in one two three four
do
( sleep $((RANDOM%20)); echo hello from process $process; exit $((RANDOM%3)); ) & 2>&1
PID+=($!)
done
# (optional) report on the status of that stuff as it exits
for pid in "${PID[@]}"
do
( wait "$pid"; echo "process $pid complemted with exit status $?") &
done
# (optional) while we wait, monitor that stuff
while ps --pid "${PID[*]}" --ppid "${PID[*]}" --format pid,ppid,command,pcpu
do
sleep 5
done | xargs -i date '+%x %X {}'
# return non-zero if any are non zero
SUCCESS=0
for pid in "${PID[@]}"
do
wait "$pid" && ((SUCCESS++)) && echo "$pid OK" || echo "$pid returned $?"
done
echo "success for $SUCCESS out of ${#PID} jobs"
exit $(( ${#PID} - SUCCESS ))
Мое решение состояло в том, чтобы использовать анонимный канал для передачи статуса в цикл мониторинга. Временные файлы, используемые для обмена статусом, не используются, поэтому очищать нечего. Если вы не уверены в количестве фоновых заданий, условие разрыва может быть[ -z "$(jobs -p)" ]
.
#!/bin/bash
exec 3<> <(:)
{ sleep 15 ; echo "sleep/exit $?" >&3 ; } &
while read -u 3 -t 1 -r STAT CODE || STAT="timeout" ; do
echo "stat: ${STAT}; code: ${CODE}"
if [ "${STAT}" = "sleep/exit" ] ; then
break
fi
done
Это может выходить за рамки вашего вопроса, однако, если вас беспокоит продолжительность времени, в течение которого выполняются процессы, вас может заинтересовать проверка состояния запущенных фоновых процессов через определенный промежуток времени. Достаточно легко проверить, какие дочерние идентификаторы еще используются pgrep -P $$
Однако я предложил следующее решение для проверки состояния выхода тех идентификаторов PID, срок действия которых уже истек:
cmd1() { sleep 5; exit 24; }
cmd2() { sleep 10; exit 0; }
pids=()
cmd1 & pids+=("$!")
cmd2 & pids+=("$!")
lasttimeout=0
for timeout in 2 7 11; do
echo -n "interval-$timeout: "
sleep $((timeout-lasttimeout))
# you can only wait on a pid once
remainingpids=()
for pid in ${pids[*]}; do
if ! ps -p $pid >/dev/null ; then
wait $pid
echo -n "pid-$pid:exited($?); "
else
echo -n "pid-$pid:running; "
remainingpids+=("$pid")
fi
done
pids=( ${remainingpids[*]} )
lasttimeout=$timeout
echo
done
какие выводы:
interval-2: pid-28083:running; pid-28084:running;
interval-7: pid-28083:exited(24); pid-28084:running;
interval-11: pid-28084:exited(0);
Примечание: вы можете изменить $pids
к строковой переменной, а не к массиву, чтобы упростить вещи, если хотите.
Если вы просто хотите запускать фиксированное количество команд параллельно и гарантировать, что ошибки не будут игнорироваться, вы можете сделать этот очень простой вариант:
#!/bin/bash
set -e
python3 -c "import time; import sys; time.sleep(1); sys.exit(1)" &
python3 -c "import time; import sys; time.sleep(3); sys.exit(0)" &
wait -n
wait -n
wait -n
ожидает завершения следующего задания и возвращает код его завершения. Потому что мы использовалиset -e
он завершит весь сценарий с ошибкой.
Обратите внимание, что другое задание по-прежнему будет выполняться в фоновом режиме. Если вы этого не хотите, вы можете сделать что-то вроде этого:
{ wait -n && wait -n ; } || { wait; exit 1; }
Я думаю, что если вам нужно что-то более сложное, вам не следует использовать сценарии оболочки. Сделайте это на Python или Deno.