繁体   English   中英

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

[英]How to explain the reentrant RuntimeError caused by printing in signal handlers?

代码:

# 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>'> 文档中我了解到信号处理程序是异步调用的,在这种情况下,两个信号 SIGINT( Ctrl+C action) 和 SIGTERM( p.terminate() ) 几乎同时发送,导致竞争条件。

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

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

我正在使用 Python 3.6.2。

您可能更喜欢禁止将 SIGINT 传递给孩子,因此没有竞争,也许是通过将它放在不同的进程组中,或者让它忽略信号。 那么只有来自父级的 SIGTERM 才重要。

要显示中断的位置,请使用以下命令:

    sig_num, frame = args
    print(dis.dis(frame.f_code.co_code))
    print(frame.f_lasti)

左边距中的字节码偏移量对应于最后一条指令执行的偏移量。

其他感兴趣的项目包括frame.f_linenoframe.f_code.co_filenameframe.f_code.co_names

这个问题在 python 3.7.3 中变得没有实际意义,它不再出现这种症状。

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

ENTER_BUFFERED() => 它锁定缓冲区

PyErr_CheckSignals() => 它调用信号处理程序

LEAVE_BUFFERED() => 解锁缓冲区

通过调用PyErr_CheckSignals() ,它调用另一个信号处理程序,在这种情况下它有另一个print 第二次print再次运行ENTER_BUFFERED() ,因为缓冲区已经被第一次信号处理程序中的先前print锁定,因此抛出reentrant异常,如下代码段所示。

    // 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) )


聚苯乙烯

Unix 环境中高级编程的可重入函数

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM