如何终止进程并确保 PID 未被重用

FJL*_*FJL 42 process kill

例如,假设您有一个类似于以下内容的 shell 脚本:

longrunningthing &
p=$!
echo Killing longrunningthing on PID $p in 24 hours
sleep 86400
echo Time up!
kill $p
Run Code Online (Sandbox Code Playgroud)

应该做的伎俩,不是吗?除了进程可能提前终止并且它的PID可能已经被回收,这意味着一些无辜的工作在其信号队列中得到了一个炸弹。在实践中这可能确实很重要,但它仍然让我担心。破解 longrunningthing 使其自身死亡,或者在 FS 上保留/删除其 PID 都可以,但我正在考虑这里的一般情况。

Sté*_*las 31

timeout如果您拥有该命令,最好使用该命令:

timeout 86400 cmd
Run Code Online (Sandbox Code Playgroud)

当前 (8.23) GNU 实现至少alarm()在等待子进程时通过使用或等效来工作。它似乎并没有防止SIGALRMwaitpid()返回和timeout退出之间传递(有效地取消该警报)。在那个小窗口期间,timeout甚至可能在 stderr 上写入消息(例如,如果孩子转储了一个核心),这将进一步扩大该竞争窗口(例如,如果 stderr 是一个完整的管道,则无限期地)。

我个人可以忍受这个限制(这可能会在未来的版本中修复)。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
Run Code Online (Sandbox Code Playgroud)

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

 timelimit -t 86400 cmd
Run Code Online (Sandbox Code Playgroud)

那一个使用alarm()类似机制,但安装了一个处理程序SIGCHLD(忽略停止的孩子)来检测孩子的死亡。它还在运行之前取消警报waitpid()SIGALRM如果它处于挂起状态,则不会取消交付,但它的编写方式,我看不出这是一个问题)并调用之前杀死waitpid()(因此不能杀死重用的pid )。

netpipes也有一个timelimit命令。那个比所有其他的早几十年,采用另一种方法,但不能正常用于停止的命令并1在超时时返回退出状态。

作为对您问题的更直接回答,您可以执行以下操作:

if [ "$(ps -o ppid= -p "$p")" -eq "$$" ]; then
  kill "$p"
fi
Run Code Online (Sandbox Code Playgroud)

也就是说,检查进程是否仍然是我们的子进程。同样,有一个小的竞争窗口(在ps检索该进程的状态和kill杀死它之间),在此期间该进程可能会死亡并且其 pid 被另一个进程重用。

使用某些 shell ( zsh, bash, mksh),您可以传递作业规范而不是 pid。

cmd &
sleep 86400
kill %
wait "$!" # to retrieve the exit status
Run Code Online (Sandbox Code Playgroud)

这仅在您仅生成一项后台作业时才有效(否则并不总是能够可靠地获得正确的作业规范)。

如果这是一个问题,只需启动一个新的 shell 实例:

bash -c '"$@" & sleep 86400; kill %; wait "$!"' sh cmd
Run Code Online (Sandbox Code Playgroud)

这是有效的,因为外壳程序会在孩子死亡时从作业表中删除作业。在这里,不应该有任何竞争窗口,因为在 shell 调用时kill(),SIGCHLD 信号没有被处理并且 pid 不能被重用(因为它没有被等待),或者它已经被处理并且作业已从进程表中删除(并kill会报告错误)。bashkill至少在访问其作业表以扩展之前阻止 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
Run Code Online (Sandbox Code Playgroud)

那个仍然有竞争条件,你失去了命令的退出状态。它还假定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...
Run Code Online (Sandbox Code Playgroud)

(尽管需要改进以处理其他类型的极端情况)。

另一种无竞争的方法可能是使用进程组:

set -m
((sleep 86400; kill 0) & exec cmd)
Run Code Online (Sandbox Code Playgroud)

但是请注意,如果涉及到终端设备的 I/O,则使用进程组可能会产生副作用。它具有额外的好处,但可以杀死由cmd.

  • 为什么不先提到最好的方法呢? (4认同)
  • @deltab:`timeout` 不可移植,答案首先提到了一个可移植的解决方案。 (2认同)

R..*_*ICE 31

一般来说,你不能。到目前为止给出的所有答案都是错误的启发式方法。只有一种情况可以安全地使用 pid 发送信号:当目标进程是将发送信号的进程的直接子进程,并且父进程尚未等待它时。在这种情况下,即使它已经退出,pid 也会被保留(这就是“僵尸进程”),直到父进程等待它。我不知道有什么方法可以用 shell 干净地做到这一点。

杀死进程的另一种安全方法是使用设置为您拥有主端的伪终端的控制 tty 来启动它们。然后您可以通过终端发送信号,例如为ptySIGTERMSIGQUIT在 pty 上写入字符。

