如何在获取所需数据后关闭Python 2.5.2的Popen子进程?

9 投票
4 回答
6797 浏览
提问于 2025-04-16 05:00

我正在使用以下版本的Python:

$ /usr/bin/env python --version                                                                                                                                                            
Python 2.5.2                                    

我运行了以下Python代码,目的是将一个子进程中的数据写入标准输出,并把这些数据读入一个叫做metadata的变量中:

# Extract metadata (snippet from extractMetadata.py)
inFileAsGzip = "%s.gz" % inFile                                                                                                                                                                                                            
if os.path.exists(inFileAsGzip):                                                                                                                                                                                                           
    os.remove(inFileAsGzip)                                                                                                                                                                                                                
os.symlink(inFile, inFileAsGzip)                                                                                                                                                                                                           
extractMetadataCommand = "bgzip -c -d -b 0 -s %s %s" % (metadataRequiredFileSize, inFileAsGzip)                                                                                                                                            
metadataPipes = subprocess.Popen(extractMetadataCommand, stdin=None, stdout=subprocess.PIPE, shell=True, close_fds=True)                                                                                                      
metadata = metadataPipes.communicate()[0]                                                                                                                                                                                                                                                                                                                                                                                                          
metadataPipes.stdout.close()                                                                                                                                                                                                             
os.remove(inFileAsGzip) 
print metadata

这个用例是这样的,我想从上面提到的代码片段中提取前十行标准输出:

$ extractMetadata.py | head

如果我把输出通过管道传递给head、awk、grep等命令,就会出现错误。

脚本最后会显示以下错误:

close failed: [Errno 32] Broken pipe

我本以为关闭管道就足够了,但显然并不是这样。

4 个回答

0

这里的信息不够多,无法给出确切的答案,但我可以做一些推测。

首先,os.remove 绝对不应该出现 EPIPE 错误。看起来也确实没有;错误信息是 close failed: [Errno 32] Broken pipe,而不是 remove failed。所以问题出在 close,而不是 remove

关闭管道的标准输出可能会导致这个错误。如果有数据在缓冲区中,Python 会在关闭文件之前先把数据写出去。如果底层的进程已经消失,执行这个操作就会引发 IOError/EPIPE 错误。不过要注意,这并不是致命错误:即使发生这种情况,文件仍然会被关闭。下面的代码大约有 50% 的概率会重现这个情况,并且证明在出现异常后文件是关闭的。(要小心;我觉得 bufsize 的行为在不同版本中可能有所变化。)

    import os, subprocess
    metadataPipes = subprocess.Popen("echo test", stdin=subprocess.PIPE,
        stdout=subprocess.PIPE, shell=True, close_fds=True, bufsize=4096)
    metadataPipes.stdin.write("blah"*1000)
    print metadataPipes.stdin
    try:
        metadataPipes.stdin.close()
    except IOError, e:
        print "stdin after failure: %s" % metadataPipes.stdin

这个情况是有点随机的;它只会在部分时间发生。这可能解释了为什么看起来添加或删除 os.remove 调用会影响错误。

不过,我看不出你提供的代码会出现这种情况,因为你并没有写入标准输入。虽然这是我能想到的最接近的情况,但没有一个可用的重现示例,可能也只能给你指个方向。

另外,你在删除一个可能不存在的文件之前,不应该检查 os.path.exists;如果另一个进程同时删除了这个文件,会导致竞争条件。相反,你应该这样做:

try:
    os.remove(inFileAsGzip)
except OSError, e:
    if e.errno != errno.ENOENT: raise

... 我通常会把它封装在一个像 rm_f 的函数里。

最后,如果你想明确地终止一个子进程,可以使用 metadataPipes.kill——仅仅关闭它的管道是无法做到的——但这并不能解释错误。此外,如果你只是读取 gzip 文件,使用 gzip 模块会比使用子进程好得多。http://docs.python.org/library/gzip.html

1

我觉得这个异常和 subprocess 的调用或者它的文件描述符没有关系(在调用 communicate 后,popen 对象就关闭了)。这似乎是一个经典的问题,就是在管道中关闭了 sys.stdout

http://bugs.python.org/issue1596

尽管这个问题已经存在三年了,但仍然没有解决。由于 sys.stdout.write(...) 似乎也没有帮助,你可以尝试使用更底层的调用,试试这个:

os.write(sys.stdout.fileno(), metadata)
4

嗯,我之前在用子进程和gzip的时候也遇到过一些“断管”的奇怪问题。我一直没搞清楚为什么会这样,但通过改变我的实现方式,我成功避免了这个问题。看起来你只是想用一个后端的gzip进程来解压文件(可能是因为Python自带的模块速度慢得离谱……我也不知道为什么,但确实是这样)。

与其使用 communicate(),不如把这个进程当作一个完全异步的后端,直接读取它的输出,等数据来了就处理。当这个进程结束时,子进程模块会帮你处理清理工作。下面的代码片段应该能提供相同的基本功能,而且不会出现断管的问题。

import subprocess

gz_proc = subprocess.Popen(['gzip', '-c', '-d', 'test.gz'], stdout=subprocess.PIPE)

l = list()
while True:
    dat = gz_proc.stdout.read(4096)
    if not d:
        break
    l.append(d)

file_data = ''.join(l)

撰写回答