Отримайте вихідний код фонового процесу


130

У мене є команда CMD, що викликається з мого основного сценарію оболонки Bourne, яка бере назавжди.

Я хочу змінити сценарій так:

  1. Виконайте команду CMD паралельно як фоновий процес ( CMD &).
  2. У головному сценарії є цикл, який слідкує за породженою командою кожні кілька секунд. Цикл також відголошує деякі повідомлення для stdout, що вказує на хід сценарію.
  3. Вийдіть з циклу, коли спаунена команда закінчується.
  4. Захопіть і повідомте про вихідний код породженого процесу.

Може хтось дасть мені покажчики на це?


1
... а переможець?
TrueY

1
@TrueY .. bob не ввійшов у систему з дня, коли він задав питання. Ми навряд чи колись дізнаємось!
ghoti

Відповіді:


128

1: У bash $!міститься PID останнього фонового процесу, який був виконаний. Це підкаже, який процес слід відстежувати.

4: wait <n>чекає, поки процес з PID <n>завершиться (він блокується, поки процес не завершиться, тому ви, можливо, не захочете викликати це, поки не будете впевнені, що процес виконаний), а потім поверне код виходу завершеного процесу.

2, 3: psабо ps | grep " $! "може сказати, чи процес все ще працює. Ви самі вирішуєте, як зрозуміти результат і вирішити, наскільки близько до закінчення. ( ps | grepне є ідіотом. Якщо у вас є час, ви можете придумати більш надійний спосіб сказати, чи процес все ще працює).

Ось сценарій скелета:

# simulate a long process that will have an identifiable exit code
(sleep 15 ; /bin/false) &
my_pid=$!

while   ps | grep " $my_pid "     # might also need  | grep -v grep  here
do
    echo $my_pid is still in the ps output. Must still be running.
    sleep 3
done

echo Oh, it looks like the process is done.
wait $my_pid
# The variable $? always holds the exit code of the last command to finish.
# Here it holds the exit code of $my_pid, since wait exits with that code. 
my_status=$?
echo The exit status of the process was $my_status

15
ps -p $my_pid -o pid=і grepне потрібно.
Призупинено до подальшого повідомлення.

54
kill -0 $!- це кращий спосіб сказати, чи процес все ще працює. Він насправді не надсилає жодного сигналу, лише перевіряє, чи є процес живим, використовуючи вбудовану оболонку замість зовнішніх процесів. Як man 2 killговориться, "Якщо sig дорівнює 0, то сигнал не надсилається, але перевірка помилок все ще виконується; це можна використовувати для перевірки наявності ідентифікатора процесу або ідентифікатора групи процесу".
ефемієнт

14
@ephemient kill -0поверне не нульове значення, якщо у вас немає дозволу надсилати сигнали до запущеного процесу. На жаль, він повертається 1як у цьому випадку, так і у випадку, коли процес не існує. Це робить його корисним, якщо ви не володієте процесом - що може бути навіть для створених вами процесів, якщо такий інструмент, як наприклад sudo, задіяний, або якщо вони встановлені (і, можливо, скидають приватні дані).
Крейг Рінгер

13
waitне повертає вихідний код у змінній $?. Він просто повертає код виходу та $?є вихідним кодом останньої програми переднього плану.
MindlessRanger

7
Для багатьох людей, які проголосували проти kill -0. Ось рецензована посилання з SO, яка показує, що коментар CraigRinger легітимно стосується: kill -0поверне не нуль для запущених процесів ..., але ps -pзавжди поверне 0 для будь-якого запущеного процесу .
Тревор Бойд Сміт

58

Ось як я вирішив це, коли мав подібну потребу:

# Some function that takes a long time to process
longprocess() {
        # Sleep up to 14 seconds
        sleep $((RANDOM % 15))
        # Randomly exit with 0 or 1
        exit $((RANDOM % 2))
}

pids=""
# Run five concurrent processes
for i in {1..5}; do
        ( longprocess ) &
        # store PID of process
        pids+=" $!"
done

# Wait for all processes to finish, will take max 14s
# as it waits in order of launch, not order of finishing
for p in $pids; do
        if wait $p; then
                echo "Process $p success"
        else
                echo "Process $p fail"
        fi
done

Мені подобається такий підхід.
Kemin Zhou

