Linux阻塞信号到Python初始化

11 投票
2 回答
1801 浏览
提问于 2025-04-16 16:43

这是我之前帖子的一部分,关于如何在Python中安装信号处理器。在Linux系统中,PID 1(也就是系统的初始化进程)会阻止所有信号,包括强制终止信号(SIGKILL),除非Init为特定信号安装了信号处理器。这是为了防止如果有人发送终止信号给PID 1,系统会崩溃。我的问题是,Python中的signal模块似乎没有以系统能识别的方式安装信号处理器。我的Python Init脚本似乎完全忽略了所有信号,因为我觉得这些信号被阻止了。

我似乎找到了解决办法;使用ctypes来通过libc中的signal()函数安装信号处理器(在这个例子中是uClibc)。下面是一个基于Python的测试初始化脚本。它在TTY2上打开一个终端,我可以从中向PID 1发送信号进行测试。看起来在我用来测试的KVM中是有效的(如果有人感兴趣,我愿意分享这个虚拟机)。

这样解决问题算是最好的办法吗?有没有其他更好的方法可以在不使用信号模块的情况下安装信号处理器?(我并不关心可移植性)

这是Python的一个bug吗?

#!/usr/bin/python

import os
import sys
import time

from ctypes import *

def SigHUP():
    print "Caught SIGHUP"
    return 0

def SigCHLD():
    print "Caught SIGCHLD"
    return 0

SIGFUNC = CFUNCTYPE(c_int)
SigHUPFunc = SIGFUNC(SigHUP)
SigCHLDFunc = SIGFUNC(SigCHLD)

libc = cdll.LoadLibrary('libc.so.0')
libc.signal(1, SigHUPFunc) # 1 = SIGHUP
libc.signal(17, SigCHLDFunc) # 17 = SIGCHLD

print "Mounting Proc: %s" % libc.mount(None, "/proc", "proc", 0, None)

print "forking for ash"
cpid = os.fork()
if cpid == 0:
    os.closerange(0, 4)
    sys.stdin = open('/dev/tty2', 'r')
    sys.stdout = open('/dev/tty2', 'w')
    sys.stderr = open('/dev/tty2', 'w')
    os.execv('/bin/ash', ('ash',))

print "ash started on tty2"

print "sleeping"
while True:
    time.sleep(0.01)

2 个回答

2

这个问题是因为Python和旧版Linux线程的uClibc 0.9.31不兼容。通过使用0.9.32-rc3版本并且采用NPTL(新线程库)来编译,问题就解决了。

8

我在KVM环境下调试时发现,内核确实会向进程ID为1的程序发送信号,当信号处理程序通过标准信号模块安装后。然而,当接收到信号时,似乎有“某种东西”导致了一个新进程的产生,而不是打印出预期的输出。

这是我向不工作的init.sig-mod发送HUP信号时的strace输出:

strace output

这导致了一个新的进程(进程ID 23)运行,它是init.sig-mod的一个克隆:

clone of init as pid 23

我没有时间深入研究原因,但这进一步缩小了问题范围。可能与Python的信号传递逻辑有关(它注册了一个C语言的钩子,当被调用时会执行你的字节码函数)。而使用ctypes的方法则绕过了这个问题。如果你想更仔细地看看,相关的Python源文件是Python/pythonrun.cModules/signalmodule.c

旧信息 -- 我不确定这能否解决你的问题,但可能会让你更接近答案。我比较了几种不同的信号处理程序安装方式:

  • 通过Python的信号模块安装处理程序。
  • Upstart的信号处理程序。
  • 使用ctypes直接调用signal()系统调用。
  • 在C语言中进行的一些快速测试。

无论是通过ctypes调用的signal()系统调用,还是Upstart的sigaction()系统调用,在注册处理程序时都会设置SA_RESTART标志。设置这个标志意味着,当进程在执行或阻塞某些系统调用(如读取、写入、等待、纳秒睡眠等)时,如果接收到信号,信号处理程序完成后,系统调用应该自动重新启动。应用程序对此并不会察觉。

当Python的信号模块注册一个处理程序时,它通过调用siginterrupt(signum, 1)来清除SA_RESTART标志。这告诉系统:“当一个系统调用被信号中断时,信号处理程序完成后,将errno设置为EINTR并从系统调用返回。”这就留给开发者来处理,并决定是否重新启动系统调用。

你可以通过以下方式注册信号来设置SA_RESTART标志:

import signal
signal.signal(signal.SIGHUP, handler)
signal.siginterrupt(signal.SIGHUP, False)

撰写回答