如何阻止Python将信号传播到子进程?

25 投票
5 回答
18982 浏览
提问于 2025-04-16 04:34

我正在用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)
    [...]

所以无论我怎么做,程序都收到了我只想让Python脚本看到的SIGINT信号。我该怎么做才能实现呢???

我还尝试过:

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信号。

谢谢!

5 个回答

4

根据POSIX的规定,使用execvp(这就是subprocess.Popen使用的方式)运行的程序应该会继承调用进程的信号屏蔽。

我可能错了,但我觉得调用signal并不会修改这个屏蔽。你需要使用sigprocmask,不过Python并没有直接提供这个功能。

虽然这有点绕,但你可以尝试通过直接调用libc来设置它,使用ctypes。我很想看到关于这个方法的更好答案。

另一种方法是,在你的主循环中轮询标准输入,等待用户输入。比如可以提示“按Q退出/暂停”之类的。这种方式可以避免处理信号的问题。

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 的调用注释掉,那么你就会看到。看起来这个方法还是能起作用的 :)

20

把其他一些回答结合起来,这样做就能解决问题——没有信号会被发送到主应用程序,也不会转发给子进程。

import os
from subprocess import Popen

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

Popen('whatever', preexec_fn = preexec)

撰写回答