Dapatkan kode keluar dari proses latar belakang

130

Saya memiliki perintah CMD yang dipanggil dari skrip shell bursa utama saya yang membutuhkan waktu selamanya.

Saya ingin mengubah skrip sebagai berikut:

  1. Jalankan perintah CMD secara paralel sebagai proses latar belakang ( CMD &).
  2. Di skrip utama, buat perulangan untuk memantau perintah yang muncul setiap beberapa detik. Loop juga menggemakan beberapa pesan ke stdout yang menunjukkan kemajuan skrip.
  3. Keluar dari loop ketika perintah yang muncul berakhir.
  4. Tangkap dan laporkan kode keluar dari proses yang muncul.

Bisakah seseorang memberi saya petunjuk untuk mencapai ini?

bob
sumber
1
...dan pemenangnya adalah?
TrueY
1
@TrueY .. bob belum login sejak hari dia mengajukan pertanyaan. Kami tidak mungkin pernah tahu!
ghoti

Jawaban:

128

1: Dalam bash, $!menyimpan PID dari proses latar belakang terakhir yang dijalankan. Itu akan memberi tahu Anda proses apa yang harus dipantau.

4: wait <n>menunggu hingga proses dengan PID<n> selesai (ini akan memblokir sampai proses selesai, jadi Anda mungkin tidak ingin memanggil ini sampai Anda yakin prosesnya selesai), dan kemudian mengembalikan kode keluar dari proses yang sudah selesai.

2, 3: psatau ps | grep " $! "dapat memberi tahu Anda apakah prosesnya masih berjalan. Terserah Anda bagaimana memahami hasilnya dan memutuskan seberapa dekat itu dengan penyelesaian. ( ps | grepbukan anti-idiot. Jika Anda punya waktu, Anda dapat menemukan cara yang lebih kuat untuk mengetahui apakah prosesnya masih berjalan).

Berikut skrip kerangka:

# 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
massa
sumber
15
ps -p $my_pid -o pid=tidak grepada yang dibutuhkan.
Dijeda sampai pemberitahuan lebih lanjut.
54
kill -0 $!adalah cara yang lebih baik untuk mengetahui apakah suatu proses masih berjalan. Itu tidak benar-benar mengirim sinyal apa pun, hanya memeriksa bahwa prosesnya hidup, menggunakan shell bawaan daripada proses eksternal. Seperti yang man 2 killdikatakan, "Jika sig adalah 0, maka tidak ada sinyal yang dikirim, tetapi pemeriksaan kesalahan masih dilakukan; ini dapat digunakan untuk memeriksa keberadaan ID proses atau ID grup proses."
ephemient
14
@ephemient kill -0akan mengembalikan bukan nol jika Anda tidak memiliki izin untuk mengirim sinyal ke proses yang sedang berjalan. Sayangnya itu kembali 1dalam kasus ini dan kasus di mana prosesnya tidak ada. Ini membuatnya berguna kecuali jika Anda tidak memiliki proses - yang dapat menjadi kasus bahkan untuk proses yang Anda buat jika alat seperti sudoterlibat atau jika mereka setuid (dan mungkin menjatuhkan privs).
Craig Ringer
13
waittidak mengembalikan kode keluar dalam variabel $?. Ini hanya mengembalikan kode keluar dan $?merupakan kode keluar dari program latar depan terbaru.
MindlessRanger
7
Untuk banyak orang yang memilih kill -0. Berikut adalah referensi peer review dari SO yang menunjukkan komentar CraigRinger sah mengenai: kill -0akan mengembalikan bukan nol untuk proses yang sedang berjalan ... tetapi ps -pakan selalu mengembalikan 0 untuk proses yang sedang berjalan .
Trevor Boyd Smith
58

Beginilah cara saya menyelesaikannya ketika saya memiliki kebutuhan serupa:

# 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
Bjorn
sumber
Saya suka pendekatan ini.
Kemin Zhou
Terima kasih! Bagi saya ini pendekatan yang paling sederhana.
Luke Davis
4
Solusi ini tidak memenuhi persyaratan # 2: loop pemantauan per proses latar belakang. waits menyebabkan skrip menunggu hingga akhir dari (setiap) proses.
DKroot
Pendekatan yang sederhana dan menyenangkan .. telah lama mencari solusi ini ..
Santosh Kumar Arjunan
Ini tidak bekerja .. atau tidak melakukan apa yang Anda inginkan: tidak memeriksa proses latar belakang status keluar?
conny
10

Pid proses anak latar belakang disimpan di $! . Anda dapat menyimpan semua pids proses anak ke dalam sebuah array, misalnya PIDS [] .

