簡體   English   中英

獲取后台進程的退出代碼

[英]Get exit code of a background process

我有一個從我的主 bourne shell 腳本調用的命令 CMD,它需要永遠。

我想修改腳本如下:

  1. 並行運行命令 CMD 作為后台進程 ( CMD & )。
  2. 在主腳本中,每隔幾秒就有一個循環來監控生成的命令。 該循環還將一些消息回顯到標准輸出,指示腳本的進度。
  3. 當生成的命令終止時退出循環。
  4. 捕獲並報告生成進程的退出代碼。

有人可以給我指點來完成這個嗎?

1:在 bash 中, $! 保存最后執行的后台進程的 PID。 無論如何,這將告訴您要監視的過程。

4: wait <n>等到 PID <n>的進程完成(它會阻塞直到進程完成,所以你可能不想在確定進程完成之前調用它),然后返回退出代碼的完成過程。

2、3: psps | grep " $! " 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

當我有類似需求時,這就是我解決它的方法:

# 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

后台子進程的 pid 存儲在$! . 您可以將所有子進程的 pid 存儲到一個數組中,例如PIDS[]

wait [-n] [jobspec or pid …]

等待每個進程 ID pid 或作業規范 jobspec 指定的子進程退出,並返回最后等待的命令的退出狀態。 如果給出了作業規范,則等待作業中的所有進程。 如果沒有給出參數,則等待所有當前活動的子進程,並且返回狀態為零。 如果提供了 -n 選項,wait 將等待任何作業終止並返回其退出狀態。 如果 jobspec 和 pid 都沒有指定 shell 的活動子進程,則返回狀態為 127。

使用wait命令可以等待所有子進程完成,同時可以通過$?獲取每個子進程的退出狀態。 並將狀態存儲到STATUS[]中。 然后你可以根據狀態做一些事情。

我嘗試了以下兩種解決方案,它們運行良好。 解決方案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
#/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 $?

正如我所看到的,幾乎所有答案都使用外部實用程序(主要是ps )來輪詢后台進程的狀態。 有一個更 unixesh 的解決方案,捕獲 SIGCHLD 信號。 在信號處理程序中,必須檢查哪個子進程已停止。 可以通過kill -0 <PID>內置(通用)或檢查/proc/<PID>目錄的存在(特定於 Linux)或使用內置jobs (特定於 jobs -l還報告pid。在這種情況下,輸出的第三個字段可以是 Stopped|Running|Done|Exit .)。

這是我的例子。

啟動的進程稱為loop.sh 它接受-x或數字作為參數。 對於-x ,退出代碼為 1。對於數字,它等待 num*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 腳本啟動進程失敗

我會稍微改變你的方法。 與其每隔幾秒鍾檢查一次命令是否仍然存在並報告一條消息,不如讓另一個進程每隔幾秒鍾報告一次命令仍在運行,然后在命令完成時終止該進程。 例如:

#!/bin/sh

cmd() { sleep 5; exit 24; }

cmd &   # Run the long running process
pid=$!  # Record the pid

# Spawn a process that coninually reports that the command is still running
while echo "$(date): $pid is still running"; do sleep 1; done &
echoer=$!

# Set a trap to kill the reporter when the process finishes
trap 'kill $echoer' 0

# Wait for the process to finish
if wait $pid; then
    echo "cmd succeeded"
else
    echo "cmd FAILED!! (returned $?)"
fi

我們的團隊對遠程 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}

一個簡單的例子,類似於上面的解決方案。 這不需要監視任何過程輸出。 下一個示例使用 tail 跟蹤輸出。

$ 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

使用 tail 跟蹤進程輸出並在進程完成時退出。

$ 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

另一種解決方案是通過 proc 文件系統監視進程(比 ps/grep 組合更安全); 當您啟動一個進程時,它在 /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 變量。

使用這種方法,您的腳本不必等待后台進程,您只需監視一個臨時文件的退出狀態。

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

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

現在,您的腳本可以做任何其他事情,而您只需要繼續監視 retFile 的內容(它還可以包含您想要的任何其他信息,例如退出時間)。

PS.:順便說一句,我用 bash 編碼思考

我的解決方案是使用匿名管道將狀態傳遞給監控循環。 沒有用於交換狀態的臨時文件,因此無需清理。 如果您不確定后台作業的數量,則中斷條件可能是[ -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

怎么樣 ...

# run your stuff
unset PID
for process in one two three four
do
    ( sleep $((RANDOM%20)); echo hello from process $process; exit $((RANDOM%3)); ) & 2>&1
    PID+=($!)
done

# (optional) report on the status of that stuff as it exits
for pid in "${PID[@]}"
do
    ( wait "$pid"; echo "process $pid complemted with exit status $?") &
done

# (optional) while we wait, monitor that stuff
while ps --pid "${PID[*]}" --ppid "${PID[*]}" --format pid,ppid,command,pcpu
do
    sleep 5
done | xargs -i date '+%x %X {}'

# return non-zero if any are non zero
SUCCESS=0
for pid in "${PID[@]}"
do
    wait "$pid" && ((SUCCESS++)) && echo "$pid OK" || echo "$pid returned $?"
done

echo "success for $SUCCESS out of ${#PID} jobs"
exit $(( ${#PID} - SUCCESS ))

這可能超出了您的問題,但是如果您擔心進程運行的時間長度,您可能有興趣在一段時間后檢查正在運行的后台進程的狀態。 使用pgrep -P $$檢查哪些子 PID 仍在運行很容易,但是我想出了以下解決方案來檢查那些已經過期的 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更改為字符串變量而不是數組以簡化操作。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM