低级select.poll()从子进程

时间:2017-06-22 18:08:59

标签: python linux python-3.x polling

我使用selectos模块中的低级POSIX工具从连接到正在运行的shell进程的管道中读取数据。为了避免无限期阻塞,我使用stdout模块将管道进程的fcntl文件描述符设置为非阻塞模式,然后使用select.poll轮询文件描述符,直到数据可用阅读一旦数据可用,我使用os.read()从管道读取一些数据,然后继续循环,直到os.read()返回一个空的bytes对象或发生一些错误。

我有它工作,除了某些原因我最终从管道读取的数据被截断。我读了大约一半的管道进程的预期输出,然后os.read()返回一个空的bytes对象。我无法弄清楚为什么我会丢失剩下的数据。

基本上,我有一个run_poll_once()函数,它只能调用poll对象的poll()方法。如果我们应该继续轮询更多数据,函数将返回True,如果我们应该停止,则返回False。该功能如下(为了清晰和相关性,删除并编辑了错误检查):

def run_poll_once(poll):
    events = poll.poll(0.10)
    for fd, event in events:
        if event & select.POLLERR:
            return False

        if (event & select.POLLIN) or (event & select.POLLHUP):
            data = os.read(fd, READ_SIZE)
            print("Read:", data)
            if len(data) == 0: return False
            # ... do stuff with data

    return True

然后我将此函数称为:

with subprocess.Popen(
        ["ls", "-lh"], 
        stdin = None, 
        stdout = subprocess.PIPE, 
        bufsize = 0
    ) as proc:

    # --- snip setting proc.stdout.fileno() to non-blocking mode
    poll = select.poll()
    event_mask = select.POLLIN | select.POLLERR | select.POLLHUP
    poll.register(proc.stdout.fileno(), event_mask)

    while run_poll_once(poll):
        pass

因此,这使我获得了来自管道进程(ls -lh)的预期输出的一半,然后os.read()过早地返回空bytes个对象。那么我在这里做错了什么?

1 个答案:

答案 0 :(得分:2)

好的,回答我自己的问题。

所以,正如评论中提到的,我之前发布了一个答案,然后将其删除。我删除的答案是:

我想通了:显然proc.stdout流对象自动执行自己的内部缓冲,尽管bufsize = 0参数传递给subprocess.Popen。流对象似乎在后台自动缓冲可用​​于在管道的stdout文件描述符上读取的数据。

基本上,我不能使用os.read直接从底层描述符中读取,因为proc.stdout BufferedReader自动执行它自己的操作通过从底层描述符读取来缓冲。为了让我的工作正常,我可以在proc.stdout.read(READ_SIZE)表示有要读取的数据之后直接拨打os.read(fd, READ_SIZE)而不是poll()。这按预期工作。

我删除了它,因为最终我意识到这个解决方案也不太正确。问题在于,即使它可能在大多数时间都有效,但是没有真正的保证这将起作用,因为对poll()的调用只会在实际的低级操作系统时返回POLLIN个事件发生 interrupt ,表示数据可在内核缓冲区中读取。但调用proc.stdout.read()不是直接从内核缓冲区读取...它是从一些内部Python缓冲区读取的。因此POLLIN事件和我们实际阅读的决定之间存在不匹配。事实上,它们完全不相关 - 因此无法保证我们的轮询工作正常,因此无法保证对proc.stdout.read()的调用不会阻塞或丢失字节。

但是如果我们使用os.read(),则无法保证我们对os.read()的调用始终能够直接从内核缓冲区读取所有字节,因为Python BufferedReader对象基本上是"与我们作斗争"做它自己的缓冲。我们都在争夺相同的底层内核缓冲区,而Python BufferedReader有时可能会为自己的缓冲提取字节,然后才能通过调用os.read()来提取这些字节。特别是,我观察到如果子进程退出或意外中止,Python BufferedReader将立即消耗内核读缓冲区中的所有剩余字节,(即使将bufsize设置为0)这就是为什么我丢失了ls -lh的部分输出。

