为什么 `killpg` 命令的所有权正确却显示“无权限”?

8

我有一些代码使用了 fork(),在子进程中调用了 setsid(),并开始一些处理过程。如果任何一个子进程退出 (waitpid(-1, 0)),我将结束所有子进程组:

child_pids = []
for child_func in child_functions:
    pid = fork()
    if pid == 0:
        setsid()
        child_func()
        exit()
    else:
        child_pids.append(pid)

waitpid(-1, 0)
for child_pid in child_pids:
    try:
        killpg(child_pid, SIGTERM)
    except OSError as e:
        if e.errno != 3: # 3 == no such process
            print "Error killing %s: %s" %(child_pid, e)

然而,有时调用killpg会失败并显示“操作不允许”的错误信息:

无法终止22841号进程:[Errno 1] Operation not permitted

为什么会出现这种情况呢?

下面是一个完整的可运行示例:

from signal import SIGTERM
from sys import exit
from time import sleep
from os import *
def slow(): fork() sleep(10)
def fast(): sleep(1)
child_pids = [] for child_func in [fast, slow, slow, fast]: pid = fork() if pid == 0: setsid() child_func() exit(0) else: child_pids.append(pid)
waitpid(-1, 0) for child_pid in child_pids: try: killpg(child_pid, SIGTERM) except OSError as e: print "无法终止%s号进程:%s" %(child_pid, e)

结果如下:

$ python killpg.py
无法终止23293号进程:[Errno 3] No such process
无法终止23296号进程:[Errno 1] Operation not permitted

1
首先,你使用的是Linux、*BSD/Mac还是其他操作系统?因为如果我没记错的话,Linux对于killpg有更复杂的权限规则。此外,即使你无法使用killpg命令,你能否使用kill命令来终止子进程?(我知道这不是解决方案,只是为了帮助诊断问题。) - abarnert
1
@abarnert 说得很对。在Linux上不会发生,在我的Mac上会发生。 - Steve Kehlet
抱歉,应该提到:这是在OS X上。看了答案,似乎是Darwin/BSD正在做的“独特之处”。 - David Wolever
似乎在FreeBSD 7以及OS X 10.5和10.8上都会发生,因此可能是所有基于BSD的系统。BSD手册中没有任何指示它应该发生,但我找不到任何相关的错误报告。回答我自己的另一个问题,kill也失败了,但是返回的是ESRCH而不是EPERM。无论如何,我认为nneonneo已经挖掘出了根本问题;你想对此做什么是另一个问题。(我的解决方法应该可以解决问题,但它很丑陋,并且两个调用之间可能存在竞争条件...) - abarnert
你的代码存在竞态条件(尽管waitpid使其极不可能发生):没有强制要求在调用killpg之前完成setsid。通常避免这种竞态条件的方法是在子进程和父进程中都设置进程组,但这样会阻止setsid,因此你可能需要使用显式同步。 - jilles
3个回答

6

我添加了一些调试信息 (稍作修改的源代码)。当您尝试杀死已经退出且处于僵尸状态的进程组时,就会出现这种情况。哦,而且只需使用 [fast, fast] 就可以轻松重复。

$ python so.py 
spawned pgrp 6035
spawned pgrp 6036
Reaped pid: 6036, status: 0
 6035  6034  6035 Z    (Python)
 6034   521  6034 S+   python so.py
 6037  6034  6034 S+   sh -c ps -e -o pid,ppid,pgid,state,command | grep -i python
 6039  6037  6034 R+   grep -i python

killing pg 6035
Error killing 6035: [Errno 1] Operation not permitted
 6035  6034  6035 Z    (Python)
 6034   521  6034 S+   python so.py
 6040  6034  6034 S+   sh -c ps -e -o pid,ppid,pgid,state,command | grep -i python
 6042  6040  6034 S+   grep -i python

killing pg 6036
Error killing 6036: [Errno 3] No such process

不确定应该如何处理。也许您可以将waitpid放入while循环中以收集所有已终止的子进程,然后继续使用pgkill()杀死其余进程组。

但是回答您的问题是,您因为未被允许杀死一个僵尸进程组的领导者而得到EPERMs(至少在Mac OS上)。

此外,这可在Python之外得到验证。如果在其中加入sleep,找到其中一些僵尸进程的pgrp,并尝试杀死它的进程组,则同样会得到EPERM:

$ kill -TERM -6115
-bash: kill: (-6115) - Operation not permitted

