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).


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

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 .


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 są 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.


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

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.


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.


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

AFAICT, to nic innego jak powtórzenie odpowiedzi Michaela Homera .
— Scott
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.