Nhận mã thoát của một quá trình nền


129

Tôi có một lệnh CMD được gọi từ tập lệnh shell bourne chính của tôi mất mãi mãi.

Tôi muốn sửa đổi tập lệnh như sau:

  1. Chạy lệnh CMD song song như một quá trình nền ( CMD &).
  2. Trong tập lệnh chính, có một vòng lặp để theo dõi lệnh được sinh ra cứ sau vài giây. Vòng lặp cũng lặp lại một số thông báo đến thiết bị xuất chuẩn cho thấy tiến trình của tập lệnh.
  3. Thoát khỏi vòng lặp khi lệnh sinh sản kết thúc.
  4. Nắm bắt và báo cáo mã thoát của quá trình sinh sản.

Ai đó có thể cho tôi con trỏ để thực hiện điều này?


1
...và người chiến thắng là?
TrueY

1
@TrueY .. bob đã không đăng nhập kể từ ngày anh ấy đặt câu hỏi. Chúng ta sẽ không bao giờ biết!
ghoti

Câu trả lời:


126

1: Trong bash, $!giữ PID của quá trình nền cuối cùng được thực thi. Điều đó sẽ cho bạn biết quá trình để theo dõi, dù sao.

4: wait <n>đợi cho đến khi quá trình với PID <n>hoàn tất (nó sẽ chặn cho đến khi quá trình hoàn tất, do đó bạn có thể không muốn gọi nó cho đến khi bạn chắc chắn quá trình được thực hiện), và sau đó trả về mã thoát của quá trình đã hoàn thành.

2, 3: pshoặc ps | grep " $! "có thể cho bạn biết liệu quy trình có còn chạy hay không. Tùy thuộc vào bạn làm thế nào để hiểu đầu ra và quyết định mức độ hoàn thiện của nó. ( ps | grepkhông phải là bằng chứng ngu ngốc. Nếu bạn có thời gian, bạn có thể đưa ra một cách mạnh mẽ hơn để biết liệu quy trình có còn chạy hay không).

Đây là một kịch bản bộ xương:

# 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=không grepcần thiết
Tạm dừng cho đến khi có thông báo mới.

53
kill -0 $!là một cách tốt hơn để biết liệu một quy trình có còn chạy hay không. Nó không thực sự gửi bất kỳ tín hiệu nào, chỉ kiểm tra xem quy trình còn sống hay không, sử dụng trình bao tích hợp thay vì các quy trình bên ngoài. Như đã man 2 killnói, "Nếu sig là 0, thì không có tín hiệu nào được gửi, nhưng việc kiểm tra lỗi vẫn được thực hiện; điều này có thể được sử dụng để kiểm tra sự tồn tại của ID tiến trình hoặc ID nhóm quy trình."
ephemient

14
@ephemient kill -0sẽ trả về giá trị khác không nếu bạn không có quyền gửi tín hiệu đến một quy trình đang chạy. Thật không may, nó trả về 1trong cả trường hợp này và trường hợp quá trình không tồn tại. Điều này làm cho nó hữu ích trừ khi bạn không sở hữu quy trình - có thể là trường hợp ngay cả đối với các quy trình bạn đã tạo nếu có một công cụ như sudocó liên quan hoặc nếu chúng được thiết lập (và có thể bỏ quyền riêng tư).
Craig Ringer

13
waitkhông trả về mã thoát trong biến $?. Nó chỉ trả về mã thoát và $?là mã thoát của chương trình tiền cảnh mới nhất.
MindlessRanger

7
Đối với nhiều người bỏ phiếu lên kill -0. Dưới đây là một tham chiếu được đánh giá ngang hàng từ SO cho thấy nhận xét của CraigRinger là hợp pháp liên quan đến: kill -0sẽ trả về giá trị khác không cho các quy trình đang chạy ... nhưng ps -psẽ luôn trả về 0 cho bất kỳ quy trình đang chạy nào .
Trevor Boyd Smith

57

Đây là cách tôi giải quyết nó khi tôi có nhu cầu tương tự:

# 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

Tôi thích cách tiếp cận này.
Kemin Zhou

Cảm ơn! Điều này dường như với tôi cách tiếp cận đơn giản nhất.
Luke Davis

4
Giải pháp này không thỏa mãn yêu cầu số 2: vòng lặp giám sát trên mỗi quy trình nền. waits gây ra kịch bản để chờ cho đến khi kết thúc (mỗi) quá trình.
DKroot

Cách tiếp cận đơn giản và tốt đẹp .. đôi khi đã tìm kiếm giải pháp này ..
Santosh Kumar Arjunan

Điều này không hoạt động .. hoặc không làm những gì bạn muốn: nó không kiểm tra trạng thái thoát quy trình nền?
Conny

10

Pid của một quá trình con nền được lưu trữ trong $! . Bạn có thể lưu trữ tất cả các giá trị quy trình con vào một mảng, ví dụ PIDS [] .

