在Linux上使用C语言检查目录是否为空

16

这是在C语言中检查目录是否为空的正确方法吗?如果目录非空且包含数千个文件,有没有更有效的方法来检查空目录?

int isDirectoryEmpty(char *dirname) {
  int n = 0;
  struct dirent *d;
  DIR *dir = opendir(dirname);
  if (dir == NULL) //Not a directory or doesn't exist
    return 1;
  while ((d = readdir(dir)) != NULL) {
    if(++n > 2)
      break;
  }
  closedir(dir);
  if (n <= 2) //Directory Empty
    return 1;
  else
    return 0;
}

如果一个目录是空的,readdir会在条目'.'和'..'后停止,因此如果n<=2,则为空。
如果目录是空的或者不存在,它应该返回1,否则返回0。
更新:
@c$ time ./isDirEmpty /fs/dir_with_1_file; time ./isDirEmpty /fs/dir_with_lots_of_files
0

real    0m0.007s
user    0m0.000s
sys 0m0.004s

0

real    0m0.016s
user    0m0.000s
sys 0m0.008s

为什么检查具有大量文件的目录需要比只有一个文件的目录更长时间?
3个回答

11

有没有更有效的方法来检查空目录,特别是如果目录中有数千个文件而不为空?

你编写的代码方式不管它有多少文件(如果n > 2,则使用break),因此你的代码使用了最多5次调用。我认为没有任何方法可以(可移植地)使其更快。


请阅读我的编辑,为什么相同的代码在文件数量较多的目录上运行所需时间比只有一个文件的目录要长? - freethinker
5
readdir(3)是一个调用getdents(2)的前端函数。在strace中看到,getdents()系统调用尝试从目录中检索32768个条目,其中1175个条目被检索出来。我猜想如果继续使用readdir(3),情况也不会有所改变。虽然getdents()的手册页面建议不要使用该函数,但如果您不关心可移植性,可以考虑使用该调用。 - Friek
@Friek,你说得对。getdents(2)在许多其他实现中出现,但它不是标准的。 - cnicutar

0

有一种棘手的策略被称为命令行rmdir,它无法删除非空目录,而这个特性可以用来检测目录是否为空。要做到这一点,尝试通过调用system("rmdir your_directory")来删除一个目录。如果目录不为空,则函数失败并返回非零值,并可能提示您rmdir: failed to remove 'your_directory': Directory not empty。可以通过将stderr重定向到/dev/null来消除提示,并且这样做可以提高其性能。否则,目录将被删除,然后您可以通过重新创建它来恢复它。

如果目录中有任何隐藏文件,这种策略将非常有帮助,因为它仍然能够检测到它们的存在。在我的情况下,rmdir会立即返回,而不管非空目录中有多少文件。

但要注意命令别名,特别是在*nix shell环境中,如果有任何rmdir命令的别名添加了一些参数,导致rmdir进行递归文件删除,则该技巧将失败并导致所有目录实际上被删除。这可以通过调用system("\rmdir your_directory")来解决,从而删除别名。


这个“策略”会不会在文件夹为空的情况下删除文件夹呢?而且,我怀疑 rmdirreaddir 更快。 - undefined

-2

也许这段代码可以帮到你:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char *cmd;
    char *folder = "/tmp";
    int status, exitcode;
    char *format="test $(ls -AU \"%s\" 2>/dev/null | head -1 | wc -l) -ne 0";
    clock_t start, stop;
    int size;

    if(argc == 2)
            folder = argv[1];

    size = strlen(format)+strlen(folder)+1;
    cmd = malloc(size * sizeof(char));

    snprintf(cmd, size, format, folder);
    printf("executing: %s\n", cmd);

    status = system(cmd);
    exitcode = WEXITSTATUS(status);

    printf ("exit code: %d, exit status: %d\n", exitcode, status);

    if (exitcode == 1)
            printf("the folder is empty\n");
    else
            printf("the folder is non empty\n");

    free(cmd);
    return 0;
}

我使用 ls -AU folder 2>/dev/null | head -1 | wc -l 检查文件夹是否为空,以计算文件夹中的文件数量。如果返回零,则文件夹为空,否则文件夹非空。WEXITSTATUS 宏返回执行命令的退出代码。head 命令不会等待 ls 完成,只需等到条件符合即可。

