7

Как в bash дождаться завершения нескольких подпроцессов и вернуть код завершения !=0, если любой подпроцесс завершился с кодом !=0?

1

Проблема: Как в bash-скрипте дождаться завершения нескольких подпроцессов и вернуть код выхода !=0, если любой из подпроцессов завершился с кодом !=0?

Я написал простой скрипт, который создает 10 подпроцессов, выполняя функцию calculations с параметрами от 0 до 9 в фоновом режиме:

#!/bin/bash
for i in `seq 0 9`; do
  calculations $i &
done
wait

Этот скрипт ожидает завершения всех 10 созданных подпроцессов, но всегда возвращает код выхода 0, даже если один или несколько из подпроцессов завершились с ненулевым кодом (см. help wait).

Как я могу модифицировать этот скрипт, чтобы он мог отслеживать коды возврата подпроцессов и возвращал код выхода 1, если любой из отпроцессов завершился с кодом !=0?

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

5 ответ(ов)

3

Этот скрипт написан на Bash и выполняет несколько фоновых процессов с использованием команды ./sleeper. Давайте разберем его по шагам.

  1. Инициализация переменной:

    FAIL=0
    

    Здесь мы инициализируем счетчик FAIL, который будет использоваться для отслеживания, сколько процессов завершились с ошибкой.

  2. Запуск фоновых процессов:

    echo "starting"
    ./sleeper 2 0 &
    ./sleeper 2 1 &
    ./sleeper 3 0 &
    ./sleeper 2 0 &
    

    echo "starting" выводит сообщение о начале работы скрипта. Команды ./sleeper запускают четыре процесса в фоновом режиме (из-за & в конце каждой строки), передавая разные параметры для каждого.

  3. Ожидание завершения процессов:

    for job in `jobs -p`
    do
        echo $job
        wait $job || let "FAIL+=1"
    done
    

    Здесь мы используем цикл для обработки каждого запущенного фонового процесса. Команда jobs -p выводит идентификаторы процессов, запущенных в фоне. Внутри цикла снова выводим идентификатор процесса, ждем его завершения с помощью wait, и если процесс завершился с ошибкой (код возврата не 0), увеличиваем счетчик FAIL.

  4. Вывод результата:

    echo $FAIL
    
    if [ "$FAIL" == "0" ]; then
        echo "YAY!"
    else
        echo "FAIL! ($FAIL)"
    fi
    

    После завершения всех процессов мы выводим общее количество неудачных завершений. Если счетчик FAIL равен 0, выводим "YAY!", в противном случае — "FAIL!" с указанием числа неудачных завершений.

Таким образом, скрипт запускает несколько процессов, ждет их завершения и проверяет, все ли выполненные процессы завершились успешно, а затем отображает итоговый результат.

1

Вот простой пример использования команды wait.

Запустим несколько процессов:

$ sleep 10 &
$ sleep 10 &
$ sleep 20 &
$ sleep 20 &

Затем дождемся их завершения с помощью команды wait:

$ wait < <(jobs -p)

^ Или просто используйте wait (без аргументов) для ожидания всех фоновых процессов.

Это позволит дождаться завершения всех запущенных в фоне задач.

^ Если указать опцию -n, команда будет ждать завершения следующей задачи и вернет ее код завершения.

Смотрите: help wait и help jobs для получения информации о синтаксисе.

Однако недостатком этого метода является то, что команда вернет только статус последнего ID, поэтому вам нужно проверить статус для каждого подсного процесса и сохранить его в переменную.

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

$ sleep 20 && true || tee fail &
$ sleep 20 && false || tee fail &
$ wait < <(jobs -p)
$ test -f fail && echo Расчет завершился с ошибкой.
0

Вот перевод вашего текста в стиле ответа на StackOverflow:


Как насчет простого варианта:

#!/bin/bash

pids=""

for i in `seq 0 9`; do
   doCalculations $i &
   pids="$pids $!"
done

wait $pids

...код продолжается здесь...

Обновление:

Как указали несколько комментаторов, приведенный выше код ожидает завершения всех процессов перед тем, как продолжить выполнение, но не завершит скрипт с ошибкой, если один из процессов завершится неудачно, что может привести к проблемам, особенно если это не последняя запись в pids. Это можно исправить с помощью следующей модификации, предложенной @Bryan, @SamBrightman и другими:

#!/bin/bash

pids=""
RESULT=0

for i in `seq 0 9`; do
   doCalculations $i &
   pids="$pids $!"
done

for pid in $pids; do
    wait $pid || let "RESULT=1"
done

if [ "$RESULT" == "1" ]; then
   exit 1
fi

...код продолжается здесь...

Надеюсь, это поможет!

0

Если у вас установлен GNU Parallel, вы можете использовать следующий код:

# Если doCalculations - это функция
export -f doCalculations
seq 0 9 | parallel doCalculations {}

GNU Parallel вернёт вам код выхода:

  • 0 - Все задачи выполнены без ошибок.
  • 1-253 - Некоторые задачи завершились с ошибками. Код выхода указывает количество неудавшихся задач.
  • 254 - Ошибки произошли в более чем 253 задачах.
  • 255 - Другая ошибка.

Посмотрите вводные видео, чтобы узнать больше: http://pi.dk/1

0

Чтобы прервать выполнение команды sleep в функции waitall, когда один из дочерних процессов завершается, можно использовать сигнал, например, SIGUSR1. Ниже приведен пример того, как можно изменить вашу функцию, чтобы она могла прерывать sleep, если обнаружится, что один из процессов завершился.

waitall() { # PID...
  ## Ждем завершения дочерних процессов и указываем, завершились ли все с кодом 0.
  local errors=0
  local pids=("$@")  # Сохраняем PID в массив
  local interrupted=0

  trap 'interrupted=1' SIGUSR1  # Устанавливаем обработчик сигнала

  while :; do
    debug "Remaining processes: ${pids[*]}"
    for pid in "${pids[@]}"; do
      if kill -0 "$pid" 2>/dev/null; then
        debug "$pid is still alive."
      elif wait "$pid"; then
        debug "$pid exited with zero exit status."
        pids=("${pids[@]/$pid}")  # Убираем завершенный PID из массива
      else
        debug "$pid exited with non-zero exit status."
        ((++errors))
        pids=("${pids[@]/$pid}")  # Убираем завершенный PID из массива
      fi
    done

    # Если все дочерние процессы завершены, выходим из цикла
    [[ "${#pids[@]}" -eq 0 ]] && break

    if [[ $interrupted -eq 1 ]]; then
      debug "Sleep interrupted by child termination."
      interrupted=0  # Сбрасываем флаг
    else
      # Пауза, если не было прерываний
      sleep ${WAITALL_DELAY:-1}
    fi
  done
  ((errors == 0))
}

debug() { echo "DEBUG: $*" >&2; }

pids=""
for t in 3 5 4; do 
  sleep "$t" &
  pids="$pids $!"
done
waitall $pids

Объяснение:

  • Я добавил конструкцию trap, которая перехватывает сигнал SIGUSR1 и устанавливает флаг interrupted, когда сигнал получен.
  • Если один из процессов завершится, мы убираем его PID из массива pids.
  • Если сигнал SIGUSR1 был получен, мы выводим отладочное сообщение и можем прервать sleep, не дожидаясь его окончания.

Такой подход позволяет избежать необходимости настраивать WAITALL_DELAY и проведения дополнительных настроек при использовании функции waitall.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь