Збір вихідних кодів паралельних фонових процесів (під оболонок)


18

Скажіть, у нас є сценарій bash:

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

чи є спосіб зібрати вихідні коди підшарів / підпроцесів? Шукаєте спосіб це зробити і нічого не можете знайти. Мені потрібно запустити ці підзаголовки паралельно, інакше так, це було б простіше.

Я шукаю загальне рішення (у мене є невідоме / динамічне число підпроцесів, які можуть працювати паралельно).


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

3
Я зараз думаю, що питання зараз добре - у мене є динамічна кількість підпроцесів. Мені потрібно зібрати всі вихідні коди. Це все.
Олександр Міллс

Відповіді:


6

Відповідь Олександра Міллса, який використовує handJobs, дала мені чудову відправну точку, але також дала мені цю помилку

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

Що може бути проблемою стану гонки

Натомість я просто зберігав під кожної дитини і чекав і отримує код виходу для кожної дитини конкретно. Я вважаю це очисником з точки зору підпроцесів, що породжує підпроцеси у функціях та уникає ризику очікування батьківського процесу, де я мав намір чекати дитини. Ясніше, що відбувається, тому що не використовує пастку.

#!/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+=("$!")чи дійсно фіксує потрібний під для додаткової оболонки.
Олександр Міллс

1
Я тестував "$ {1}", і він не працює. Я передаю масив функції, і, мабуть, це потребує особливої ​​уваги в bash. $! є Ідентифікатором останнього породив завдання см tldp.org/LDP/abs/html/internalvariables.html здається, правильно працювати в моїх тестах, і я в даний час використовую в в unRAID сценарії cache_dirs, і це , здається, робить її робота. Я використовую bash 4.4.12.
Арберг

приємно так, здається, ти маєш рацію
Олександр Міллс

20

Використовуйте waitPID, який :

Зачекайте, поки не завершиться дочірній процес, визначений кожним під- кодом ідентифікатора процесу або специфікацією завдання jobspec, і поверніть статус виходу останньої очікуваної команди.

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

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

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

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

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

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

Якщо ви хочете , щоб уникнути зриву , як це, ви можете встановити trapнаSIGCHLD , підраховують кількість закінчень, і обробляти всі waitз на один раз , коли вони вже все закінчили. Напевно, ви можете піти від використання waitсамостійно майже весь час.


1
так, вибачте, мені потрібно паралельно запускати ці підрозділи, я зазначу це у питанні ...
Олександр Міллз

ніколи не знаю, можливо, це працює з моїм налаштуванням ... де команда очікування грає у вашому коді? Я не дотримуюся
Олександр Міллс

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

Використовуйте a 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

ок, дякую, чи є спосіб узагальнити це? У мене не просто 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 для обробки дітей, коли вони виходять, наприклад, роздрукуючи стан у той час. Або оновлення лічильника прогресу: оголосіть функцію, тоді використовуйте "trap function_name CHLD", хоча це також може вимагати включення опції в неінтерактивній оболонці, наприклад, можливо "set -m"
Chunko

1
Також "wait -n" буде чекати будь-якої дитини, а потім повертати статус виходу цієї дитини в $? змінна. Таким чином, ви можете надрукувати прогрес під час кожного виходу. Однак зауважте, що якщо ви не використовуєте пастку CHLD, ви можете пропустити, щоб якась дитина виходила таким чином.
Чунько

@Chunko дякую! це гарна інформація, чи не могли б ви оновити відповідь чимось, на вашу думку, найкращим?
Олександр Міллс

дякую @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

АФАЙКТ, це не що інше, як повторна відповідь Майкла Гомера .
Скотт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.