Сложность поиска дубликатов файлов в bash

мне пришлось написать сценарий Bash для удаления дубликатов файлов сегодня, используя их хэши md5. Я сохранил эти хэши как файлы во временном каталоге:

for i in * ; do
    hash=$(md5sum /tmp/msg | cut -d " " -f1) ;
    if [ -f /tmp/hashes/$hash ] ;
    then
        echo "Deleted $i" ;
        mv $i /tmp/deleted ;
    else
        touch /tmp/hashes/$hash ;
    fi ;
done

это сработало отлично, но заставило меня задаться вопросом: это эффективный во времени способ сделать это? Сначала я думал о хранении хэшей MD5 в файле, но затем я подумал: "Нет, потому что проверка того, находится ли данный MD5 в этом файле, требует перечитывать его полностью каждый раз". Теперь мне интересно: это то же самое при использовании " create файлы в каталоге " метод? Имеет ли Bash [ -f ] проверить линейную или квазипостоянную сложность, когда в одном каталоге много файлов?

Если это зависит от файловой системы, какова сложность tmpfs?

4 ответов


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

во-первых, поиск каталогов tmpfs полагается (в ядре) на поиск хэш-таблицы кэша записей каталогов, которые не так чувствительны к количеству файлов в вашем каталоге. Они затронуты, но сублинейно. Это связано с тем, что правильный поиск хэш-таблицы занимает некоторое постоянное время,O(1), независимо от количество элементов в хэш-таблице.

объяснить, мы можем посмотреть на работы test -f или [ -f X ], из coreutils (gitweb будет):

case 'e':
   unary_advance ();
   return stat (argv[pos - 1], &stat_buf) == 0;
... 
case 'f':                   /* File is a file? */
   unary_advance ();
   /* Under POSIX, -f is true if the given file exists
      and is a regular file. */
   return (stat (argv[pos - 1], &stat_buf) == 0
           && S_ISREG (stat_buf.st_mode));

поэтому он использует stat() на имя напрямую. Никакой список каталогов не выполняется явно test, но и время выполнения stat может зависеть от количества файлов в каталоге. Время завершения для stat вызов будет зависеть от unterlying файловой системы реализация.

для каждой файловой системы stat разделит путь на компоненты каталога и проведет его вниз. Например, для пути /tmp/hashes/the_md5: во-первых /, получает свой индекс, затем смотрит вверх tmp внутри него, получает этот индекс (это новая точка монтирования), затем получает hashes inode, и, наконец, затем тестовое имя файла и его индекс. Вы можете ожидать, что inodes полностью /tmp/hashes/ кэшироваться, потому что они повторяются на каждой итерации, поэтому эти поиски являются быстрыми и вероятно, не требуется доступ к диску. Каждый поиск будет зависеть от файловой системы, в которой находится родительский каталог. После /tmp/ часть, поиск происходит на tmpfs (что все в памяти, за исключением случаев, когда у вас заканчивается память и вам нужно использовать swap).

tmpfs в linux полагается на simple_lookup для получения индекса файла в каталоге. tmpfs находится под своим старым именем в дереве linux mm / shmem.c . tmpfs, как и ramfs, похоже, не реализует данные собственные структуры для отслеживания виртуальных данных он просто полагается на кэши записей каталога VFS (под Кэш Записей Каталога).

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

другие файловые системы как и Ext2 / 3, однако, будет нести линейный штраф с количеством файлов, присутствующих в каталоге.

хранение их в

как предлагали другие, вы также можете хранить MD5 в памяти, сохраняя их в переменных bash, и избегать штрафов файловой системы (и связанных syscall). Сохранение их в файловой системе имеет то преимущество, что вы можете возобновить с того места, где вы оставили, если вы должны были прервать цикл (ваш md5 может быть символической ссылкой на файл, дайджест которого соответствует, на который вы могли бы положиться, при последующих запусках), но медленнее.

MD5=d41d8cd98f00b204e9800998ecf8427e
let SEEN_${MD5}=1
...
digest=$(md5hash_of <filename>)
let exists=SEEN_$digest
if [[ "$exists" == 1 ]]; then
   # already seen this file
fi

более быстрые тесты

и вы можете использовать [[ -f my_file ]] вместо [ -f my_file ]. Команда [[ является встроенным bash и намного быстрее, чем нерест нового процесса (/usr/bin/[) для каждого сравнения. Это будет иметь еще большее значение.

что такое / usr/bin / [

/usr/bin/test и /usr/bin/[ два разных программы, но исходный код для [ (lbracket.c) это то же самое, что и тест.c (снова в coreutils):

#define LBRACKET 1
#include "test.c"

таким образом, они взаимозаменяемы.


Я поклонник использования правильного инструмента для работы. В этом случае, вы хотите видеть только дубликаты файлов. Я протестировал это против нескольких тысяч файлов в моем распоряжении, и перечитывание файла, похоже, не имело никаких проблем. Кроме того, я заметил, что у меня есть сотни дубликатов файлов. Когда я храню хэши в отдельных файлах, а затем обрабатываю это большое количество файлов, моя система медленно ползет после примерно 10 000 хэш-файлов в одном каталоге. Наличие всех хэшей в одном файле сильно ускорил это.

# This uses md5deep.  An alternate is presented later.
md5deep -r some_folder > hashes.txt

# If you do not have md5deep
find . -type f -exec md5sum \{\} \;

это дает вам хэши все.

cut -b -32 hashes.txt | sort | uniq -d > dupe_hashes.txt

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

(for hash in $(cat dupe_hashes.txt); do
    grep "^$hash" hashes.txt | tail -n +2 | cut -b 35-
done) > dupe_files.txt

это, кажется, не работает медленно для меня. Ядро Linux делает очень хорошую работу, сохраняя файлы в памяти, а не читая их с диска часто. Если вы предпочитаете заставить это быть в памяти, вы можете просто использовать /dev/shm/hashes.txt вместо hashes.txt. Я обнаружил, что в моих тестах в этом не было необходимости.

это дает вам каждый файл, который является дубликатом. Пока все идет хорошо. Возможно, вы захотите просмотреть этот список. Если вы хотите также перечислить исходный, удалите tail -n +2 | бит из команды.

когда вам удобно, что вы можете удалить каждый указанный файл, вы можете передать вещи xargs. Это приведет к удалению файлов в группах 50.

xargs -L 50 rm < dupe_files.txt

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

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


Я не" хэшировал " это, но я бы попытался сохранить ваши md5sums в хэше bash.

посмотреть как определить хэш-таблицы в Bash?

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