Python IOError虽然有很多,但无法分配内存

时间:2013-07-04 13:39:32

标签: python ioerror

我编写了一个基本程序来检查包含许多jpeg文件(500000+)的目录树 验证它们是否已损坏(大约3-5%的文件似乎在某种程度上已损坏),然后获取文件的sha1sum(甚至是损坏的文件)并将信息保存到数据库中。

有问题的jpeg文件位于Windows系统上,并通过cifs安装在linux盒子上。它们的大小大多在4兆字节左右,尽管有些可能略大或略小。

当我运行该程序时,它似乎运行了一段时间,然后它会因以下错误而崩溃。这是在它处理了大约1100个文件之后(错误表明在尝试打开4.5兆字节的文件时发生了问题)。

现在我明白我可以抓住这个错误并继续或重试等但我很好奇为什么它首先发生,如果捕获和重试实际上是要解决问题 - 或者它会得到它卡住了重试(除非我限制了当然的重试,但后来又跳过了一个文件)。

我在debian系统上使用“Python 2.7.5+”来运行它。该系统至少有4 Gig(可能是8)的ram和top报告说该脚本在运行时随时使用不到1%的RAM和不到3%的cpu。类似地,这个脚本运行的jpeginfo也使用同样少量的内存和cpu。

为了避免在阅读文件时使用太多内存,我采用了另一个问题的答案中给出的方法:https://stackoverflow.com/a/1131255/289545

另外,您可能会注意到“jpeginfo”命令在while循环中寻找“[OK]”响应。 这是因为如果“jpeginfo”认为它找不到文件,则返回0,因此subprocess.check_output调用不会将其视为错误状态。

我确实想知道jpeginfo在第一次尝试时似乎无法找到某些文件的事实可能是相关的(我怀疑它是)但是返回的错误说不能分配内存而不是找不到文件。

错误:

Traceback (most recent call last):
  File "/home/m3z/jpeg_tester", line 95, in <module>
    main()
  File "/home/m3z/jpeg_tester", line 32, in __init__
    self.recurse(self.args.dir, self.scan)
  File "/home/m3z/jpeg_tester", line 87, in recurse
    cmd(os.path.join(root, name))
  File "/home/m3z/jpeg_tester", line 69, in scan
    with open(filepath) as f:
IOError: [Errno 12] Cannot allocate memory: '/path/to/file name.jpg'

