级别: 初级 Chris Herborth (chrish@pobox.com), 自由撰稿人, 作家
2006 年 6 月 26 日 利用 readdir() 和 stat() 函数浏览目录中的条目。因为 UNIX® 系统中存在大量的文件和目录,所以您需要了解如何使用 readdir() 函数处理这些目录条目,以及如何使用 stat() 函数提取这些条目的相关信息。在您的 UNIX 程序开发工作中,这些基础的文件系统操作可以很好地为您提供服务,让您可以轻松地发现并读取 UNIX 系统中文件、目录和符号链接。 引言 UNIX® 中任何事物都是文件 的观点意味着,您将始终会与文件和目录打交道,无论您开发的是何种类型的应用程序。任何事物都存储为文件,从数据到配置文件、甚至是设备,在对 UNIX 编程经过几个小时的学习之后,stdio.h 系统 Header 中的函数将能够为您提供很好的帮助。 一个时常困扰 UNIX 编程新手的问题是,如何浏览一个目录,并对其中的文件、目录和符号链接进行相应的处理。如何能够获取它们的列表,以及如何能够确定它们究竟是什么? 请继续阅读本文,以学习如何使用 dirent.h 函数系列 (opendir()/readdir()/closedir() ) 来读取目录中的条目,以及使用 stat() 函数来确定这些条目所对应的内容。
开始之前 本文中的示例代码(请参见下载)使用 C/C++ 开发工具 (CDT) 在 Eclipse 3.1 中编写,readdir_demo 项目是一个托管的 Make 项目,该项目通过使用 CDT 程序生成规则构建。您在这个项目中找不到 Makefile,但是它们非常简单,如果需要在 Eclipse 之外编译这些代码,您可以很容易地生成相应的 Makefile。 如果您还没有尝试使用 Eclipse,那么您真的应该试一试。它是一个非常好的集成开发环境 (IDE),并且随着发行版本的不断更新,它变得更加完善。它来自于生命力顽强的 EMACS 以及基于 Makefile 的开发工具。请参阅本文结尾处的参考资料部分,其中提供了一些很好的 Eclipse 文章的链接。
读取目录条目 对于一个给定路径的目录,应该如何读取其中的条目呢?您无法像操作文件那样打开目录(使用 open() 或 fopen() 函数),并且即便可以这样做,所得到的数据可能是您正在使用的文件系统的专用格式,而对于不十分熟悉的程序员来说,直接访问这些数据将使情况变得更糟。 dirent.h 函数,opendir() 、readdir() 和 closedir() ,它们正是您所需要的。这些函数的使用与用来对文件进行操作的 open/read/close 的习惯用法非常相似,但有一点除外:对于每个目录条目,readdir() 函数一次返回一个指向特殊结构(struct dirent 类型)的指针。通常,对目录进行浏览类似于清单 1 中所示的伪代码。 清单 1. 读取目录中的内容
dir = opendir( "some/path/name" )entry = readdir( dir )while entry is not NULL: do_something_with( entry ) entry = readdir( dir )closedir( dir ) |
在出现问题时,opendir() 和 readdir() 函数都会返回 NULL,并且将设置全局变量 errno 的值,以指出所出现的错误。如果 readdir() 返回 NULL,并且 errno 为 0(有时也称为 EOK 或 ENOERROR),则表示没有其他的目录条目。 有一点需要注意,每个目录都包含“.”(对该目录的引用)和“..”(对该目录的父目录的引用)条目。根据您所进行的操作,可能需要忽略对这些条目的处理。 请注意,readdir() 不是线程安全的,因为所返回的结构是存储在函数库中的一个静态变量。大多数现代的 UNIX 系统都具有线程安全的 readdir_r() ,如果您正在编写线程代码,可以使用这个函数作为替代。
struct dirent 中包含了哪些内容呢? POSIX 1003.1 标准仅仅为 struct dirent 定义了一个必需的条目,即 char 数组 d_name 。这是用标准的以 NULL 结尾的字符串表示的该条目的名称。这个结构中任何其他内容都是特定于您的 UNIX 系统的。 的确如此,struct dirent 中其他所有内容 都是不可移植的。严格满足一致性的系统不应该在其中包含任何其他的内容。如果您编写了使用额外结构成员的代码,那么您必须将其标记为不可移植的,并且包含一个完成相同任务的替换代码路径,如果您认为这样做特别友好的话。 例如,许多 UNIX 包含一个 d_type 成员和一些附加常量,这样一来,您无需额外的 stat() 调用就可以检查目录条目的类型。除了减少另外的系统调用之外,这种不可移植的扩展还减少了从文件系统获取更多元数据的开销非常高的访问操作。众所周知,在大多数 UNIX 上,stat() 函数的执行速度非常慢。
获取文件信息 除了获取目录中条目的名称之外,您可能还需要一些附加信息,以确定下一步要进行的操作。至少,仅根据目录条目的名称,您无法辨别文件条目。 stat() 函数会将特定文件的相关信息填入 struct stat 结构中,如果您获得的是文件描述符而不是文件名,那么作为替代,您可以使用 fstat() 函数。如果您想能够检测出符号链接,那么可以对文件名使用 lstat() 。
与 readdir() 返回的 struct dirent 不同,struct stat 具有相当多的标准的、必需的成员: st_mode ——文件权限(用户、其他用户、组)和标志 st_ino ——文件序列号 st_dev ——文件设备号 st_nlink ——文件连接计数 st_uid ——所有者用户 ID st_gid ——所有者组 ID st_size ——以字节表示的文件大小(针对普通文件) st_atime ——最后的访问时间 st_mtime ——最后的修改时间 st_ctime ——文件的创建时间
对 st_mode 成员使用 S_*() 宏,这样就可以找出您所处理的目录条目的类型: - S_ISBLK(mode)——是否为块特殊文件?(通常是某种基于块的设备)
- S_ISCHR(mode)——是否为字符特殊文件?(通常是某种基于字符的设备)
- S_ISDIR(mode)——是否为目录?
- S_ISFIFO(mode)——是否为管道或 FIFO 特殊文件?
- S_ISLNK(mode)——是否为符号链接?
- S_ISREG(mode)——是否为普通文件?
众所周知,在大多数文件系统上,stat() 函数的执行速度非常慢,所以如果您打算在将来再次使用该信息,可能需要对其进行缓存。 关于符号链接的说明 通常,您并不关心符号链接。如果对符号链接调用 stat() ,那么您将获取该链接所指向的文件的相关信息。这和用户的体验是一致的,因为控制与该文件交互的是目标文件的权限,而不是符号链接本身。 有些应用程序,如 ls 和备份程序,需要能够显示链接文件本身的相关信息,例如它所指向的文件。当您使用 lstat() 来代替 stat() 时,以及当您出于特定的目的而需要获取符号链接本身的相关信息,而不是直接与其链接的文件打交道时,情况也是这样的。
将其组合在一起 既然已经学习了如何使用 readdir() 和 stat() 来查找目录中的条目,那么让我们来看看演示这些函数的一些实际代码。 这里所介绍的代码将浏览命令行中指定的一个或多个目录,并显示在该目录中找到的每个条目的相关信息。当它找到另一个目录时,它会对该目录进行同样的处理。对于符号链接,将显示其目标文件,并且还将显示普通文件的大小。将忽略特殊文件。 如清单 2 所示,这个简单的演示应用程序中包含了各种 Header 文件。程序的开始块中包含了大多数程序中使用的标准部分,并且后面的四项是在该程序中使用 readdir() 和 stat() 所必需的。 清单 2. Header 和常量
#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <string.h>#include <limits.h>#include <sys/types.h>#include <sys/stat.h>#include <dirent.h>#include <unistd.h> |
process_directory() 函数(开始于清单 3,结束于清单 6)读取了指定的目录,并显示了每个条目的相关信息。opendir() 返回的 DIR 指针与 fopen() 返回的 FILE 指针类似,它是一个用于跟踪目录流的操作系统特定的对象,您应该忽略其具体内容。
清单 3. 处理一个目录
unsigned process_directory( char *theDir ){ DIR *dir = NULL; struct dirent entry; struct dirent *entryPtr = NULL; int retval = 0; unsigned count = 0; char pathName[PATH_MAX + 1]; /* Open the given directory, if you can. */ dir = opendir( theDir ); if( dir == NULL ) { printf( "Error opening %s: %s", theDir, strerror( errno ) ); return 0; } |
在打开了指定的目录之后,调用 readdir_r() (请参见清单 4)以获取关于第一个条目的信息,随后每次调用 readdir_r() 都将返回下一个条目,直到到达了目录末尾,并且 entryPtr 被设置为 NULL。这里还使用了 strncmp() 来检查“.”和“..”条目,以便略过它们。如果不略过它们,您将永远都在处理类似“theDir/./././././././././.”等这样的目录。 清单 4. 读取一个目录条目
retval = readdir_r( dir, &entry, &entryPtr ); while( entryPtr != NULL ) { struct stat entryInfo; if( ( strncmp( entry.d_name, ".", PATH_MAX ) == 0 ) || ( strncmp( entry.d_name, "..", PATH_MAX ) == 0 ) ) { /* Short-circuit the . and .. entries. */ retval = readdir_r( dir, &entry, &entryPtr ); continue; } |
既然已经得到了目录的条目名称,那么您需要构造一个更加完整的路径(请参见清单 5),然后调用 lstat() 以获取该条目的相关信息。因为符号链接需要特殊的处理,所以这里使用了 lstat() 函数。您可以使用 readlink() 函数找到其目标文件。 如果该条目是一个目录,那么对这个目录递归地调用 process_directory() ,并将其中所找到的条目数加到运行总数中。如果该条目是一个文件,那么显示其名称和字节数(可在 struct stat 的 st_size 成员中找到)。 清单 5. 处理条目
(void)strncpy( pathName, theDir, PATH_MAX ); (void)strncat( pathName, "/", PATH_MAX ); (void)strncat( pathName, entry.d_name, PATH_MAX ); if( lstat( pathName, &entryInfo ) == 0 ) { /* stat() succeeded, let's party */ count++; if( S_ISDIR( entryInfo.st_mode ) ) { /* directory */ printf( "processing %s//n", pathName ); count += process_directory( pathName ); } else if( S_ISREG( entryInfo.st_mode ) ) { /* regular file */ printf( "/t%s has %lld bytes/n", pathName, (long long)entryInfo.st_size ); } else if( S_ISLNK( entryInfo.st_mode ) ) { /* symbolic link */ char targetName[PATH_MAX + 1]; if( readlink( pathName, targetName, PATH_MAX ) != -1 ) { printf( "/t%s -> %s/n", pathName, targetName ); } else { printf( "/t%s -> (invalid symbolic link!)/n", pathName ); } } } else { printf( "Error statting %s: %s/n", pathName, strerror( errno ) ); } |
在 while 循环的底部,读取另一个目录条目并对其进行处理。如果您完成了对目录条目的处理,那么关闭当前打开的目录,并返回经过处理的条目的数目。 清单 6. 读取另一个条目
retval = readdir_r( dir, &entry, &entryPtr ); } /* Close the directory and return the number of entries. */ (void)closedir( dir ); return count;} |
最后,清单 7 显示了该程序的 main() 函数,它只是对命令行中传递的每个参数调用了 process_directory() 函数。一个真正的程序应该具有使用方法消息,并且在用户没有指定任何参数时,提供某种形式的反馈信息,但我把这项内容作为练习留给读者。 清单 7. 主线
/* readdir_demo main() * * Run through the specified directories, and pass them * to process_directory(). */int main( int argc, char **argv ){ int idx = 0; unsigned count = 0; for( idx = 1; idx < argc; idx++ ) { count += process_directory( argv[idx] ); } return EXIT_SUCCESS;} |
这就是整个程序。尽管包含了较多的文件,但处理目录条目并不是十分困难。
结束语 使用 readdir() 和 stat() 函数浏览目录中的条目并确定对其进行的额外处理,是非常简单的,在您需要列举目录中的内容时,也可能会使用到这种处理方法。它是一种很实用的方法,但是对于一些没有经验的 UNIX 开发人员来说,却难以掌握。本文的目的是降低其难度,使得 UNIX 开发人员能够充分利用这些有价值的函数。
|