Uzyskaj kod zakończenia procesu w tle

130

Mam polecenie wywołane przez CMD z mojego głównego skryptu powłoki Bourne, które trwa wiecznie.

Chcę zmodyfikować skrypt w następujący sposób:

  1. Uruchom polecenie CMD równolegle jako proces w tle ( CMD &).
  2. W głównym skrypcie użyj pętli, aby monitorować pojawiające się polecenie co kilka sekund. Pętla odbija również echo niektórych komunikatów na stdout wskazujących postęp skryptu.
  3. Wyjdź z pętli po zakończeniu spawnowanego polecenia.
  4. Przechwyć i zgłoś kod zakończenia zwołanego procesu.

Czy ktoś może mi dać wskazówki, jak to osiągnąć?

pion
źródło
1
...A zwycięzcą jest?
TrueY,
1
@TrueY .. bob nie zalogował się od dnia, w którym zadał pytanie. Nigdy się nie dowiemy!
ghoti

Odpowiedzi:

127

1: W bash $!przechowuje PID ostatniego wykonanego procesu w tle. To i tak powie ci, jaki proces monitorować.

4: wait <n>czeka, aż proces z PID <n>zostanie zakończony (będzie blokować do zakończenia procesu, więc możesz nie chcieć wywoływać tego, dopóki nie będziesz pewien, że proces się zakończył), a następnie zwraca kod zakończenia ukończonego procesu.

2, 3: pslub ps | grep " $! "może powiedzieć, czy proces nadal działa. To od Ciebie zależy, jak zrozumiesz wynik i zdecydujesz, jak blisko jest zakończenia. ( ps | grepnie jest odporny na idiotę. Jeśli masz czas, możesz wymyślić bardziej niezawodny sposób na sprawdzenie, czy proces nadal działa).

Oto szkielet skryptu:

# 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
tłum
źródło
15
ps -p $my_pid -o pid=nie grepjest potrzebne.
Wstrzymano do odwołania.
54
kill -0 $!to lepszy sposób sprawdzenia, czy proces nadal działa. W rzeczywistości nie wysyła żadnego sygnału, a jedynie sprawdza, czy proces działa, używając wbudowanej powłoki zamiast procesów zewnętrznych. Jak man 2 killpowiedziano: „Jeśli sig ma wartość 0, to żaden sygnał nie jest wysyłany, ale sprawdzanie błędów jest nadal wykonywane; można to wykorzystać do sprawdzenia istnienia identyfikatora procesu lub identyfikatora grupy procesów”.
ephemient
14
@ephemient kill -0zwróci wartość niezerową, jeśli nie masz uprawnień do wysyłania sygnałów do uruchomionego procesu. Niestety powraca 1zarówno w tym przypadku, jak iw przypadku, gdy proces nie istnieje. To sprawia, że ​​jest to przydatne, chyba że nie jesteś właścicielem procesu - co może mieć miejsce nawet w przypadku procesów, które utworzyłeś, jeśli sudozaangażowane jest takie narzędzie, jak i jeśli są ustawione (i prawdopodobnie porzucają uprawnienia).
Craig Ringer
13
waitnie zwraca kodu wyjścia w zmiennej $?. Po prostu zwraca kod zakończenia i $?jest kodem zakończenia najnowszego programu na pierwszym planie.
MindlessRanger
7
Dla wielu osób głosujących na kill -0. Oto recenzowane referencje od SO, które pokazują, że komentarz CraigRingera jest uzasadniony: kill -0zwróci wartość niezerową dla uruchomionych procesów ... ale ps -pzawsze zwróci 0 dla każdego działającego procesu .
Trevor Boyd Smith
58

Tak to rozwiązałem, gdy miałem podobną potrzebę:

# 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
Bjorn
źródło
Podoba mi się to podejście.
Kemin Zhou
Dzięki! To wydaje mi się najprostsze.
Luke Davis
4
To rozwiązanie nie spełnia wymagania nr 2: pętli monitorowania na proces w tle. waits powodują, że skrypt czeka do samego końca (każdego) procesu.
DKroot
Proste i miłe podejście .. zostały poszukiwania takiego rozwiązania dość kiedyś ..
Santosh Kumar Arjunan
To nie działa ... lub nie robi tego, co chcesz: nie sprawdza statusów zakończenia procesów w tle?
conny
10

Pid procesu potomnego w tle jest przechowywany w $! . Możesz przechowywać pidy wszystkich procesów potomnych w tablicy, np. PIDS [] .

wait [-n] [jobspec or pid …]

