Дождитесь окончания процесса

147

Есть ли в Bash встроенная функция для ожидания завершения процесса?

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

Механический способ сделать это заключается в следующем, но я хотел бы знать, есть ли какая-либо встроенная функция в Bash.

while ps -p `cat $PID_FILE` > /dev/null; do sleep 1; done
Бисваджиоти Дас
источник
4
Позвольте мне дать два предостережения : 1. Как указывалось ниже по mp3foley , «убийство -0» не всегда работает для POSIX. 2. Вероятно, вы также хотите убедиться, что процесс не является зомби, который является практически завершенным процессом. Смотрите комментарий mp3foley и мой для деталей.
Тейка Казура
2
Другая осторожность ( первоначально указывал на ks1322 ниже): Использование PID, кроме дочернего процесса не является устойчивой. Если вы хотите безопасный способ, используйте, например, IPC.
Тейка Казура

Ответы:

138

Дождаться окончания любого процесса

Linux:

tail --pid=$pid -f /dev/null

Дарвин (требует наличия $pidоткрытых файлов):

lsof -p $pid +r 1 &>/dev/null

С таймаутом (в секундах)

Linux:

timeout $timeout tail --pid=$pid -f /dev/null

Дарвин (требует наличия $pidоткрытых файлов):

lsof -p $pid +r 1m%s -t | grep -qm1 $(date -v+${timeout}S +%s 2>/dev/null || echo INF)
Рауно Палосаари
источник
42
Кто бы знал, tailчто сделает это.
ctrl-alt-delor
8
tailработает под капотом путем опроса с kill(pid, SIG_0)процессом (обнаружен с помощью strace).
Att Righ
2
Обратите внимание, что lsof использует опрос, то +r 1есть время ожидания, я лично ищу решение для MacOS, которое не использует опрос.
Александр Миллс
1
Этот трюк не работает для зомби. Это нормально для процессов, которые вы не можете убить; tailимеет линию kill (pid, 0) != 0 && errno != EPERM.
Тейка Казура
2
@AlexanderMills, если вы можете допустить, чтобы ваша система macOS не спала во время выполнения команды, caffeinate -w $pidсделают свое дело.
zneak
83

Там нет встроенных. Используйте kill -0в цикле для работоспособного решения:

anywait(){

    for pid in "$@"; do
        while kill -0 "$pid"; do
            sleep 0.5
        done
    done
}

Или как более простой способ для простого однократного использования:

while kill -0 PIDS 2> /dev/null; do sleep 1; done;

Как отметили несколько комментаторов, если вы хотите дождаться процессов, на которые у вас нет прав отправлять сигналы, вы найдете другой способ определить, запущен ли процесс, чтобы заменить kill -0 $pidвызов. В Linux test -d "/proc/$pid"работает, в других системах вам, возможно, придется использовать pgrep(если доступно) или что-то подобное ps | grep "^$pid ".

Тедди
источник
2
Внимание : Это не всегда работает, как и указывал ниже по mp3foley . Смотрите этот комментарий и мой для деталей.
Тейка Казура
2
Предупреждение 2 (о зомби): последующего комментария от Тедди пока недостаточно, поскольку они могут быть зомби. Смотрите мой ответ ниже для решения Linux.
Тейка Казура
4
Разве это решение не рискует гонкой? Во время сна sleep 0.5процесс с $pidможет умереть, и другой процесс может быть создан с тем же $pid. И мы в конечном итоге ждем 2 разных процесса (или даже больше) с одинаковыми $pid.
ks1322
2
@ ks1322 Да, в этом коде действительно есть условие гонки.
Тедди
4
ПИДы обычно не генерируются последовательно? Какова вероятность того, что отсчет оборачивается за одну секунду?
esmiralha
53

Я обнаружил, что "kill -0" не работает, если процесс принадлежит пользователю root (или другому), поэтому я использовал pgrep и придумал:

while pgrep -u root process_name > /dev/null; do sleep 1; done

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

mp3foley
источник
2
Хорошее наблюдение. В POSIX системный вызов kill(pid, sig=0)завершается ошибкой, если у вызывающего процесса нет привилегии на уничтожение. Таким образом, / bin / kill -0 и «kill -0» (встроенный в bash) тоже терпят неудачу при одинаковых условиях.
Тейка Казура
31

Этот цикл bash-скрипта заканчивается, если процесс не существует или это зомби.

PID=<pid to watch>
while s=`ps -p $PID -o s=` && [[ "$s" && "$s" != 'Z' ]]; do
    sleep 1
done

EDIT : Возможно, приведенный выше сценарий был приведен ниже по Rockallite . Спасибо!

Мой оригинальный ответ ниже работает для Linux, полагаясь на procfsто есть /proc/. Я не знаю его переносимость:

while [[ ( -d /proc/$PID ) && ( -z `grep zombie /proc/$PID/status` ) ]]; do
    sleep 1
done

Это не ограничивается оболочкой, но сами ОС не имеют системных вызовов для наблюдения за завершением не дочерних процессов.

Тейка Казура
источник
1
Хороший. Хотя мне приходилось окружать grep /proc/$PID/statusдвойными кавычками ( bash: test: argument expected)
Griddo
Хм ... просто попробовал еще раз, и это сработало. Я думаю, что я сделал что-то не так в прошлый раз.
Гриддо
7
Илиwhile s=`ps -p $PID -o s=` && [[ "$s" && "$s" != 'Z' ]]; do sleep 1; done
Rockallite
1
К сожалению, это не работает в BusyBox - в его ps, ни -pили s=поддерживаются
ZimbiX
14

FreeBSD и Solaris имеют эту удобную pwait(1)утилиту, которая делает именно то, что вы хотите.

Я считаю, что другие современные ОС также имеют необходимые системные вызовы (например, MacOS реализует BSD kqueue), но не все делают это доступным из командной строки.

Михаил Т.
источник
2
> BSD and Solaris: Проверка трех больших BSD, которые приходят на ум; ни OpenBSD, ни NetBSD не имеют этой функции (на их man-страницах), только FreeBSD, как вы можете легко проверить на man.openbsd.org .
benaryorg
Похоже, ты прав. То есть виноват ... Все они реализованы kqueue, поэтому компиляция FreeBSD pwait(1)будет тривиальной. Почему бы другим BSD не импортировать эту функцию, чтобы избежать меня ...
Михаил Т.
1
plink me@oracle box -pw redacted "pwait 6998";email -b -s "It's done" etcпросто позволил мне идти домой вместо часов.
zzxyz
11

Из man-страницы bash

   wait [n ...]
          Wait for each specified process and return its termination  status
          Each  n  may be a process ID or a job specification; if a
          job spec is given, all processes  in  that  job's  pipeline  are
          waited  for.  If n is not given, all currently active child processes
          are waited for, and the return  status  is  zero.   If  n
          specifies  a  non-existent  process or job, the return status is
          127.  Otherwise, the return status is the  exit  status  of  the
          last process or job waited for.
Charlweed
источник
56
Это правда, но он может только ждать дочернего элемента текущей оболочки. Вы не можете ждать какого-либо процесса.
Gumik
@gumik: «Если n не задано, ожидаются все активные в данный момент дочерние процессы» . Это работает отлично ... waitбез аргументов заблокирует процесс, пока не завершатся какие-либо дочерние процессы. Честно говоря, я не вижу смысла ждать какого-либо процесса, потому что всегда происходят системные процессы.
кодер-спасение
1
@coderofsalvation (сон 10 и сон 3 и ожидание) занимает 10 секунд, чтобы вернуться: ожидание без аргументов заблокирует, пока ВСЕ дочерние процессы не завершатся. ОП хочет получить уведомление о завершении первого дочернего (или назначенного) процесса.
android.weasel
Также не работает, если процесс не является фоновым или предопределенным (в Solaris, Linux или Cygwin). ех. sleep 1000 ctrl-z wait [sleep pid]возвращается немедленно
zzxyz
6

