Làm thế nào để giết một quá trình con sau một thời gian chờ ở Bash?


178

Tôi có một tập lệnh bash khởi chạy một tiến trình con bị treo (thực tế là bị treo) theo thời gian và không có lý do rõ ràng (nguồn đóng, vì vậy tôi không thể làm gì nhiều về nó). Do đó, tôi muốn có thể khởi chạy quy trình này trong một khoảng thời gian nhất định và tiêu diệt nó nếu nó không trở lại thành công sau một khoảng thời gian nhất định.

Có cách nào đơn giảnmạnh mẽ để đạt được điều đó bằng cách sử dụng bash không?

PS: cho tôi biết nếu câu hỏi này phù hợp hơn với serverfault hoặc superuser.



Phản hồi rất đầy đủ ở đây: stackoverflow.com/a/58873049/2635443
Orsiris de Jong

Câu trả lời:


260

(Như đã thấy trong: BASH Câu hỏi thường gặp # 68: "Làm cách nào để tôi chạy lệnh và hủy bỏ lệnh (hết thời gian chờ) sau N giây?" )

Nếu bạn không phiền khi tải xuống một cái gì đó, hãy sử dụng timeout( sudo apt-get install timeout) và sử dụng nó như sau: (hầu hết các Hệ thống đã cài đặt nó nếu không sử dụng sudo apt-get install coreutils)

timeout 10 ping www.goooooogle.com

Nếu bạn không muốn tải xuống một cái gì đó, hãy làm những gì hết thời gian trong nội bộ:

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

Trong trường hợp bạn muốn thực hiện thời gian chờ cho mã bash dài hơn, hãy sử dụng tùy chọn thứ hai như sau:

( cmdpid=$BASHPID; 
    (sleep 10; kill $cmdpid) \
   & while ! ping -w 1 www.goooooogle.com 
     do 
         echo crap; 
     done )

8
Trả lời của Re Ignacio trong trường hợp bất kỳ ai khác tự hỏi tôi đã làm gì: cmdpid=$BASHPIDsẽ không lấy phần vỏ của cuộc gọi mà là phần con (đầu tiên) được bắt đầu bằng (). Các (sleep... điều gọi là subshell thứ hai trong subshell đầu tiên phải chờ 10 giây ở chế độ nền và giết subshell đầu tiên đó, sau khi đã đưa ra quá trình subshell kẻ giết người, tiến hành để thực hiện khối lượng công việc của mình ...
jamadagni

17
timeoutlà một phần của lõi GNU, do đó, nên đã được cài đặt trong tất cả các hệ thống GNU.
Sameer

1
@Sameer: ​​Chỉ tính đến phiên bản 8.
Ignacio Vazquez-Abrams

3
Tôi không chắc chắn 100% về điều đó, nhưng theo như tôi biết (và tôi biết những gì trang của tôi đã nói với tôi) timeoutbây giờ là một phần của coreutils.
benaryorg

5
Lệnh này không "kết thúc sớm". Nó sẽ luôn giết quá trình khi hết thời gian - nhưng sẽ không xử lý tình huống mà nó không hết thời gian.
hawkeye

28
# Spawn a child process:
(dosmth) & pid=$!
# in the background, sleep for 10 secs then kill that process
(sleep 10 && kill -9 $pid) &

hoặc để lấy mã thoát là tốt:

# 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=$?

8
Bạn không nên sử dụng kill -9trước khi thử các tín hiệu rằng quy trình có thể xử lý trước.
Tạm dừng cho đến khi có thông báo mới.

Đúng vậy, tuy nhiên tôi đã tìm cách khắc phục nhanh và chỉ cho rằng anh ta muốn quá trình chết ngay lập tức vì anh ta nói rằng nó bị sập
Dan

8
Đó thực sự là một giải pháp rất tệ. Điều gì sẽ xảy ra nếu dosmthkết thúc sau 2 giây, một quá trình khác sẽ lấy cái cũ và bạn giết cái mới?
Dịch chuyển con dê

Tái chế PID hoạt động bằng cách đạt đến giới hạn và bao quanh. Rất khó có khả năng cho một quá trình khác sử dụng lại PID trong vòng 8 giây còn lại, trừ khi hệ thống đang hoạt động hoàn toàn.
kittydoor

13
sleep 999&
t=$!
sleep 10
kill $t

Nó phải chịu sự chờ đợi quá mức. Điều gì xảy ra nếu một lệnh thực sự ( sleep 999ở đây) thường kết thúc nhanh hơn so với giấc ngủ được áp đặt ( sleep 10)? Nếu tôi muốn cho nó cơ hội tối đa 1 phút, 5 phút thì sao? Điều gì sẽ xảy ra nếu tôi có một loạt các trường hợp như vậy trong kịch bản của mình :)
it3xl

3

Tôi cũng đã có câu hỏi này và tìm thấy hai điều nữa rất hữu ích:

  1. Biến SECONDS trong bash.
  2. Lệnh "pgrep".

Vì vậy, tôi sử dụng một cái gì đó như thế này trên dòng lệnh (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

Vì đây là một vòng lặp nên tôi đã bao gồm "ngủ 0,2" để giữ cho CPU luôn mát mẻ. ;-)

(BTW: ping dù sao cũng là một ví dụ tồi, bạn chỉ cần sử dụng tùy chọn "-t" (thời gian chờ) tích hợp.)


1

Giả sử bạn có (hoặc có thể dễ dàng tạo) tệp pid để theo dõi pid của trẻ, sau đó bạn có thể tạo một tập lệnh kiểm tra thời gian sửa đổi của tệp pid và giết / phản hồi quy trình khi cần. Sau đó, chỉ cần đặt tập lệnh trong crontab để chạy ở khoảng thời gian bạn cần.

Hãy cho tôi biết nếu như bạn cần thêm chị tiết. Nếu điều đó không có vẻ phù hợp với nhu cầu của bạn, vậy còn những người mới bắt đầu thì sao?


1

Một cách là chạy chương trình trong một lớp con và giao tiếp với lớp con thông qua một đường ống có tên bằng readlệnh. Bằng cách này, bạn có thể kiểm tra trạng thái thoát của quá trình đang chạy và liên lạc lại thông qua đường ống.

Đây là một ví dụ về thời gian ra yeslệnh sau 3 giây. Nó nhận được PID của quá trình sử dụng pgrep(có thể chỉ hoạt động trên Linux). Cũng có một số vấn đề với việc sử dụng một đường ống trong đó một quá trình mở một đường ống để đọc sẽ bị treo cho đến khi nó cũng được mở để ghi và ngược lại. Vì vậy, để ngăn chặn readlệnh treo, tôi đã "nêm" mở đường ống để đọc với một khung con nền. (Một cách khác để ngăn chặn việc đóng băng để mở đường ống đọc-ghi, nghĩa là read -t 5 <>finished.pipe- tuy nhiên, điều đó cũng có thể không hoạt động trừ 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

0

Đây là một nỗ lực nhằm tránh làm chết một tiến trình sau khi nó đã thoát, điều này làm giảm cơ hội giết một tiến trình khác có cùng ID tiến trình (mặc dù có lẽ không thể tránh được loại lỗi này hoàn toàn).

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
  )
}

