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


40

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

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

应该做到这一点,不是吗?除了该进程可能提前终止并且其PID可能已被回收,这意味着某些无辜的工作会在其信号队列中放入炸弹。在实践中,这可能确实很重要,但仍然令我担忧。窃取长途运行的东西以自行消失或在FS上保留/删除其PID都可以,但是我在这里想到了一般情况。


3
您确保如果目标进程死亡,它将杀死该进程中的杀手er。
mikeserv

2
使用killall名称匹配的,因此至少您只杀死与相同名称的进程longrunningthing。假设您一次只能运行其中之一。
LawrenceC

8
您可以保存原始进程的开始时间,并在终止之前检查具有该pid的进程的开始时间是否与您保存的时间相匹配。pid,start-time对 Linux中进程的唯一标识符。
2015年

1
请问您为什么需要这个?您要实现的基本目标是什么?(某些内容会连续运行,但每24小时会重置一次?)
Olivier Dulac

2
@mikeserv进程无法保证某些事件会在其自身死亡的情况下发生。
卡巴斯德(Kasperd),2015年

Answers:


29

最好是使用timeout具有以下用途的命令:

timeout 86400 cmd

当前的(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

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被其他进程重用。

对于某些贝壳(zshbashmksh),你可以通过工作规范,而不是的PID。

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

仅当您仅生成一个后台作业时,这种方法才有效(否则始终无法可靠地获得正确的jobspec)。

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

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

之所以行得通,是因为外壳在孩子死亡时从作业表中删除了作业。在这里,不应有任何竞争窗口,因为到shell调用时kill(),要么尚未处理SIGCHLD信号并且pid无法重用(因为尚未等待pid),要么已经被处理并且作业已从过程表中删除(并且kill将报告错误)。bashkill至少街区SIGCHLD它访问它的工作表扩大之前%后和疏导它kill()

为了避免具有另一种选择sleep过程即使挂在cmd已经死亡,bash或者ksh93是使用管道用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


4
为什么不先提到最好的方法呢?
2015年

2
@deltab:timeout不是可移植的,答案首先提到了可移植的解决方案。
cuonglm

1
@deltab:它提供了有关事物运作方式的见解,尤其是“常识”方法可能失败的原因(Stephane更喜欢教一个先钓鱼的人,这是我喜欢的)。希望人们能读完整的答案
Olivier Dulac

@Stephane:对于“始终无法可靠地获得正确的作业规范”:您不能先计算其输出jobs然后知道(因为它是您自己的shell,可以控制接下来发生的事情)的下一个背景工作会是N + 1吗?[然后您可以保存N,然后杀死%N + 1])
Olivier Dulac

1
@OlivierDulac,这将假定在您开始一个新作业时,过去的作业没有终止(shell重用作业号)。
斯特凡Chazelas

28

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

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

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


4
我不明白为什么不能在shell中做到这一点。我已经给出了几种解决方案。
斯特凡Chazelas

3
您能解释一下竞赛窗口和其他缺点吗?否则,“到目前为止给出的所有答案都是错误的启发式”,这是毫无必要的对抗,没有任何好处。
彼得,2015年

3
@peterph:通常,对pid的任何使用都是TOCTOU竞赛-不管您如何检查它是否仍然指向您期望它引用的相同过程,它都可以停止引用该过程并引用一些新的在使用间隔之前进行处理(发送信号)。防止这种情况的唯一方法是能够阻止pid的释放/重用,并且唯一可以实现此目的的过程是直接父级。
R.,

2
@StéphaneChazelas:如何防止Shell等待退出的后台进程的pid?如果可以这样做,则可以在OP需要的情况下轻松解决此问题。
R.,

5
@peterph:“竞赛窗口很小”不是解决方案。种族的稀有性取决于顺序的pid分配。导致每年一次非常不好的错误发生的错误要比一直发生的错误严重得多,因为它们几乎无法诊断和修复。
R.,

10
  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!
    
  2. 在尝试终止该进程之前,先将其停止(这不是真正必要的方法,但这是避免争用情况的一种方法:如果停止该进程,则其pid无法重用)

    kill -s STOP "$p"
    
  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
    

