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


130

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

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

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

Может кто-нибудь подскажет, как это сделать?


1
...и победителем становится?
TrueY 07

1
@TrueY .. Боб не входил в систему с того дня, как задал вопрос. Мы вряд ли когда-нибудь узнаем!
ghoti

Ответы:


128

1: В bash $!содержит PID последнего выполненного фонового процесса. Это все равно скажет вам, какой процесс отслеживать.

4: wait <n>ожидает завершения процесса с PID <n>(он будет блокироваться, пока процесс не завершится, поэтому вы, возможно, не захотите вызывать это, пока не будете уверены, что процесс завершен), а затем возвращает код выхода завершенного процесса.

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
# The variable $? always holds the exit code of the last command to finish.
# Here it holds the exit code of $my_pid, since wait exits with that code. 
my_status=$?
echo The exit status of the process was $my_status

15
ps -p $my_pid -o pid=ни то, ни другое grepне требуется.
Приостановлено до дальнейшего уведомления.

54
kill -0 $!- лучший способ узнать, выполняется ли процесс. На самом деле он не отправляет никакого сигнала, а только проверяет, что процесс жив, используя встроенную оболочку вместо внешних процессов. Как man 2 killсказано: «Если sig равен 0, то сигнал не отправляется, но проверка ошибок по-прежнему выполняется; это можно использовать для проверки наличия идентификатора процесса или идентификатора группы процессов».
ephemient

14
@ephemient kill -0вернет ненулевое значение, если у вас нет разрешения на отправку сигналов запущенному процессу. К сожалению, он возвращается 1как в этом случае, так и в случае, когда процесс не существует. Это делает его полезным, если вы не являетесь владельцем процесса - что может иметь место даже для процессов, которые вы создали, если sudoзадействован такой инструмент, или если они установлены с установленным идентификатором (и, возможно, отбрасывают привилегии).
Крейг Рингер,

13
waitне возвращает код выхода в переменной $?. Он просто возвращает код выхода и $?является кодом выхода последней программы переднего плана.
MindlessRanger

7
Для многих голосующих kill -0. Вот рецензируемая ссылка от SO, которая показывает, что комментарий CraigRinger является законным в отношении: kill -0будет возвращать ненулевое значение для запущенных процессов ... но ps -pвсегда будет возвращать 0 для любого запущенного процесса .
Trevor Boyd Smith

58

Вот как я решил это, когда у меня была аналогичная потребность:

# 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 finish, will take max 14s
# as it waits in order of launch, not order of finishing
for p in $pids; do
        if wait $p; then
                echo "Process $p success"
        else
                echo "Process $p fail"
        fi
done

Мне нравится такой подход.
Кемин Чжоу

Спасибо! Это кажется мне самым простым подходом.
Люк Дэвис

4
Это решение не удовлетворяет требованию № 2: цикл мониторинга для каждого фонового процесса. waits заставляют сценарий ждать до самого конца (каждого) процесса.
DKroot

Простой и приятный подход ... давно искали это решение ..
Сантош Кумар Арджунан

Это не работает ... или не делает того, что вы хотите: не проверяет статусы завершения фоновых процессов?
conny

10

Pid фонового дочернего процесса хранится в $! , Вы можете хранить идентификаторы всех дочерних процессов в массиве, например, PIDS [] .

wait [-n] [jobspec or pid …]

Подождите, пока дочерний процесс, указанный в каждом идентификаторе процесса или спецификации задания, не завершится, и не вернет статус выхода последней ожидаемой команды. Если задана спецификация задания, ожидаются все процессы в задании. Если аргументы не указаны, ожидаются все активные в данный момент дочерние процессы, и статус возврата равен нулю. Если указана опция -n, wait ожидает завершения любого задания и возвращает его статус завершения. Если ни jobpec, ни pid не указывают активный дочерний процесс оболочки, статус возврата - 127.

Используйте команду wait, вы можете дождаться завершения всех дочерних процессов, в то время как вы можете получить статус выхода каждого дочернего процесса с помощью $? и сохранить статус в СТАТУС [] . Тогда вы можете делать что-то в зависимости от статуса.

Я пробовал следующие 2 решения, и они работают хорошо. solution01 более лаконичен, а solution02 немного сложнее.

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

Я пробовал и доказал, что он работает хорошо. Вы можете прочитать мое объяснение в коде.
Терри

Пожалуйста, прочтите « Как мне написать хороший ответ? », Где вы найдете следующую информацию: ... постарайтесь указать в своем ответе любые ограничения, предположения или упрощения. Краткость приемлема, но более полные объяснения лучше. Таким образом, ваш ответ приемлем, но у вас гораздо больше шансов получить положительные голоса, если вы сможете подробнее рассказать о проблеме и своем решении. :-)
Ноэль Видмер

1
pid=$!; PIDS[$i]=${pid}; ((i+=1))можно написать более просто, как PIDS+=($!)который просто добавляется к массиву без необходимости использования отдельной переменной для индексации или самого pid. То же самое и с STATUSмассивом.
codeforester