确认这种情况在 Linux 上也不会发生。


5

显然你不能杀死一个由僵尸进程组成的进程组。当进程退出时,它会变成一个僵尸进程,直到有人调用waitpid函数。通常情况下,init会接管父进程已经死亡的子进程,以避免孤儿僵尸进程。

因此,该进程在某种意义上仍然“存在”,但它不会获得CPU时间,并忽略任何直接发送给它的kill命令。然而,如果一个进程组完全由僵尸进程组成,则行为似乎是杀死该进程组会抛出EPERM而不是静默失败。请注意,杀死包含非僵尸进程的进程组仍将成功。

以下是演示此问题的示例程序:

import os
import time

res = os.fork()

if res:
    time.sleep(0.2)
    pgid = os.getpgid(res)
    print pgid

    while 1:
        try:
            print os.kill(-pgid, 9)
        except Exception, e:
            print e
            break

    print 'wait', os.waitpid(res, 0)

    try:
        print os.kill(-pgid, 9)
    except Exception, e:
        print e

else:
    os.setpgid(0, 0)
    while 1:
        pass

输出结果如下:
56621
None
[Errno 1] Operation not permitted
wait (56621, 9)
[Errno 3] No such process

父进程使用SIGKILL信号杀死子进程,然后再次尝试。第二次尝试时,它会得到EPERM错误,因此它等待子进程(对其进行回收并销毁其进程组)。因此,第三个kill按预期产生了ESRCH错误。


不错的调查。听起来这解释了我之前只能察觉到的行为。 - abarnert
我认为这是操作系统的一个漏洞。根据POSIX标准,向僵尸进程发送信号应该会悄无声息地成功(因为进程的生命周期一直延续到僵尸进程被回收)。 - jilles
@jilles:我想要一个引用。还要注意,如果pid是僵尸进程,则kill(pid,sig)会悄悄成功,但是如果pgid是单例僵尸进程组,则kill(-pgid,sig)会失败。 - nneonneo
啊,有趣。感谢调查。 - David Wolever
由于僵尸进程最终会成为 init 进程的职责,如果一个进程组的所有子进程都是僵尸进程,那么整个进程组可能被 init 接管,而原来的父进程将不能再对它进行操作。 - mrgrieves

1

从添加更多日志信息来看,似乎有时killpg返回的是EPERM而不是ESRCH:

#!/usr/bin/python

from signal import SIGTERM
from sys import exit
from time import sleep
from os import *

def slow():
    fork()
    sleep(10)

def fast():
    sleep(1)

child_pids = []
for child_func in [fast, slow, slow, fast]:
    pid = fork()
    if pid == 0:
        setsid()
        print child_func, getpid(), getuid(), geteuid()
        child_func()
        exit(0)
    else:
        child_pids.append(pid)

print waitpid(-1, 0)
for child_pid in child_pids:
    try:
        print child_pid, getpgid(child_pid)
    except OSError as e:
        print "Error getpgid %s: %s" %(child_pid, e)      
    try:
        killpg(child_pid, SIGTERM)
    except OSError as e:
        print "Error killing %s: %s" %(child_pid, e)

每当killpg因EPERM失败时,getpgid之前就已经因ESRCH失败了。例如:

<function fast at 0x109950d70> 26561 503 503
<function slow at 0x109950a28> 26562 503 503
<function slow at 0x109950a28> 26563 503 503
<function fast at 0x109950d70> 26564 503 503
(26564, 0)
26561 Error getpgid 26561: [Errno 3] No such process
Error killing 26561: [Errno 1] Operation not permitted
26562 26562
26563 26563
26564 Error getpgid 26564: [Errno 3] No such process
Error killing 26564: [Errno 3] No such process

我不知道为什么会发生这种情况——它是合法行为还是来自FreeBSD或其他继承的Darwin中的错误等。

看起来你可以通过调用kill(child_pid, 0)来双重检查EPERM来解决这个问题;如果返回ESRCH,则没有实际的权限问题。当然,这在代码中看起来相当丑陋:

for child_pid in child_pids:
    try:
        killpg(child_pid, SIGTERM)
    except OSError as e:
        if e.errno != 3: # 3 == no such process
            if e.errno == 1:
                try:
                    kill(child_pid, 0)
                except OSError as e2:
                    if e2.errno != 3:
                        print "Error killing %s: %s" %(child_pid, e)
            else:
                print "Error killing %s: %s" %(child_pid, e)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接