Bagaimana cara membunuh proses anak setelah batas waktu yang diberikan di Bash?

178

Saya memiliki skrip bash yang meluncurkan proses anak yang macet (sebenarnya, hang) dari waktu ke waktu dan tanpa alasan yang jelas (sumber tertutup, jadi tidak banyak yang bisa saya lakukan mengenai hal itu). Sebagai hasilnya, saya ingin dapat meluncurkan proses ini untuk jumlah waktu tertentu, dan membunuhnya jika tidak berhasil kembali setelah jumlah waktu tertentu.

Apakah ada cara sederhana dan kuat untuk mencapainya menggunakan bash?

PS: beri tahu saya apakah pertanyaan ini lebih cocok untuk serverfault atau superuser.

Greg
sumber
Respons yang sangat lengkap di sini: stackoverflow.com/a/58873049/2635443
Orsiris de Jong

Jawaban:

260

(Seperti yang terlihat di: BASH FAQ entri # 68: "Bagaimana saya menjalankan perintah, dan membatalkannya (batas waktu) setelah N detik?" )

Jika Anda tidak keberatan mengunduh sesuatu, gunakan timeout( sudo apt-get install timeout) dan gunakan seperti: (sebagian besar Sistem sudah menginstalnya jika tidak gunakan sudo apt-get install coreutils)

timeout 10 ping www.goooooogle.com

Jika Anda tidak ingin mengunduh sesuatu, lakukan apa yang dilakukan timeout secara internal:

( cmdpid=$BASHPID; (sleep 10; kill $cmdpid) & exec ping www.goooooogle.com )

Jika Anda ingin melakukan timeout untuk kode bash yang lebih lama, gunakan opsi kedua seperti itu:

( cmdpid=$BASHPID; 
    (sleep 10; kill $cmdpid) \
   & while ! ping -w 1 www.goooooogle.com 
     do 
         echo crap; 
     done )
Ignacio Vazquez-Abrams
sumber
8
Jawaban Re Ignacio kalau-kalau ada orang bertanya-tanya apa yang saya lakukan: cmdpid=$BASHPIDtidak akan mengambil pid dari shell panggilan tetapi subshell (pertama) yang dimulai oleh (). The (sleep... Hal panggilan subkulit kedua dalam subkulit pertama yang menunggu 10 detik di latar belakang dan membunuh subkulit pertama yang, setelah meluncurkan proses subkulit pembunuh, hasil untuk melaksanakan beban kerja ...
Jamadagni
17
timeoutadalah bagian dari GNU coreutils, jadi harus sudah diinstal di semua sistem GNU.
Sameer
1
@Sameer: ​​Hanya pada versi 8.
Ignacio Vazquez-Abrams
3
Saya tidak 100% yakin akan hal itu, tetapi sejauh yang saya tahu (dan saya tahu apa yang dikatakan halaman manual saya) timeoutsekarang menjadi bagian dari coreutils.
benaryorg
5
Perintah ini tidak 'selesai lebih awal'. Itu akan selalu mematikan proses pada saat timeout - tetapi tidak akan menangani situasi di mana ia tidak timeout.
hawkeye
28
# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) &

atau untuk mendapatkan kode keluar juga:

# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) & waiter=$!
# wait on our worker process and return the exitcode
exitcode=$(wait $pid && echo $?)
# kill the waiter subshell, if it still runs
kill -9 $waiter 2>/dev/null
# 0 if we killed the waiter, cause that means the process finished before the waiter
finished_gracefully=$?
Dan
sumber
8
Anda sebaiknya tidak menggunakan kill -9sebelum Anda mencoba sinyal bahwa suatu proses dapat diproses terlebih dahulu.
Dijeda sampai pemberitahuan lebih lanjut.
Benar, saya akan melakukan perbaikan cepat dan hanya berasumsi bahwa dia ingin proses mati seketika karena dia mengatakan itu crash
Dan
8
Itu sebenarnya solusi yang sangat buruk. Bagaimana jika dosmthberakhir dalam 2 detik, proses lain mengambil pid lama, dan Anda membunuh yang baru?
Teleporting Goat
Daur ulang PID bekerja dengan mencapai batas dan melilit. Sangat tidak mungkin bagi proses lain untuk menggunakan kembali PID dalam 8 detik tersisa, kecuali jika sistem benar-benar rusak.
kittydoor
13
sleep 999&
t=$!
sleep 10
kill $t
DigitalRoss
sumber
Itu membuat menunggu berlebihan. Bagaimana jika perintah nyata (di sleep 999sini) sering selesai lebih cepat dari pada tidur yang dikenakan ( sleep 10)? Bagaimana jika saya ingin memberikannya kesempatan hingga 1 menit, 5 menit? Bagaimana jika saya memiliki banyak kasus seperti itu dalam skrip saya :)
it3xl
3

Saya juga punya pertanyaan ini dan menemukan dua hal lagi yang sangat berguna:

  1. Variabel DETIK dalam bash.
  2. Perintah "pgrep".

Jadi saya menggunakan sesuatu seperti ini di baris perintah (OSX 10.9):

ping www.goooooogle.com & PING_PID=$(pgrep 'ping'); SECONDS=0; while pgrep -q 'ping'; do sleep 0.2; if [ $SECONDS = 10 ]; then kill $PING_PID; fi; done

Karena ini adalah loop, saya menyertakan "sleep 0.2" untuk menjaga CPU tetap dingin. ;-)

