Zbierz kody zakończenia równoległych procesów w tle (podpowłoki)

18

Powiedzmy, że mamy taki skrypt bashowy:

echo "x" &
echo "y" &
echo "z" &
.....
echo "Z" &
wait

czy istnieje sposób na zebranie kodów wyjścia z podpowłok / podprocesów? Szukasz sposobu na zrobienie tego i nic nie możesz znaleźć. Muszę uruchomić te podpowłoki równolegle, w przeciwnym razie tak byłoby łatwiej.

Szukam ogólnego rozwiązania (mam nieznaną / dynamiczną liczbę podprocesów do równoległego działania).

Alexander Mills
źródło
1
Sugeruję, abyś zastanowił się, czego chcesz, a następnie zadał nowe pytanie, starając się wyjaśnić dokładnie, czego szukasz (na przykład za pomocą pseudokodu lub większego przykładu).
Michael Homer,
3
Myślę, że pytanie jest teraz dobre - mam dynamiczną liczbę podprocesów. Muszę zebrać wszystkie kody wyjścia. To wszystko.
Alexander Mills,

Odpowiedzi:

6

Odpowiedź Alexandra Millsa, która używa handleJobs, dała mi świetny punkt wyjścia, ale także dała mi ten błąd

ostrzeżenie: run_pending_traps: zła wartość na trap_list [17]: 0x461010

Co może być problemem wyścigu bash

Zamiast tego po prostu zapisałem pid każdego dziecka i czekam, a konkretnie dostaje kod wyjścia dla każdego dziecka. Uważam to za czystsze, jeśli chodzi o podprocesy spawnujące podprocesy w funkcjach i unikające ryzyka oczekiwania na proces nadrzędny, w którym chciałem czekać na dziecko. Wyraźniejsze jest to, co się dzieje, ponieważ nie używa pułapki.

#!/usr/bin/env bash

# it seems it does not work well if using echo for function return value, and calling inside $() (is a subprocess spawned?) 
function wait_and_get_exit_codes() {
    children=("$@")
    EXIT_CODE=0
    for job in "${children[@]}"; do
       echo "PID => ${job}"
       CODE=0;
       wait ${job} || CODE=$?
       if [[ "${CODE}" != "0" ]]; then
           echo "At least one test failed with exit code => ${CODE}" ;
           EXIT_CODE=1;
       fi
   done
}

DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
    )

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

children_pids=()
for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    children_pids+=("$!")
    echo "$i ith command has been issued as a background job"
done
# wait; # wait for all subshells to finish - its still valid to wait for all jobs to finish, before processing any exit-codes if we wanted to
#EXIT_CODE=0;  # exit code of overall script
wait_and_get_exit_codes "${children_pids[@]}"

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end
arberg
źródło
spoko, myślę, że for job in "${childen[@]}"; dopowinno być for job in "${1}"; do, dla jasności
Alexander Mills
jedynym problemem, jaki mam z tym skryptem, jest to, czy children_pids+=("$!")faktycznie przechwytuje pożądany pid dla podpowłoki.
Alexander Mills,
1
Testowałem z „$ {1}” i to nie działa. Podaję tablicę do funkcji i najwyraźniej wymaga specjalnej uwagi w bash. $! jest numerem pid ostatniego zadania spawnowanego , patrz tldp.org/LDP/abs/html/internalvariables.html Wygląda na to, że działa poprawnie w moich testach, a teraz używam w skrypcie unRAID cache_dirs i wydaje się, że działa jego praca. Używam bash 4.4.12.
arberg
fajnie tak, wygląda na to, że masz rację
Alexander Mills,
20

Używaj waitz PID, który :

Poczekaj, aż proces potomny określony przez każdy identyfikator procesu pid lub specyfikacja zadania specyfikacja zadania zakończy działanie i zwróć status wyjścia ostatniej oczekującej komendy.

Musisz zapisać PID każdego procesu na bieżąco:

echo "x" & X=$!
echo "y" & Y=$!
echo "z" & Z=$!

Możesz także włączyć kontrolę zadań w skrypcie set -mi używać %nspecyfikacji zadań, ale prawie na pewno nie chcesz - kontrola zadań ma wiele innych skutków ubocznych .

