Fast Linux File Count для большого количества файлов

Я пытаюсь найти лучший способ найти количество файлов в определенном каталоге, когда есть очень большое количество файлов ( > 100 000).

Когда есть много файлов, выполнение "ls | wc -l" занимает довольно много времени. Я считаю, что это потому, что он возвращает имена всех файлов. Я стараюсь как можно меньше использовать диск IO.

Я экспериментировал с некоторыми сценариями оболочки и Perl безрезультатно. Любые идеи?

Ответ 1

По умолчанию ls сортирует имена, которые могут занять некоторое время, если их много. Также не будет выхода, пока все имена не будут прочитаны и отсортированы. Используйте параметр ls -f, чтобы отключить сортировку.

ls -f | wc -l

Обратите внимание, что это также включит -a, поэтому ., .. и другие файлы, начинающиеся с ., будут подсчитаны.

Ответ 2

Самый быстрый способ - специально разработанная программа, например:

#include <stdio.h>
#include <dirent.h>

int main(int argc, char *argv[]) {
    DIR *dir;
    struct dirent *ent;
    long count = 0;

    dir = opendir(argv[1]);

    while((ent = readdir(dir)))
            ++count;

    closedir(dir);

    printf("%s contains %ld files\n", argv[1], count);

    return 0;
}

Из моего тестирования, независимо от кеша, я каждый раз повторял каждую из них примерно по 50 раз по одному и тому же каталогу, чтобы избежать перекоса данных на основе кеша, и я получил примерно следующие показатели производительности (в режиме реального времени ):

ls -1  | wc - 0:01.67
ls -f1 | wc - 0:00.14
find   | wc - 0:00.22
dircnt | wc - 0:00.04

Последняя, ​​ dircnt, является программой, скомпилированной из указанного выше источника.

EDIT 2016-09-26

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

Поскольку ясно, что некоторые люди хотят знать, как это сделать, у меня есть много комментариев в коде, чтобы попытаться сделать очевидным, что происходит. Я написал это и протестировал его на 64-разрядной Linux, но он должен работать на любой POSIX-совместимой системе, включая Microsoft Windows. Отчеты об ошибках приветствуются; Я рад обновить это, если вы не можете заставить его работать с AIX или OS/400 или что-то еще.

Как вы можете видеть, это намного сложнее оригинала и обязательно так: по крайней мере одна функция должна существовать, чтобы ее можно было называть рекурсивно, если вы не хотите, чтобы код стал очень сложным (например, управление стеком подкаталогов и обработкой, которое в одном петля). Поскольку мы должны проверять типы файлов, вступают в игру различия между различными ОС, стандартными библиотеками и т.д., Поэтому я написал программу, которая пытается использоваться в любой системе, где она будет компилироваться.

Проверка ошибок очень мало, а сама функция count не сообщает об ошибках. Единственные вызовы, которые могут действительно терпеть неудачу, - это opendir и stat (если вам не повезло и у вас есть система, в которой dirent уже содержит тип файла). Я не параноик относительно проверки общей длины имен путей subdir, но теоретически система не должна допускать, чтобы какое-либо имя пути больше, чем PATH_MAX. Если есть проблемы, я могу это исправить, но это просто больше кода, который должен быть объяснен тому, кто учится писать C. Эта программа предназначена для того, чтобы быть примером того, как рекурсивно переходить в подкаталоги.

#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <sys/stat.h>

#if defined(WIN32) || defined(_WIN32) 
#define PATH_SEPARATOR '\\' 
#else
#define PATH_SEPARATOR '/' 
#endif

/* A custom structure to hold separate file and directory counts */
struct filecount {
  long dirs;
  long files;
};

/*
 * counts the number of files and directories in the specified directory.
 *
 * path - relative pathname of a directory whose files should be counted
 * counts - pointer to struct containing file/dir counts
 */
