Python子进程文件描述符耗尽

7 投票
4 回答
5700 浏览
提问于 2025-04-16 21:22

我有一个运行很久的Python项目,它使用subprocess模块来启动其他程序。它会等每个程序完成后,再结束这个包装函数,然后回到等待循环中。

但是,最终这个项目让运行它的电脑变得非常慢,出现了“没有更多文件描述符可用”的错误。

我在subprocess文档里找不到关于子进程关闭时文件描述符会发生什么的说明。起初,我以为它们会自动关闭,因为subprocess.call()命令会等到子进程结束。

但如果真是这样,我就不会遇到问题了。我还以为如果有剩余的文件描述符,Python会在函数结束时自动清理它们,因为这些文件描述符会超出作用域。但看起来也不是这样。

我该如何访问这些文件描述符呢?subprocess.call()函数只返回退出代码,并不返回打开的文件描述符。我是不是漏掉了什么?

这个项目就像是不同企业应用之间的粘合剂。这些应用不能串联在一起,而且它们是图形用户界面系统。所以,我能做的就是用它们内置的宏来启动它们。这些宏会输出文本文件,我用这些文件作为下一个程序的输入。

是的,情况确实糟糕。幸运的是,所有的文件名都相当独特。所以在接下来的几天里,我会使用下面建议的sys internals工具来尝试找到这些文件。我会告诉你结果如何。

大部分文件我并不打开,只是用win32file.CopyFile()函数把它们移动。

4 个回答

1

这个问题在我进行了一次大规模的代码重构后就解决了,所以我在这里想记录一下,我遇到的问题之一是寻找Python的内存调试工具。

后来我找到了heapy这个工具。

2

你正在使用哪个版本的Python?

有一个已知的问题,就是使用subprocess.Popen()时会出现文件描述符泄漏,这个问题可能也会影响到subprocess.call()。

http://bugs.python.org/issue6274

正如你所看到的,这个问题只在Python 2.6版本中被修复了。

5

我也遇到过同样的问题。

我们经常在Windows环境中使用subprocess.Popen()来调用外部工具。某个时候,我们遇到了一个问题,就是没有更多的文件描述符可用了。经过深入调查,我们发现subprocess.Popen在Windows和Linux中的表现是不同的。

如果Popen实例没有被销毁(比如说我们保留了对它的引用,这样垃圾回收机制就无法销毁这个对象),那么在Windows中,调用时创建的管道会一直保持打开状态,而在Linux中,调用Popen.communicate()后,这些管道会自动关闭。如果这种情况持续发生,后续的调用中,来自管道的“僵尸”文件描述符会不断堆积,最终导致Python抛出一个异常 IOError: [Errno 24] Too many open files

如何在Python中获取打开的文件描述符

为了排查我们的问题,我们需要一种方法来获取Python脚本中的有效文件描述符。因此,我们编写了以下脚本。请注意,我们只检查0到100之间的文件描述符,因为我们不会同时打开这么多文件。

fd_table_status.py :

import os
import stat

_fd_types = (
    ('REG', stat.S_ISREG),
    ('FIFO', stat.S_ISFIFO),
    ('DIR', stat.S_ISDIR),
    ('CHR', stat.S_ISCHR),
    ('BLK', stat.S_ISBLK),
    ('LNK', stat.S_ISLNK),
    ('SOCK', stat.S_ISSOCK)
)

def fd_table_status():
    result = []
    for fd in range(100):
        try:
            s = os.fstat(fd)
        except:
            continue
        for fd_type, func in _fd_types:
            if func(s.st_mode):
                break
        else:
            fd_type = str(s.st_mode)
        result.append((fd, fd_type))
    return result

def fd_table_status_logify(fd_table_result):
    return ('Open file handles: ' +
            ', '.join(['{0}: {1}'.format(*i) for i in fd_table_result]))

def fd_table_status_str():
    return fd_table_status_logify(fd_table_status())

if __name__=='__main__':
    print fd_table_status_str()

直接运行这个脚本,它会显示所有打开的文件描述符及其类型:

$> python fd_table_status.py
Open file handles: 0: CHR, 1: CHR, 2: CHR
$>

通过Python代码调用fd_table_status_str()时,输出也是一样的。关于“CHR”和相应的“短代码”的具体含义,可以查看Python文档中的stat

测试文件描述符的行为

尝试在Linux和Windows中运行以下脚本:

test_fd_handling.py :

import fd_table_status
import subprocess
import platform

fds = fd_table_status.fd_table_status_str

if platform.system()=='Windows':
    python_exe = r'C:\Python27\python.exe'
else:
    python_exe = 'python'

print '1) Initial file descriptors:\n' + fds()
f = open('fd_table_status.py', 'r')
print '2) After file open, before Popen:\n' + fds()
p = subprocess.Popen(['python', 'fd_table_status.py'],
                     stdin=subprocess.PIPE,
                     stdout=subprocess.PIPE,
                     stderr=subprocess.PIPE)
print '3) After Popen, before reading piped output:\n' + fds()
result = p.communicate()
print '4) After Popen.communicate():\n' + fds()
del p
print '5) After deleting reference to Popen instance:\n' + fds()
del f
print '6) After deleting reference to file instance:\n' + fds()
print '7) child process had the following file descriptors:'
print result[0][:-1]

Linux输出

1) Initial file descriptors:
Open file handles: 0: CHR, 1: CHR, 2: CHR
2) After file open, before Popen:
Open file handles: 0: CHR, 1: CHR, 2: CHR, 3: REG
3) After Popen, before reading piped output:
Open file handles: 0: CHR, 1: CHR, 2: CHR, 3: REG, 5: FIFO, 6: FIFO, 8: FIFO
4) After Popen.communicate():
Open file handles: 0: CHR, 1: CHR, 2: CHR, 3: REG
5) After deleting reference to Popen instance:
Open file handles: 0: CHR, 1: CHR, 2: CHR, 3: REG
6) After deleting reference to file instance:
Open file handles: 0: CHR, 1: CHR, 2: CHR
7) child process had the following file descriptors:
Open file handles: 0: FIFO, 1: FIFO, 2: FIFO, 3: REG

Windows输出

1) Initial file descriptors:
Open file handles: 0: CHR, 1: CHR, 2: CHR
2) After file open, before Popen:
Open file handles: 0: CHR, 1: CHR, 2: CHR, 3: REG
3) After Popen, before reading piped output:
Open file handles: 0: CHR, 1: CHR, 2: CHR, 3: REG, 4: FIFO, 5: FIFO, 6: FIFO
4) After Popen.communicate():
Open file handles: 0: CHR, 1: CHR, 2: CHR, 3: REG, 5: FIFO, 6: FIFO
5) After deleting reference to Popen instance:
Open file handles: 0: CHR, 1: CHR, 2: CHR, 3: REG
6) After deleting reference to file instance:
Open file handles: 0: CHR, 1: CHR, 2: CHR
7) child process had the following file descriptors:
Open file handles: 0: FIFO, 1: FIFO, 2: FIFO

正如你在第4步中看到的,Windows的表现与Linux不同。必须销毁Popen实例才能关闭管道。

顺便提一下,第7步的差异显示了Python解释器在Windows中的另一种行为问题,关于这两个问题的更多细节可以在这里查看。

撰写回答