【C语言】遍历目录树

在 Linux 环境下,如果编写程序且需要通过函数接口来遍历目录树,可以考虑使用以下几个常用的调用:

1. opendir() / readdir() / closedir():

   这是 POSIX 标准定义的函数,用于遍历目录。`opendir()` 用于打开一个目录,`readdir()` 用于读取目录内的项,`closedir()` 用于关闭目录。遍历目录时,通常会对获取的每一个条目进行判断,以确定它是文件还是目录。对于目录项需要递归地调用遍历函数。

   #include 
   #include 
   #include 
   #include 
   #include 

   void listdir(const char *name, int indent)
   {
       DIR *dir;
       struct dirent *entry;

       if (!(dir = opendir(name)))
           return;

       while ((entry = readdir(dir)) != NULL) {
           if (entry->d_type == DT_DIR) {
               char path[1024];
               if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
                   continue;
               snprintf(path, sizeof(path), "%s/%s", name, entry->d_name);
               printf("%*s[%s]\n", indent, "", entry->d_name);
               listdir(path, indent + 2);
           } else {
               printf("%*s- %s\n", indent, "", entry->d_name);
           }
       }
       closedir(dir);
   }

   int main(void)
   {
       listdir(".", 0);
       return 0;
   }

2. ftw() / nftw():

   这两个函数是用于遍历目录树的更高级的接口。`ftw()` 是 File Tree Walk 的缩写,而 nftw() 是 ftw() 的 改进版本。它们允许你设定回调函数,该函数会为目录树中的每个条目被调用。

#define _XOPEN_SOURCE 500
#include 
#include 
#include 
#include    // Include this header for intmax_t
#include 

static int display_info(const char *fpath, const struct stat *sb,
                        int tflag, struct FTW *ftwbuf)
{
    printf("%-3s %2d %7jd   %-40s %d %s\n",
        (tflag == FTW_D) ?   "d"   : (tflag == FTW_DNR) ? "dnr" :
        (tflag == FTW_DP) ? "dp"  : (tflag == FTW_F) ?   "f" :
        (tflag == FTW_NS) ? "ns" : (tflag == FTW_SL) ?  "sl" :
        (tflag == FTW_SLN) ? "sln" : "???",
        ftwbuf->level, (intmax_t)sb->st_size,
        fpath, ftwbuf->base, fpath + ftwbuf->base);
    return 0;           /* To tell nftw() to Continue */
}

int main(int argc, char *argv[])
{
    int flags = 0;

    if (argc > 2 && strchr(argv[2], 'd') != NULL)
        flags |= FTW_DEPTH;
    if (argc > 2 && strchr(argv[2], 'p') != NULL)
        flags |= FTW_PHYS;

    if (nftw((argc < 2) ? "." : argv[1], display_info, 20, flags)
            == -1) {
        perror("nftw");
        exit(EXIT_FAILURE);
    }
    exit(EXIT_SUCCESS);
}

这两种方法都需要使用递归机制来遍历目录与子目录。opendir()/readdir()/closedir() 提供了更基础的控制,而 ftw()/nftw() 提供了一个简化的接口。可以根据需要选择合适的方法。在实际使用中,可能还需要检查文件权限和错误处理等。

3. 函数接口与内核

在Linux内核代码中,文件系统遍历需要处理不同的文件系统类型、内核同步机制以及与硬件的交互等复杂情况。
Linux内核的目录树遍历通常涉及以下几个关键的内核结构和函数:
1. struct inode: 在内核中,每个文件都和`struct inode`数据结构相关联。这个结构包含了大多数文件的元数据信息,如文件权限、所有者、文件大小以及指向文件内容的指针。
2. struct dentry: 代表目录项的数据结构,用于文件名和对应`inode`之间的映射。
3. struct file: 用来表示打开的文件描述符,包含了读写位置等信息。
4. struct dir_context: 用与遍历目录中的条目,被用于填充 readdir 函数。
遍历目录的基本操作通常遵循如下步骤:
1. 打开目录: 使用`kern_path()`或`filp_open()`来获取目录的`struct file`指针。
2. 读取目录: 使用内核的 iterate_dir() 函数(也可能是更特定文件系统的函数如 ext4_readdir())。这些函数会每次返回目录中的一个条目。
3. 对于每个条目: 如果它是一个目录,可能需要递归,继续遍历。如果是文件,则可以根据需要处理。
4. 关闭目录: 通过调用`filp_close()`来释放资源。
因为Linux内核处理操作通常需要等级比较高的权限和对内核编程有实质性的理解,普通用户级应用通常不会直接与内核代码交互来遍历目录树。而是会通过系统调用与内核通信,系统调用(例如`open()`, readdir(), read())会代理用户空间的请求到内核空间的相应操作。
内核代码一般也不会直接执行比较两个目录树的操作,这通常会在用户空间中,使用系统调用来执行。代码库如`rsync`、`diff`或者`git`都有执行这类操作的功能,并通过用户空间应用程序接口与内核交互。