完整的程序代码:

  1 #!/usr/bin/env python
  2
  3 import os
  4 import time
  5 import subprocess
  6 import argparse
  7 import hashlib
  8 import oursql as sql
  9
 10
 11
 12 class main:
 13     def __init__(self):
 14         parser = argparse.ArgumentParser(description='Check jpeg files in a given directory for errors')
 15         parser.add_argument('dir',action='store', help="absolute path to the directory to check")
 16         parser.add_argument('-r, --recurse', dest="recurse", action='store_true', help="should we check subdirectories")
 17         parser.add_argument('-s, --scan', dest="scan", action='store_true', help="initiate scan?")
 18         parser.add_argument('-i, --index', dest="index", action='store_true', help="should we index the files?")
 19
 20         self.args = parser.parse_args()
 21         self.results = []
 22
 23         if not self.args.dir.startswith("/"):
 24                 print "dir must be absolute"
 25                 quit()
 26
 27         if self.args.index:
 28                 self.db = sql.connect(host="localhost",user="...",passwd="...",db="fileindex")
 29                 self.cursor = self.db.cursor()
 30
 31         if self.args.recurse:
 32                 self.recurse(self.args.dir, self.scan)
 33         else:
 34                 self.scan(self.args.dir)
 35
 36         if self.db:
 37                 self.db.close()
 38
 39         for line in self.results:
 40                 print line
 41
 42
 43
 44     def scan(self, dirpath):
 45         print "Scanning %s" % (dirpath)
 46         filelist = os.listdir(dirpath)
 47         filelist.sort()
 48         total = len(filelist)
 49         index = 0
 50         for filen in filelist:
 51                 if filen.lower().endswith(".jpg") or filen.lower().endswith(".jpeg"):
 52                         filepath = os.path.join(dirpath, filen)
 53                         index = index+1
 54                         if self.args.scan:
 55                                 try:
 56                                         procresult = subprocess.check_output(['jpeginfo','-c',filepath]).strip()
 57                                         while "[OK]" not in procresult:
 58                                                 time.sleep(0.5)
 59                                                 print "\tRetrying %s" % (filepath)
 60                                                 procresult = subprocess.check_output(['jpeginfo','-c',filepath]).strip()
 61                                         print "%s/%s: %s" % ('{:>5}'.format(str(index)),total,procresult)
 62                                 except subprocess.CalledProcessError, e:
 63                                         os.renames(filepath, os.path.join(dirpath, "dodgy",filen))
 64                                         filepath = os.path.join(dirpath, "dodgy", filen)
 65                                         self.results.append("Trouble with: %s" % (filepath))
 66                                         print "%s/%s: %s" % ('{:>5}'.format(str(index)),total,e.output.strip())
 67                         if self.args.index:
 68                                 sha1 = hashlib.sha1()
 69                                 with open(filepath) as f:
 70                                         while True:
 71                                                 data = f.read(8192)
 72                                                 if not data:
 73                                                         break
 74                                                 sha1.update(data)
 75                                 sqlcmd = ("INSERT INTO `index` (`sha1`,`path`,`filename`) VALUES (?, ?, ?);", (buffer(sha1.digest()), dirpath, filen))
 76                                 self.cursor.execute(*sqlcmd)
 77
 78
 79     def recurse(self, dirpath, cmd, on_files=False):
 80         for root, dirs, files in os.walk(dirpath):
 81             if on_files:
 82                 for name in files:
 83                     cmd(os.path.join(root, name))
 84             else:
 85                 cmd(root)
 86                 for name in dirs:
 87                     cmd(os.path.join(root, name))
 88
 89
 90
 91
 92
 93
 94 if __name__ == "__main__":
 95     main()

1 个答案:

答案 0 :(得分:4)

在我看来,Python只是在传递基础open()调用的错误,而真正的罪魁祸首是Linux CIFS支持 - 我怀疑Python会合成ENOMEM,除非系统内存是真的很累(甚至可能在那时我希望调用Linux OOM killer而不是获取ENOMEM)。

不幸的是,它可能需要一些Linux文件系统专家来弄清楚那里发生了什么,但是看看sources for CIFS in the Linux kernel,我可以看到各种内核时返回ENOMEM的各种地方 - 特定资源已耗尽,而不是整个系统内存,但我不熟悉它,说明它们有多可能。

要排除任何特定于Python的内容,您可以在strace下运行该过程,这样您就可以看到Python从Linux获得的确切返回代码。为此,请运行以下命令:

strace -eopen -f python myscript.py myarg1 myarg2 2>strace.log

-f将遵循子进程(即您运行的jpeginfo命令),-eopen只显示open()次调用,而不是所有系统调用(这是strace默认执行的操作。这可以生成合理数量的输出,这就是我在上面的示例中将其重定向到文件的原因,但如果您愿意,可以将其显示在终端上。

我希望你在得到异常之前就会看到类似的内容:

open("/path/to/file name.jpg", O_RDONLY) = -1 ENOMEM (Cannot allocate memory)

如果是这样,这个错误直接来自文件系统open()调用,而你在Python脚本中几乎无法做到这一点。如果jpeginfo失败,您可以捕获异常并重试(可能是在短暂的延迟之后),但很难说这个策略在不知道导致错误的原因的情况下会有多成功。

当然,您可以在本地复制这些文件,但听起来很麻烦,因为有很多文件。

编辑:顺便说一下,您会看到很多open()次调用与您的脚本无关,因为strace正在跟踪每个 Python调用,包括打开自己的.py.pyc文件。只要忽略那些没有引用你感兴趣的文件的那些。