Python 的 signal 处理与 print() 的 reentrant call 运行时错误
在前文《为何 Windows 下无法用 Ctrl+C 终止 Python 进程》中,讲解了 Python 信号处理的基本原理。当时为了撰写文章而编写了一些测试代码,在运行某例测试代码时,发生了奇怪的 reentrant call 运行时错误。代码如下:
pythonimport signal
signal.signal(signal.SIGINT, lambda signum, frame: print('test'))
while True: print('test')
在程序运行中按下 Ctrl+C,程序抛出 RuntimeError 异常。完整错误信息如下:
Traceback (most recent call last):
File "test.py", line 4, in <module>
while True: print('test')
^^^^^^^^^^^^^
File "test.py", line 3, in <lambda>
signal.signal(signal.SIGINT, lambda signum, frame: print('test'))
^^^^^^^^^^^^^
RuntimeError: reentrant call inside <_io.BufferedWriter name='<stdout>'>
错误信息中所提到的 reentrant call 是一个计算机术语,它的意思是程序或者子例程(subroutine)在执行过程中被中断,此时再次调用该程序或者子例程。由于 Python 的信号处理器是运行在主线程中,所以不存在多线程冲突的可能。因此,猜想错误可能是由如下的代码引起的:
pythonentrant_status = 0
def print(text):
if (entrant_status > 0):
raise RuntimeError(
"reentrant call inside <_io.BufferedWriter name='<stdout>'>")
entrant_status = 1
'''
此时因按下 Ctrl+C 而产生软中断
Python 中止执行此处代码
转而调用 signal_handler()
'''
signal_handler(signum, frame)
entrant_status = 0
return
def signal_handler(signum, frame):
print('test')
print('test')
但是根据 Python 官方文档的描述,Python VM 是在两条 bytecode 指令之间调用信号处理器的。而 print()
属于 builtin function,调用 print()
只需要一条 CALL_FUNCTION
指令。那么唯一的可能就是,官方文档的描述并不准确,Python 还有其他唤起信号处理器的方式。看来只能从 Python 源代码中去寻找答案了。
根据错误提示,在 Python 的源代码中搜索「reentrant call inside」,发现 Modules/_io/bufferedio.c 文件的第 262 行高度疑似为抛出异常的代码。此处附近完整的代码如下:
cstatic 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;
}
relax_locking = _Py_IsFinalizing();
Py_BEGIN_ALLOW_THREADS
if (!relax_locking)
st = PyThread_acquire_lock(self->lock, 1);
else {
/* When finalizing, we don't want a deadlock to happen with daemon
* threads abruptly shut down while they owned the lock.
* Therefore, only wait for a grace period (1 s.).
* Note that non-daemon threads have already exited here, so this
* shouldn't affect carefully written threaded I/O code.
*/
st = PyThread_acquire_lock_timed(self->lock, (PY_TIMEOUT_T)1e6, 0);
}
Py_END_ALLOW_THREADS
if (relax_locking && st != PY_LOCK_ACQUIRED) {
PyObject *ascii = PyObject_ASCII((PyObject*)self);
_Py_FatalErrorFormat(__func__,
"could not acquire lock for %s at interpreter "
"shutdown, possibly due to daemon threads",
ascii ? PyUnicode_AsUTF8(ascii) : "<ascii(self) failed>");
}
return 1;
}
#define ENTER_BUFFERED(self) \
( (PyThread_acquire_lock(self->lock, 0) ? \
1 : _enter_buffered_busy(self)) \
&& (self->owner = PyThread_get_thread_ident(), 1) )
RuntimeError 是在 _enter_buffered_busy
函数中被抛出的,而该函数被 ENTER_BUFFERED
宏所调用:当使用 PyThread_acquire_lock()
获取锁失败时便会抛出异常。进一步在当前文件中搜索ENTER_BUFFERED
,发现第 1916 行处的调用可能是抛出异常的上层函数,该函数名为 io_BufferedWriter_write_impl
,正好和错误信息中的 <_io.BufferedWriter name='<stdout>'>
相对应。该函数的完整代码如下:
c/*[clinic input]
_io.BufferedWriter.write
buffer: Py_buffer
/
[clinic start generated code]*/
static PyObject *
_io_BufferedWriter_write_impl(buffered *self, Py_buffer *buffer)
/*[clinic end generated code: output=7f8d1365759bfc6b input=dd87dd85fc7f8850]*/
{
PyObject *res = NULL;
Py_ssize_t written, avail, remaining;
Py_off_t offset;
CHECK_INITIALIZED(self)
if (!ENTER_BUFFERED(self))
return NULL;
/* Issue #31976: Check for closed file after acquiring the lock. Another
thread could be holding the lock while closing the file. */
if (IS_CLOSED(self)) {
PyErr_SetString(PyExc_ValueError, "write to closed file");
goto error;
}
/* Fast path: the data to write can be fully buffered. */
if (!VALID_READ_BUFFER(self) && !VALID_WRITE_BUFFER(self)) {
self->pos = 0;
self->raw_pos = 0;
}
avail = Py_SAFE_DOWNCAST(self->buffer_size - self->pos, Py_off_t, Py_ssize_t);
if (buffer->len <= avail) {
memcpy(self->buffer + self->pos, buffer->buf, buffer->len);
if (!VALID_WRITE_BUFFER(self) || self->write_pos > self->pos) {
self->write_pos = self->pos;
}
ADJUST_POSITION(self, self->pos + buffer->len);
if (self->pos > self->write_end)
self->write_end = self->pos;
written = buffer->len;
goto end;
}
/* First write the current buffer */
res = _bufferedwriter_flush_unlocked(self);
if (res == NULL) {
Py_ssize_t *w = _buffered_check_blocking_error();
if (w == NULL)
goto error;
if (self->readable)
_bufferedreader_reset_buf(self);
/* Make some place by shifting the buffer. */
assert(VALID_WRITE_BUFFER(self));
memmove(self->buffer, self->buffer + self->write_pos,
Py_SAFE_DOWNCAST(self->write_end - self->write_pos,
Py_off_t, Py_ssize_t));
self->write_end -= self->write_pos;
self->raw_pos -= self->write_pos;
self->pos -= self->write_pos;
self->write_pos = 0;
avail = Py_SAFE_DOWNCAST(self->buffer_size - self->write_end,
Py_off_t, Py_ssize_t);
if (buffer->len <= avail) {
/* Everything can be buffered */
PyErr_Clear();
memcpy(self->buffer + self->write_end, buffer->buf, buffer->len);
self->write_end += buffer->len;
self->pos += buffer->len;
written = buffer->len;
goto end;
}
/* Buffer as much as possible. */
memcpy(self->buffer + self->write_end, buffer->buf, avail);
self->write_end += avail;
self->pos += avail;
/* XXX Modifying the existing exception e using the pointer w
will change e.characters_written but not e.args[2].
Therefore we just replace with a new error. */
_set_BlockingIOError("write could not complete without blocking",
avail);
goto error;
}
Py_CLEAR(res);
/* Adjust the raw stream position if it is away from the logical stream
position. This happens if the read buffer has been filled but not
modified (and therefore _bufferedwriter_flush_unlocked() didn't rewind
the raw stream by itself).
Fixes issue #6629.
*/
offset = RAW_OFFSET(self);
if (offset != 0) {
if (_buffered_raw_seek(self, -offset, 1) < 0)
goto error;
self->raw_pos -= offset;
}
/* Then write buf itself. At this point the buffer has been emptied. */
remaining = buffer->len;
written = 0;
while (remaining > self->buffer_size) {
Py_ssize_t n = _bufferedwriter_raw_write(
self, (char *) buffer->buf + written, buffer->len - written);
if (n == -1) {
goto error;
} else if (n == -2) {
/* Write failed because raw file is non-blocking */
if (remaining > self->buffer_size) {
/* Can't buffer everything, still buffer as much as possible */
memcpy(self->buffer,
(char *) buffer->buf + written, self->buffer_size);
self->raw_pos = 0;
ADJUST_POSITION(self, self->buffer_size);
self->write_end = self->buffer_size;
written += self->buffer_size;
_set_BlockingIOError("write could not complete without "
"blocking", written);
goto error;
}
PyErr_Clear();
break;
}
written += n;
remaining -= n;
/* Partial writes can return successfully when interrupted by a
signal (see write(2)). We must run signal handlers before
blocking another time, possibly indefinitely. */
if (PyErr_CheckSignals() < 0)
goto error;
}
if (self->readable)
_bufferedreader_reset_buf(self);
if (remaining > 0) {
memcpy(self->buffer, (char *) buffer->buf + written, remaining);
written += remaining;
}
self->write_pos = 0;
/* TODO: sanity check (remaining >= 0) */
self->write_end = remaining;
ADJUST_POSITION(self, remaining);
self->raw_pos = 0;
end:
res = PyLong_FromSsize_t(written);
error:
LEAVE_BUFFERED(self)
return res;
}
在写入 Buffer 的 while
循环中发现如下注释和代码:
c/* Partial writes can return successfully when interrupted by a
signal (see write(2)). We must run signal handlers before
blocking another time, possibly indefinitely. */
if (PyErr_CheckSignals() < 0)
goto error;
显然,这里就是 Python 唤起信号处理器的地方。PyErr_CheckSignals()
的作用就是检查信号标志位,如果设置了标志位就立即调用信号处理器。在 Python 源代码中的搜索「PyErr_CheckSignals」,能找到20多处调用。果然,官方文档中的描述是有问题的——事实上,Python 不仅仅在两次 bytecode 执行之间会调用信号处理器,在内置函数中也可能会调用信号处理器。