Дякую! Цей мені здається найпростіший підхід.
Люк Девіс

4
Це рішення не відповідає вимозі №2: цикл моніторингу за фоновим процесом. waits викликати сценарій чекати до самого кінця (кожного) процесу.
DKroot

Простий і приємний підхід .. шукали це рішення десь ..
Сантош Кумар Арджунан,

Це не працює .. або не робить те, що ви хочете: він не перевіряє фонові процеси виходу з фонових процесів?
conny

10

Pid фонового дочірнього процесу зберігається в $! . Ви можете зберігати всі підрозділи дочірніх процесів у масив, наприклад, PIDS [] .

wait [-n] [jobspec or pid …]

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

Використовуйте команду wait, ви можете дочекатися завершення всіх дочірніх процесів, тим часом ви можете отримати статус виходу кожного дочірнього процесу через $? і зберігати статус у STATUS [] . Тоді ви можете робити щось залежно від статусу.

Я спробував наступні 2 рішення, і вони працюють добре. рішення01 є більш стислим, тоді як рішення02 трохи складніше.

рішення01

#!/bin/bash

# start 3 child processes concurrently, and store each pid into array PIDS[].
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
  ./${app} &
  PIDS+=($!)
done

# wait for all processes to finish, and store each process's exit code into array STATUS[].
for pid in ${PIDS[@]}; do
  echo "pid=${pid}"
  wait ${pid}
  STATUS+=($?)
done

# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
  if [[ ${st} -ne 0 ]]; then
    echo "$i failed"
  else
    echo "$i finish"
  fi
  ((i+=1))
done

рішення02

#!/bin/bash

# start 3 child processes concurrently, and store each pid into array PIDS[].
i=0
process=(a.sh b.sh c.sh)
for app in ${process[@]}; do
  ./${app} &
  pid=$!
  PIDS[$i]=${pid}
  ((i+=1))
done

# wait for all processes to finish, and store each process's exit code into array STATUS[].
i=0
for pid in ${PIDS[@]}; do
  echo "pid=${pid}"
  wait ${pid}
  STATUS[$i]=$?
  ((i+=1))
done

# after all processed finish, check their exit codes in STATUS[].
i=0
for st in ${STATUS[@]}; do
  if [[ ${st} -ne 0 ]]; then
    echo "$i failed"
  else
    echo "$i finish"
  fi
  ((i+=1))
done

Я спробував і довів, що це працює добре. Ви можете прочитати моє пояснення в коді.
Террі

Будь ласка, прочитайте " Як мені написати гарну відповідь? ", Де ви знайдете таку інформацію: ... спробуйте вказати будь-які обмеження, припущення чи спрощення у своїй відповіді. Стислість прийнятна, але повніші пояснення краще. Отже, ви відповідаєте, це прийнятно, але у вас є набагато більші шанси отримати резюме, якщо ви зможете детальніше розглянути проблему та своє рішення. :-)
Ноель Відмер

1
pid=$!; PIDS[$i]=${pid}; ((i+=1))можна записати простіше як те, PIDS+=($!)що просто додається до масиву, не використовуючи окрему змінну для індексації або сам pid. Те ж саме стосується STATUSмасиву.
codeforester

1
@codeforester, дякую за ваші суджені дії, я змінив свій первинний код на розв’язання01, це виглядає більш стисло.
Террі

Те саме стосується інших місць, де ви додаєте речі в масив.
codeforester

8

Як я бачу, майже всі відповіді використовують зовнішні утиліти (в основному ps) для опитування стану фонового процесу. Є більш універсальне рішення, що вловлює сигнал SIGCHLD. У обробнику сигналу слід перевірити, який дочірній процес був зупинений. Це можна зробити за допомогою kill -0 <PID>вбудованого (універсального) або перевірки існування /proc/<PID>каталогу (специфічного для Linux) або за допомогою jobsвбудованого (специфічні. jobs -lтакож повідомляє про під. У цьому випадку третє поле виводу може бути зупинено | Запуск | Готово | Вихід. ).

Ось мій приклад.

Запущений процес називається loop.sh. Він приймає -xабо число як аргумент. Для -xє вихід з кодом виходу 1. Для номера він чекає число * 5 секунд. Кожні 5 секунд він друкує свій PID.

Процес запуску називається launch.sh:

#!/bin/bash