waitzwróci ten sam kod, co zakończony proces. Możesz użyć wait $Xw dowolnym (rozsądnym) momencie, aby uzyskać dostęp do końcowego kodu jako $?lub po prostu użyć go jako true / false:

echo "x" & X=$!
echo "y" & Y=$!
...
wait $X
echo "job X returned $?"

wait zatrzyma się, aż polecenie zakończy się, jeśli jeszcze tego nie zrobiło.

Jeśli chcesz uniknąć przeciągnięcia tak, można ustawić trapnaSIGCHLD , policzyć liczbę zakończeń i obsługiwać wszystkie waits od razu, kiedy już wszystko jest gotowe. Prawdopodobnie możesz przez waitcały czas uniknąć samodzielnego używania .

Michael Homer
źródło
1
ughh, przepraszam, muszę uruchomić te podpowłoki równolegle, sprecyzuję to w pytaniu ...
Alexander Mills
nieważne, może to działa z moją konfiguracją ... gdzie w twoim kodzie pojawia się polecenie czekania? Nie śledzę
Alexander Mills,
1
@AlexanderMills Oni wyświetlane równolegle. Jeśli masz ich zmienną liczbę, użyj tablicy. (jak np. tutaj, co może być duplikatem).
Michael Homer,
tak, dziękuję. Sprawdzę to, jeśli polecenie oczekiwania dotyczy twojej odpowiedzi, to dodaj ją
Alexander Mills,
Biegniesz wait $Xw dowolnym (rozsądnym) momencie.
Michael Homer,
5

Jeśli masz dobry sposób na identyfikację poleceń, możesz wydrukować ich kod wyjścia do pliku tmp, a następnie uzyskać dostęp do określonego pliku, który Cię interesuje:

#!/bin/bash

for i in `seq 1 5`; do
    ( sleep $i ; echo $? > /tmp/cmd__${i} ) &
done

wait

for i in `seq 1 5`; do # or even /tmp/cmd__*
    echo "process $i:"
    cat /tmp/cmd__${i}
done

Nie zapomnij usunąć plików tmp.

Rolf
źródło
4

Użyj a compound command- umieść instrukcję w nawiasach:

( echo "x" ; echo X: $? ) &
( true ; echo TRUE: $? ) &
( false ; echo FALSE: $? ) &

da wynik

x
X: 0
TRUE: 0
FALSE: 1

Zupełnie innym sposobem równoległego uruchamiania kilku poleceń jest użycie GNU Parallel . Zrób listę poleceń do uruchomienia i umieść je w pliku list:

cat > list
sleep 2 ; exit 7
sleep 3 ; exit 55
^D

Uruchom wszystkie polecenia równolegle i zbierz kody wyjścia w pliku job.log:

cat list | parallel -j0 --joblog job.log
cat job.log

a wynikiem jest:

Seq     Host    Starttime       JobRuntime      Send    Receive Exitval Signal  Command
1       :       1486892487.325       1.976      0       0       7       0       sleep 2 ; exit 7
2       :       1486892487.326       3.003      0       0       55      0       sleep 3 ; exit 55
hschou
źródło
ok dzięki, czy istnieje sposób na wygenerowanie tego? Nie mam tylko 3 procesów podrzędnych, mam procesy podrzędne Z.
Alexander Mills,
Zaktualizowałem oryginalne pytanie, aby odzwierciedlić, że szukam ogólnego rozwiązania, dzięki
Alexander Mills,
Jednym ze sposobów na wygenerowanie tego może być użycie pętli?
Alexander Mills,
Zapętlasz? Czy masz ustaloną listę poleceń, czy jest to kontrolowane przez użytkownika? Nie jestem pewien, czy rozumiem, co próbujesz zrobić, ale może PIPESTATUSwarto to sprawdzić. Te seq 10 | gzip -c > seq.gz ; echo ${PIPESTATUS[@]}powroty 0 0(kod wyjścia z polecenia pierwszego i ostatniego).
hschou
Tak, zasadniczo kontrolowany przez użytkownika
Alexander Mills,
2

to jest ogólny skrypt, którego szukasz. Jedynym minusem jest to, że twoje polecenia są w cudzysłowach, co oznacza, że ​​podświetlanie składni za pomocą IDE nie będzie naprawdę działać. W przeciwnym razie wypróbowałem kilka innych odpowiedzi i ta jest najlepsza. Ta odpowiedź zawiera pomysł użycia wait <pid>podany przez @Michaela, ale idzie o krok dalej, używając trappolecenia, które wydaje się działać najlepiej.