对于无法重现此问题的人,请确保您使用的子进程输出大量数据,例如至少约15K。

那么,解决方案是什么?

解决方案1:

我意识到尝试通过使用我自己的低级系统调用来绕过Python缓冲来尝试对抗Python自己的缓冲设施只是一个非首发。因此,使用subprocess模块基本上是不合适的。我通过os模块直接使用低级操作系统设施重新实现了这一点。基本上,我做了在C中经常做的事情:使用对os.pipe()的调用创建管道文件描述符,然后使用os.fork(),然后使用os.dup()将管道的读取端指向子进程的sys.stdout.fileno()描述符。最后,调用子进程中的os.exec函数之一来开始执行实际的子进程。

除非不是100%正确。除非您碰巧创建一个开始向sys.stdout.fileno()输出大量字节的子进程,否则这几乎在所有时间都有效。在这种情况下,你遇到了OS管道缓冲区的问题,这有一些限制(我认为它在Linux上是65K)。一旦OS管道缓冲区填满,该进程可能会挂起,因为子进程正在使用的任何库进行I / O可能正在进行自己的缓冲。

就我而言,子进程使用C ++ <ostream>工具来进行I / O.这也有自己的缓冲,所以在管道缓冲区填满的某个时候,子进程就会挂起。我从未完全弄明白确切的原因。据推测,如果管道缓冲区已满,它应该挂起 - 但我想如果父进程(我控制)在管道的读取端调用os.read(),子进程可以恢复输出。我怀疑这是孩子进程自己缓冲的另一个问题。 C / C ++标准库输出函数(如C中的printf或C ++中的std::cout)不直接写入stdout,而是执行自己的内部缓冲。我怀疑发生的事情是管道缓冲区已经填满,所以有些调用printfstd::cout只是在无法完全刷新缓冲区后挂起。

所以这让我想到......

解决方案2:

事实证明,使用管道来实现这一点实际上已经彻底打破了。似乎没有人在成千上万的教程中说出这一点,所以也许我错了,但我声称使用管道与子进程通信是一种从根本上被打破的方法。在不同级别上进行的各种缓冲都有太多可能出错的事情。如果您完全控制子进程,您可以使用(在Python中)类似stdout的内容直接写入os.write(1, mybuffer),但大多数情况下您无法控制子进程,大多数程序直接写入stdout,而是使用一些标准的I / O设备,这些设备有自己的缓冲方式。

所以,忘了管道。实现此目的的真正方法是使用伪终端。这可能不那么便携,但它应该适用于大多数符合POSIX标准的平台。伪终端基本上是类似管道的I / O对象,其行为类似于标准控制台输出描述符stdoutstderr。重要的是,对于伪终端,低级别iocontrol系统调用isatty会返回true,因此C中的stdio.h等标准I / O工具会对待管道就像一个行缓冲控制台。

在Python中,您可以使用pty模块创建伪终端。要创建子流程,然后将其stdout连接到父流程中的伪终端,您可以执行以下操作:

out_master, out_slave = pty.openpty()
os.set_inheritable(out_master, True)
os.set_inheritable(out_slave, True)

pid = os.fork()

if pid == 0: # child process
  try:
    assert(os.isatty(out_slave))
    os.dup2(out_slave, sys.stdout.fileno())
    os.close(out_master)
    os.execlp(name_of_child_process, shell_command_to_execute_child_process)
  except Exception as e:
    os._exit(os.EX_OSERR)
else: # parent process
  os.close(out_slave)

现在您可以从out_master读取以获取子进程写入stdout的任何内容的输出,并且由于您正在使用伪终端,因此子进程将完全表现好像它正在输出到控制台 - 所以它完美地工作,没有缓冲问题。当然,您也可以使用stderr执行与上述完全相同的操作。

令人惊讶的是,这个解决方案很简单,但我必须自己发现它,因为几乎所有关于与子进程通信的互联网上的教程或指南都会坚持使用管道,这似乎是一种根本性的破解方法。 / p>