Sử dụng like run_with_timeout 3 sleep 10000, chạy sleep 10000nhưng kết thúc sau 3 giây.

Điều này giống như các câu trả lời khác sử dụng quy trình hết thời gian nền để giết tiến trình con sau khi trì hoãn. Tôi nghĩ rằng điều này gần giống với câu trả lời mở rộng của Dan ( https://stackoverflow.com/a/5161274/1351983 ), ngoại trừ lớp vỏ thời gian chờ sẽ không bị giết nếu nó đã kết thúc.

Sau khi chương trình này kết thúc, vẫn còn một vài quá trình "ngủ" kéo dài, nhưng chúng sẽ vô hại.

Đây có thể là một giải pháp tốt hơn câu trả lời khác của tôi vì nó không sử dụng tính năng vỏ không di động read -tvà không sử dụng pgrep.


Sự khác biệt giữa (exec sh -c "$*") &và là sh -c "$*" &gì? Cụ thể, tại sao sử dụng cái trước thay vì cái sau?
Justin C

0

Đây là câu trả lời thứ ba tôi đã gửi ở đây. Điều này xử lý các tín hiệu ngắt và làm sạch các quá trình nền khi SIGINTnhận được. Nó sử dụng $BASHPIDexecmẹo được sử dụng trong câu trả lời hàng đầu để có được PID của một quá trình (trong trường hợp này là $$trong một shlời gọi). Nó sử dụng một FIFO để liên lạc với một subshell chịu trách nhiệm giết chóc và dọn dẹp. (Điều này giống như đường ống trong câu trả lời thứ hai của tôi , nhưng có một đường ống được đặt tên có nghĩa là trình xử lý tín hiệu cũng có thể ghi vào nó.)

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
}

Tôi đã cố gắng tránh các điều kiện cuộc đua càng xa càng tốt. Tuy nhiên, một nguồn lỗi tôi không thể xóa là khi quá trình kết thúc gần cùng thời gian với thời gian chờ. Ví dụ, run_with_timeout 2 sleep 2hoặc run_with_timeout 0 sleep 0. Đối với tôi, cái sau đưa ra một lỗi:

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

vì nó đang cố giết một quá trình đã tự thoát ra.

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.