Zaczekaj, aż proces potomny określony przez każdy identyfikator procesu pid lub specyfikację zadania jobspec zakończy działanie i zwróć kod zakończenia ostatniej oczekiwanej komendy. Jeśli podano specyfikację zadania, oczekiwane są wszystkie procesy w zadaniu. Jeśli nie podano żadnych argumentów, czekane są wszystkie aktualnie aktywne procesy potomne, a zwracany kod wynosi zero. Jeśli podano opcję -n, wait czeka na zakończenie dowolnego zadania i zwraca jego kod zakończenia. Jeśli ani specyfikacja zadania, ani pid nie określają aktywnego procesu potomnego powłoki, zwracany jest kod 127.

Użyj polecenia czekaj , możesz poczekać na zakończenie wszystkich procesów potomnych, w międzyczasie możesz uzyskać status zakończenia każdego procesu potomnego za pomocą $? i zapisz status w STATUS [] . Następnie możesz zrobić coś w zależności od statusu.

Wypróbowałem następujące 2 rozwiązania i działają dobrze. rozwiązanie01 jest bardziej zwięzłe, podczas gdy rozwiązanie02 jest trochę skomplikowane.

rozwiązanie01

#!/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

rozwiązanie02

#!/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
Terry
źródło
Próbowałem i udowodniłem, że działa dobrze. Możesz przeczytać moje wyjaśnienie w kodzie.
Terry,
Przeczytaj „ Jak napisać dobrą odpowiedź? ”, Gdzie znajdziesz następujące informacje: ... spróbuj wspomnieć o ograniczeniach, założeniach lub uproszczeniach w swojej odpowiedzi. Zwięzłość jest akceptowalna, ale pełniejsze wyjaśnienia są lepsze. Twoja odpowiedź jest zatem akceptowalna, ale masz znacznie większe szanse na uzyskanie pozytywnych głosów, jeśli możesz rozwinąć problem i swoje rozwiązanie. :-)
Noel Widmer
1
pid=$!; PIDS[$i]=${pid}; ((i+=1))można napisać prościej, jako PIDS+=($!)który po prostu dołącza się do tablicy bez konieczności używania oddzielnej zmiennej do indeksowania lub samego pid. To samo dotyczy STATUStablicy.
codeforester
1
@codeforester, dziękuję za twoją sugestię, zmodyfikowałem mój kod początkowy do solution01, wygląda bardziej zwięźle.
Terry
To samo dotyczy innych miejsc, w których dodajesz elementy do tablicy.
codeforester
8

Jak widzę, prawie wszystkie odpowiedzi używają zewnętrznych narzędzi (głównie ps) do badania stanu procesu w tle. Istnieje bardziej unixeshowe rozwiązanie, wyłapujące sygnał SIGCHLD. W programie obsługi sygnału należy sprawdzić, który proces potomny został zatrzymany. Można to zrobić poprzez kill -0 <PID>wbudowane (uniwersalne) lub sprawdzenie istnienia /proc/<PID>katalogu (specyficzne dla Linuksa) lub użycie jobswbudowanego (konkretny. jobs -lzgłasza również pid. W tym przypadku trzecie pole wyjścia może zostać zatrzymane | uruchomione | gotowe | wyjście. ).

Oto mój przykład.

Uruchomiony proces nazywa się loop.sh. Akceptuje -xlub liczbę jako argument. For -xjest zakończone z kodem zakończenia 1. W przypadku liczby czeka num * 5 sekund. Co 5 sekund drukuje swój PID.

Proces uruchamiania nazywa się 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

Aby uzyskać więcej informacji, zobacz: Uruchomienie procesu ze skryptu bash nie powiodło się

Prawda
źródło
1
Skoro mówimy o Bashu, pętla for mogłaby zostać zapisana jako: for i in ${!pids[@]};using parameter expand_more.
PlasmaBinturong
7
#/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 $?
Abu Aqil
źródło
2
Oto sztuczka, aby uniknąć grep -v. Możesz ograniczyć wyszukiwanie do początku linii: grep '^'$pidPoza tym możesz to zrobić ps p $pid -o pid=. Ponadto tail -fnie skończy się, dopóki go nie zabijesz, więc nie sądzę, że jest to dobry sposób na demonstrację tego (przynajmniej bez wskazywania na to). Możesz chcieć przekierować dane wyjściowe swojego pspolecenia do /dev/nulllub zostanie ono wyświetlone na ekranie przy każdej iteracji. Twój exitpowoduje waitpominięcie pliku - prawdopodobnie powinien to być plik break. Ale czy while/ psi nie są waitzbędne?
Wstrzymano do odwołania.
5
Dlaczego wszyscy o tym zapominają kill -0 $pid? W rzeczywistości nie wysyła żadnego sygnału, a jedynie sprawdza, czy proces działa, używając wbudowanej powłoki zamiast procesów zewnętrznych.
ephemient
3
Ponieważ możesz zabić tylko proces, którego jesteś właścicielem:bash: kill: (1) - Operation not permitted
errant.info
2
Pętla jest zbędna. Poczekaj. Mniej kodu => mniej przypadków skrajnych.
Brais Gabin
@Brais Gabin Pętla monitorująca jest wymaganiem nr 2 pytania
DKroot
5