(BTW: ping adalah contoh yang buruk, Anda hanya akan menggunakan opsi "-t" (batas waktu) bawaan.)

Ulrich
sumber
1

Dengan asumsi Anda memiliki (atau dapat dengan mudah membuat) file pid untuk melacak pid anak, Anda kemudian dapat membuat skrip yang memeriksa modtime dari file pid dan membunuh / merespons proses yang diperlukan. Kemudian cukup letakkan skrip di crontab untuk dijalankan pada sekitar periode yang Anda butuhkan.

Beri tahu saya jika Anda membutuhkan detail lebih lanjut. Jika itu kedengarannya tidak sesuai dengan kebutuhan Anda, bagaimana dengan pemula?

kojiro
sumber
1

Salah satu caranya adalah dengan menjalankan program dalam subkulit, dan berkomunikasi dengan subkulit melalui pipa bernama dengan readperintah. Dengan cara ini Anda dapat memeriksa status keluar dari proses yang sedang berjalan dan mengkomunikasikannya kembali melalui pipa.

Berikut adalah contoh penghentian yesperintah setelah 3 detik. Ia mendapat PID dari proses menggunakan pgrep(mungkin hanya bekerja di Linux). Ada juga beberapa masalah dengan menggunakan pipa dalam proses membuka pipa untuk membaca akan menggantung sampai juga dibuka untuk menulis, dan sebaliknya. Jadi untuk mencegah readperintah menggantung, saya sudah "terjepit" membuka pipa untuk dibaca dengan subkulit latar belakang. (Cara lain untuk mencegah pembekuan untuk membuka pipa baca-tulis, yaitu read -t 5 <>finished.pipe- namun, itu juga mungkin tidak berfungsi kecuali dengan Linux.)

rm -f finished.pipe
mkfifo finished.pipe

{ yes >/dev/null; echo finished >finished.pipe ; } &
SUBSHELL=$!

# Get command PID
while : ; do
    PID=$( pgrep -P $SUBSHELL yes )
    test "$PID" = "" || break
    sleep 1
done

# Open pipe for writing
{ exec 4>finished.pipe ; while : ; do sleep 1000; done } &  

read -t 3 FINISHED <finished.pipe

if [ "$FINISHED" = finished ] ; then
  echo 'Subprocess finished'
else
  echo 'Subprocess timed out'
  kill $PID
fi

rm finished.pipe
Gavin Smith
sumber
0

Berikut adalah upaya yang mencoba untuk menghindari membunuh proses setelah itu sudah keluar, yang mengurangi kemungkinan membunuh proses lain dengan ID proses yang sama (walaupun mungkin mustahil untuk menghindari kesalahan semacam ini sepenuhnya).

