Process

如何殺死程序並確保 PID 未被重用

  • May 13, 2021

例如,假設您有一個類似於以下內容的 shell 腳本:

longrunningthing &
p=$!
echo Killing longrunningthing on PID $p in 24 hours
sleep 86400
echo Time up!
kill $p

應該做的伎倆,不是嗎?除了程序可能已經提前終止並且它的 PID 可能已經被回收,這意味著一些無辜的工作會在它的信號隊列中得到一個炸彈。在實踐中,這可能確實很重要,但它仍然讓我擔心。破解 longrunningthing 自己死掉,或者在 FS 上保留/刪除它的 PID 就可以了,但我在想這裡的一般情況。

最好是使用該timeout命令,如果你有它的意思是:

timeout 86400 cmd

目前的(8.23)GNU 實現至少alarm()在等待子程序時通過使用或等效的方式工作。它似乎並沒有防止在返回和退出SIGALRM之間傳遞(有效地取消了那個警報)。在那個小視窗期間,甚至可能在 stderr 上寫入消息(例如,如果孩子轉儲了一個核心),這將進一步擴大該競爭視窗(例如,如果 stderr 是一個完整的管道,則無限期地擴大)。waitpid()``timeout``timeout

我個人可以忍受這個限制(可能會在未來的版本中修復)。timeout還將特別注意報告正確的退出狀態,處理其他極端情況(如 SIGALRM 在啟動時被阻止/忽略,處理其他信號……)比您可能手動完成的要好。

作為一個近似值,你可以這樣寫perl

perl -MPOSIX -e '
 $p = fork();
 die "fork: $!\n" unless defined($p);
 if ($p) {
   $SIG{ALRM} = sub {
     kill "TERM", $p;
     exit 124;
   };
   alarm(86400);
   wait;
   exit (WIFSIGNALED($?) ? WTERMSIG($?)+128 : WEXITSTATUS($?))
 } else {exec @ARGV}' cmd

http://devel.ringlet.net/sysutils/timelimit/上有一個timelimit命令(比 GNU 早了幾個月)。timeout

timelimit -t 86400 cmd

那個使用alarm()類似 - 的機制,但在SIGCHLD(忽略停止的孩子)上安裝了一個處理程序來檢測孩子的死亡。它還在執行之前取消警報waitpid()(如果它處於未決狀態,則不會取消傳遞SIGALRM,但它的編寫方式,我看不出這是一個問題)並在呼叫之前waitpid()終止(所以不能終止重用的 pid )。

netpipes也有一個timelimit命令。那個比所有其他的早幾十年,採用另一種方​​法,但對於停止的命令不能正常工作,並1在超時時返回退出狀態。

作為對您問題的更直接回答,您可以執行以下操作:

if [ "$(ps -o ppid= -p "$p")" -eq "$$" ]; then
 kill "$p"
fi

也就是說,檢查該程序是否仍然是我們的子程序。同樣,有一個小的競爭視窗(在ps檢索該程序的狀態和kill殺死它之間),在此期間程序可能會死亡並且它的 pid 被另一個程序重用。

使用一些 shell ( zsh, bash, mksh),您可以傳遞作業規範而不是 pid。

cmd &
sleep 86400
kill %
wait "$!" # to retrieve the exit status

這僅在您僅生成一個後台作業時才有效(否則並不總是能夠可靠地獲得正確的作業規範)。

如果這是一個問題,只需啟動一個新的 shell 實例:

bash -c '"$@" & sleep 86400; kill %; wait "$!"' sh cmd

這是有效的,因為外殼會在孩子死亡時從作業表中刪除作業。在這裡,不應該有任何競爭視窗,因為在 shell 呼叫kill()時,SIGCHLD 信號尚未處理並且 pid 無法重用(因為它沒有被等待),或者它已被處理並且作業已從程序表中刪除(並kill會報告錯誤)。bashkill至少在 SIGCHLD 訪問其作業表以擴展 之前阻塞 SIGCHLD%並在 . 之後解除阻塞kill()

sleep即使在cmd死後也可以避免該程序掛起的另一種選擇是使用bashksh93使用管道read -t而不是sleep

{
 {
   cmd 4>&1 >&3 3>&- &
   printf '%d\n.' "$!"
 } | {
   read p
   read -t 86400 || kill "$p"
 }
} 3>&1

那個仍然有競爭條件,你失去了命令的退出狀態。它還假設cmd不關閉其 fd 4。

您可以嘗試實施無種族解決方案,perl例如:

perl -MPOSIX -e '
  $p = fork();
  die "fork: $!\n" unless defined($p);
  if ($p) {
    $SIG{CHLD} = sub {
      $ss = POSIX::SigSet->new(SIGALRM); $oss = POSIX::SigSet->new;
      sigprocmask(SIG_BLOCK, $ss, $oss);
      waitpid($p,WNOHANG);
      exit (WIFSIGNALED($?) ? WTERMSIG($?)+128 : WEXITSTATUS($?))
          unless $? == -1;
      sigprocmask(SIG_UNBLOCK, $oss);
    };
    $SIG{ALRM} = sub {
      kill "TERM", $p;
      exit 124;
    };
    alarm(86400);
    pause while 1;
  } else {exec @ARGV}' cmd args...

(儘管需要改進以處理其他類型的極端情況)。

另一種無競爭的方法可能是使用程序組:

set -m
((sleep 86400; kill 0) & exec cmd)

但是請注意,如果涉及到終端設備的 I/O,則使用程序組可能會產生副作用。它有一個額外的好處,雖然可以殺死所有其他由cmd.

引用自:https://unix.stackexchange.com/questions/181001