Zmieniłbym nieco twoje podejście. Zamiast sprawdzać co kilka sekund, czy polecenie nadal działa i zgłaszać komunikat, skorzystaj z innego procesu, który co kilka sekund zgłasza, że ​​polecenie nadal działa, a następnie zabij ten proces po zakończeniu działania polecenia. Na przykład:

#! / bin / sh

cmd () {sen 5; wyjście 24; }

cmd & # Uruchom długotrwały proces
pid = $! # Zapisz pid

# Stwórz proces, który ciągle zgłasza, że ​​polecenie nadal działa
while echo "$ (data): $ pid nadal działa"; śpij 1; Gotowe &
echoer = $!

# Ustaw pułapkę, aby zabić reportera po zakończeniu procesu
trap 'kill $ echoer' 0

# Poczekaj na zakończenie procesu
jeśli czekasz $ pid; następnie
    echo "cmd powiodło się"
jeszcze
    echo "cmd FAILED !! (zwrócił $?)"
fi
William Pursell
źródło
świetny szablon, dzięki za udostępnienie! Wierzę, że zamiast pułapki możemy też zrobić while kill -0 $pid 2> /dev/null; do X; done, mam nadzieję, że przyda się komuś w przyszłości, kto przeczyta tę wiadomość;)
punkbit
3

Nasz zespół miał tę samą potrzebę ze zdalnym skryptem wykonanym przez SSH, który wygasał po 25 minutach bezczynności. Oto rozwiązanie z pętlą monitorowania sprawdzającą proces w tle co sekundę, ale drukującą tylko co 10 minut, aby wyeliminować limit czasu braku aktywności.

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}
DKroot
źródło
2

Prosty przykład, podobny do powyższych rozwiązań. Nie wymaga to monitorowania żadnego wyniku procesu. Następny przykład używa taila do śledzenia wyniku.

$ 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

Użyj tail, aby śledzić wyjście procesu i zakończyć, gdy proces się zakończy.

$ 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
Darren Weber
źródło
1

Innym rozwiązaniem jest monitorowanie procesów poprzez system plików proc (bezpieczniejsze niż kombi ps / grep); kiedy uruchamiasz proces, ma on odpowiedni folder w / proc / $ pid, więc rozwiązaniem może być

#!/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
....

Teraz możesz dowolnie używać zmiennej $ exit_status.

Искрен Хаджинедев
źródło
Nie działa w bash? Syntax error: "else" unexpected (expecting "done")
benjaoming
1

Dzięki tej metodzie twój skrypt nie musi czekać na proces w tle, będziesz musiał tylko monitorować plik tymczasowy pod kątem statusu wyjścia.

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

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

teraz twój skrypt może zrobić cokolwiek innego, podczas gdy ty musisz po prostu monitorować zawartość retFile (może również zawierać dowolne inne informacje, takie jak czas wyjścia).

PS .: przy okazji, kodowałem myślenie w bash

Moc Wodnika
źródło
0

Może to wykraczać poza twoje pytanie, jednak jeśli martwisz się o czas działania procesów, możesz być zainteresowany sprawdzeniem stanu uruchomionych procesów w tle po pewnym czasie. Dość łatwo jest sprawdzić, które PID-y podrzędne nadal działają pgrep -P $$, jednak wymyśliłem następujące rozwiązanie, aby sprawdzić status wyjścia tych PIDów, które już wygasły:

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

które wyjścia:

interval-2: pid-28083:running; pid-28084:running; 
interval-7: pid-28083:exited(24); pid-28084:running; 
interval-11: pid-28084:exited(0); 

Uwaga: Możesz zmienić $pidsna zmienną łańcuchową zamiast tablicy, aby uprościć sprawę, jeśli chcesz.

errant.info
źródło
0

Moim rozwiązaniem było użycie anonimowego potoku do przekazania statusu do pętli monitorowania. Nie ma plików tymczasowych używanych do wymiany statusu, więc nie ma nic do czyszczenia. Jeśli nie masz pewności co do liczby zadań w tle, może to być stan zerwania [ -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
James Dingwall
źródło