#!/usr/bin/env bash

set -m # allow for job control
EXIT_CODE=0;  # exit code of overall script

function handleJobs() {
     for job in `jobs -p`; do
         echo "PID => ${job}"
         CODE=0;
         wait ${job} || CODE=$?
         if [[ "${CODE}" != "0" ]]; then
         echo "At least one test failed with exit code => ${CODE}" ;
         EXIT_CODE=1;
         fi
     done
}

trap 'handleJobs' CHLD  # trap command is the key part
DIRN=$(dirname "$0");

commands=(
    "{ echo 'a'; exit 1; }"
    "{ echo 'b'; exit 0; }"
    "{ echo 'c'; exit 2; }"
)

clen=`expr "${#commands[@]}" - 1` # get length of commands - 1

for i in `seq 0 "$clen"`; do
    (echo "${commands[$i]}" | bash) &   # run the command via bash in subshell
    echo "$i ith command has been issued as a background job"
done

wait; # wait for all subshells to finish

echo "EXIT_CODE => $EXIT_CODE"
exit "$EXIT_CODE"
# end

dzięki @michael homer za doprowadzenie mnie na właściwą ścieżkę, ale użycie trappolecenia jest najlepszym podejściem AFAICT.

Alexander Mills
źródło
1
Możesz także użyć pułapki SIGCHLD, aby przetworzyć dzieci, gdy wychodzą, na przykład wydrukować status w tym czasie. Lub zaktualizuj licznik postępu: zadeklaruj funkcję, a następnie użyj „pułapki nazwa_funkcji CHLD”, chociaż może to również wymagać włączenia opcji w nieinteraktywnej powłoce, np. „Set -m”
Chunko
1
Również „wait -n” będzie czekać na dowolne dziecko, a następnie zwróci status wyjścia tego dziecka w $? zmienna. Możesz więc drukować postępy przy każdym wyjściu. Pamiętaj jednak, że o ile nie użyjesz pułapki CHLD, możesz przegapić niektóre wyjścia dziecka w ten sposób.
Chunko,
@Chunko dzięki! to dobra informacja, czy możesz zaktualizować odpowiedź o coś, co uważasz za najlepsze?
Alexander Mills,
dzięki @Chunko, pułapka działa lepiej, masz rację. Z czekaniem <pid> dostałem przewrotu.
Alexander Mills,
Czy możesz wyjaśnić, w jaki sposób i dlaczego uważasz, że wersja z pułapką jest lepsza niż ta bez niej? (Uważam, że nie jest lepiej, a zatem jest gorszy, ponieważ jest bardziej złożony bez żadnych korzyści.)
Scott
1

Kolejna odmiana odpowiedzi @rolf:

Innym sposobem na zapisanie statusu wyjścia byłoby coś takiego

mkdir /tmp/status_dir

a następnie mieć każdy skrypt

script_name="${0##*/}"  ## strip path from script name
tmpfile="/tmp/status_dir/${script_name}.$$"
do something
rc=$?
echo "$rc" > "$tmpfile"

Daje to unikalną nazwę dla każdego pliku statusu, w tym nazwę skryptu, który go utworzył, i jego identyfikator procesu (w przypadku, gdy uruchomiona jest więcej niż jedna instancja tego samego skryptu), którą można zapisać do późniejszego wykorzystania i umieścić je w to samo miejsce, więc możesz po prostu usunąć cały podkatalog po zakończeniu.

Możesz nawet zapisać więcej niż jeden status z każdego skryptu, robiąc coś takiego

tmpfile="$(/bin/mktemp -q "/tmp/status_dir/${script_name}.$$.XXXXXX")"

który tworzy plik jak poprzednio, ale dodaje do niego unikatowy ciąg losowy.

Lub możesz po prostu dołączyć więcej informacji o stanie do tego samego pliku.

Joe
źródło
1

script3zostanie wykonany tylko wtedy, gdy script1i script2są skuteczne i script1i script2będą realizowane równolegle:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0  ]];then
./script3
fi
venkata
źródło
AFAICT, to nic innego jak powtórzenie odpowiedzi Michaela Homera .
Scott