在具有自定义信号处理程序的情况下读取 python 2 中的 sys.stdin 时出现奇怪的阻塞行为

Strange blocking behaviour when reading sys.stdin in python 2 while having a custom signal handler in place

考虑这个 python 脚本 odd-read-blocking.py:

#!/usr/bin/python

import signal
import sys

sig = None


def handler(signum, frame):
    global sig
    sig = signum


signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGTERM, handler)

x = sys.stdin.read(3)

print 'signal', sig
print 'read bytes', len(x)

exit(0)

I 运行 并用两个字节的标准输入数据 ('a' + '\n'):

> echo a | ./odd-read-blocking.py 
signal None
read bytes 2
>

很好。

现在我用相同的两个字节输入它(通过在其标准输入中键入 'a' + '\n')。请注意,标准输入还没有到达 EOF,并且可能会有更多数据到来。所以读取块,因为它期望多一个字节。我在脚本上使用 Ctrl+C

> ./odd-read-blocking.py 
a
^Csignal 2
read bytes 2
>

很好。我们看到已经读取了两个字节并且收到了信号2。

现在我打开一个标准输入流,但不发送任何字节。读取块如预期。如果我现在在脚本上使用 Ctrl+C,它会一直坐在那里等待。读取不会中断。 SIGINT 将不会被处理。

> ./odd-read-blocking.py 
^C

这里什么都没有。脚本仍然 运行ning(似乎在读取时被阻止)。

现在按一次 return,然后再按一次 Ctrl+C

^Csignal 2
read bytes 1
>

因此,只有在其标准输入上至少接收到一些数据(在本例中为单个“\n”)后,脚本才会按我预期的方式运行并正确中断被阻止的读取并告诉我它已收到信号2 并读取 1 个字节。

备选方案 1:我没有使用 Ctrl+C,而是使用 尝试了同样的事情kill <em>pid</em> 从一个单独的终端。行为相同。

备选方案 2:我没有使用上述 shell 标准输入,而是这样做了:

> sleep 2000 | ./odd-read-blocking.py

当使用 kill <em>pid</em> 将 SIGTERM 发送到 odd-read-blocking.py 进程时,我得到了相同的行为。这里脚本进程只能使用SIGKILL(9).

来杀掉

当它阻塞在一个尚未空但仍处于活动状态的标准输入流上时,为什么读取没有被中断?

我觉得这很奇怪。谁没有?谁能解释一下?

短版

如果 Python 信号处理程序抛出异常以放弃正在进行的 file.read,任何已读取的数据都会 丢失 。 (任何异步异常,比如默认的 KeyboardInterrupt,基本上不可能阻止这种失败,除非你有一个 way to mask it。)

为了最大限度地减少对此的需求,当它被信号——请注意,这是对 EOF 和 non-blocking I/O 案例的补充!但是,当它还没有数据时它不能这样做,因为它 return 是表示 EOF 的空字符串。

详情

一如既往,理解这种行为的方法是strace

阅读(2)

实际的 read 系统调用在进程被阻塞时信号到达时有一个进退两难的问题。首先,(C) 信号处理程序被调用——但因为这可能发生在任何两条指令之间,所以除了设置标志(或写入 self-pipe 之外,它几乎无能为力。然后呢?如果设置了SA_RESTART,则恢复通话;否则……

如果尚未传输任何数据,read 可能会失败,客户端可以检查其信号标志。特殊的 EINTR 无法澄清 I/O.

实际上没有任何问题

如果一些数据已经被写入(用户空间)缓冲区,它不能只是return "failure",因为数据会丢失——客户端不知道有多少(如果有的话)数据在缓冲区中。所以它刚刚 return 成功(到目前为止读取的字节数)!像这样的短读总是有可能的:客户端必须再次调用 read 以检查它是否已到达文件末尾。 (就像 file.read,0 字节的短读取将 变成 EOF。)因此,客户端必须在每次读取后检查其信号标志,无论是否成功。 (请注意,这仍然不是 perfectly reliable,但对于许多交互式用例来说已经足够了。)

file.read()

系统调用不是全部:毕竟,终端的正常配置在看到换行符后立即return。 Python 2 的 low-level file.read 是一个 wrapper for fread,如果一个不足,它会发出另一个 read。但是当读取失败并显示 EINTR 时,fread return 提前并且 file.read 调用您的 (Python) 信号处理程序。 (如果你向它添加输出,你会看到它会立即为你发送的每个信号调用,即使 file.read 没有 return。)

然后它面临着一个类似于系统调用的困境:正如所讨论的,短读不能为空,因为它意味着EOF。然而,与 C 信号处理程序不同的是,Python 可以执行任意工作(包括引发异常以立即中止 I/O,但代价是如开头提到的那样冒着数据丢失的风险),并且它是被认为是对界面进行方便的简化以隐藏可能性 EINTR。所以 fread 调用只是默默地重复。

Python3.5

重试规则 changed in 3.5. Now the io.IOBase.read resumes even if it has data in hand; this is more consistent, but it 使用异常来停止读取,这意味着您不能选择等待某些数据以免丢失您已有的数据。非常重量级的方案是改成复用I/O,使用signal.set_wakeup_fd();这具有允许 SIGINT 影响主线程的额外优势,而不必费心在所有其他线程中屏蔽它。