Получить код завершения фонового процесса

У меня есть команда CMD, вызванная из моего основного сценария оболочки Bourne, которая выполняется вечно.

Я хочу изменить скрипт следующим образом:

  1. Запустите команду CMD параллельно в качестве фонового процесса ($CMD &).
  2. В основном сценарии есть цикл для отслеживания порожденной команды каждые несколько секунд. Цикл также выводит некоторые сообщения на стандартный вывод, указывающие на ход выполнения скрипта.
  3. Выйдите из цикла, когда порожденная команда завершится.
  4. Захватите и сообщите код выхода порожденного процесса.

Может кто-нибудь дать мне указатели для достижения этой цели?

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.

Другие вопросы по тегам