在Python中将标准输出设置为非阻塞

12

先警告一下:出于好奇目的,我在这里进行了一些黑客活动。我没有具体的原因去做下面的事情!

以下是在MacOS 10.12.5上使用Python 2.7.13完成的

我正在使用python进行黑客活动,我想知道如果我使stdout无阻塞会发生什么有趣的事情。

fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
调用 fcntl 的操作一定成功。然后,我尝试写入大量数据(大于OSX管道的最大缓冲区大小-即65536字节)。我以各种方式尝试,并获得不同的结果,有时是异常,有时似乎是硬性失败。 情况1
fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
try:
    sys.stdout.write("A" * 65537)
except Exception as e:
    time.sleep(1)
    print "Caught: {}".format(e)

# Safety sleep to prevent quick exit
time.sleep(1)

这总是抛出异常 Caught: [Errno 35] Resource temporarily unavailable。我觉得很有道理。更高级别的文件对象包装器告诉我写入调用失败。

案例2

fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
try:
    sys.stdout.write("A" * 65537)
except Exception as e:
    print "Caught: {}".format(e)

# Safety sleep to prevent quick exit
time.sleep(1)

有时会抛出异常Caught: [Errno 35] Resource temporarily unavailable,有时则没有捕获到任何异常并且看到以下输出:

close failed in file object destructor:
sys.excepthook is missing
lost sys.stderr

第三案例

fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
try:
    sys.stdout.write("A" * 65537)
except Exception as e:
    print "Caught: {}".format(e)

# Safety sleep to prevent quick exit
time.sleep(1)

print "Slept"

有时会抛出异常 Caught: [Errno 35] Resource temporarily unavailable,有时则不会抛出任何异常并且只会看到 "Slept"。似乎通过 print "Slept" 我没有收到 Case 2 中的错误信息。

Case 4

fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
try:
    os.write(sys.stdout.fileno(), "A" * 65537)
except Exception as e:
    print "Caught: {}".format(e)

# Safety sleep to prevent quick exit
time.sleep(1)

一直都没问题!

案例5

fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
try:
    print os.write(sys.stdout.fileno(), "A" * 65537)
except Exception as e:
    print "Caught: {}".format(e)

# Safety sleep to prevent quick exit
time.sleep(1)

有时候这是可以的,有时候会输出close failed in file object destructor错误信息。

我的问题是,为什么在Python中会出现这种失败情况?我在Python或系统级别上做错了什么根本性的事情吗?

似乎当写入已经失败时太早写入stdout会导致错误消息。该错误似乎不是异常。不知道它来自哪里。

N.B. 我可以用C编写等效的程序并且它可以正常工作:

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <sys/fcntl.h>
#include <unistd.h>

int main(int argc, const char * argv[])
{
    const size_t NUM_CHARS = 65537;
    char buf[NUM_CHARS];

    // Set stdout non-blocking
    fcntl(fileno(stdout), F_SETFL, O_NONBLOCK);

    // Try to write a large amount of data
    memset(buf, 65, NUM_CHARS);
    size_t written = fwrite(buf, 1, NUM_CHARS, stdout);

    // Wait briefly to give stdout a chance to be read from
    usleep(1000);

    // This will be written correctly
    sprintf(buf, "\nI wrote %zd bytes\n", written);
    fwrite(buf, 1, strlen(buf), stdout);
    return 0;
}

我认为问题在于非阻塞IO,调用os.write可能会在写入完成之前返回,由于这会结束您的简单测试程序,因此sys.stdout也可能在写入完成之前关闭。请注意,您的C程序等待(usleep(1000)),而您的Python代码没有。 - chepner
谢谢您的回复。我应该发布更完整的Python代码片段。在写入调用之后,我有一个time.sleep(1),但它没有任何区别:( - Andrew Parker
你用的是什么操作系统?你可以尝试在类似于“strace”的工具下运行Python,查看哪些系统调用被执行。 - Andrew Henle
让我稍微更新一下这篇文章,因为我可以澄清一些事情。不同的调用会有略微不同的行为。 - Andrew Parker
@Andrew Parker 使用延迟代替fflush。这个想法很有趣。实际上,它来自于Arduino的经验吗? :/ - 0___________
抱歉,帖子有些变化。希望现在更清楚我所看到的内容。我肯定已经消除了自己思维中的一些不一致之处。还不太确定“预期”的行为是什么。@PeterJ 在编写一个子进程包装器时,我的思维稍微偏离了一下,这个包装器将实时从子进程管道中读取数据。我故意在子进程中填充了一个管道,而没有从中读取,并且只是想知道如果我使子进程的标准输出非阻塞会发生什么。 - Andrew Parker
1个回答

3

这很有趣。我已经发现了几个问题:

情况 1

这是因为sys.stdout.write 要么写入整个字符串,要么抛出异常,在使用 O_NONBLOCK 时不是期望的行为。当底层调用的 write 返回 EAGAIN (在OS X上为Errno 35) 时,应该再次尝试使用剩余的数据。应该使用os.write ,并检查返回值以确保写入了所有数据。

下面的代码可以正常工作:

fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)

def stdout_write(s):
    written = 0
    while written < len(s):
        try:
            written = written + os.write(sys.stdout.fileno(), s[written:])
        except OSError as e:
            pass

stdout_write("A" * 65537)

案例2

我怀疑这个错误消息是由于https://bugs.python.org/issue11380引起的:

close failed in file object destructor:
sys.excepthook is missing
lost sys.stderr

我不确定为什么有时会被调用。可能是因为在except语句中有一个print,它试图使用刚刚失败的stdout

情况 3

这与情况 1 类似。这段代码对我来说总是有效的:

fcntl.fcntl(sys.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)

def stdout_write(s):
    written = 0
    while written < len(s):
        try:
            written = written + os.write(sys.stdout.fileno(), s[written:])
        except OSError as e:
            pass

stdout_write("A" * 65537)

time.sleep(1)

print "Slept"

案例 4

请确保检查 os.write 的返回值,我怀疑完整的 65537 字节没有被成功写入。

案例 5

这与案例 2 类似。


感谢@Tim提供的反馈。在过去的一周中,我没有机会回到这个问题上。我仔细地看了看这个问题。简而言之,sys.stdout.write引发的异常是完全可以预料的(通过查看源代码得知)。如果写入的字节数少于期望值或者底层的FILE句柄的ferror返回非零值,file.write将引发异常。所以现在的谜团就围绕着错误消息。我已经查看了源代码并调试了自己的构建,但还没有完全确定根本原因(将在下一个评论中继续)。 - Andrew Parker
错误发生在解释器关闭时,当解释器关闭标准文件句柄(按顺序为1、0、2即标准输出、标准输入、标准错误输出)时。关闭标准输出时失败(不足为奇)。具体而言,fclose操作失败。它恰好在底层stdoutFILE句柄的_flags被设置为__SERR时发生(这是OSX的libc实现细节)。我原以为是在关闭时调用刷新函数时发生的,但似乎不是这样。无论如何,Python的表现是“不错”的,因为它会在未能正确关闭stdout时打印错误信息。然而,... - Andrew Parker
我不完全理解是什么导致了FILE句柄处于那种状态。我已经尝试在C中重现,但没有成功。我也在解释器中进行了一些修改,但并没有更加明智。我想我可以找到问题,但我已经深入到兔子洞里了,所以需要暂时放下它 :) 我可能会出于好奇心回来看看这个问题。这里可能存在一个奇怪的边缘情况bug,但即使存在这样的情况,也很少有人会这样做! - Andrew Parker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接