之所以可行,是因为在给定的OS上,只有一个进程具有相同的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()

UNIX中的Python规则...我不确定为什么大多数答案都不会从那里开始,因为我确定大多数系统都不会禁止使用它。
Mascaro先生

以前我使用过类似的方案(使用开始时间),但是您的sh脚本编写技能比我的整洁!谢谢。
FJL 2015年

这意味着您可能会停止错误的过程。请注意,ps -o start=一段时间后格式从18:12更改为Jan26。还要注意DST的更改。如果在Linux上,您可能会更喜欢TZ=UTC0 ps -o lstart=
斯特凡Chazelas

@StéphaneChazelas是的,但是您让它随后继续。我明确地说过:根据该进程正在执行的任务类型,您可能很难在几毫秒内停止它。感谢您的提示lstart,我将其编辑
。– Bakuriu 2015年

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

7

在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

如果尚未安排用户名称空间,那么仍然可以通过立即放弃特权来安全地执行任意命令。该runuser命令是程序包提供的另一个(非setuid)二进制文件util-linux,合并后的命令可能类似于:

sudo unshare -fp runuser -u "$USER" -- sh -c '...'

...等等。

在上面的示例中,两个开关传递到标志,unshare(1)--fork标志使被调用的sh -c进程成为第一个创建的子进程并确保其init状态,而--pid标志则指示unshare(1)创建pid名称空间。

sh -c过程产生五个后台子外壳程序-每个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

有趣的答案似乎很可靠。基本用法可能有点过大,但值得思考。
Uriel 2015年

我看不到如何或为什么保持PID名称空间有效会阻止PID的重用。您引用的联机帮助页- 只要此文件描述符保持打开状态,即使该名称空间中的所有进程都终止,该名称空间也将保持活动状态 -表示该进程可能仍会终止(因此可能回收了其进程ID)。保持PID名称空间有效与防止PID自身被另一个进程重复使用有什么关系?
davmac

5

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

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

    kill $(cat pidfile)
    

    包装程序将仅确保删除了pidfile。

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

    #!/bin/bash
    p=0
    trap killit USR1

    killit () {
        printf "USR1 caught, killing %s\n" "$p"
        kill -9 $p
    }

    printf "monitor $$ is waiting\n"
    therealstuff &
    p=%1
    wait $p
    printf "monitor exiting\n"

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

对于最近的Linux内核(已经使用了两年),可以通过使用cgroups(即冷冻机)很好地解决这一问题,我想这是某些现代Linux init系统使用的冷冻机


谢谢大家。我现在正在阅读everinging。关键longrunningthing是您无法控制它。我还给出了一个Shell脚本示例,因为它可以解释问题。我喜欢您的以及这里的所有其他创造性解决方案,但是如果您使用的是Linux / bash,则有一个内置的“超时”。我想我应该得到它的源代码,看看它是如何做到的!
FJL 2015年

@FJL,timeout不是一个shell内建命令。timeoutLinux命令有多种实现方式,最近(2008年)将一种命令添加到了GNU coreutils中(因此不是Linux专用的),而这正是当今大多数Linux发行版所使用的。
斯特凡Chazelas

@Stéphane-谢谢-随后我找到了对GNU coreutils的引用。它们可能是可移植的,但是除非它在基本系统中,否则不能依靠它。我对了解它的工作方式更感兴趣,尽管我在其他地方注意到您的评论表明它不是100%可靠的。考虑到该线程已消失的方式,我并不感到惊讶!
FJL 2015年

1

如果你在Linux(和其他几个* nixes)上运行,你可以检查,如果你打算杀死进程仍在使用,并在命令行你漫长的过程相匹配。就像是 :

echo Time up!
grep -q longrunningthing /proc/$p/cmdline 2>/dev/null
if [ $? -eq 0 ]
then
  kill $p
fi

一种替代方法是使用来检查您要终止的进程运行了多长时间ps -p $p -o etime=。您可以通过从中提取此信息来自己做到这一点/proc/$p/stat,但这很棘手(时间以动静为单位,并且您也必须使用系统正常运行时间/proc/stat)。

无论如何,您通常不能确保检查之后和取消之前不会替换该过程。