Все эти решения протестированы в Ubuntu 14.04:

Решение 1 (с помощью команды ps): просто, чтобы добавить к ответу Pierz, я бы предложил:

while ps axg | grep -vw grep | grep -w process_name > /dev/null; do sleep 1; done

В этом случае grep -vw grepгарантирует, что grep соответствует только process_name, а не самому grep. Преимущество заключается в поддержке случаев, когда имя_процесса находится не в конце строки в ps axg.

Решение 2 (с помощью команды top и имени процесса):

while [[ $(awk '$12=="process_name" {print $0}' <(top -n 1 -b)) ]]; do sleep 1; done

Замените process_nameна имя процесса, которое отображается в top -n 1 -b. Пожалуйста, сохраняйте кавычки.

Чтобы увидеть список процессов, которые вы ожидаете, чтобы завершить их, вы можете запустить:

while : ; do p=$(awk '$12=="process_name" {print $0}' <(top -n 1 -b)); [[ $b ]] || break; echo $p; sleep 1; done

Решение 3 (с помощью команды top и идентификатора процесса):

while [[ $(awk '$1=="process_id" {print $0}' <(top -n 1 -b)) ]]; do sleep 1; done

Замените process_idна идентификатор процесса вашей программы.

Саид Б.К.
источник
4
Downvote: длинный grep -v grepконвейер - это массивный антипаттерн, и это предполагает, что у вас нет связанных процессов с таким же именем. Если вместо этого вам известны идентификаторы PID, это можно адаптировать к правильно работающему решению.
tripleee
Спасибо tripleee за комментарий. Я добавил флаг, -wчтобы избежать проблемы grep -v grep в некоторой степени . Я также добавил еще два решения на основе вашего комментария.
Саид Б.К.
5

Итак, похоже, ответ - нет, встроенного инструмента нет.

После установки /proc/sys/kernel/yama/ptrace_scopeна 0, можно использовать straceпрограмму. Другие переключатели могут быть использованы, чтобы заставить его замолчать, чтобы он действительно пассивно ожидал:

strace -qqe '' -p <PID>
эму
источник
1
Хороший! Кажется, однако, что невозможно прикрепить к данному PID из двух разных мест (я получаю Operation not permittedдля второго экземпляра strace); Вы можете это подтвердить?
eudoxos
@eudoxos Да, на странице ptrace написано: (...)"tracee" always means "(one) thread"(и я подтверждаю ошибку, о которой вы упоминаете). Чтобы позволить таким процессам ждать больше, вам нужно создать цепочку.
Эму
2

Блокирующее решение

Используйте waitцикл in для ожидания завершения всех процессов:

function anywait()
{

    for pid in "$@"
    do
        wait $pid
        echo "Process $pid terminated"
    done
    echo 'All processes terminated'
}

Эта функция будет завершена немедленно после завершения всех процессов. Это самое эффективное решение.

Неблокирующее решение

Используйте kill -0цикл in, чтобы дождаться завершения всех процессов + сделать что-нибудь между проверками:

function anywait_w_status()
{
    for pid in "$@"
    do
        while kill -0 "$pid"
        do
            echo "Process $pid still running..."
            sleep 1
        done
    done
    echo 'All processes terminated'
}

Время реакции уменьшилось до sleepвремени, потому что нужно предотвратить высокую загрузку процессора.

Реалистичное использование:

Ожидание завершения всех процессов + информирование пользователя обо всех запущенных PID.

