【问题标题】:Python subprocess running out of file descriptorsPython子进程用完文件描述符
【发布时间】:2014-05-20 22:52:22
【问题描述】:

我有一个长期运行的 python 项目,它使用 subprocess 模块来启动各种其他程序。它等待每个程序完成,然后结束包装函数并返回其等待循环。

最终,这会使正在运行它的计算机停止运行,并显示没有更多可用文件描述符的错误。

我无法在subprocess docs 中找到子进程关闭时文件描述符会发生什么情况。起初,我以为它们会自动关闭,因为 subprocess.call() 命令一直等到子进程终止。

但如果是这样的话,我就不会有问题了。我还认为,如果有任何剩余,python 会在函数完成时对它进行垃圾收集,并且文件描述符超出范围。但这似乎也并非如此。

如何访问这些文件描述符? subprocess.call() 函数只返回退出代码,不返回打开的文件描述符。我这里还有什么遗漏的吗?

此项目充当各种企业应用之间的粘合剂。所述应用程序无法流水线化,它们是 gui 系统。所以,我唯一能做的就是用它们的内置宏启动它们。这些宏输出文本文件,我将其用于管道中的下一个程序。

是的,它和听起来一样糟糕。幸运的是,所有文件最终都有非常独特的名称。所以,在接下来的几天里,我将使用下面建议的 sys internals 工具来尝试追踪文件。我会让你知道结果如何。

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

【问题讨论】:

  • 也许您正在运行的进程会打开另一个进程?那么当您的过程结束时,您认为您已清洁但实际上并非如此?你检查 ps/top/task manager 看看你是否有正在运行的进程?
  • 这个“使用子进程模块启动各种其他程序的python项目”是为子进程构建管道还是重定向标准输入或标准输出?如果是这样,你应该总结一下这个模块发生了什么。

标签: python garbage-collection file-descriptor


【解决方案1】:

我也遇到了同样的问题。

我们经常使用 subprocess.Popen() 在 Windows 环境中调用外部工具。在某些时候,我们遇到了没有更多文件描述符可用的问题。我们深入研究了这个问题,发现 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 documentation on 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 步中的差异显示了有关 Windows 中 Python 解释器行为的不同问题,您可以查看有关这两个问题的更多详细信息 here

【讨论】:

  • 精彩的答案!这可以解释为什么我的重构解决了这个问题,因为 Popen 实例确实被完全破坏了。我之前曾假设当它们超出范围时 gc 会得到它们,但开放的管道可能会保留它。
  • 很可能在您的代码中某处对 Popen 实例有一些挥之不去的引用,这并没有让 GC 破坏它。这个问题与整体文件描述符继承问题密切相关。看看我在inherited file descriptors in Python 上做的更多“挖掘”。
【解决方案2】:

你用的是什么python版本? 有一个已知的带有 subprocess.Popen() 的文件描述符泄漏,这也可能影响 subprocess.call()

http://bugs.python.org/issue6274

如您所见,这仅在 python-2.6 中修复

【讨论】:

  • 我目前使用的是 2.7,所以我认为这不是问题。虽然如果它发生过一次,它可能会再次发生......
【解决方案3】:

在进行重大重构后问题就消失了,所以我只想在这里指出,我遇到的部分问题是为 python 寻找内存调试工具。

后来我找到了heapy

【讨论】:

  • 我在这个问题上遇到了同样的问题。我也有一个长时间运行的 python 脚本使用子进程。现在,我的文件描述符已经用完了,你能告诉我更多关于你的major refactoring 是如何解决问题的吗?谢谢
  • @HVNSweeting 我将代码库分解为具有明确角色的单个对象。只有一个对象负责运行子进程,它是由一个主长时间运行的对象创建和销毁的,该对象本身不打开任何文件。这样,当处理对象完成时,销毁它干净地剪断引用链,垃圾收集器可以清理。
  • 我解决了我的问题并发现它与子流程无关。发生错误时不关闭套接字是我的错误。感谢您的帮助。
【解决方案4】:

文件描述符在进程完成时消失,因此它必须是持有文件描述符的父级(您可以使用lsof 验证这一点)。父级中的代码是做什么的?

【讨论】:

  • 该项目是各种不同外部程序之间的“胶水”。它监视目录中的新文件,并按顺序启动每个外部进程。为了让它们玩得很好,它必须在每个文件完成后移动一些文件。不幸的是,它必须在 windows 机器上运行,所以我没有 lsof。
  • 在 Windows 上,Sysinternals Process Monitor 会做同样的事情。不幸的是,没有人能帮助您确定在 Python 中文件的打开位置。
猜你喜欢
  • 1970-01-01
  • 2012-04-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多