在Linux内核中遍历目录树通常是通过深度优先搜索(Depth-First Search,DFS)算法实现的。这意味着它会从根目录开始,沿一条路径尽可能深地进入目录树,直到到达叶子节点(即没有子目录的目录)。一旦无法继续前进,它将回溯到上一个具有未探索子目录的节点,并重复此过程,直到探索完全部目录树。
在Linux内核的文件系统代码中,这种遍历可能是显式实现的,也可能是隐式的。例如,当打开一个目录并读取它的内容时,内核需要遍历这个目录节点的子节点来获取目录项。内核代码使用了诸如 readdir 系列函数来迭代目录项。相对较低级的目录遍历功能可能涉及VFS(Virtual File System)层,这是一个文件系统独立的抽象层,允许内核以统一的方式处理不同文件系统提供的目录结构。
对于内核开发人员而言,内核API提供了一系列函数和宏来操作目录项和inode,而对于遍历目录树这样的任务,这些API提供了必要的基础设施。
举个例子,在目录树遍历中可能涉及到的函数是:
- vfs_readdir():这个函数用于读取目录项,通常由用户空间程序通过`readdir`调用。
- filldir():这是在`vfs_readdir()`调用中使用的一个回调函数,用于处理读取到的目录项。
内核并不总是提供一个简单的“遍历目录树”的函数,因为这通常是在用户空间完成的操作。内核中目录树的遍历更多是在需要时由内核自身或者文件系统实现的特定代码按需进行的。如用户请求、权限检查、inode查找和路径解析等操作。

在UNIX-like系统中,`readdir()` 是一个系统调用(wrapper)用于读取目录的内容,它会与内核中的文件系统代码交互以获取目录信息。`readdir()`最终运行的是内核层面上的代码。
ftw() (File Tree Walk) 和 nftw() (New File Tree Walk) 是两个用于递归遍历文件系统目录树的库函数。这些函数不是系统调用,而是标准库函数,通常实现在`libc`这样的标准库中。它们的工作原理通常是封装了`open()`, read(), close(),和`readdir()`等系统调用来逐个访问目录和文件。
当调用`ftw()`或`nftw()`时,它们会在用户空间创建一个递归循环,但是在需要获取文件属性或读取目录内容时,它们将调用执行系统调用,那些系统调用是在内核中执行的。所以,虽然`ftw()`和`nftw()`本身不是系统调用,但它们最终会触发运行内核中代码的系统调用。

ftw()(File Tree Walk)和`nftw()`(New File Tree Walk)是C标准库中提供的两个函数,用于遍历文件系统的目录树。这些函数提供了一种简便的方式来递归地遍历目录及其子目录,并对其中的每个文件调用用户提供的回调函数。ftw()和`nftw()`在内部可能会使用`readdir()`或类似的系统调用(如`open()`, read(), close() 以及可能的`stat()` 或 lstat())来获取目录内容及文件信息。系统调用`readdir()`用于读取目录流(`DIR*`)中的条目,它是对底层文件系统操作的封装,最终也会运行内核中的代码。
ftw()和`nftw()`对`readdir()`进行了高级封装,简化了递归遍历文件系统的复杂性。使用它们,开发者只需提供一个回调函数来处理每个遍历到的文件或目录。在这个回调函数中,可以获取到文件的路径、属性和类型,然后进行相应的操作。
nftw()是`ftw()`的增强版本,它支持更多的功能,如可同时传递文件状态信息,支持遍历时设置一些标志,比如是否遵循符号链接等。
`ftw()`和`nftw()`通过对`readdir()`和其他文件操作系统调用的组合使用,提供了一个便于文件树遍历的高级接口,它们本质上是用户空间的库函数。

4. 文件中的时间

