niezawodny kod powrotu procesu w tle

14

Załóżmy następujący fragment kodu bash:

foo > logfile 2>&1 &
foo_pid=$!

while ps -p$foo_pid
do
    ping -c 1 localhost
done

wait $foo_pid

if [[ $? == 0 ]]
then
    echo "foo success"
fi

Czy można bezpiecznie założyć, że $?rzeczywiście zawiera kod powrotu, fooa nie kod powrotu ping? Jeśli odpowiedź na to pytanie brzmi: „Nie możesz tego założyć”. to jak mogę zmodyfikować ten fragment kodu, aby mieć pewność, że $?zawsze zawiera kod powrotu foo?

Christopher Schmidt
źródło

Odpowiedzi:

12

Dzięki bashnie będziesz mieć takiej gwarancji, chyba że rozpoczniesz inne zadanie w tle (i strzeż się, że zadania w tle można uruchamiać z, &ale także z coproci z zastępowaniem procesów) między foo &i wait.

POSIX wymaga, aby powłoka zapamiętała status wyjścia z co najmniej 25 zadań po ich odejściu , ale bashpamięta o wiele więcej.

Teraz, jeśli to zrobisz:

foo & pid=$!
...
bar &
wait "$pid"

Nie masz żadnej gwarancji, że barnie otrzymasz tego samego pid jak foo(jeśli foowygasł przed czasem bar), więc nawet jeśli jest mało prawdopodobne, wait "$pid"może dać ci status wyjścia bar.

Możesz go odtworzyć za pomocą:

bash -c '(exit 12; foo) & pid=$!
         while : bar & [ "$pid" != "$!" ]; do :;done
         wait "$pid"; echo "$?"'

które (ostatecznie) dadzą ci 0zamiast 12.

Aby uniknąć problemu, jednym ze sposobów byłoby napisanie go jako:

{
  foo_pid=$!

  while ps -p "$foo_pid"
  do
      ping -c 1 localhost
  done

  bar &
  ...

  read <&3 ret
  if [ "$ret" = 0 ]; then
    echo foo was sucessful.
  fi
} 3< <(foo > logfile 2>&1; echo "$?")
Stéphane Chazelas
źródło
4

Tak, możesz polegać na wait "$!"uzyskaniu statusu zadania w tle. Podczas uruchamiania jako skrypt bash nie zbiera automatycznie ukończonych zadań w tle. Tak więc, jeśli uruchomisz wait, zbierze zadanie w momencie, gdy waitzostanie wywołany.

Możesz to przetestować za pomocą prostego skryptu:

#!/bin/bash
sh -c 'sleep 1; exit 22' &
sleep 5
echo "FG: $?"
wait %1
echo "BG: $?"

Co da wynik:

FG: 0
BG: 22
Patrick
źródło
Kluczową częścią tego stwierdzenia był początek „podczas działania jako skrypt”. W trybie interaktywnym waitnie działa. Proces jest gromadzony, a status wyjścia odrzucany tuż przed wyświetleniem monitu (domyślnie).
Patrick
Właśnie wypróbowałem to na bash 4.2.37, 4.1.2 i 3.2.48. Wszystkie zachowują się dokładnie tak samo (dosłownie skopiuj / wklej kod w mojej odpowiedzi). wait %1Nie powiedzie się z „braku takiej pracy” jako proces w tle są zbierane bezpośrednio po „5” dopełnia sen.
Patrick
Ach ok, przepraszam, rozumiem teraz. Tęskniłem za tobą %1zamiast $!.
Stéphane Chazelas
Zauważ, że bash -c '(sleep 1;exit 5) & sleep 2; wait %1; echo $?'(także nieinteraktywne) nie uzyskuje statusu wyjścia z tej martwej pracy. Brzmi jak błąd.
Stéphane Chazelas
nie działało to dla mnie w przepisie Makefile, dopóki nie dołączyłem set +e. Wygląda na to, że set -efunkcja bash zabija skrypt, gdy tylko zostanie zgłoszony zły kod wyjściawait
user5359531
0

Uważam, że twoje założenie jest prawidłowe. Oto fragment man bashdotyczący oczekiwania na procesy w tle.

Jeśli n określa nieistniejący proces lub zadanie, stanem zwrotnym jest 127. W przeciwnym razie stanem zwrotnym jest status zakończenia ostatniego procesu lub zadania, na które czekał.

Więc może powinieneś sprawdzić 127

Jest podobne pytanie z zupełnie inną odpowiedzią, niż mogłoby pomóc.

Skrypt Bash czeka na procesy i otrzymuje kod powrotu

edycja 1

Zainspirowany komentarzami i odpowiedziami @ Stephane'a rozszerzyłem jego skrypt. Mogę rozpocząć około 34 procesów w tle, zanim zacznie tracić orientację.

tback

$ cat tback 
plist=()
elist=()
slist=([1]=12 [2]=15 [3]=17 [4]=19 [5]=21 [6]=23)
count=30

#start background tasksto monitor
for i in 1 2 3 4
do
  #echo pid $i ${plist[$i]} ${slist[$i]}
  (echo $BASHPID-${slist[$i]} running; exit ${slist[$i]}) & 
  plist[$i]=$!
done

echo starting $count background echos to test history
for i in `eval echo {1..$count}`
do
  echo -n "." &
  elist[$i]=$! 
done
# wait for each background echo to complete
for i in `eval echo {1..$count}`
do
  wait ${elist[$i]}
  echo -n $? 
done
echo ""
# Now wait for each monitored process and check return status with expected
failed=0
for i in 1 2 3 4
do
  wait ${plist[$i]}
  rv=$?
  echo " pid ${plist[$i]} returns $rv should be ${slist[$i]}"
  if [[ $rv != ${slist[$i]} ]] 
  then
    failed=1
  fi
done

wait
echo "Complete $failed"
if [[ $failed = "1" ]]
then
  echo Failed
else
  echo Success
fi
exit $failed
$ 

w moim systemie produkuje

$ bash tback
14553-12 running
14554-15 running
14555-17 running
starting 30 background echos to test history
14556-19 running
..............................000000000000000000000000000000
 pid 14553 returns 12 should be 12
 pid 14554 returns 15 should be 15
 pid 14555 returns 17 should be 17
 pid 14556 returns 19 should be 19
Complete 0
Success
X Tian
źródło
1
Nie, zobacz mój komentarz do odpowiedzi umlaute i spróbuj sam zbash -c '(exit 12) & sleep 1; wait "$!"; echo "$?"'
Stéphane Chazelas
Nigdy nie widziałem bashluźnego toru (nawet po rozpoczęciu tysięcy prac), mój przykład demonstrował ponowne użycie pid, co może być również tym, co zaobserwowałeś w twoim przypadku.
Stéphane Chazelas