void count(char *path, struct filecount *counts) {
    DIR *dir;                /* dir structure we are reading */
    struct dirent *ent;      /* directory entry currently being processed */
    char subpath[PATH_MAX];  /* buffer for building complete subdir and file names */
    /* Some systems don't have dirent.d_type field; we'll have to use stat() instead */
#if !defined ( _DIRENT_HAVE_D_TYPE )
    struct stat statbuf;     /* buffer for stat() info */
#endif

/* fprintf(stderr, "Opening dir %s\n", path); */
    dir = opendir(path);

    /* opendir failed... file likely doesn't exist or isn't a directory */
    if(NULL == dir) {
        perror(path);
        return;
    }

    while((ent = readdir(dir))) {
      if (strlen(path) + 1 + strlen(ent->d_name) > PATH_MAX) {
          fprintf(stdout, "path too long (%ld) %s%c%s", (strlen(path) + 1 + strlen(ent->d_name)), path, PATH_SEPARATOR, ent->d_name);
          return;
      }

/* Use dirent.d_type if present, otherwise use stat() */
#if defined ( _DIRENT_HAVE_D_TYPE )
/* fprintf(stderr, "Using dirent.d_type\n"); */
      if(DT_DIR == ent->d_type) {
#else
/* fprintf(stderr, "Don't have dirent.d_type, falling back to using stat()\n"); */
      sprintf(subpath, "%s%c%s", path, PATH_SEPARATOR, ent->d_name);
      if(lstat(subpath, &statbuf)) {
          perror(subpath);
          return;
      }

      if(S_ISDIR(statbuf.st_mode)) {
#endif
          /* Skip "." and ".." directory entries... they are not "real" directories */
          if(0 == strcmp("..", ent->d_name) || 0 == strcmp(".", ent->d_name)) {
/*              fprintf(stderr, "This is %s, skipping\n", ent->d_name); */
          } else {
              sprintf(subpath, "%s%c%s", path, PATH_SEPARATOR, ent->d_name);
              counts->dirs++;
              count(subpath, counts);
          }
      } else {
          counts->files++;
      }
    }

/* fprintf(stderr, "Closing dir %s\n", path); */
    closedir(dir);
}

int main(int argc, char *argv[]) {
    struct filecount counts;
    counts.files = 0;
    counts.dirs = 0;
    count(argv[1], &counts);

    /* If we found nothing, this is probably an error which has already been printed */
    if(0 < counts.files || 0 < counts.dirs) {
        printf("%s contains %ld files and %ld directories\n", argv[1], counts.files, counts.dirs);
    }

    return 0;
}

EDIT 2017-01-17

Я включил два изменения, предложенные @FlyingCodeMonkey:

  • Используйте lstat вместо stat. Это изменит поведение программы, если у вас есть символические каталоги в сканируемом каталоге. Предыдущее поведение заключалось в том, что в подкаталоге (связанный) будет добавлен счетчик файлов к общему счету; новое поведение заключается в том, что связанный каталог будет считаться одним файлом, а его содержимое не будет считаться.
  • Если путь к файлу слишком длинный, будет выведено сообщение об ошибке, и программа остановится.

EDIT 2017-06-29

Если повезет, это будет последнее изменение этого ответа:)

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

Источник доступен под лицензией Apache 2.0. Патчи * приветствуются!


  • "патч" - это то, что старые люди вроде меня называют "запрос тянуть".

Ответ 3

Вы пытались найти? Например:

find . -name "*.ext" | wc -l

Ответ 4

find, ls и perl проверены на 40 000 файлов: с той же скоростью (хотя я не пытался очистить кеш):

[[email protected] logs]$ time find . | wc -l
42917

real    0m0.054s
user    0m0.018s
sys     0m0.040s
[[email protected] logs]$ time /bin/ls -f | wc -l
42918

real    0m0.059s
user    0m0.027s
sys     0m0.037s

и с perl opendir/readdir, в то же время:

[[email protected] logs]$ time perl -e 'opendir D, "."; @files = readdir D; closedir D; print scalar(@files)."\n"'
42918

real    0m0.057s
user    0m0.024s
sys     0m0.033s

note: Я использовал /bin/ls -f, чтобы обойти опцию псевдонима, которая могла немного замедлить и -f, чтобы избежать упорядочения файлов. ls без -f в два раза медленнее, чем find/perl за исключением случаев, когда ls используется с -f, это похоже на одно и то же время:

[[email protected] logs]$ time /bin/ls . | wc -l
42916

real    0m0.109s
user    0m0.070s
sys     0m0.044s

Я также хотел бы, чтобы некоторые script запрашивали файловую систему напрямую без всякой ненужной информации.

тесты, основанные на ответе Питера ван дер Хейдена, glenn jackman и mark4o.

Томас

Ответ 5

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

dir=/tmp/count_these/ ; for i in $(ls -1 ${dir} | sort -n) ; { echo "$i => $(find ${dir}${i} -type f | wc -l),"; }

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

Результат примерно так:

1 => 38,
65 => 95052,
66 => 12823,
67 => 10572,
69 => 67275,
70 => 8105,
71 => 42052,
72 => 1184,

Ответ 6

Удивительно для меня, голая находка очень сопоставима с ls -f

> time ls -f my_dir | wc -l
17626

real    0m0.015s
user    0m0.011s
sys     0m0.009s

против

> time find my_dir -maxdepth 1 | wc -l
17625

real    0m0.014s
user    0m0.008s
sys     0m0.010s

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

Ответ 7

Вы можете попробовать, если использовать opendir() и readdir() в Perl быстрее. Для примера этой функции смотрите здесь

Ответ 8

Просто добавьте это ради полноты. Правильный ответ, конечно, уже был опубликован кем-то другим, но вы также можете получить количество файлов и каталогов с помощью древовидной программы.

Запустите команду tree | tail -n 1, чтобы получить последнюю строку, которая скажет что-то вроде "763 каталогов, 9290 файлов". Это рассчитывает файлы и папки рекурсивно, за исключением скрытых файлов, которые можно добавить с флагом -a. Для справки, это заняло 4,8 секунды на моем компьютере, поскольку дерево подсчитывало весь мой домашний каталог, который был 24777 каталогами, 238680 файлов. find -type f | wc -l заняло 5,3 секунды, на полсекунды больше, поэтому я думаю, что дерево довольно конкурентоспособно по скорости.

Пока у вас нет подпапок, дерево - это быстрый и простой способ подсчета файлов.