wait [-n] [jobspec or pid …]

Tunggu hingga proses turunan yang ditentukan oleh setiap ID proses pid atau spesifikasi pekerjaan jobspec keluar dan mengembalikan status keluar dari perintah terakhir yang menunggu. Jika spesifikasi pekerjaan diberikan, semua proses dalam pekerjaan akan ditunggu. Jika tidak ada argumen yang diberikan, semua proses anak yang sedang aktif akan ditunggu, dan status kembaliannya adalah nol. Jika opsi -n tersedia, tunggu menunggu pekerjaan apa pun berhenti dan mengembalikan status keluarnya. Jika baik jobspec maupun pid menentukan proses turunan aktif dari shell, status kembaliannya adalah 127.

Gunakan perintah wait Anda bisa menunggu semua proses anak selesai, sementara Anda bisa mendapatkan status keluar dari setiap proses anak melalui $? dan menyimpan status ke STATUS [] . Kemudian Anda dapat melakukan sesuatu tergantung status.

Saya telah mencoba 2 solusi berikut dan semuanya berjalan dengan baik. solution01 lebih ringkas, sedangkan solution02 sedikit rumit.

solusi01

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

solusi02

#!/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
Terry
sumber
Saya telah mencoba dan membuktikannya berjalan dengan baik. Anda dapat membaca penjelasan saya dalam kode.
Terry
Silakan baca " Bagaimana cara menulis jawaban yang baik? " Di mana Anda akan menemukan info berikut: ... coba sebutkan batasan, asumsi, atau penyederhanaan dalam jawaban Anda. Singkatnya bisa diterima, tetapi penjelasan yang lebih lengkap lebih baik. Oleh karena itu, jawaban Anda dapat diterima tetapi Anda memiliki peluang yang jauh lebih baik untuk mendapatkan suara positif jika Anda dapat menguraikan masalah dan solusi Anda. :-)
Noel Widmer
1
pid=$!; PIDS[$i]=${pid}; ((i+=1))dapat ditulis lebih sederhana karena PIDS+=($!)hanya menambahkan ke array tanpa harus menggunakan variabel terpisah untuk mengindeks atau pid itu sendiri. Hal yang sama berlaku untuk STATUSarray.
codeforester
1
@codeforester, terima kasih atas sarannya, saya telah memodifikasi kode inital saya menjadi solution01, terlihat lebih ringkas.
Terry
Hal yang sama berlaku untuk tempat lain di mana Anda menambahkan sesuatu ke dalam larik.
codeforester
8

Seperti yang saya lihat hampir semua jawaban menggunakan utilitas eksternal (kebanyakan ps) untuk melakukan polling status proses latar belakang. Ada solusi yang lebih unixesh, menangkap sinyal SIGCHLD. Dalam penangan sinyal harus diperiksa proses anak mana yang dihentikan. Ini dapat dilakukan dengan kill -0 <PID>built-in (universal) atau memeriksa keberadaan /proc/<PID>direktori (khusus Linux) atau menggunakan filejobs built-in (spesifik. jobs -ljuga melaporkan pid. Dalam hal ini bidang ke-3 dari keluaran dapat Berhenti | Berjalan | Selesai | Keluar. ).

Inilah contoh saya.

Proses yang diluncurkan disebut loop.sh. Itu menerima -xatau angka sebagai argumen. Untuk -xkeluar dengan kode keluar 1. Untuk nomor itu menunggu num * 5 detik. Dalam setiap 5 detik ia mencetak PID-nya.

Proses peluncur disebut 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

Untuk penjelasan lebih lanjut lihat: Memulai proses dari skrip bash gagal

Benar
sumber
1
Karena kita berbicara tentang Bash, loop for dapat ditulis sebagai: for i in ${!pids[@]};menggunakan ekspansi parameter.
PlasmaBinturong
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 $?
Abu Aqil
sumber
2
Berikut trik untuk menghindari file grep -v. Anda dapat membatasi pencarian di awal baris: grep '^'$pidPlus, Anda tetap bisa melakukannya ps p $pid -o pid=. Juga, tail -ftidak akan berakhir sampai Anda membunuhnya, jadi menurut saya ini bukan cara yang baik untuk mendemonstrasikan ini (setidaknya tanpa menunjukkan itu). Anda mungkin ingin mengarahkan output dari psperintah Anda ke /dev/nullatau itu akan pergi ke layar di setiap iterasi. exitPenyebab Anda waitharus dilewati - mungkin harus a break. Tapi bukankah while/ psdan itu waitberlebihan?
Dijeda sampai pemberitahuan lebih lanjut.
5
Mengapa semua orang lupa kill -0 $pid? Itu tidak benar-benar mengirim sinyal apa pun, hanya memeriksa bahwa prosesnya hidup, menggunakan shell bawaan daripada proses eksternal.
ephemient
3
Karena Anda hanya dapat menghentikan proses yang Anda miliki:bash: kill: (1) - Operation not permitted
errant.info
2
Loop ini berlebihan. Tunggu saja. Lebih sedikit kode => lebih sedikit kasus tepi.
Brais Gabin
@Brais Gabin Loop pemantauan adalah persyaratan # 2 dari pertanyaan
DKroot
5