这仍然是不正确的,因为它没有摆脱比赛条件。
strcat

@strcat的确,没有成功的保证,但是大多数脚本甚至不费心去做这种检查,只是直截了当地杀死cat pidfile结果。我不记得只有在shell中才能做到的干净方法。提议的名称空间答案似乎是一个
有趣的

-1

这实际上是一个很好的问题。

确定过程唯一性的方法是查看(a)它在内存中的位置;(b)该记忆所包含的内容。具体来说,我们想知道用于初始调用的程序文本在内存中的哪个位置,因为我们知道每个线程的文本区域将在内存中占据不同的位置。如果进程死亡,而另一个进程使用相同的pid启动,则新进程的程序文本将不在内存中占据相同的位置,并且将不包含相同的信息。

因此,在启动过程之后,立即执行md5sum /proc/[pid]/maps并保存结果。以后,当您想终止该进程时,请执行另一个md5sum并进行比较。如果匹配,则杀死该pid。如果没有,那就不要。

若要亲自查看,请启动两个相同的bash shell。检查/proc/[pid]/maps它们,您会发现它们不同。为什么?因为即使是同一程序,它们在内存中的位置也不同,并且堆栈的地址也不同。因此,如果您的进程死了并且其PID被重用,即使通过使用相同参数重新启动同一命令,“ maps”文件也会有所不同,并且您将知道您没有在处理原始进程。

有关详细信息,请参见:proc手册页

请注意,该文件/proc/[pid]/stat已经包含其他发布者在其答案中提到的所有信息:进程的年龄,父pid等。此文件同时包含静态信息和动态信息,因此,如果您希望将此文件用作基础比较,然后在启动时longrunningthing,您需要从stat文件中提取以下静态字段,并将其保存以供以后比较:

pid,文件名,父进程的pid,进程组ID,控制终端,系统启动后启动进程的时间,驻留集大小,堆栈起始地址,

综上所述,以上内容唯一地标识了过程,因此这代表了另一条路。实际上,您可以高度自信地摆脱“ pid”和“系统启动后启动时间过程”的束缚。只需从stat文件中提取这些字段并将其保存在启动过程中。稍后在杀死它之前,再次将其提取并进行比较。如果它们匹配,则可以确保您正在寻找原始过程。


1
/proc/[pid]/maps随着分配内存的增加,堆栈的增加或新文件的映射,随着时间的变化,这通常将不起作用。启动后立即意味着什么?映射完所有库之后?您如何确定?
斯特凡Chazelas

我现在正在使用两个进程在系统上进行测试,一个进程是一个Java应用程序,另一个进程是cfengine服务器。我每15分钟md5sum对他们的地图文件执行一次。我将其运行一两天,然后在此处报告结果。
迈克尔·马丁内斯

@StéphaneChazelas:我已经检查了我的两个进程已经16个小时了,md5sum并没有变化
Michael Martinez

-1

另一种方法是在终止进程之前检查进程的年龄。这样,您可以确保不会杀死少于24小时内没有产生的进程。您可以根据if条件添加一个条件,然后再终止该进程。

if [[ $(ps -p $p -o etime=) =~ 1-. ]] ; then
    kill $p
fi

if条件将检查进程ID $p是否少于24小时(86400秒)。

PS:-该命令ps -p $p -o etime=将具有以下格式<no.of days>-HH:MM:SS


mtime/proc/$p没有任何与过程的开始时间。
斯特凡Chazelas

谢谢@StéphaneChazelas。你是对的。我已编辑答案以更改if条件。如果有错误,请随时发表评论。
2015年

-3

我要做的是,在终止该过程之后,再次执行此操作。每当我这样做时,答案就会回来,“没有这样的过程”

allenb   12084  5473  0 08:12 pts/4    00:00:00 man man
allenb@allenb-P7812 ~ $ kill -9 12084
allenb@allenb-P7812 ~ $ kill -9 12084
bash: kill: (12084) - No such process
allenb@allenb-P7812 ~ $ 

再简单不过了,多年来我一直没有任何问题。


那是在回答“我该如何使它变得更糟”的问题,而不是在回答“我该如何解决”。
斯特凡Chazelas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.