在Unix-like系统中,每个文件都关联着一个inode,提供了该文件的元信息。在inode中记录着文件的三种主要时间戳,每一种都有其特定含义:
1. 修改时间 (Modification time, st_mtime): 这个时间戳反映了文件内容上次被修改的时间。每当文件的内容被编辑或以任何方式改变时,此时间戳会被更新。例如,如果你编辑一个文本文件并保存了更改,`st_mtime` 将被设置为当前时间。
2. 访问时间 (Access time, st_atime): 此时间戳表示文件内容最后一次被访问(读取)的时间。例如,当你查看或者用程序读取一个文件的内容时,`st_atime` 会被更新。许多现代文件系统以及运行中的系统默认配置可能不会因为性能考虑而及时更新访问时间,比如默认挂载选项中的 noatime。
3. 状态改变时间 (Change time, st_ctime): 此时间戳反映了文件状态最后一次被更改的时间,它代表文件的元数据(比如权限或所有权)的更改,并非内容。重要的是要注意 st_ctime 并不是文件“创建”时间,而是“元数据更改”时间。例如,如果你改变了文件的权限或所有者,`st_ctime` 将会被更新。
在编程中,可以通过调用各种系统函数(如 stat(), fstat(), lstat() 等)来获取这些时间戳信息。当使用 ftw() 或 nftw() 函数遍历目录树时,这些函数会为文件树中的每个条目调用一个用户定义的回调函数,其中 stat 结构体会作为一个参数传入,包含了上述提到的时间戳。因此,可以在回调函数中访问文件的修改时间、访问时间和状态改变时间。

在Unix-like系统中,文件的三种时间戳有以下含义:
1. 修改时间 (st_mtime): 文件内容上次修改的时间。
2. 访问时间 (st_atime): 文件上次被访问的时间。
3. 状态改变时间 (st_ctime): 文件的元数据或状态最后一次改变的时间(例如权限或所有权改变),注意它并不是"创建时间"。
当文件从一个目录复制到另一个目录时,以下情形通常会发生:
- 新文件的`st_mtime`和`st_atime`会被更新为复制操作发生的时间。这是因为复制操作实际上是创建了一个新的文件,然后写入了原文件的数据。
- 新文件的`st_ctime`也会是复制操作的时间,因为这是文件状态的一个变更(新文件被创建的时间)。
确定文件内容没有被修改,只从时间戳很难得出确定的结论,因为复制文件本身就会更新时间戳。但如果想验证一个文件是否被内容上修改过,可以用其他方法,比如计算并比较文件的散列值(例如SHA-256)。如果两个文件的散列值相同,那几乎可以确定它们的内容是相同的。使用如下命令:

sha256sum filename

对源文件和目标文件都执行这个命令,然后比较两个文件的散列值是否相同。如果相同,那么文件内容没有被修改过。这种方法比仅依赖时间戳更准确,因为即使文件的元数据发生了变化,只要内容相同,散列值也会相同。

文件的修改时间 (st_mtime), 访问时间 (st_atime), 和状态改变时间 (st_ctime) 不是直接保存在文件内容中的,而是在文件系统的元数据(metadata)中记录的。这些时间戳用于跟踪文件状态的变化,通常由文件系统负责更新和维护。
- st_mtime:这是文件内容最后被修改的时间戳。当编辑一个文件并保存时,这个时间戳会更新。
- st_atime:这是文件最后被访问的时间戳。每当文件数据被读取时,访问时间就可能被更新(但这取决于文件系统的挂载选项,因为某些文件系统可能以一种称为“noatime”的方式挂载,以降低对存储设备的写入频率,特别是在SSD等设备上)。
- st_ctime:这是文件状态(不是文件内容)最后被改变的时间戳。文件状态改变指的是文件的元数据改变,比如权限或所有者改变。在某些文件系统中,`st_ctime` 也可以在文件内容被修改时更新。
在大多数现代文件系统中,这些时间戳会被记录在文件的inode(索引节点)结构中。当从一个目录复制文件到另一个目录时,正常的复制操作(比如使用系统的复制命令)会创建一个新的文件,而这个新文件会拥有自己的元数据和时间戳。通常情况下,新文件的`st_mtime`和`st_atime`会被设置成复制操作的时间。
若要保持原始文件的时间戳不变,通常需要使用特定的命令或选项,例如在Linux中`cp`命令的`-p`(preserve)选项,可以在复制文件时保留原始文件的时间戳。 

ftw() (File Tree Walk) 和 nftw() (New File Tree Walk) 是两个用于遍历目录树的函数,它们允许程序访问文件系统中每一个条目,并且可以对这些条目执行某些操作。
使用 ftw() 或 nftw() 遍历目录树时,它们会调用一个用户提供的回调函数,为遍历到的每一个文件或目录调用一次这个函数。这些函数会传递一个结构体给用户的回调函数,这个结构体中包含了文件或目录的一些信息。
对于 ftw(),它传递的结构体较为基础,不包含文件修改时间;而 nftw() 传递的 struct stat 结构体中包含了文件的元信息,包括文件的修改时间(`st_mtime`)、访问时间(`st_atime`)和状态改变时间(`st_ctime`)。
因此,如果需要获取文件或目录的修改时间,应该使用 nftw() 函数而不是 ftw()。在 nftw() 的回调函数中,将能够通过 struct stat 结构体获取到文件的各种状态信息,包括修改时间。
以下是一个简洁的 nftw() 使用例子,展示如何打印遍历到的文件或目录的名称和修改时间:

#define _XOPEN_SOURCE 500 // 或者 #define _GNU_SOURCE
#include 
#include 
#include 
#include 

static int display_info(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) {
    // 仅在文件或目录成功被 `stat` 时打印信息
    if (typeflag != FTW_NS) {
        printf("文件/目录: %s, 修改时间: %s", fpath, ctime(&sb->st_mtime));
    }
    return 0; // 返回0以继续遍历
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "用法: %s <目录路径>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    int flags = 0;

    // 调用 `nftw()` 遍历目录
    if (nftw(argv[1], display_info, 20, flags) == -1) {
        perror("nftw");
        exit(EXIT_FAILURE);
    }

    exit(EXIT_SUCCESS);
}

这个示例程序定义了一个 display_info 函数,将在 nftw() 遍历时为每个条目调用,并且会打印出每个文件或目录的路径和最后修改时间。注意,`nftw()` 要求定义 _XOPEN_SOURCE 或 _GNU_SOURCE 以确保正确的特性支持。此外,`ctime()` 函数用于将 time_t 类型的时间转换为可读格式。

5.比较目录树

在C语言中,可以使用POSIX标准下的`opendir()`、`readdir()`和`closedir()`这些函数来遍历目录树。为了比较两个目录树的不同,需要递归地遍历两个目录树,同时比较当前遍历的节点。
以下是一个简化的C语言实现示例,用于说明如何遍历并比较两个目录树:

#include 
#include 
#include 
#include 

void compare_directories(const char *dir1, const char *dir2);

// 实用函数,用于构建路径
void build_path(char *dest, const char *base, const char *name) {
    strcpy(dest, base);
    if (dest[strlen(base) - 1] != '/')
        strcat(dest, "/");
    strcat(dest, name);
}

// 递归遍历目录的函数
void walk_directory(const char *dir_path, void (*callback)(const char *, const struct stat *, void *), void *data) {
    DIR *dir;
    struct dirent *entry;
    struct stat info;
    char path[1024];

    if (!(dir = opendir(dir_path)))
        return;

    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
            continue;

        build_path(path, dir_path, entry->d_name);
        if (stat(path, &info) == 0) {
            callback(path, &info, data);
            if (S_ISDIR(info.st_mode))
                walk_directory(path, callback, data);
        }
    }
    closedir(dir);
}

// 回调函数,用于比较文件
void compare_file(const char *path, const struct stat *info, void *data) {
    char *other_dir = (char *)data;
    char other_path[1024];
    struct stat other_info;

    build_path(other_path, other_dir, path);

    if (stat(other_path, &other_info) != 0 || info->st_size != other_info.st_size) {
        printf("%s is different or does not exist in %s\n", path, other_dir);
    }
    // 更多的比较可以加在这里(比如比较修改时间等)
}

// 开始遍历和比较两个目录
void compare_directories(const char *dir1, const char *dir2) {
    walk_directory(dir1, compare_file, (void *)dir2);
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s  \n", argv[0]);
        return 1;
    }

    compare_directories(argv[1], argv[2]);

    return 0;
}

这个代码只是一个简单的示例并且不完整,比如它不会检查目录树2中独有的文件,只会对目录树1中的每个文件尝试找到目录树2中的对应文件并比较。为了一个完整的双向比较,会需要跟踪已经比较过的文件并适当处理遗漏的文件。
注意:
- 这段代码仅演示了基本的框架和概念。
- 对于实际的重要数据,应实现更细致的比较逻辑,包括但不限于文件内容的比较、文件权限和属性的比较等。
- 错误处理在该示例中是省略的,实际代码中应检查每个函数调用的返回值,并处理错误情况(如权限不足、文件不存在等)。
在实际应用中,还可能想使用库函数,比如 ftw()(file tree walk)或递归版本`nftw()`,它们提供了遍历整个目录树的便捷方式。不过上面的代码提供了一种更直接、更可控的方式来自定义遍历和比较目录树的逻辑。