handle_chld() {
    local tmp=()
    for((i=0;i<${#pids[@]};++i)); do
        if [ ! -d /proc/${pids[i]} ]; then
            wait ${pids[i]}
            echo "Stopped ${pids[i]}; exit code: $?"
        else tmp+=(${pids[i]})
        fi
    done
    pids=(${tmp[@]})
}

set -o monitor
trap "handle_chld" CHLD

# Start background processes
./loop.sh 3 &
pids+=($!)
./loop.sh 2 &
pids+=($!)
./loop.sh -x &
pids+=($!)

# Wait until all background processes are stopped
while [ ${#pids[@]} -gt 0 ]; do echo "WAITING FOR: ${pids[@]}"; sleep 2; done
echo STOPPED

Докладніше див.: Не вдалося запустити процес із скрипту bash


1
Оскільки ми говоримо про Bash, цикл for може бути записаний так: for i in ${!pids[@]};використовуючи розширення параметрів.
ПлазмаBinturong

8
#/bin/bash

#pgm to monitor
tail -f /var/log/messages >> /tmp/log&
# background cmd pid
pid=$!
# loop to monitor running background cmd
while :
do
    ps ax | grep $pid | grep -v grep
    ret=$?
    if test "$ret" != "0"
    then
        echo "Monitored pid ended"
        break
    fi
    sleep 5

done

wait $pid
echo $?

2
Ось хитрість, щоб уникнути цього grep -v. Ви можете обмежити пошук до початку рядка: grep '^'$pidПлюс, ви ps p $pid -o pid=все одно можете зробити . Крім того, tail -fце не закінчиться, поки ви не вб'єте його, тому я не думаю, що це дуже хороший спосіб демонструвати це (принаймні, не вказуючи на це). Можливо, ви захочете переспрямувати вихід своєї psкоманди на, /dev/nullабо він перейде на екран при кожній ітерації. Ваші exitпричини waitпропускання - це, мабуть, має бути а break. Але чи не while/ psта waitнадлишкові?
Призупинено до подальшого повідомлення.

5
Чому всі забувають kill -0 $pid? Він насправді не надсилає жодного сигналу, лише перевіряє, чи є процес живим, використовуючи вбудовану оболонку замість зовнішніх процесів.
ефемія

3
Тому що ви можете вбити лише власний процес:bash: kill: (1) - Operation not permitted
errant.info

2
Петля є зайвою. Просто зачекайте. Менше коду => менше крайових справ.
Браїс Габін

@Brais Gabin. Цикл моніторингу є вимогою № 2 запитання
DKroot

5

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

#! / бін / ш

cmd () {сон 5; вихід 24; }

cmd & # Запуск тривалого процесу
pid = $! # Запишіть під

# Породжує процес, який постійно повідомляє, що команда все ще виконується
while echo "$ (дата): $ pid все ще працює"; спати 1; зробив &
ехоер = $!

# Встановіть пастку, щоб убити репортера, коли процес закінчиться
пастка 'вбити $ echoer' 0

# Зачекайте, поки процес закінчиться
якщо зачекати $ pid; тоді
    відлуння "cmd вдалося"
ще
    відлуння "cmd FAILED !! (return $?)"
фі

чудовий шаблон, дякую за обмін! Я вважаю, що замість пастки ми також можемо робити while kill -0 $pid 2> /dev/null; do X; done, сподіваюся, що це буде корисним для когось іншого, хто читатиме це повідомлення;)
панкбіт

3

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

long_running.sh & 
pid=$!

# Wait on a background job completion. Query status every 10 minutes.
declare -i elapsed=0
# `ps -p ${pid}` works on macOS and CentOS. On both OSes `ps ${pid}` works as well.
while ps -p ${pid} >/dev/null; do
  sleep 1
  if ((++elapsed % 600 == 0)); then
    echo "Waiting for the completion of the main script. $((elapsed / 60))m and counting ..."
  fi
done

# Return the exit code of the terminated background process. This works in Bash 4.4 despite what Bash docs say:
# "If neither jobspec nor pid specifies an active child process of the shell, the return status is 127."
wait ${pid}

2

Простий приклад, схожий на рішення вище. Для цього не потрібен контроль будь-якого результату процесу. У наступному прикладі використовується хвіст, щоб слідкувати за результатами.

$ echo '#!/bin/bash' > tmp.sh
$ echo 'sleep 30; exit 5' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh &
[1] 7454
$ pid=$!
$ wait $pid
[1]+  Exit 5                  ./tmp.sh
$ echo $?
5

Використовуйте хвіст, щоб слідкувати за результатами процесу та вийти, коли процес закінчиться.

$ echo '#!/bin/bash' > tmp.sh
$ echo 'i=0; while let "$i < 10"; do sleep 5; echo "$i"; let i=$i+1; done; exit 5;' >> tmp.sh
$ chmod +x tmp.sh
$ ./tmp.sh
0
1
2
^C
$ ./tmp.sh > /tmp/tmp.log 2>&1 &
[1] 7673
$ pid=$!
$ tail -f --pid $pid /tmp/tmp.log
0
1
2
3
4
5
6
7
8
9
[1]+  Exit 5                  ./tmp.sh > /tmp/tmp.log 2>&1
$ wait $pid
$ echo $?
5

1

Іншим рішенням є моніторинг процесів за допомогою файлової системи proc (безпечніше, ніж ps / grep combo); коли ви запускаєте процес, він має відповідну папку в / proc / $ pid, тому рішення може бути

#!/bin/bash
....
doSomething &
local pid=$!
while [ -d /proc/$pid ]; do # While directory exists, the process is running
    doSomethingElse
    ....
else # when directory is removed from /proc, process has ended
    wait $pid
    local exit_status=$?
done
....

Тепер ви можете використовувати змінну $ exit_status, скільки завгодно.


Не працює в баші? Syntax error: "else" unexpected (expecting "done")
benjaoming

1

З цим методом сценарій не повинен чекати фонового процесу, вам доведеться лише контролювати тимчасовий файл за станом виходу.

FUNCmyCmd() { sleep 3;return 6; };

export retFile=$(mktemp); 
FUNCexecAndWait() { FUNCmyCmd;echo $? >$retFile; }; 
FUNCexecAndWait&

Тепер ваш сценарій може робити все інше, тоді як вам просто потрібно буде постійно контролювати вміст retFile (він також може містити будь-яку іншу інформацію, яку ви хочете, як час виходу).

PS: btw, я закодував думки в баш


0

Це може виходити за рамки вашого питання, однак якщо вас турбує тривалість процесів, вам може бути цікаво перевірити стан запущених фонових процесів через проміжок часу. Досить просто перевірити, які дочірні PID-адреси все ще використовуються pgrep -P $$, проте я придумав таке рішення, щоб перевірити стан виходу тих PID, які вже минули:

cmd1() { sleep 5; exit 24; }
cmd2() { sleep 10; exit 0; }

pids=()
cmd1 & pids+=("$!")
cmd2 & pids+=("$!")

lasttimeout=0
for timeout in 2 7 11; do
  echo -n "interval-$timeout: "
  sleep $((timeout-lasttimeout))

  # you can only wait on a pid once
  remainingpids=()
  for pid in ${pids[*]}; do
     if ! ps -p $pid >/dev/null ; then
        wait $pid
        echo -n "pid-$pid:exited($?); "
     else
        echo -n "pid-$pid:running; "
        remainingpids+=("$pid")
     fi
  done
  pids=( ${remainingpids[*]} )

  lasttimeout=$timeout
  echo
done

який виводить:

interval-2: pid-28083:running; pid-28084:running; 
interval-7: pid-28083:exited(24); pid-28084:running; 
interval-11: pid-28084:exited(0); 

Примітка. Ви можете змінити $pidsна змінну рядка, а не на масив, щоб спростити речі, якщо вам це подобається.


0

Моє рішення полягало в тому, щоб використовувати анонімну трубку для передачі стану в контур моніторингу. Немає тимчасових файлів, які використовуються для обміну статусом, тому нічого не потрібно очищати. Якби ви не були впевнені у кількості фонових завдань, умова перерви могла бути [ -z "$(jobs -p)" ].

#!/bin/bash

exec 3<> <(:)

{ sleep 15 ; echo "sleep/exit $?" >&3 ; } &

while read -u 3 -t 1 -r STAT CODE || STAT="timeout" ; do
    echo "stat: ${STAT}; code: ${CODE}"
    if [ "${STAT}" = "sleep/exit" ] ; then
        break
    fi
done
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.