深入linux下遍历目录树的方法总结分析
前几天需要实现对整个目录树的遍历,查阅了相关的一些资料。开始找到的原始的方法是使用readdir()与lstat()函数实现递归遍历,后来发现linux对于目录遍历这种最常用的操作已经提供了很完善的接口:ftw()与nftw()。下面就这两种方法具体说明一下。
1、手动实现递归
1.1 stat()函数族
stat函数族包括:stat,fstat以及lstat函数,都是向用户返回文件的属性信息(元数据)。
代码如下:
view plaincopy to clipboardprint?
#include <sys/stat.h>
int stat(const char*pathname,struct stat*buf);
int fstat(int filedes,struct stat*buf);
int lstat(const char *pathname,struct stat*buf);
#include <sys/stat.h>
int stat(const char*pathname,struct stat*buf);
int fstat(int filedes,struct stat*buf);
int lstat(const char *pathname,struct stat*buf);
三个函数的返回:若成功为0,出错为-1。对一个pathname,stat函数返回一个与此命名文件有关的信息结构,fstat函数获得已在描述符filedes上打开的文件的有关信息。 lstat函数类似于stat,但是当命名的文件是一个符号连接时,lstat返回该符号连接的有关信息,而不是由该符号连接引用的文件的信息。
第二个参数是个指针,它指向一个我们应提供的结构。这些函数填写由buf指向的结构。该结构的实际定义可能所实施而有所不同,但其基本形式是:
代码如下:
struct stat{
mode st_mode; /*文件类型和方式(许可数)*/
ino st_ino;/* i-节点号(序列号)*/
dev st_dev;/*设备号(文件系统)*/
dev st_rdev;/*特殊文件的设备号*/
nlink st_nlink;/*连接数*/
uid st_uid;/*属主的用户ID*/
gid st_gid;/*属主的组ID*/
off st_size;/*普通文件的字节长度*/
time st_atime;/*最后存取时间*/
time st_mtime;/*最后修改存取时间*/
time st_ctime;/*最后文件状态更改时间*/
long st_blksize;/*最佳I/O块长*/
long st_blocks;/*分配的512字节块块数
};
view plaincopy to clipboardprint?
#include<unistd.h>
#include<sys/stat.h>
#include<stdio.h>
int
main(int argc, char **argv){
struct stat buf;
if(stat(argv[1],&buf)) {
printf("[stat]:error!/n");
return -1;
}
printf("st_dev:%d/n",buf.st_dev);
printf("st_ino:%d/n",buf.st_ino);
printf("st_mode:%d S_ISDIR:%d/n",buf.st_mode,S_ISDIR(buf.st_mode));
printf("st_nlink:%d/n",buf.st_nlink);
printf("st_uid:%d/n",buf.st_uid);
printf("st_gid:%d/n",buf.st_gid);
printf("st_rdev:%d/n",buf.st_rdev);
printf("st_size:%d/n",buf.st_size);
printf("st_blksize:%lu/n",buf.st_blksize);
printf("st_blocks:%lu/n",buf.st_blocks);
printf("st_atime:%ld/n",buf.st_atime);
printf("st_mtime:%ld/n",buf.st_mtime);
printf("st_ctime:%ld/n",buf.st_ctime);
return 0;
}
#include<unistd.h>
#include<sys/stat.h>
#include<stdio.h>
int
main(int argc, char **argv){
struct stat buf;
if(stat(argv[1],&buf)) {
printf("[stat]:error!/n");
return -1;
}
printf("st_dev:%d/n",buf.st_dev);
printf("st_ino:%d/n",buf.st_ino);
printf("st_mode:%d S_ISDIR:%d/n",buf.st_mode,S_ISDIR(buf.st_mode));
printf("st_nlink:%d/n",buf.st_nlink);
printf("st_uid:%d/n",buf.st_uid);
printf("st_gid:%d/n",buf.st_gid);
printf("st_rdev:%d/n",buf.st_rdev);
printf("st_size:%d/n",buf.st_size);
printf("st_blksize:%lu/n",buf.st_blksize);
printf("st_blocks:%lu/n",buf.st_blocks);
printf("st_atime:%ld/n",buf.st_atime);
printf("st_mtime:%ld/n",buf.st_mtime);
printf("st_ctime:%ld/n",buf.st_ctime);
return 0;
}
这里补充说明一下linux中文件的基本类型。
1.普通文件(Regular file)。这是最常见的文件类型,这种文件包含了某种形式的数据。至于这种数据是文本还是二进制数据对于系统核而言并无区别。对普通文件内容的解释由处理该文件的应用程序进行。
2.目录文件(Directory file)。这种文件包含了其它文件的名字以及指向与这些文件有关信息的指针。对一个目录文件具有读许可数的任一进程都可以读该目录的内容,但只有系统核可以写目录文件。
3.字符特殊文件(Charocter special file)。这种文件用于系统中的某些类型的设备。
4.块特殊文件(Block special file)。这种文件典型地用于磁盘设备。系统中的所有设备或者是字符特殊文件,或者是块特殊文件。
5.FIFO。这种文件用于进程间的通信,有时也将其称为命名管道。
6.套接口(socket)。这种文件用于进程间的网络通信。套接口也可用于在一台宿主机上的进程之间的非网络通信。
7.符号连接(Symboliclink)。这种文件指向另一个文件。
对于文件类型,可以利用定义的宏比如S_ISDIR()等测试st_mode,判断文件类型。宏有S_ISREG、S_ISDIR、S_ISCHR、S_ISBLK、S_ISFIFO、S_ISLNK、S_ISSOCK。
1.2 遍历目录例子
引用别人的一个例子,现在把许多文件处理函数集中在一起使用,程序遍历指定目录的文件,同时也要进到下级子目录中进行遍历,这一点是将子目录递归传递到opendir中去,需要指出的是,这就决定了如果子目录嵌套过深,程序将失败返回,因为允许打开的子目录流数量是有上限的。
代码如下:
view plaincopy to clipboardprint?
/* We start with the appropriate headers and then a function, printdir,
which prints out the current directory.
It will recurse for subdirectories, using the depth parameter is used for indentation. */
#include <unistd.h>
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
void printdir(char *dir, int depth)
{
DIR *dp;
struct dirent *entry;
struct stat statbuf;
if((dp = opendir(dir)) == NULL) {
fprintf(stderr,"cannot open directory: %s/n", dir);
return;
}
chdir(dir);
while((entry = readdir(dp)) != NULL) {
lstat(entry->d_name,&statbuf);
if(S_ISDIR(statbuf.st_mode)) {
/**//* Found a directory, but ignore . and .. */
if(strcmp(".",entry->d_name) == 0 ||
strcmp("..",entry->d_name) == 0)
continue;
printf("%*s%s//n",depth,"",entry->d_name);
/**//* Recurse at a new indent level */
printdir(entry->d_name,depth+4);
}
else printf("%*s%s/n",depth,"",entry->d_name);
}
chdir("..");
closedir(dp);
}
/**//* Now we move onto the main function. */
int main(int argc, char* argv[])
{
char *topdir, pwd[2]=".";
if (argc != 2)
topdir=pwd;
else
topdir=argv[1];
printf("Directory scan of %s/n",topdir);
printdir(topdir,0);
printf("done./n");
exit(0);
}
/* We start with the appropriate headers and then a function, printdir,
which prints out the current directory.
It will recurse for subdirectories, using the depth parameter is used for indentation. */
#include <unistd.h>
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
void printdir(char *dir, int depth)
{
DIR *dp;
struct dirent *entry;
struct stat statbuf;
if((dp = opendir(dir)) == NULL) {
fprintf(stderr,"cannot open directory: %s/n", dir);
return;
}
chdir(dir);
while((entry = readdir(dp)) != NULL) {
lstat(entry->d_name,&statbuf);
if(S_ISDIR(statbuf.st_mode)) {
/**//* Found a directory, but ignore . and .. */
if(strcmp(".",entry->d_name) == 0 ||
strcmp("..",entry->d_name) == 0)
continue;
printf("%*s%s//n",depth,"",entry->d_name);
/**//* Recurse at a new indent level */
printdir(entry->d_name,depth+4);
}
else printf("%*s%s/n",depth,"",entry->d_name);
}
chdir("..");
closedir(dp);
}
/**//* Now we move onto the main function. */
int main(int argc, char* argv[])
{
char *topdir, pwd[2]=".";
if (argc != 2)
topdir=pwd;
else
topdir=argv[1];
printf("Directory scan of %s/n",topdir);
printdir(topdir,0);
printf("done./n");
exit(0);
}
2、使用ftw调用遍历目录
2.1ftw函数族
使用readdir函数等实现递归遍历目录树的方法比较原始,glibc2.1收录了ftw等函数,可以方便实现目录树的遍历。
代码如下:
view plaincopy to clipboardprint?
#include <ftw.h>
int ftw(const char *dirpath,
int (*fn) (const char *fpath, const struct stat *sb,int typeflag),
int nopenfd);
#define _XOPEN_SOURCE 500
#include <ftw.h>
int nftw(const char *dirpath,
int (*fn) (const char *fpath, const struct stat *sb,int typeflag, struct FTW *ftwbuf),
int nopenfd, int flags);
#include <ftw.h>
int ftw(const char *dirpath,
int (*fn) (const char *fpath, const struct stat *sb,int typeflag),
int nopenfd);
#define _XOPEN_SOURCE 500
#include <ftw.h>
int nftw(const char *dirpath,
int (*fn) (const char *fpath, const struct stat *sb,int typeflag, struct FTW *ftwbuf),
int nopenfd, int flags);
具体的英文解释可以参考文章《 ftw, nftw - file tree walk 》。
ftw()
函数说明:ftw() 会从参数dirpath指定的目录开始,往下一层层地递归式遍历子目录。ftw()会传三个参数给fn(), 第一个参数*fpath指向当时所在的目录路径,第二个参数是*sb, 为stat结构指针,第三个参数为flag,有下面几种可能值
FTW_F 一般文件
FTW_D 目录
FTW_DNR 不可读取的目录,此目录以下将不被遍历
FTW_SL 符号连接
FTW_NS 无法取得stat结构数据,有可能是权限问题
最后一个参数depth代表ftw()在进行遍历目录时同时打开的文件数。ftw()在遍历时每一层目录至少需要一个文件描述词,如果遍历时用完了depth所给予的限制数目,整个遍历将因不断地关文件和开文件操作而显得缓慢。(实际做测试的时候未发现...)
如果要结束ftw()的遍历,fn()只需返回一非零值即可,此值同时也会是ftw()的返回值。否则ftw()会试着走完所有的目录,然后返回0
返 回 值:遍历中断则返回fn()函数的返回值,全部遍历则返回0,若有错误发生则返回-1
附加说明:由于ftw()会动态配置内存使用,请使用正常方式(fn函数返回非零值)来中断遍历,不要在fn函数中使用longjmp()
nftw()
函数说明:nftw()与ftw()很像,都是从参数dirpath指定的目录开始, 往下一层层地递归遍历子目录。 每进入一个目录,便会调用参数*fn定义的函数来处理。nftw()会传四个参数给fn(). 第一个参数*fpath指向当时所在的目录路径,第二个参数是*sb, 为stat结构指针(结构定义请参考stat()),第三个参数为typeflag,有底下几种可能值:
FTW_F 一般文件
FTW_D 目录
FTW_DNR 不可读取的目录。此目录以下将不被遍历
FTW_SL 符号连接
FTW_NS 无法取得stat结构数据,在可能是权限问题
FTW_DP 目录,而且子目录都已被遍历过了
FTW_SLN 符号连接,但连接不存在的文件
fn()的第四个参数是FTW结构,定义如下:
代码如下:
struct FTW
{
int base;
int level; //level代表遍历时的深度
}
nftw()第三个参数depth代表nftw()在进行遍历目录时可同时打开的文件数。
ftw()在遍历时每一层目录至少需要一个文件描述词,如果遍历时用完了depth所给予的限制数目,整个遍历将因不断地关文件和开文件操作而显得的缓慢
nftw()最后一个参数flags用来指定遍历时的动作,可指定下列的操作或用OR组合
FTW_CHDIR 在读目录之前先用chdir()移到此目录
FTW_DEPTH 执行深度优先搜索。在遍历此目录前先将所有子目录遍历完
FTW_MOUNT 遍历时不要跨越到其他文件系统
FTW_PHYS 不要遍历符号连接的目录。预设会遍历符号连接目录
如果要结束nftw()的遍历,fn()只需返回一非0值即可,此值同时也会是nftw()的返回值。否则nftw()会试着遍历完所有目录,然后返回0.
返 回 值 :遍历中断则返回fn()函数的返回值, 全部遍历完则返回0,若有错误发生则返回-1
区别:ftw 对于每一个文件他都会调用stat函数,这就造成程序会跟随符号链接。这就可能导致在某些情况下重复某些目录或者循环统计某些目录文件(这是因为符号链接的原因,详细参见UNIX环境高级编程)。
nftw将调用lstat函数所以不存在跟随符号链接的问题。
注意:使用nftw函数时,必须定义#define _XOPEN_SOURCE 500,否则会出现未定义等错误。
有一个没搞清楚的问题是我使用FTW_DEPTH 来遍历整个目录树的时候,遍历到proc目录下存在异常返回,可能还需要指定FTW_PHYS使其不遍历符号链接目录,这个有空查一下。
2、遍历的例子
自己写的一个测试的小例子。遍历指定目录,输出文件元数据和遍历深度等信息。
代码如下:
view plaincopy to clipboardprint?
#define _XOPEN_SOURCE 500
#include<ftw.h>
#include<sys/stat.h>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#define FILEOPEN 1024
int gb_filecount;
int getMetadata(const char *dirpath, const struct stat *sb,int typeflag, struct FTW *ftwbuf);
int main(int argc, char ** argv){
int ret = 0;
struct stat pathbuf;
if(argc > 2){
printf("-nfwt_t:invalid arguments /n ");
return -1;
}
if(stat(argv[1],&pathbuf)){
printf("-nfwt_t:invalid dirpath:%s/n",argv[1]);
return -1;
}else{
if(0 == S_ISDIR(pathbuf.st_mode)){
printf("-nfwt_t:/"%s/" is not dirpath/n",argv[1]);
return -1;
}
}
gb_filecount=0;
ret = nftw(argv[1],getMetadata,FILEOPEN,FTW_PHYS);
if(ret<0){
printf("-nftw:[wrong:%d]ntfw search %d files/n",ret,gb_filecount);
return -1;
}else{
printf("-nftw:[done:%d]trasvers in %s search %d files/n",ret,argv[1],gb_filecount);
return 0;
}
}
int
getMetadata(const char *dirpath, const struct stat *sb,int typeflag, struct FTW *ftwbuf){
printf("num:%d path:%s ",++gb_filecount,dirpath);
printf("st_dev:%d ",(*sb).st_dev);
printf("st_ino:%d ",(*sb).st_ino);
printf("st_mode:%d S_ISDIR:%d ",(*sb).st_mode,S_ISDIR((*sb).st_mode));
printf("st_nlink:%d ",(*sb).st_nlink);
printf("st_uid:%d ",(*sb).st_uid);
printf("st_gid:%d ",(*sb).st_gid);
printf("st_rdev:%d ",(*sb).st_rdev);
printf("st_size:%d ",(*sb).st_size);
printf("st_blksize:%lu ",(*sb).st_blksize);
printf("st_blocks:%lu ",(*sb).st_blocks);
printf("st_atime:%ld ",(*sb).st_atime);
printf("st_mtime:%ld ",(*sb).st_mtime);
printf("st_ctime:%ld ",(*sb).st_ctime);
printf("typeflag:%d ",typeflag);
printf("FTW_base:%d FTW_level:%d /n",(*ftwbuf).base,(*ftwbuf).level);
return 0;
}