Saya akan sedikit mengubah pendekatan Anda. Daripada memeriksa setiap beberapa detik jika perintah masih hidup dan melaporkan pesan, lakukan proses lain yang melaporkan setiap beberapa detik bahwa perintah masih berjalan dan kemudian matikan proses itu saat perintah selesai. Sebagai contoh:

#! / bin / sh

cmd () {tidur 5; keluar 24; }

cmd & # Jalankan proses yang berjalan lama
pid = $! # Rekam pid

# Menelurkan proses yang secara terus menerus melaporkan bahwa perintah masih berjalan
sementara echo "$ (tanggal): $ pid masih berjalan"; lakukan tidur 1; selesai &
echoer = $!

# Pasang jebakan untuk membunuh pelapor saat proses selesai
trap 'kill $ echoer' 0

# Tunggu hingga proses selesai
jika menunggu $ pid; kemudian
    echo "cmd berhasil"
lain
    echo "cmd GAGAL !! (mengembalikan $?)"
fi
William Pursell
sumber
template yang bagus, terima kasih telah berbagi! Saya percaya bahwa alih-alih jebakan, kita juga bisa melakukannya while kill -0 $pid 2> /dev/null; do X; done, semoga bermanfaat bagi orang lain di masa depan yang membaca pesan ini;)
punkbit
3

Tim kami memiliki kebutuhan yang sama dengan skrip yang dijalankan SSH jarak jauh yang kehabisan waktu setelah 25 menit tidak aktif. Berikut adalah solusi dengan loop pemantauan yang memeriksa proses latar belakang setiap detik, tetapi mencetak hanya setiap 10 menit untuk menekan batas waktu tidak aktif.

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}
DKroot
sumber
2

Contoh sederhana, mirip dengan solusi di atas. Ini tidak memerlukan pemantauan keluaran proses apa pun. Contoh selanjutnya menggunakan tail untuk mengikuti keluaran.

$ 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

Gunakan tail untuk mengikuti keluaran proses dan berhenti ketika proses selesai.

$ 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
Darren Weber
sumber
1

Solusi lain adalah memantau proses melalui sistem file proc (lebih aman daripada ps / grep combo); ketika Anda memulai proses, ia memiliki folder yang sesuai di / proc / $ pid, jadi solusinya bisa

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

Sekarang Anda dapat menggunakan variabel $ exit_status sesuka Anda.

Искрен Хаджинедев
sumber
Tidak bekerja di bash? Syntax error: "else" unexpected (expecting "done")
benjaoming
1

Dengan metode ini, skrip Anda tidak harus menunggu proses latar belakang, Anda hanya perlu memantau file sementara untuk mengetahui status keluar.

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

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

sekarang, skrip Anda dapat melakukan hal lain sementara Anda hanya perlu terus memantau konten retFile (ini juga dapat berisi informasi lain yang Anda inginkan seperti waktu keluar).

PS: btw, saya mengkodekan berpikir dalam bash

Kekuatan Aquarius
sumber
0

Ini mungkin melampaui pertanyaan Anda, namun jika Anda khawatir tentang lamanya waktu proses berjalan, Anda mungkin tertarik untuk memeriksa status menjalankan proses latar belakang setelah selang waktu tertentu. Cukup mudah untuk memeriksa PID anak mana yang masih berjalan pgrep -P $$, namun saya menemukan solusi berikut untuk memeriksa status keluar dari PID yang sudah kedaluwarsa:

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

keluaran yang mana:

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

Catatan: Anda dapat mengubah $pidsvariabel string daripada array untuk menyederhanakan hal-hal jika Anda mau.

errant.info
sumber
0

Solusi saya adalah menggunakan pipa anonim untuk meneruskan status ke loop pemantauan. Tidak ada file sementara yang digunakan untuk bertukar status jadi tidak ada yang perlu dibersihkan. Jika Anda tidak yakin tentang jumlah pekerjaan latar belakang, kondisi istirahat bisa terjadi [ -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
James Dingwall
sumber