wait [-n] [jobspec or pid …]

Đợi cho đến khi tiến trình con được chỉ định bởi mỗi pid ID quy trình hoặc jobspec đặc tả công việc thoát và trả về trạng thái thoát của lệnh cuối cùng được chờ. Nếu một đặc tả công việc được đưa ra, tất cả các quy trình trong công việc được chờ đợi. Nếu không có đối số nào được đưa ra, tất cả các quy trình con hiện đang hoạt động đều được chờ đợi và trạng thái trả về bằng không. Nếu tùy chọn -n được cung cấp, chờ đợi cho bất kỳ công việc nào kết thúc và trả về trạng thái thoát của nó. Nếu cả jobspec và pid chỉ định một tiến trình con hoạt động của shell, trạng thái trả về là 127.

Sử dụng lệnh chờ bạn có thể đợi cho tất cả các tiến trình con kết thúc, trong khi đó bạn có thể nhận trạng thái thoát của từng tiến trình con thông qua $? và lưu trữ trạng thái vào TÌNH TRẠNG [] . Sau đó, bạn có thể làm một cái gì đó tùy theo tình trạng.

Tôi đã thử 2 giải pháp sau đây và chúng chạy tốt. Solution01 ngắn gọn hơn, trong khi Solution02 hơi phức tạp.

giải pháp01

#!/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

dung dịch02

#!/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

Tôi đã thử và chứng minh nó chạy tốt. Bạn có thể đọc giải thích của tôi trong mã.
Terry

Vui lòng đọc " Làm thế nào để tôi viết một câu trả lời hay? " Nơi bạn sẽ tìm thấy thông tin sau: ... cố gắng đề cập đến bất kỳ giới hạn, giả định hoặc đơn giản hóa nào trong câu trả lời của bạn. Brevity là chấp nhận được, nhưng giải thích đầy đủ hơn là tốt hơn. Do đó, bạn trả lời là chấp nhận được nhưng bạn có nhiều cơ hội nhận được nhiều hơn nếu bạn có thể giải quyết vấn đề và giải pháp của mình. :-)
Noel Widmer

1
pid=$!; PIDS[$i]=${pid}; ((i+=1))có thể được viết đơn giản hơn vì PIDS+=($!)nó chỉ đơn giản gắn vào mảng mà không phải sử dụng một biến riêng để lập chỉ mục hoặc chính nó. Điều tương tự áp dụng cho STATUSmảng.
codeforester

1
@codeforester, cảm ơn bạn vì sugesstion của bạn, tôi đã sửa đổi mã bẩm sinh của mình thành giải pháp01, nó có vẻ súc tích hơn.
Terry

Điều tương tự áp dụng cho những nơi khác mà bạn đang thêm mọi thứ vào một mảng.
codeforester

8

Như tôi thấy hầu hết tất cả các câu trả lời đều sử dụng các tiện ích bên ngoài (hầu hết ps) để thăm dò trạng thái của quá trình nền. Có một giải pháp unixesh hơn, bắt tín hiệu SIGCHLD. Trong bộ xử lý tín hiệu, nó phải được kiểm tra xem tiến trình con nào đã bị dừng. Nó có thể được thực hiện bằng cách tích kill -0 <PID>hợp (phổ quát) hoặc kiểm tra sự tồn tại của /proc/<PID>thư mục (cụ thể của Linux) hoặc sử dụng tích jobshợp (riêng. jobs -lcũng báo cáo các pid. Trong trường hợp này, trường thứ 3 của đầu ra có thể bị dừng | Chạy | Xong | Thoát. ).

Đây là ví dụ của tôi.

Quá trình khởi động được gọi là loop.sh. Nó chấp nhận -xhoặc một số như là một đối số. Dành cho-x là thoát với mã thoát 1. Đối với một số, nó chờ num * 5 giây. Trong mỗi 5 giây, nó in ra PID của nó.

Quá trình khởi chạy được gọi là 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

Để biết thêm giải thích, hãy xem: Bắt đầu một quá trình từ tập lệnh bash không thành công


1
Vì chúng ta đang nói về Bash, vòng lặp for có thể được viết là: for i in ${!pids[@]};sử dụng mở rộng tham số.
PlasmaBinturong

7
#/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
Đây là một mẹo để tránh grep -v. Bạn có thể giới hạn tìm kiếm ở đầu dòng: grep '^'$pidPlus ps p $pid -o pid=, dù sao bạn cũng có thể làm được . Ngoài ra, tail -fsẽ không kết thúc cho đến khi bạn giết nó vì vậy tôi không nghĩ rằng đó là một cách rất tốt để giới thiệu điều này (ít nhất là không chỉ ra điều đó). Bạn có thể muốn chuyển hướng đầu ra của pslệnh tới /dev/nullhoặc nó sẽ đi đến màn hình ở mỗi lần lặp. exitNguyên nhân của bạn waitbị bỏ qua - nó có thể là một break. Nhưng không phải là while/ pswaitdư thừa?
Tạm dừng cho đến khi có thông báo mới.

