Python子进程文件描述符耗尽问题

7
我有一个长时间运行的Python项目,使用子进程模块启动各种其他程序。它等待每个程序完成,然后结束包装函数并返回到其等待循环。
最终,这会使计算机停滞不前,并出现无法再使用文件描述符的错误。
我无法在 subprocess文档中找到任何有关子进程关闭时文件描述符会发生什么情况的信息。起初,我认为它们会自动关闭,因为subprocess.call()命令会等待子进程终止。
但如果是这样的话,我就不会有问题了。我还以为如果还有什么东西剩下来,Python会在函数完成时进行垃圾回收,文件描述符会超出范围。但这似乎也不是这种情况。
我该如何访问这些文件描述符? subprocess.call()函数只返回退出代码,而不是打开的文件描述符。我是否还有其他遗漏的地方?
这个项目作为各种企业应用程序之间的粘合剂。这些应用程序无法进行流水线处理,它们都是图形用户界面系统。因此,我能做的唯一一件事就是使用它们内置的宏启动它们。这些宏会输出文本文件,我将这些文件用于管道中的下一个程序。
是的,情况确实像听起来的那样糟糕。幸运的是,所有文件最终都具有相当独特的名称。因此,在接下来的几天里,我将使用下面建议的sys internals工具来尝试跟踪该文件。我会告诉你结果如何。
大多数文件我不打开,只是使用win32file.CopyFile()函数移动它们。

也许你正在运行一个打开另一个进程的过程?然后当你的进程结束时,你认为你已经清理干净了,但实际上并没有? 你是否检查了ps/top/task manager以查看是否有正在运行的进程? - RoeeK
这个使用子进程模块启动其他程序的Python项目是否正在构建管道或重定向子进程的stdin或stdout?如果是,您应该总结一下这个模块中正在发生的事情。 - S.Lott
4个回答

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”和尊重“短码”含义的详细信息,请参见《stat》的Python文档

测试文件描述符行为

在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上的行为的不同问题,您可以在此处查看更多详细信息


太棒了!这就解释了为什么我的重构修复了问题,因为Popen实例已经完全被销毁了。之前我曾经假设当它们超出作用域时gc会将它们清理掉,但是打开的管道可能让它们继续存在着。 - Spencer Rathbun
很可能你的代码中还有一些对Popen实例的持久引用,这导致垃圾回收器无法销毁它。这个问题与文件描述符继承问题密切相关。请查看我在inherited file descriptors in Python上进行的更深入挖掘。 - mihalop

2
你使用的Python版本是什么? 已知subprocess.Popen()存在文件描述符泄漏问题,这也可能影响subprocess.call()。

http://bugs.python.org/issue6274

正如你所看到的,这只在 Python 2.6 中得到修复。

1
我目前正在使用2.7版本,所以我不认为那是一个问题。尽管如果它曾经发生过一次,它可能会再次发生... - Spencer Rathbun

1
问题在进行了重构后消失了,所以我在这里只是想记录一下我遇到的问题的一部分是寻找 Python 的内存调试工具。
此后,我已经找到了heapy

我在这个问题上也遇到了和你一样的问题。我也使用了 subprocess 运行了一个长时间运行的 Python 脚本。现在,我已经用尽了文件描述符,请问您能否告诉我更多关于您的“重构”是如何解决这个问题的呢? 谢谢。 - HVNSweeting
1
@HVNSweeting 我将代码库分解成具有明确定义角色的单个对象。只有一个对象负责运行子进程,而它是由一个主长时间运行的对象创建和销毁的,该对象本身不打开任何文件。这样,当处理对象完成时,清理它可以切断引用链,垃圾回收器可以进行清理。 - Spencer Rathbun
我解决了我的问题,并发现它与子进程无关。是我的错误在出现错误时没有关闭套接字。谢谢你的帮助。 - HVNSweeting

0

当进程结束时,文件描述符会消失,因此必须是父进程持有文件描述符(您可以使用 lsof 验证此操作)。那么父进程中的代码是做什么的呢?


这个项目是各种不同外部程序之间的“粘合剂”。它会监视一个目录以寻找新文件,并按顺序启动每个外部进程。为了让它们运行良好,每完成一个进程后都必须移动一些文件。不幸的是,它必须在 Windows 机器上运行,所以我没有 lsof。 - Spencer Rathbun
1
在Windows上,Sysinternals Process Monitor也可以做同样的事情。不幸的是,这两个工具都无法帮助您确定Python中哪些文件被打开。 - geekosaur

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