Кроме того, и исключительно для удовольствия, вы можете использовать tree | grep '^├', чтобы отображать только файлы/папки в текущем каталоге - это в основном гораздо более медленная версия ls.

Ответ 9

ls тратит больше времени на сортировку имен файлов, используя -f, чтобы отключить сортировку, будет когда-нибудь сэкономить:

ls -f | wc -l

или вы можете использовать find:

find . -type f | wc -l

Ответ 10

Я пришел сюда, пытаясь подсчитать файлы в наборе данных из ~ 10K папок с ~ 10K файлами. Проблема со многими подходами заключается в том, что они неявно стат 100M файлы, которые занимают много времени.

Я взял на себя смелость расширить подход christopher-schultz, чтобы он поддерживал передачу каталогов через args (его рекурсивный подход также использует stat).

Поместите следующее в файл dircnt_args.c:

#include <stdio.h>
#include <dirent.h>

int main(int argc, char *argv[]) {
    DIR *dir;
    struct dirent *ent;
    long count;
    long countsum = 0;
    int i;

    for(i=1; i < argc; i++) {
        dir = opendir(argv[i]);
        count = 0;
        while((ent = readdir(dir)))
            ++count;

        closedir(dir);

        printf("%s contains %ld files\n", argv[i], count);
        countsum += count;
    }
    printf("sum: %ld\n", countsum);

    return 0;
}

После gcc -o dircnt_args dircnt_args.c вы можете вызвать его следующим образом:

dircnt_args /your/dirs/*

В 100-мегабайтных файлах в папках 10K это выполняется довольно быстро (~ 5 минут для первого запуска, последующее кэширование: ~ 23 с).

Единственный другой подход, который завершился менее чем за час, был ls с примерно 1 минутой в кеше: ls -f /your/dirs/* | wc -l. Счетчик отключен несколькими новыми строками в каждом каталоге, хотя...

За исключением ожидаемого, ни одна из моих попыток с find не возвращалась в течение часа: -/

Ответ 11

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

Что касается ответа Кристофера Шульца, я предлагаю изменить stat на lstat и, возможно, добавление проверки границ, чтобы избежать переполнения буфера:

if (strlen(path) + strlen(PATH_SEPARATOR) + strlen(ent->d_name) > PATH_MAX) {
    fprintf(stdout, "path too long (%ld) %s%c%s", (strlen(path) + strlen(PATH_SEPARATOR) + strlen(ent->d_name)), path, PATH_SEPARATOR, ent->d_name);
    return;
}

Рекомендуется использовать lstat, чтобы избежать следования символических ссылок, которые могут привести к циклам, если каталог содержит символическую ссылку в родительский каталог.

Ответ 12

Первые 10 руководств с наивысшим количеством файлов.

dir =/; для я в $(ls -1 ${dir} | sort -n); {echo "$ (найти ${dir} ${i} -тип f | wc -l) = > $i,";} | sort -nr | head -10

Ответ 13

Этот ответ здесь быстрее, чем почти все остальное на этой странице для очень больших, очень вложенных каталогов:

https://serverfault.com/a/691372/84703

locate -r '.' | grep -c "^$PWD"

Ответ 14

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

ls -1 /path/to/dir > count.txt && cat count.txt | wc -l

Ответ 15

Самый быстрый способ в linux (вопрос отмечен как linux) - использовать прямой системный вызов. Здесь небольшая программа, которая подсчитывает файлы (только, без директорий) в каталоге. Вы можете сосчитать миллионы файлов, и это примерно в 2,5 раза быстрее, чем "ls -f" и примерно в 1,3-1,5 раза быстрее, чем Кристофер Шульц.

#define _GNU_SOURCE
#include <dirent.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/syscall.h>

#define BUF_SIZE 4096

struct linux_dirent {
    long d_ino;
    off_t d_off;
    unsigned short d_reclen;
    char d_name[];
};

int countDir(char *dir) {


    int fd, nread, bpos, numFiles = 0;
    char d_type, buf[BUF_SIZE];
    struct linux_dirent *dirEntry;

    fd = open(dir, O_RDONLY | O_DIRECTORY);
    if (fd == -1) {
        puts("open directory error");
        exit(3);
    }
    while (1) {
        nread = syscall(SYS_getdents, fd, buf, BUF_SIZE);
        if (nread == -1) {
            puts("getdents error");
            exit(1);
        }
        if (nread == 0) {
            break;
        }

        for (bpos = 0; bpos < nread;) {
            dirEntry = (struct linux_dirent *) (buf + bpos);
            d_type = *(buf + bpos + dirEntry->d_reclen - 1);
            if (d_type == DT_REG) {
                // Increase counter
                numFiles++;
            }
            bpos += dirEntry->d_reclen;
        }
    }
    close(fd);

    return numFiles;
}

int main(int argc, char **argv) {

    if (argc != 2) {
        puts("Pass directory as parameter");
        return 2;
    }
    printf("Number of files in %s: %d\n", argv[1], countDir(argv[1]));
    return 0;
}

PS: Это не рекурсивно, но вы можете изменить его, чтобы достичь этого.