1
@codeforester, спасибо за ваше предложение, я изменил свой исходный код на solution01, он выглядит более кратким.
Терри

То же самое относится и к другим местам, где вы добавляете объекты в массив.
codeforester

8

Как я вижу, почти во всех ответах используются внешние утилиты (в основном ps) для опроса состояния фонового процесса. Есть более unixesh-решение, перехватывающее сигнал SIGCHLD. В обработчике сигналов необходимо проверить, какой дочерний процесс был остановлен. Это можно сделать с помощью kill -0 <PID>встроенного (универсального) или проверки существования /proc/<PID>каталога (для Linux) или с помощью jobsвстроенного (конкретный. jobs -lтакже сообщает pid. В этом случае 3-е поле вывода может быть Остановлено | Выполнено | Готово | Выход. ).

Вот мой пример.

Запущенный процесс называется 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.


1
Поскольку мы говорим о Bash, цикл for можно было бы записать как: for i in ${!pids[@]};с использованием расширения параметров.
PlasmaBinturong

8
#/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 $?

2
Вот уловка, чтобы избежать grep -v. Вы можете ограничить поиск до начала строки: grep '^'$pidПлюс, вы все ps p $pid -o pid=равно можете это сделать . Кроме того, tail -fон не закончится, пока вы его не убьете, поэтому я не думаю, что это очень хороший способ продемонстрировать это (по крайней мере, не указывая на это). Возможно, вы захотите перенаправить вывод своей psкоманды, /dev/nullиначе он будет отображаться на экране при каждой итерации. Ваши exitпричины waitпропуска файла - вероятно, это должен быть файл break. Но разве while/ psи не waitлишние?
Приостановлено до дальнейшего уведомления.

5
Почему все забывают kill -0 $pid? На самом деле он не отправляет никакого сигнала, а только проверяет, что процесс жив, используя встроенную оболочку вместо внешних процессов.
ephemient

3
Потому что вы можете убить только процесс, которым владеете:bash: kill: (1) - Operation not permitted
errant.info

2
Шлейф является избыточным. Просто подожди. Меньше кода => меньше крайних случаев.
Brais Gabin

@Brais Gabin Цикл мониторинга - требование №2 вопроса
DKroot

5

Я бы немного изменил ваш подход. Вместо того, чтобы проверять каждые несколько секунд, активна ли команда и сообщать о сообщении, используйте другой процесс, который каждые несколько секунд сообщает, что команда все еще выполняется, а затем завершает этот процесс, когда команда завершается. Например:

#! / Bin / ш

cmd () {сна 5; выход 24; }

cmd & # Запустить длительный процесс
PID = $! # Записываем pid

# Создать процесс, который постоянно сообщает, что команда все еще выполняется
while echo "$ (date): $ pid все еще работает"; спать 1; сделано &
echoer = $!

# Установите ловушку, чтобы убить репортера, когда процесс завершится
ловушка 'kill $ echoer' 0

# Дождитесь завершения процесса
если подождать $ pid; затем
    echo "cmd успешно завершен"
еще
    echo "cmd FAILED !! (вернул $?)"
фи

отличный шаблон, спасибо, что поделился! Я считаю, что вместо ловушки мы также можем сделать while kill -0 $pid 2> /dev/null; do X; done, надеюсь, это будет полезно для кого-то в будущем, кто прочитает это сообщение;)
панкбит

3

У нашей команды была такая же потребность в удаленном 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}

2

Простой пример, аналогичный приведенным выше решениям. Это не требует мониторинга выходных данных процесса. В следующем примере хвост используется для отслеживания вывода.

$ 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

1

Другое решение - контролировать процессы через файловую систему 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 как хотите.


Не работает в bash? Syntax error: "else" unexpected (expecting "done")
benjaoming

1

С помощью этого метода ваш сценарий не должен ждать фонового процесса, вам нужно будет только отслеживать временный файл для статуса выхода.

FUNCmyCmd() { sleep 3;return 6; };

export retFile=$(mktemp); 
FUNCexecAndWait() { FUNCmyCmd;echo $? >$retFile; }; 
FUNCexecAndWait&

теперь ваш скрипт может делать что угодно, а вам просто нужно следить за содержимым retFile (он также может содержать любую другую информацию, которую вы хотите, например время выхода).

PS: кстати, я закодировал мышление на bash


0

Это может выходить за рамки вашего вопроса, однако, если вас беспокоит продолжительность выполнения процессов, вам может быть интересно проверить статус запущенных фоновых процессов через определенный промежуток времени. Достаточно легко проверить, какие дочерние PID все еще используются 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строковую переменную, а не массив, чтобы упростить задачу, если хотите.


0

Мое решение состояло в том, чтобы использовать анонимный канал для передачи статуса в цикл мониторинга. Временные файлы, используемые для обмена статусом, не используются, поэтому очищать нечего. Если вы не уверены в количестве фоновых заданий, причиной может быть остановка [ -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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.