使用脚本编写更方便的另一种方法是使用命名screen会话并将命令发送到屏幕会话以结束它。此过程通过根据屏幕会话命名的管道或 unix 套接字进行,如果您选择安全的唯一名称,则不会自动重用。

  • @peterph:“比赛窗口很小”不是解决方案。比赛的稀有性取决于顺序 pid 分配。导致每年发生一次非常糟糕的事情的错误比一直发生的错误要严重得多,因为它们几乎不可能诊断和修复。 (7认同)
  • @peterph:一般来说,pid 的任何使用都是 TOCTOU 竞赛——无论您如何检查它是否仍然引用您期望它引用的同一进程,它可能会停止引用该进程并引用一些新的在您使用之前的时间间隔内处理(发送信号)。防止这种情况的唯一方法是能够阻止 pid 的释放/重用,唯一可以做到这一点的进程是直接父进程。 (5认同)
  • 我不明白为什么不能在 shell 中完成。我已经给出了几种解决方案。 (4认同)
  • 您能否对比赛窗口和其他缺点进行解释和某种定量讨论?没有那个,*“到目前为止给出的所有答案都是错误的启发式方法”* 只是不必要的对抗,没有任何好处。 (4认同)
  • @StéphaneChazelas:如何防止 shell 等待已退出的后台进程的 pid?如果你能做到这一点,那么在 OP 需要的情况下,这个问题很容易解决。 (3认同)
  • @kasperd:确实 ptrace 有效,但这不是一个好主意。它不是可移植的,它排除了调试/跟踪(因为通常只能有一个跟踪进程),而且强化系统有时会因为历史漏洞而完全禁用 ptrace 系统调用。 (2认同)
  • @MichaelMartinez:那没有帮助。还有一个TOCTOU竞赛:首先你*检查*`/proc/[pid]/stats`然后你*使用*pid来`kill`。 (2认同)
  • 你认为它不太可能发生的事实并不会让它变得不那么激烈。读取统计数据和击杀之间的潜在时间是无限的;代码可能位于不同的页面中,其中一个页面是常驻页面,另一个页面已被丢弃,需要从处于高 IO 负载下的磁盘重新加载。另外,强化系统应该随机分配 pid,而不是按顺序分配,并且恶意负载源很容易在几秒钟内通过 32k 的默认 pid 空间。 (2认同)
  • @MichaelMartinez:grsec 有一个选项。见 http://grsecurity.net/confighelp.php/ (2认同)

Bak*_*riu 11

  1. 启动进程时保存其启动时间:

    longrunningthing &
    p=$!
    stime=$(TZ=UTC0 ps -p "$p" -o lstart=)
    
    echo "Killing longrunningthing on PID $p in 24 hours"
    sleep 86400
    echo Time up!
    
    Run Code Online (Sandbox Code Playgroud)
  2. 在试图终止进程之前停止它(这不是真正必要的,但它是一种避免竞争条件的方法:如果你停止进程,它的 pid 不能重用)

    kill -s STOP "$p"
    
    Run Code Online (Sandbox Code Playgroud)
  3. 检查具有该 PID 的进程是否具有相同的启动时间,如果是,则终止它,否则让进程继续:

    cur=$(TZ=UTC0 ps -p "$p" -o lstart=)
    
    if [ "$cur" = "$stime" ]
    then
        # Okay, we can kill that process
        kill "$p"
    else
        # PID was reused. Better unblock the process!
        echo "long running task already completed!"
        kill -s CONT "$p"
    fi
    
    Run Code Online (Sandbox Code Playgroud)

这是有效的,因为在给定的操作系统上只能有一个具有相同 PID启动时间的进程。

在检查期间停止进程使得竞争条件不再是问题。显然这有一个问题,一些随机过程可能会停止几毫秒。根据流程的类型,这可能是也可能不是问题。


就我个人而言,我只是使用 python 并psutil自动处理 PID 重用:

import time

import psutil

# note: it would be better if you were able to avoid using
#       shell=True here.
proc = psutil.Process('longrunningtask', shell=True)
time.sleep(86400)

# PID reuse handled by the library, no need to worry.
proc.terminate()   # or: proc.kill()
Run Code Online (Sandbox Code Playgroud)

  • 请注意(除非您的系统限制每个用户的进程数),任何人都很容易用僵尸进程填充进程表。一旦只剩下 3 个可用的 pid,任何人都可以轻松地在一秒钟内启动数百个具有相同 pid 的不同进程。因此,严格来说,您的“在给定操作系统上只能有一个具有相同 PID 和启动时间的进程”并不一定是正确的。 (2认同)

mik*_*erv 9