run_with_timeout ()
{
  t=$1
  shift

  echo "running \"$*\" with timeout $t"

  (
  # first, run process in background
  (exec sh -c "$*") &
  pid=$!
  echo $pid

  # the timeout shell
  (sleep $t ; echo timeout) &
  waiter=$!
  echo $waiter

  # finally, allow process to end naturally
  wait $pid
  echo $?
  ) \
  | (read pid
     read waiter

     if test $waiter != timeout ; then
       read status
     else
       status=timeout
     fi

     # if we timed out, kill the process
     if test $status = timeout ; then
       kill $pid
       exit 99
     else
       # if the program exited normally, kill the waiting shell
       kill $waiter
       exit $status
     fi
  )
}

Gunakan like run_with_timeout 3 sleep 10000, yang berjalan sleep 10000tetapi mengakhirinya setelah 3 detik.

Ini seperti jawaban lain yang menggunakan proses timeout latar belakang untuk membunuh proses anak setelah penundaan. Saya pikir ini hampir sama dengan jawaban diperpanjang Dan ( https://stackoverflow.com/a/5161274/1351983 ), kecuali shell timeout tidak akan dibunuh jika sudah berakhir.

Setelah program ini berakhir, masih akan ada beberapa proses "sleep" yang berjalan, tetapi tidak berbahaya.

Ini mungkin solusi yang lebih baik daripada jawaban saya yang lain karena tidak menggunakan fitur shell non-portabel read -tdan tidak menggunakan pgrep.

Gavin Smith
sumber
Apa perbedaan antara (exec sh -c "$*") &dan sh -c "$*" &? Secara khusus, mengapa menggunakan yang pertama dan bukan yang terakhir?
Justin C
0

Inilah jawaban ketiga yang saya kirimkan di sini. Yang ini menangani interupsi sinyal dan membersihkan proses latar belakang saat SIGINTditerima. Ia menggunakan $BASHPIDdan exectrik yang digunakan dalam jawaban atas untuk mendapatkan PID dari suatu proses (dalam hal ini $$dalam shdoa). Ia menggunakan FIFO untuk berkomunikasi dengan subkulit yang bertanggung jawab atas pembunuhan dan pembersihan. (Ini seperti pipa di jawaban kedua saya , tetapi memiliki pipa bernama berarti penangan sinyal juga bisa menulis ke dalamnya.)

run_with_timeout ()
{
  t=$1 ; shift

  trap cleanup 2

  F=$$.fifo ; rm -f $F ; mkfifo $F

  # first, run main process in background
  "$@" & pid=$!

  # sleeper process to time out
  ( sh -c "echo \$\$ >$F ; exec sleep $t" ; echo timeout >$F ) &
  read sleeper <$F

  # control shell. read from fifo.
  # final input is "finished".  after that
  # we clean up.  we can get a timeout or a
  # signal first.
  ( exec 0<$F
    while : ; do
      read input
      case $input in
        finished)
          test $sleeper != 0 && kill $sleeper
          rm -f $F
          exit 0
          ;;
        timeout)
          test $pid != 0 && kill $pid
          sleeper=0
          ;;
        signal)
          test $pid != 0 && kill $pid
          ;;
      esac
    done
  ) &

  # wait for process to end
  wait $pid
  status=$?
  echo finished >$F
  return $status
}

cleanup ()
{
  echo signal >$$.fifo
}

Saya sudah mencoba menghindari kondisi balapan sejauh yang saya bisa. Namun, satu sumber kesalahan yang tidak bisa saya hapus adalah ketika proses berakhir dekat waktu yang sama dengan batas waktu. Misalnya, run_with_timeout 2 sleep 2atau run_with_timeout 0 sleep 0. Bagi saya, yang terakhir memberikan kesalahan:

timeout.sh: line 250: kill: (23248) - No such process

karena sedang mencoba untuk membunuh proses yang sudah keluar dengan sendirinya.

Gavin Smith
sumber