python - 如何解释在信号处理程序中打印导致的可重入 RuntimeError?

标签 python python-3.x signals race-condition reentrancy

代码:

# callee.py
import signal
import sys
import time


def int_handler(*args):
    for i in range(10):
        print('INTERRUPT', args)
    sys.exit()


if __name__ == '__main__':

    signal.signal(signal.SIGINT, int_handler)
    signal.signal(signal.SIGTERM, int_handler)
    while 1:
        time.sleep(1)

# caller.py
import subprocess
import sys


def wait_and_communicate(p):
    out, err = p.communicate(timeout=1)
    print('========out==========')
    print(out.decode() if out else '')
    print('========err==========')
    print(err.decode() if err else '')
    print('=====================')


if __name__ == '__main__':

    p = subprocess.Popen(
        ['/usr/local/bin/python3', 'callee.py'],
        stdout=sys.stdout,
        stderr=subprocess.PIPE,
    )
    while 1:
        try:
            wait_and_communicate(p)
        except KeyboardInterrupt:
            p.terminate()
            wait_and_communicate(p)
            break
        except subprocess.TimeoutExpired:
            continue

只需执行 caller.py然后按 Ctrl+C ,程序将提高 RuntimeError: reentrant call inside <_io.BufferedWriter name='<stdout>'>随机的。来自documentation我了解到信号处理程序是异步调用的,在这种情况下,两个信号 SIGINT( Ctrl+C action) 和 SIGTERM( p.terminate() ) 几乎同时发送,导致竞争条件。

但是,从这个post我了解到 signal模块不在低级 (C) 处理程序中执行信号处理程序。相反,它设置一个标志,解释器检查字节码指令之间的标志,然后调用 python 信号处理程序。换句话说,虽然信号处理程序可能会扰乱主线程中的控制流,但字节码指令始终是原子的。

这似乎与我的示例程序的结果相矛盾。就我而言,print和隐含的 _io.BufferedWriter都是用纯 C 实现的,因此调用 print函数应该只使用一个字节码指令(CALL_FUNCTION)。我很困惑:在一个线程上的一个不间断指令中,一个函数怎么可以重入?

我正在使用 Python 3.6.2。

最佳答案

信号在操作码之间进行处理(参见 eval_frame_handle_pending() 在 python 的操作码处理器循环中),但不限于此。 print 就是一个完美的例子。它是基于_io_BufferedWriter_write_impl()实现的,其结构如下

ENTER_BUFFERED() => it locks buffer

PyErr_CheckSignals() => it invoke signal handler

LEAVE_BUFFERED() => it unlocks buffer

通过调用 PyErr_CheckSignals(),它会调用另一个信号处理程序,在本例中它有另一个 print。第二个 print 再次运行 ENTER_BUFFERED(),因为缓冲区已经被第一个信号处理程序中的前一个 print 锁定,所以 可重入 异常被抛出,如下片段所示。

    // snippet of ENTER_BUFFERED
    static int
    _enter_buffered_busy(buffered *self)
    {
        int relax_locking;
        PyLockStatus st;
        if (self->owner == PyThread_get_thread_ident()) {
            PyErr_Format(PyExc_RuntimeError,
                         "reentrant call inside %R", self);
            return 0;
        }
    }
    
    #define ENTER_BUFFERED(self) \
        ( (PyThread_acquire_lock(self->lock, 0) ? \
           1 : _enter_buffered_busy(self)) \
         && (self->owner = PyThread_get_thread_ident(), 1) )


附言

可重入函数 来自 Advanced Programming in the Unix Environment .

单一 UNIX 规范指定了保证可以从信号处理程序中安全调用的函数。这些函数是可重入的,被称为异步信号安全的。大多数不可重入的函数是因为

  1. 众所周知,他们使用静态数据结构,
  2. 他们调用 malloc 或 free
  3. 它们是标准 I/O 库的一部分。大多数标准 I/O 库的实现都以不可重入的方式使用全局数据结构。 Python 中的print 属于此类。

关于python - 如何解释在信号处理程序中打印导致的可重入 RuntimeError?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45680378/

相关文章:

python - 从一个变量中生成两列?

Python:如何异步 for 循环

c - 收到的 IM-MSG 信号消息

windows-7 - 在 Windows 7 中,如何将 Ctrl-C 或 Ctrl-Break 发送到单独的进程

python - 我怎样才能做到这一点,但骰子的数量是可变的?

python - 是否可以更改提示中显示的 python virtualenv 名称?

python numpy 掩码意味着性能

python - 复杂的排序,使用 cmp 函数很容易完成,但我如何为 Python 3 做计划?

python - 遍历反向(列表)是否会增加我的函数的时间复杂度?

linux - Perl:关闭信号处理程序中的子进程管道挂起?