function anywait_w_status2()
{
    while true
    do
        alive_pids=()
        for pid in "$@"
        do
            kill -0 "$pid" 2>/dev/null \
                && alive_pids+="$pid "
        done

        if [ ${#alive_pids[@]} -eq 0 ]
        then
            break
        fi

        echo "Process(es) still running... ${alive_pids[@]}"
        sleep 1
    done
    echo 'All processes terminated'
}

Ноты

Эти функции получают PID с помощью аргументов в $@виде массива BASH.

andras.tim
источник
2

У меня была та же проблема, я решил проблему, убив процесс и затем дождавшись завершения каждого процесса с использованием файловой системы PROC:

while [ -e /proc/${pid} ]; do sleep 0.1; done
littlebridge
источник
опрос очень плохой, вы можете получить визит от полиции :)
Александр Миллс
2

Нет встроенной функции для ожидания завершения какого-либо процесса.

Вы можете отправлять kill -0на любой найденный PID, чтобы вас не озадачили зомби и другие вещи, которые все еще будут видны ps(пока вы будете получать список PID с помощью ps).

TheBonsai
источник
1

Используйте inotifywait для отслеживания некоторых файлов, которые закрываются, когда ваш процесс завершается. Пример (в Linux):

yourproc >logfile.log & disown
inotifywait -q -e close logfile.log

-e указывает ожидаемое событие, -q означает минимальный вывод только после завершения. В этом случае это будет:

logfile.log CLOSE_WRITE,CLOSE

Одну команду ожидания можно использовать для ожидания нескольких процессов:

yourproc1 >logfile1.log & disown
yourproc2 >logfile2.log & disown
yourproc3 >logfile3.log & disown
inotifywait -q -e close logfile1.log logfile2.log logfile3.log

Выходная строка inotifywait сообщит вам, какой процесс завершен. Это работает только с «настоящими» файлами, а не с чем-то в / proc /

Бургхард Хоффманн
источник
0

В такой системе, как OSX, у вас может не быть pgrep, поэтому вы можете попробовать это приложение при поиске процессов по имени:

while ps axg | grep process_name$ > /dev/null; do sleep 1; done

$Символ в конце имени процесса гарантирует , что GREP матчи только process_name до конца строки в выходной пс , а не сам по себе.

Пирз
источник
Ужасно: может быть несколько процессов с таким именем где-то в командной строке , включая ваш собственный grep. Вместо перенаправления на /dev/null, -qследует использовать с grep. Другой случай процесса мог начаться, когда ваш цикл спал, и вы никогда не узнаете ...
Михаил Т.
Я не уверен, почему вы выбрали этот ответ как «Ужасный» - так как подобный подход был предложен другими? Хотя это -qпредложение действительно, как я упомянул в своем ответе, в частности, завершающее $средство grep не будет совпадать с именем «где-то в командной строке» и не будет совпадать с самим собой. Вы действительно пробовали это на OSX?
Pierz
0

Решение Рауно Палосаари для Timeout in Seconds Darwin, является отличным обходным решением для UNIX-подобной ОС, которая не имеет GNU tail(она не является специфической для Darwin). Но, в зависимости от возраста UNIX-подобной операционной системы, предлагаемая командная строка является более сложной, чем необходимо, и может дать сбой:

lsof -p $pid +r 1m%s -t | grep -qm1 $(date -v+${timeout}S +%s 2>/dev/null || echo INF)

По крайней мере на одном старом UNIX lsofаргумент +r 1m%sтерпит неудачу (даже для суперпользователя):

lsof: can't read kernel name list.

Это m%sспецификация выходного формата. Более простой постпроцессор не требует этого. Например, следующая команда ожидает PID 5959 до пяти секунд:

lsof -p 5959 +r 1 | awk '/^=/ { if (T++ >= 5) { exit 1 } }'

В этом примере, если PID 5959 выходит сам по себе до истечения пяти секунд, ${?}это 0. Если не ${?}возвращается 1через пять секунд.

Возможно, стоит прямо отметить +r 1, что 1это интервал опроса (в секундах), поэтому его можно изменить в соответствии с ситуацией.

kbulgrien
источник