Python中哪些函数在信号库处理中是可重入的?
在讨论关于 Python中的信号处理和日志记录 时,我脑海中冒出了一个问题,那就是在Python中哪些函数是可重入的。
信号库提到:
虽然从Python用户的角度来看,信号处理程序是异步调用的,但它们只能在Python解释器的原子指令之间发生。这意味着,在进行长时间的计算时(比如在大文本上进行正则表达式匹配),如果信号到达,可能会被延迟很长时间。
而且,日志库指出,重入性并不是很常见:
如果你使用信号模块实现异步信号处理程序,你可能无法在这些处理程序中使用日志记录。这是因为线程模块中的锁实现并不总是可重入的,因此不能在这些信号处理程序中调用。
我有点困惑,因为信号库提到全局解释器锁(GIL)是“..在原子指令之间..”。在这种情况下,信号会被推迟,并在GIL被释放/解锁后执行。这就像是一个信号队列。
这很有道理,但如果被推迟的信号处理程序调用的函数是可重入的,那也没关系,因为它们并不是在真正的POSIX信号处理程序中被调用的,而是有“可重入”限制的:
只有一份定义好的POSIX C函数列表被声明为可重入的,并且可以在POSIX信号处理程序中调用。IEEE Std 1003.1列出了118个可重入的UNIX函数,你可以在https://www.opengroup.org/找到(需要登录)。
2 个回答
有些人可能更喜欢使用 pselect()、ppoll() 或 Linux 的 signalfd 来监听信号。不过,pselect() 和 ppoll() 在 Python 的 select
模块中是没有的。
一些事件循环声称支持信号。如果你考虑使用事件循环,可以查看它的文档。例如:https://docs.python.org/3/library/asyncio-eventloop.html#unix-signals
一些事件循环,比如内置的 asyncio 模块,目前是通过 signal.set_wakeup_fd() 实现的。这是有问题的。请看下面的标题。
另外,针对你问题的字面意思:可以使用 os.write()
。然后你可以使用自管道技巧。
import os
import fcntl
import errno
(sigint_write_pipe, sigint_read_pipe) = os.pipe()
fcntl.fcntl(sigint_write_pipe, fcntl.SET_FL,
os.O_NONBLOCK | os.O_CLOEXEC)
def handle_sigint():
try:
os.write(sigint_write_pipe, b'\0')
except IOError as e:
if e.errno = errno.EWOULDBLOCK:
pass # pipe is already full. no problem.
else:
raise
signal.signal(signal.SIGINT, handle_sigint)
# Now listen to sigint_read_pipe, using your preferred
# select() / poll() / event loop etc
...
有几种方法可以让一个函数实现异步信号安全。os.write()
是最有可能符合第一个标准的函数:
- 纯用 C 实现的函数。因为 Python 层的信号处理程序不会中断 C 函数。
- 不访问可变全局变量的 Python 函数。
- 访问可变全局变量的 Python 函数,但它们的“不变性”从未被暂时打破。例如,一个没有不变性适用的单一变量。
在很多情况下,异步信号安全会被视为一个私有的实现细节,而不是对未来行为的公开保证。这在 C 语言中也是如此。官方的 Python 文档没有提到你的担忧。因此,我们不应该把 Python 文档当作这里的指导。
signal.set_wakeup_fd()
如果你仍然相信 Python 文档,还有一个第二个选项是“常用的”。将一个管道传递给 signal.set_wakeup_fd()
,然后轮询管道的另一端。这可以让你检测到程序是否被信号中断。但它无法让你知道是什么信号,因为可能有多个信号,它们可能会溢出管道缓冲区而丢失。
我认为,日志模块之所以不能重入,是因为它使用了一个 threading.Lock
(而不是 RLock
)来让多个线程在记录日志时保持同步,这样就不会出现信息交错的情况。
这就意味着,如果一个正在记录日志的调用被信号处理程序打断,而这个信号处理程序又想记录日志,就会出现死锁的情况,永远在等待之前的 acquire
被释放。
顺便说一下,这些锁和全局解释器锁(GIL)没有关系,它们是“用户创建”的锁,GIL是解释器使用的一种锁(属于实现细节)。