5
Tại sao mọi người quên mất kill -0 $pid? Nó không thực sự gửi bất kỳ tín hiệu nào, chỉ kiểm tra xem quy trình còn sống hay không, sử dụng trình bao tích hợp thay vì các quy trình bên ngoài.
ephemient

3
Bởi vì bạn chỉ có thể giết một quá trình bạn sở hữu:bash: kill: (1) - Operation not permitted
errant.info

2
Vòng lặp là dư thừa. Đợi đấy. Ít mã hơn => ít trường hợp cạnh.
Brais Gabin

@Brais Gabin Vòng lặp giám sát là yêu cầu số 2 của câu hỏi
DKroot

5

Tôi sẽ thay đổi cách tiếp cận của bạn một chút. Thay vì kiểm tra cứ sau vài giây nếu lệnh vẫn còn và báo cáo một thông báo, có một quy trình khác báo cáo cứ sau vài giây là lệnh vẫn chạy và sau đó giết quá trình đó khi lệnh kết thúc. Ví dụ:

#! / thùng / sh

cmd () {ngủ 5; lối ra 24; }

cmd & # Chạy quá trình chạy dài
pid = $! # Ghi lại pid

# Tạo ra một quy trình báo cáo rằng lệnh vẫn đang chạy
trong khi echo "$ (ngày): $ pid vẫn đang chạy"; ngủ 1 giấc; làm xong &
tiếng vang = $!

# Đặt bẫy để giết phóng viên khi quá trình kết thúc
bẫy 'giết $ echo' 0

# Đợi quá trình hoàn tất
nếu chờ $ pid; sau đó
    tiếng vang "cmd đã thành công"
khác
    echo "cmd FAILED !! (trả lại $?)"
fi

mẫu tuyệt vời, cảm ơn đã chia sẻ! Tôi tin rằng thay vì bẫy, chúng ta cũng có thể làm được while kill -0 $pid 2> /dev/null; do X; done, hy vọng nó hữu ích cho người khác trong tương lai đọc tin nhắn này;)
punkbit

3

Nhóm của chúng tôi có cùng nhu cầu với tập lệnh thực thi SSH từ xa đã hết thời gian sau 25 phút không hoạt động. Dưới đây là một giải pháp với vòng lặp giám sát kiểm tra quá trình nền mỗi giây, nhưng chỉ in 10 phút một lần để loại bỏ thời gian không hoạt động.

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

Một ví dụ đơn giản, tương tự như các giải pháp trên. Điều này không yêu cầu giám sát bất kỳ đầu ra quá trình. Ví dụ tiếp theo sử dụng đuôi để theo đầu ra.

$ 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

Sử dụng đuôi để theo dõi quá trình đầu ra và thoát khi quá trình hoàn tất.

$ 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

Một giải pháp khác là giám sát các quá trình thông qua hệ thống tập tin Proc (an toàn hơn so với kết hợp ps / grep); Khi bạn bắt đầu một quy trình, nó có một thư mục tương ứng trong / Proc / $ pid, vì vậy giải pháp có thể là

#!/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
....

Bây giờ bạn có thể sử dụng biến $ exit_status theo cách bạn muốn.


Không làm việc trong bash? Syntax error: "else" unexpected (expecting "done")
benjaoming

1

Với phương pháp này, tập lệnh của bạn không phải chờ quá trình nền, bạn sẽ chỉ phải theo dõi một tệp tạm thời cho trạng thái thoát.

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

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

bây giờ, tập lệnh của bạn có thể làm bất cứ điều gì khác trong khi bạn chỉ cần theo dõi nội dung của retFile (nó cũng có thể chứa bất kỳ thông tin nào khác bạn muốn như thời gian thoát).

PS.: Btw, tôi mã hóa suy nghĩ trong bash


0

Điều này có thể vượt ra ngoài câu hỏi của bạn, tuy nhiên nếu bạn lo lắng về thời gian của các quy trình đang chạy, bạn có thể quan tâm đến việc kiểm tra trạng thái của các quy trình chạy nền sau một khoảng thời gian. Thật dễ dàng để kiểm tra các PID con nào vẫn đang chạy pgrep -P $$, tuy nhiên tôi đã đưa ra giải pháp sau đây để kiểm tra trạng thái thoát của các PID đó đã hết hạn:

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

đầu ra nào:

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

Lưu ý: Bạn có thể thay đổi $pidsthành một biến chuỗi thay vì mảng để đơn giản hóa mọi thứ nếu bạn muốn.


0

Giải pháp của tôi là sử dụng một đường ống ẩn danh để chuyển trạng thái sang vòng lặp giám sát. Không có tệp tạm thời được sử dụng để trao đổi trạng thái nên không có gì để dọn dẹp. Nếu bạn không chắc chắn về số lượng công việc nền thì điều kiện nghỉ có thể [ -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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.