Собирать коды выхода параллельных фоновых процессов (подоболочек)

18

Скажем, у нас есть скрипт bash, например:

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

Есть ли способ собрать коды выхода для вложенных оболочек / подпроцессов? Ищите способ сделать это и ничего не можете найти. Мне нужно запустить эти подоболочки параллельно, иначе да, это будет проще.

Я ищу общее решение (у меня есть неизвестное / динамическое число подпроцессов для параллельной работы).

Александр Миллс
источник
1
Я собираюсь предложить вам выяснить, что именно вы хотите, а затем задать новый вопрос, пытаясь уточнить, какое именно поведение вы ищете (возможно, с помощью псевдокода или более крупного примера).
Майкл Гомер
3
Я действительно думаю, что вопрос сейчас хорош - у меня динамическое количество подпроцессов. Мне нужно собрать все коды выхода. Это все.
Александр Миллс

Ответы:

6

Ответ Александра Миллса, который использует handleJobs, дал мне отличную отправную точку, но также дал мне эту ошибку

предупреждение: run_pending_traps: неверное значение в trap_list [17]: 0x461010

Что может быть проблемой состояния гонок bash

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

#!/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
Арберг
источник
круто, я думаю, for job in "${childen[@]}"; doдолжно быть, for job in "${1}"; doхотя, для ясности
Александр Миллс
единственное беспокойство, которое я имею с этим сценарием, это если children_pids+=("$!")фактически захват желаемого pid для вложенной оболочки.
Александр Миллс
1
Я протестировал с "$ {1}", и он не работает. Я передаю массив функции, и, видимо, это требует особого внимания в bash. $! pid последней порожденной работы, см. tldp.org/LDP/abs/html/internalvariables.html Кажется, она работает правильно в моих тестах, и я сейчас использую в сценарии unRAID cache_dirs, и, похоже, его работа Я использую Bash 4.4.12.
Арберг
хороший да, кажется, что вы правы
Александр Миллс
20

Используйте waitс PID, который будет:

Подождите , пока дочерний процесс , указанный каждый идентификатор процесса PID или спецификации задания jobspec выходов и вернуть статус выхода последней команды ждали.

Вам нужно будет сохранять PID каждого процесса по ходу:

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

Вы также можете включить управление заданиями в сценарии set -mи использовать %nспецификацию заданий, но вы почти наверняка этого не захотите - контроль заданий имеет много других побочных эффектов .

waitвернет тот же код, что и процесс, завершенный. Вы можете использовать wait $Xв любой (разумный) момент для доступа к окончательному коду $?или просто использовать его как true / false:

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

wait будет приостанавливаться до тех пор, пока команда не завершится, если это еще не сделано

Если вы хотите , чтобы избежать срыва , как это, вы можете установить trapнаSIGCHLD , подсчитывают количество окончаний, и обрабатывать все waitс на один раз , когда они уже все закончили. Вы можете, вероятно, сойти с рук в waitодиночку почти все время.

Майкл Гомер
источник
1
тьфу, извините, мне нужно запустить эти субоболочки параллельно, я
Александр Миллс
не берите в голову, может быть, это работает с моей установкой ... где команда wait вступает в игру в вашем коде? Я не слежу
Александр Миллс
1
@AlexanderMills Они будут работать в параллель. Если у вас есть переменное число, используйте массив. (например, здесь, который может быть дубликатом).
Майкл Гомер
да, спасибо, я проверю это, если команда ожидания относится к вашему ответу, то, пожалуйста, добавьте его
Александр Миллс
Вы бежите wait $Xв любой (разумный) более поздний момент.
Майкл Гомер
5

Если у вас есть хороший способ идентифицировать команды, вы можете напечатать их код выхода в файл tmp, а затем получить доступ к конкретному файлу, который вас интересует:

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

Не забудьте удалить файлы tmp.

Рольф
источник
4

Используйте compound command- поставьте оператор в скобках:

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

даст выход

x
X: 0
TRUE: 0
FALSE: 1

Действительно другой способ запустить несколько команд параллельно - это использовать GNU Parallel . Составьте список команд для запуска и поместите их в файл list:

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

Запустите все команды параллельно и соберите коды выхода в файле job.log:

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

и вывод:

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
источник
хорошо, спасибо, есть ли способ сделать это? У меня не только 3 подпроцесса, у меня есть Z подпроцессов.
Александр Миллс
Я обновил исходный вопрос, чтобы отразить, что я ищу общее решение, спасибо
Александр Миллс,
Одним из способов его обобщения может быть использование циклической конструкции?
Александр Миллс
Циклическое? У вас есть фиксированный список команд или это контролируется пользователем? Я не уверен, что понимаю, что вы пытаетесь сделать, но, возможно, PIPESTATUSвам стоит это проверить. Это seq 10 | gzip -c > seq.gz ; echo ${PIPESTATUS[@]}возвращает 0 0(код выхода из первой и последней команды).
hschou
Да, по сути, контролируемый пользователем
Александр Миллс
2

это общий сценарий, который вы ищете. Единственным недостатком является то, что ваши команды заключены в кавычки, что означает, что подсветка синтаксиса через вашу среду IDE не будет работать. В противном случае, я попробовал пару других ответов, и это лучший. Этот ответ включает в себя идею использования wait <pid>данных @Michael, но идет дальше, используя trapкоманду, которая, кажется, работает лучше всего.

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

спасибо @michael homer за то, что я выбрал правильный путь, но использование trapкоманды - лучший подход AFAICT.

Александр Миллс
источник
1
Вы также можете использовать ловушку SIGCHLD для обработки детей при выходе, например, для распечатки статуса в это время. Или обновить счетчик прогресса: объявить функцию затем использовать «ловушку function_name CHLD» , хотя это может потребовать также возможность быть включен в не-интерактивной оболочки, такие , как , возможно , «множества -m»
Chunko
1
Также «wait -n» будет ждать любого потомка и затем возвращать статус выхода этого потомка в $? переменная. Таким образом, вы можете распечатать прогресс по мере выхода. Однако учтите, что если вы не используете ловушку для CHLD, вы можете пропустить выход ребенка таким образом.
Chunko
@ Чунько спасибо! Это хорошая информация, не могли бы вы обновить ответ на что-то, что вы считаете лучшим?
Александр Миллс
спасибо @Чунько, ловушка работает лучше, ты прав. С ожиданием <pid> я получил провал.
Александр Миллс
Можете ли вы объяснить, как и почему вы считаете, что версия с ловушкой лучше, чем версия без нее? (Я полагаю, что это не лучше, и поэтому, что это хуже, потому что это более сложно без выгоды.)
Скотт
1

Еще один вариант ответа @rolf:

Еще один способ сохранить статус выхода будет что-то вроде

mkdir /tmp/status_dir

а затем каждый сценарий

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

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

Вы даже можете сохранить более одного состояния из каждого сценария, выполнив что-то вроде

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

который создает файл, как и раньше, но добавляет к нему уникальную случайную строку.

Или вы можете просто добавить больше информации о состоянии в тот же файл.

Джо
источник
1

script3будет выполнен только в том случае, если script1и script2успешно, script1и script2будет выполняться параллельно:

./script1 &
process1=$!

./script2 &
process2=$!

wait $process1
rc1=$?

wait $process2
rc2=$?

if [[ $rc1 -eq 0 ]] && [[ $rc2 -eq 0  ]];then
./script3
fi
Venkata
источник
AFAICT, это не что иное, как повторение ответа Майкла Гомера .
Скотт,