Python IOError не может выделять память, хотя есть много

Я написал базовую программу для проверки дерева каталогов, содержащего много файлов jpeg (500000+) убедитесь, что они не повреждены (примерно 3-5% файлов, по-видимому, повреждены), а затем возьмите файл sha1sum (даже поврежденные) и сохраните информацию в базе данных.

Файлы jpeg, о которых идет речь, находятся в системе Windows и устанавливаются в окне linux через cifs. Они в основном размером около 4 мегабайт, хотя некоторые могут быть немного больше или меньше.

Когда я запускаю программу, она работает довольно хорошо некоторое время, а затем она падает с ошибкой ниже. Это было после обработки около 1100 файлов (ошибка показала, что проблема возникла при попытке открыть файл размером 4,5 мегабайта).

Теперь я понимаю, что я могу уловить эту ошибку и продолжить или повторить попытку и т.д., но мне любопытно, почему это происходит в первую очередь, и если ловить и повторить попытку на самом деле собирается решить проблему - или она просто получит (если я не ограничиваю повторные попытки, но затем файл пропущен).

Я использую "Python 2.7.5+" для дебианской системы для ее запуска. Система имеет как минимум 4 гигабайта (возможно, 8) бара, а верхняя часть сообщает, что script использует менее 1% бара и менее 3% процессора в любое время, когда он работает. Аналогично, jpeginfo, который запускает этот script, также использует одинаково небольшие объемы памяти и процессора.

Чтобы избежать слишком большого объема памяти при чтении файлов, я принял подход, приведенный в этом ответе, к другому вопросу: qaru.site/info/32672/...

Также вы можете заметить, что команда 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

Мне кажется, что Python просто передает ошибку из базового вызова open(), и настоящим виновником здесь является поддержка Linux CIFS. Я сомневаюсь, что Python будет синтезировать ENOMEM, если системная память не будет действительно исчерпана (и вероятно, даже тогда я ожидал, что вместо ENOMEM) будет вызван Linux OOM killer.

К сожалению, может понадобиться что-то вроде эксперта по файловой системе Linux, чтобы выяснить, что там происходит, но посмотрите на источники для CIFS в ядре Linux, Я вижу множество мест, где 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 script. Вы можете поймать исключение и повторить попытку (возможно, после небольшой задержки), как вы уже делаете, если jpeginfo не удается, но трудно сказать, насколько успешной эта стратегия будет, не зная, что вызывает ошибки в первую очередь.

Вы могли бы, конечно, скопировать файлы локально, но похоже, что это будет серьезная боль, так как их так много.

EDIT:. В стороне, вы ожидаете увидеть множество вызовов open(), которые не имеют ничего общего с вашим script, потому что strace отслеживает каждый вызов, сделанный Python, который включает его, например, открытие собственных файлов .py и .pyc. Просто игнорируйте те, которые не относятся к интересующим вас файлам.