如何阻止Python将信号传递给子进程?

25

我正在使用Python来管理一些模拟。我使用以下代码构建参数并运行程序:

pipe = open('/dev/null', 'w')
pid = subprocess.Popen(shlex.split(command), stdout=pipe, stderr=pipe)
我的代码可以处理不同的信号。按Ctrl+C将停止模拟,询问我是否要保存并优雅地退出。我还有其他的信号处理程序(例如强制数据输出)。
我想要的是向我的Python脚本发送一个信号(SIGINT,Ctrl+C),它将询问用户想要发送哪个信号给程序。
唯一阻止代码工作的事情似乎是无论我做什么,Ctrl+C都会被“转发”到子进程:代码也会捕获它并退出:
try:
  <wait for available slots>
except KeyboardInterrupt:
  print "KeyboardInterrupt catched! All simulations are paused. Please choose the signal to send:"
  print "  0: SIGCONT (Continue simulation)"
  print "  1: SIGINT  (Exit and save)"
  [...]
  answer = raw_input()
  pid.send_signal(signal.SIGCONT)
  if   (answer == "0"):
    print "    --> Continuing simulation..."
  elif (answer == "1"):
    print "    --> Exit and save."
    pid.send_signal(signal.SIGINT)
    [...]

无论我做什么,程序都会接收到SIGINT信号,而我只希望我的Python脚本能够接收到。我该如何做?

我也尝试了:

signal.signal(signal.SIGINT, signal.SIG_IGN)
pid = subprocess.Popen(shlex.split(command), stdout=pipe, stderr=pipe)
signal.signal(signal.SIGINT, signal.SIG_DFL)

尝试运行程序,但结果相同:程序捕获了SIGINT信号。

谢谢!


这里有很多好的答案:https://dev59.com/5WYr5IYBdhLWcg3wi6vd - totaam
5个回答

20

结合其他答案,以下方法可以解决问题——不会将发送到主应用程序的任何信号转发到子进程。

import os
from subprocess import Popen

def preexec(): # Don't forward signals.
    os.setpgrp()

Popen('whatever', preexec_fn = preexec)

14
如果您使用Python 2.x,请考虑使用subprocess32而不是subprocess,并且**不要再使用preexec_fn**,因为它不安全。改用新的Popen start_new_session=True参数。 - gps
这正是我在寻找的。谢谢。 - Giampaolo Rodolà
5
除了安全性论点外,上述代码之所以能够工作,是因为通常情况下,当您按下Ctrl-C时,SIGINT信号会发送到进程组。默认情况下,所有子进程都与父进程在同一进程组中。通过调用setpgrp()函数,您可以将子进程放置在新的进程组中,以便它不会收到来自父进程的信号。 - Cenk Alti
5
在风格上,更Pythonic的说法是:Popen('whatever', preexec_fn=os.setpgrp)。不需要定义preexec()函数。 - Scott
1
为什么不在preexec中只使用signal.signal(signal.SIGINT, signal.SIG_IGN)呢? - omikron

8
这确实可以使用ctypes完成。我并不是特别推荐这种解决方案,但我很有兴趣尝试一下,所以想分享一下。 parent.py
#!/usr/bin/python

from ctypes import *
import signal
import subprocess
import sys
import time

# Get the size of the array used to
# represent the signal mask
SIGSET_NWORDS = 1024 / (8 * sizeof(c_ulong))

# Define the sigset_t structure
class SIGSET(Structure):
    _fields_ = [
        ('val', c_ulong * SIGSET_NWORDS)
    ]

# Create a new sigset_t to mask out SIGINT
sigs = (c_ulong * SIGSET_NWORDS)()
sigs[0] = 2 ** (signal.SIGINT - 1)
mask = SIGSET(sigs)

libc = CDLL('libc.so.6')

def handle(sig, _):
    if sig == signal.SIGINT:
        print("SIGINT from parent!")

def disable_sig():
    '''Mask the SIGINT in the child process'''
    SIG_BLOCK = 0
    libc.sigprocmask(SIG_BLOCK, pointer(mask), 0)

# Set up the parent's signal handler
signal.signal(signal.SIGINT, handle)

# Call the child process
pid = subprocess.Popen("./child.py", stdout=sys.stdout, stderr=sys.stdin, preexec_fn=disable_sig)

while (1):
    time.sleep(1)

child.py

#!/usr/bin/python
import time
import signal

def handle(sig, _):
    if sig == signal.SIGINT:
        print("SIGINT from child!")

signal.signal(signal.SIGINT, handle)
while (1):
    time.sleep(1)

请注意,这种方法对于各种libc结构做了很多假设,因此可能非常脆弱。运行时,您将看不到“来自子进程的SIGINT!”消息打印。但是,如果您注释掉对sigprocmask的调用,则可以看到该消息。似乎可以完成工作 :)

谢谢您的建议。虽然我认为这对于我的目标来说有点复杂了。基本上,我只想暂停父脚本,询问用户一些问题,并向所有子进程发送一个信号。也许另一个键盘输入可以暂停父脚本,比如ctrl+x? - big_gie
是的,正如我所说,我不会真正推荐这个解决方案。如果您在单独的线程中侦听键盘事件,则可以使用任何键组合来暂停父级。 - Michael Mior

4

POSIX规定使用execvp(这是subprocess.Popen使用的方法)运行的程序应该继承调用进程的信号掩码。

我可能错了,但我认为调用signal不会修改掩码。你需要sigprocmask,而Python没有直接暴露它。

这可能是一种hack方法,但你可以尝试通过ctypes直接调用libc来设置它。我肯定很想看到更好的答案。

另一种策略是在主循环中轮询stdin以获取用户输入。 (“按Q退出/暂停”之类的东西。)这避免了处理信号的问题。


2

我通过创建一个辅助应用程序来解决这个问题,而不是直接创建子进程。这个辅助应用程序会更改其父组,然后生成真正的子进程。

import os
import sys

from time import sleep
from subprocess import Popen

POLL_INTERVAL=2

# dettach from parent group (no more inherited signals!)
os.setpgrp()

app = Popen(sys.argv[1:])
while app.poll() is None:
    sleep(POLL_INTERVAL)

exit(app.returncode)

我在父组件中调用这个帮助函数,并将真实的子组件以及其参数作为参数传递:
Popen(["helper", "child", "arg1", ...])

我必须这样做是因为我的子应用程序不在我的控制范围内,如果在我的控制范围内,我可以在那里添加setpgrp并完全绕过helper。


1

这个函数的作用:

os.setpgrp()

仅当紧随其后调用Popen时才能正常工作。如果您试图防止信号传播到任意软件包的子进程,则该软件包可能会在创建子进程之前覆盖此设置,导致信号仍然被传播。例如,在尝试防止从Selenium包生成的Web浏览器进程中传播信号时就是这种情况。

此功能还删除了在没有诸如套接字之类的东西的情况下轻松在分离的进程之间进行通信的能力。

对于我的目的,这似乎有些过度。我使用了自定义信号SIGUSR1而不是担心信号传播。许多Python软件包忽略SIGUSR1,因此即使将其发送到所有子进程,它通常也会被忽略。

可以在Ubuntu上使用bash将其发送到进程中

kill -10 <pid>

它可以通过您的代码进行识别,方式是:


signal.signal(signal.SIGUSR1, callback_function)

可在Ubuntu上找到的信号编号位于/usr/include/asm/signal.h

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