在C语言中,`ftw()`和`nftw()`函数允许以递归方式遍历目录树。这里将提供一个简化的代码示例,该示例展示了如何使用`nftw()`函数遍历两个目录,并收集有关每个文件和目录的信息。随后比较这些信息来确定两个目录树之间的不同。
不过,请注意,这个示例假设我们只对文件的名称和类型进行比较,实际在比较文件时可能还要对比内容、修改时间、大小等。为了保持例子的简化性和清晰性,下面的代码只是一个起点。
首先,包括必要的头文件,并定义我们将用于存储目录信息的数据结构:

#define _XOPEN_SOURCE 500  // Needed for nftw to be included
#include 
#include 
#include 
#include 
#include 

typedef struct FileEntry {
    char *path;
    int type; // Use the FTW_* macros to represent the file type
    struct FileEntry *next;
} FileEntry;

FileEntry *add_entry(FileEntry **list, const char *path, int type) {
    FileEntry *entry = malloc(sizeof(FileEntry));
    if (entry == NULL ) {
        perror("malloc failed");
        exit(EXIT_FAILURE);
    }
    entry->path = strdup(path);
    entry->type = type;
    entry->next = *list;
    *list = entry;
    return entry;
}

void free_entries(FileEntry *list) {
    while (list) {
        FileEntry *temp = list;
        list = list->next;
        free(temp->path);
        free(temp);
    }
}

int compare_entries(const FileEntry *list1, const FileEntry *list2) {
    // In a real program, you'd need to compare all items in the two lists
    // Here we're only checking if lists are empty or not for simplicity
    if (list1 == NULL && list2 == NULL) {
        printf("The directory trees are identical.\n");
        return 0;
    } else {
        printf("The directory trees are different.\n");
        return 1;
    }
}

int store_entry(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftwbuf) {
    static FileEntry **current_list = NULL;
    if (current_list == NULL) {
        static FileEntry *first_list = NULL;
        current_list = &first_list;
    } else if (ftwbuf->level == 0) {
        // We are at the start of the second tree
        static FileEntry *second_list = NULL;
        current_list = &second_list;
    }

    add_entry(current_list, fpath + ftwbuf->base, tflag);
    return 0; // To tell nftw to continue
}

void compare_directories(const char *dirpath1, const char *dirpath2) {
    if (nftw(dirpath1, store_entry, 20, 0) != 0) {
        perror("nftw");
        exit(EXIT_FAILURE);
    }

    // Set so that store_entry knows to start a new list
    FileEntry **current_list = NULL;

    if (nftw(dirpath2, store_entry, 20, 0) != 0) {
        perror("nftw");
        exit(EXIT_FAILURE);
    }

    // Now compare the two lists
    // This logic should be replaced with a proper comparison of the lists
    compare_entries(*current_list, *(current_list + 1));

    // Don't forget to free the memory
    free_entries(*current_list);
    free_entries(*(current_list + 1));
}
/*请注意,这段代码所做的事情只是遍历目录和记录了每个条目的路径和类型。细节比较和结果输出的工作是留给了`compare_entries`函数,这里的实现很简单,只是一个占位符,真正的实现将需要详细地遍历这两个链表,比较每个成员的不同点。
要实际执行比较,将需要调用`compare_directories`函数并传入两个目录路径的字符串:*/

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s  \n", argv[0]);
        return EXIT_FAILURE;
    }

    compare_directories(argv[1], argv[2]);

    return EXIT_SUCCESS;
}

编译并运行时确保系统支持`nftw()`函数。这段代码还不是生产就绪的代码,它缺少详尽的错误处理和一些其他关键功能,比如实际的文件内容比较。此外,相对路径处理逻辑在`store_entry`函数里需要完善。一种更完善的实现应该创建一个包含相对路径信息的数据结构,比对所有属性,如文件大小、修改时间等,甚至可能涉及进行文件内容的哈希比较。
另外,针对大型的目录结构,可能需要考虑性能问题,如使用更高效的数据结构(例如使用哈希表而不是链表存储文件信息),以及控制内存使用。
要完成这项任务,需要详细设计`compare_entries`函数,对每一个文件的相对路径、大小、最后修改时间等进行比较。如果有需要,可以考虑使用第三方库,例如`md5`或`sha1`等,对文件内容进行哈希,以便比较是否存在内容上的差异。
最后,本示例中的`store_entry`函数会受到全局变量的影响,这在并发环境中是不安全的。应该使用线程本地存储或者其他机制来避免潜在的数据竞态问题。
需要注意的是,这仅是一个概念性的样板代码,需要额外的工作来转变为一个可用的、健壮的目录对比工具。

你可能感兴趣的:(编程,#,C语言,#,linux,c语言,算法,开发语言)