在 linux 系统上,您可以通过保持其 pid 命名空间处于活动状态来确保不会重用 pid。这可以通过/proc/$pid/ns/pid文件来完成。

  • man namespaces ——

    将此目录中的文件之一绑定安装(请参阅mount(2)到文件系统中的其他位置,即使当前名称空间中的所有进程都终止,pid 指定的进程的相应名称空间仍处于活动状态。

    打开此目录中的文件之一(或绑定安装到这些文件之一的文件)将返回由 pid 指定的进程的相应命名空间的文件句柄。只要这个文件描述符保持打开状态,命名空间就会保持活动状态,即使命名空间中的所有进程都终止了。文件描述符可以传递给setns(2).

您可以隔离一组进程 - 基本上是任意数量的进程 - 通过将它们的init.

  • man pid_namespaces ——

    在一个新的命名空间中创建的第一过程(即,过程中使用创建clone(2)CLONE_NEWPID标志,或在通话结束后由一个过程中创建的第一个孩子unshare(2)使用 CLONE_NEWPID标志)具有PID 1,并且是init用于命名空间处理(见init(1)。在命名空间内孤立的子进程将被重新分配给该进程而不是init(1) (除非同一PID命名空间prctl(2) 中子进程的一个祖先使用PR_SET_CHILD_SUBREAPER命令将自己标记为孤立后代进程的收割者)

    如果PID命名空间的init进程终止,内核将通过SIGKILL 信号终止命名空间中的所有进程。这种行为反映了这样一个事实,即进程对于PID命名空间的正确操作至关重要。init

util-linux包提供了许多用于操作命名空间的有用工具。例如,unshare如果您尚未在用户命名空间中安排其权限,则需要超级用户权限:

unshare -fp sh -c 'n=
    echo "PID = $$"
    until   [ "$((n+=1))" -gt 5 ]
    do      while   sleep 1
            do      date
            done    >>log 2>/dev/null   &
    done;   sleep 5' >log
cat log; sleep 2
echo 2 secs later...
tail -n1 log
Run Code Online (Sandbox Code Playgroud)

如果您没有安排用户命名空间,那么您仍然可以通过立即删除权限来安全地执行任意命令。该runuser命令是包提供的另一个(非 setuid)二进制文件,util-linux并且合并它可能如下所示:

sudo unshare -fp runuser -u "$USER" -- sh -c '...'
Run Code Online (Sandbox Code Playgroud)

...等等。

在上述例子中的两个开关被传递给unshare(1)所述--fork标志这使得调用的sh -c过程中的第一个孩子创建并确保其init状态,并且--pid其指示标志unshare(1)以创建一个pid命名空间。

sh -c过程产生五个后台子shell - 每个都是一个无限while循环,只要返回true ,它将继续将输出附加date到末尾。生成这些进程后,再调用5 秒钟,然后终止。logsleep 1shsleep

可能值得注意的是,如果-f不使用该标志,则背景while循环都不会终止,但是有了它......

输出:

PID = 1
Mon Jan 26 19:17:45 PST 2015
Mon Jan 26 19:17:45 PST 2015
Mon Jan 26 19:17:45 PST 2015
Mon Jan 26 19:17:45 PST 2015
Mon Jan 26 19:17:45 PST 2015
Mon Jan 26 19:17:46 PST 2015
Mon Jan 26 19:17:46 PST 2015
Mon Jan 26 19:17:46 PST 2015
Mon Jan 26 19:17:46 PST 2015
Mon Jan 26 19:17:46 PST 2015
Mon Jan 26 19:17:47 PST 2015
Mon Jan 26 19:17:47 PST 2015
Mon Jan 26 19:17:47 PST 2015
Mon Jan 26 19:17:47 PST 2015
Mon Jan 26 19:17:47 PST 2015
Mon Jan 26 19:17:48 PST 2015
Mon Jan 26 19:17:48 PST 2015
Mon Jan 26 19:17:48 PST 2015
Mon Jan 26 19:17:48 PST 2015
Mon Jan 26 19:17:48 PST 2015
2 secs later...
Mon Jan 26 19:17:48 PST 2015
Run Code Online (Sandbox Code Playgroud)


pet*_*rph 5

考虑让你的longrunningthing行为更好一点,更像守护进程。例如,您可以让它创建一个pidfile,该文件将允许至少对进程进行一些有限的控制。有几种方法可以在不修改原始二进制文件的情况下执行此操作,所有方法都涉及包装器。例如:

  1. 一个简单的包装脚本,它将在后台启动所需的作业(带有可选的输出重定向),将此进程的 PID 写入文件,然后等待进程完成(使用wait)并删除文件。如果在等待过程中被杀死,例如

    kill $(cat pidfile)
    
    Run Code Online (Sandbox Code Playgroud)

    包装器只会确保删除 pidfile。

  2. 一个监视器包装器,它将自己的PID 放在某处并捕获(并响应)发送给它的信号。简单的例子:

kill $(cat pidfile)
Run Code Online (Sandbox Code Playgroud)

现在,正如@R.. 和@StéphaneChazelas 指出的那样,这些方法通常在某处存在竞争条件,或者对可以生成的进程数量施加限制。此外,它不处理longrunningthing可能分叉和孩子分离的情况(这可能不是原始问题中的问题)。

对于最近(读了几年)的 Linux 内核,这可以通过使用cgroups很好地处理,即freezer - 我想,这是一些现代 Linux init 系统使用的。