使用 find 命令生成长文件列表的一些示例表明它确实非常有效。

不带 head 命令的示例:

/usr/bin/time -p -v find / -print | wc -l

output
Command being timed: "find / -print"
    User time (seconds): 0.63
    System time (seconds): 1.28
    Percent of CPU this job got: 98%
    Elapsed (wall clock) time (h:mm:ss or m:ss): 0:01.94
    Average shared text size (kbytes): 0
    Average unshared data size (kbytes): 0
    Average stack size (kbytes): 0
    Average total size (kbytes): 0
    Maximum resident set size (kbytes): 6380
    Average resident set size (kbytes): 0
    Major (requiring I/O) page faults: 0
    Minor (reclaiming a frame) page faults: 3419
    Voluntary context switches: 7
    Involuntary context switches: 140
    Swaps: 0
    File system inputs: 0
    File system outputs: 0
    Socket messages sent: 0
    Socket messages received: 0
    Signals delivered: 0
    Page size (bytes): 4096
    Files counted: 1043497

使用 head 修改的命令

/usr/bin/time -p -v find / -print | head -1 | wc -l

Command terminated by signal 13
    Command being timed: "find / -print"
    User time (seconds): 0.00
    System time (seconds): 0.00
    Percent of CPU this job got: 100%
    Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.00
    Average shared text size (kbytes): 0
    Average unshared data size (kbytes): 0
    Average stack size (kbytes): 0
    Average total size (kbytes): 0
    Maximum resident set size (kbytes): 2864
    Average resident set size (kbytes): 0
    Major (requiring I/O) page faults: 0
    Minor (reclaiming a frame) page faults: 136
    Voluntary context switches: 1
    Involuntary context switches: 0
    Swaps: 0
    File system inputs: 0
    File system outputs: 0
    Socket messages sent: 0
    Socket messages received: 0
    Signals delivered: 0
    Page size (bytes): 4096
    Files counted: 1

正如您所看到的,第一个没有“head”的命令执行需要1.28秒,而使用“head”修改后的命令执行需要0秒。

此外,如果我们测量上述核心的执行时间,有和没有头部的区别。

普通的ls:

/usr/bin/time -p ls -A /var/lib/dpkg/info/
real 0.67
user 0.06
sys 0.06

无头程序

/usr/bin/time -p ./empty.exe /var/lib/dpkg/info/
executing: test $(ls -AU "/var/lib/dpkg/info/" 2>/dev/null | wc -l) -ne 0
exit code: 0, exit status: 0
the folder is non empty
real 0.01
user 0.00
sys 0.01

使用头文件的程序

/usr/bin/time -p ./empty.exe /var/lib/dpkg/info/
executing: test $(ls -AU "/var/lib/dpkg/info/" 2>/dev/null | head -1 | wc -l) -ne 0
exit code: 0, exit status: 0
the folder is non empty
real 0.00
user 0.00
sys 0.00

注意:如果文件夹不存在或您没有正确的访问权限,则此程序必须打印“文件夹为空”。
该程序是使用以下命令构建的:gcc empty.c -o empty.exe。

-1 OP 要求速度,但这显然不够快,此外它没有转义给定的字符串并使用了一个过短的缓冲区,因此容易受到 shell 注入攻击,并可能意外截断给定的路径,从而导致其他错误。简而言之:它既是一个糟糕的例子,也是一个非常糟糕的例子。 - ntninja
首先,少于100毫秒对于解决像判断目录是否为空这样简单的问题来说是非常长的时间。想象一下使用这种方法会导致Web服务器的性能下降...如果这种方法与基于readdir的解决方案相比,我会感到非常惊讶。我同意盲目复制粘贴通常不是一个好主意,但是缺乏经验的人会这样做,因此代码中的问题会倍增。此外,我没有轻易给任何答案打负分 - 而且你的答案仍然存在shell注入问题,至少截断已经被修复了。 - ntninja
我认为,如果我们不是批评而是通过提出建议来改进代码,那么社区将会得到最大的发展。我已经做出了我的贡献,你呢? - kato2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接