Python子进程文件描述符耗尽
我有一个运行很久的Python项目,它使用subprocess模块来启动其他程序。它会等每个程序完成后,再结束这个包装函数,然后回到等待循环中。
但是,最终这个项目让运行它的电脑变得非常慢,出现了“没有更多文件描述符可用”的错误。
我在subprocess文档里找不到关于子进程关闭时文件描述符会发生什么的说明。起初,我以为它们会自动关闭,因为subprocess.call()命令会等到子进程结束。
但如果真是这样,我就不会遇到问题了。我还以为如果有剩余的文件描述符,Python会在函数结束时自动清理它们,因为这些文件描述符会超出作用域。但看起来也不是这样。
我该如何访问这些文件描述符呢?subprocess.call()函数只返回退出代码,并不返回打开的文件描述符。我是不是漏掉了什么?
这个项目就像是不同企业应用之间的粘合剂。这些应用不能串联在一起,而且它们是图形用户界面系统。所以,我能做的就是用它们内置的宏来启动它们。这些宏会输出文本文件,我用这些文件作为下一个程序的输入。
是的,情况确实糟糕。幸运的是,所有的文件名都相当独特。所以在接下来的几天里,我会使用下面建议的sys internals工具来尝试找到这些文件。我会告诉你结果如何。
大部分文件我并不打开,只是用win32file.CopyFile()函数把它们移动。
4 个回答
这个问题在我进行了一次大规模的代码重构后就解决了,所以我在这里想记录一下,我遇到的问题之一是寻找Python的内存调试工具。
后来我找到了heapy这个工具。
你正在使用哪个版本的Python?
有一个已知的问题,就是使用subprocess.Popen()时会出现文件描述符泄漏,这个问题可能也会影响到subprocess.call()。
http://bugs.python.org/issue6274
正如你所看到的,这个问题只在Python 2.6版本中被修复了。
我也遇到过同样的问题。
我们经常在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中的另一种行为问题,关于这两个问题的更多细节可以在这里查看。