5. 目录操作

在Linux“一切皆文件”的设计体系中,目录属于特殊的文件类型,专门用于存储文件的索引信息与层级结构, 是文件系统实现文件管理、路径定位的核心载体。相较于普通文件的读写操作,目录操作有着专属的API接口与操作规范, 无法直接用标准I/O或系统I/O的读写函数处理,必须借助专门的目录操作函数完成打开、读取、关闭、遍历等动作。

本章的示例代码目录为:base_linux/file_io/dir_traversal

5.1. 目录操作基础

5.1.1. 目录流概念

Linux系统中,操作目录的核心是目录流(DIR),它是一个封装了目录当前状态、读取偏移、缓存信息的抽象结构体, 类似于普通文件操作中的FILE*流或文件描述符,用户无需关注结构体内部细节,只需通过DIR*指针完成目录的各类操作。 目录流由opendir函数创建,操作完毕后需通过closedir函数关闭,避免资源泄漏, 这一流程和普通文件的打开、关闭逻辑高度相似,便于开发者快速上手。

5.1.2. 目录项结构体

调用readdir函数读取目录时,会返回目录中每一个文件项的信息,这些信息存储在struct dirent结构体中, 该结构体定义在<dirent.h>头文件中,是解析目录内容的核心载体,不同系统版本的成员略有差异, Linux下核心成员及含义如下:

1
2
3
4
5
6
7
 struct dirent {
     ino_t          d_ino;       // 目录项对应的文件inode号,和文件属性中的st_ino一致
     off_t          d_off;       // 目录项的偏移量,系统内部使用,用户层一般无需关注
     unsigned short d_reclen;    // 目录项的长度,可变长,适配不同文件名长度
     unsigned char  d_type;      // 文件类型,可快速判断文件类型,无需额外调用stat函数
     char           d_name[256]; // 文件名,字符串形式,最大支持255个字符
 };

d_name成员存储的是文件名,并非文件全路径,若需操作该文件,需拼接目录路径与文件名; d_type成员可快速判断文件类型,提升遍历效率,部分老旧文件系统可能不支持该成员,需结合stat函数做兼容处理。

5.1.3. 目录操作核心头文件

目录操作相关函数与结构体均依赖专属头文件,开发时必须提前包含, 否则会出现函数未定义、结构体未声明的编译错误,核心头文件如下:

  • <dirent.h>:核心头文件,包含DIR结构体、struct dirent结构体、opendir/readdir/closedir等函数声明

  • <sys/types.h>:基础类型头文件,定义ino_t、off_t等基础数据类型

  • <stdio.h>:标准IO头文件,用于打印信息、错误输出

  • <stdlib.h>:标准库头文件,用于内存管理、程序退出处理

  • <string.h>:字符串头文件,用于路径拼接、文件名对比

  • <errno.h>:错误码头文件,结合perror/strerror处理错误

  • <unistd.h>:系统调用头文件,适配各类系统级操作

5.2. 目录操作核心函数

目录操作的核心流程为:打开目录->读取目录项->关闭目录,三大函数各司其职,缺一不可, 下面对每个函数做详细说明。

5.2.1. 打开目录

5.2.1.1. opendir函数

opendir函数用于根据传入的目录路径,打开指定目录并创建对应的目录流,成功打开后, 目录流指向目录的起始位置,为后续readdir读取目录项做准备。 opendir函数仅打开目录、初始化目录流,不会读取目录内的具体文件信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 // 所需头文件
 #include <sys/types.h>
 #include <dirent.h>

 // 函数原型
 DIR *opendir(const char *name);

 // 参数:
 // name:待打开的目录路径字符串,const char*类型,支持绝对路径和相对路径

 // 返回值:
 // 成功:返回指向对应目录流的DIR*指针,后续所有目录操作均依赖该指针
 // 失败:返回NULL,同时设置全局errno变量

使用要点:

  1. 打开目录后必须校验返回值,严禁直接使用NULL指针操作;

  2. 目录使用完毕后,必须调用closedir关闭,避免文件描述符泄漏。

5.2.2. 读取目录项

5.2.2.1. readdir函数

readdir函数用于从已打开的目录流中,依次读取下一个目录项,返回包含该目录项信息的struct dirent结构体指针, 每调用一次,目录流自动向后偏移,直至读取完所有目录项。 readdir函数是实现目录遍历的核心,可循环调用获取目录内所有文件/子目录信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 // 所需头文件
 #include <sys/types.h>
 #include <dirent.h>

 // 函数原型
 struct dirent *readdir(DIR *dirp);

 // 参数:
 // dirp:已通过opendir函数成功打开的DIR*目录流指针,必须为有效指针,不能为NULL

 // 返回值
 // 成功:返回指向当前目录项的struct dirent*指针,可通过该指针获取d_name、d_ino、d_type等信息
 // 读取完毕:返回NULL,且errno值不变
 // 失败:返回NULL,且errno值被设置为非0值

使用要点:

  1. 循环调用readdir时,需先判断返回值是否为NULL,再区分是读取完毕还是失败;

  2. 目录项包含“.”和“..”,遍历时常需过滤这两个特殊项;

  3. 返回的struct dirent*指针指向的内存由系统分配,无需用户手动free;

  4. 多次调用readdir会覆盖上一次的目录项数据,如需保存需自行拷贝;

  5. 子目录需单独调用opendir打开,才能继续遍历下一级目录。

5.2.3. 关闭目录

5.2.3.1. closedir函数

closedir函数用于关闭已打开的目录流,释放系统为该目录流分配的内存、文件描述符等资源, 目录操作完成后必须执行该操作,避免长期运行导致资源耗尽。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 // 所需头文件
 #include <sys/types.h>
 #include <dirent.h>

 // 函数原型
 int closedir(DIR *dirp);


 // 参数:
 // dirp:待关闭的DIR*目录流指针,必须是opendir返回的有效指针,不能为NULL

 // 返回值说明
 // 成功:返回0
 // 失败:返回-1,同时设置全局errno变量

使用要点:

  1. 目录关闭后,对应的DIR*指针变为野指针,建议置为NULL;

  2. 无论目录操作是否成功,只要opendir成功,就必须执行closedir;

  3. 程序退出前,需确保所有打开的目录流均已关闭。

5.3. 目录操作拓展函数

除三大核心函数外,Linux还提供了辅助目录操作的拓展函数,用于重置目录流偏移、定位目录项、获取当前偏移, 适配复杂的目录操作场景。

5.3.1. 重置目录流

5.3.1.1. rewinddir函数

rewinddir函数用于将目录流的读取偏移重置到目录起始位置,可重新从头读取目录项。 适用需要重复遍历同一目录时,无需重新打开目录,直接重置偏移即可。

1
2
3
4
5
6
7
8
 // 所需头文件
 #include <sys/types.h>
 #include <dirent.h>

 // 函数原型
 void rewinddir(DIR *dirp);

 // 参数:dirp 有效DIR*目录流指针

5.3.2. 获取目录流偏移

5.3.2.1. telldir函数

telldir函数用于获取当前目录流的读取偏移量,记录当前读取位置。 适用需要暂停遍历,后续恢复读取位置的场景。

1
2
3
4
5
6
7
8
9
 // 所需头文件
 #include <sys/types.h>
 #include <dirent.h>

 // 函数原型
 long telldir(DIR *dirp);

 // 参数:dirp 有效DIR*目录流指针
 // 返回值:成功返回当前偏移量,失败返回-1并设置errno

5.3.3. 定位目录流

5.3.3.1. seekdir函数

seekdir函数用于将目录流偏移定位到指定位置,配合telldir实现目录读取位置跳转。 适用恢复之前暂停的目录遍历,跳转到指定目录项位置的场景。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 // 所需头文件
 #include <sys/types.h>
 #include <dirent.h>

 // 函数原型
 void seekdir(DIR *dirp, long offset);


 // 参数:
 // dirp:有效目录流指针
 // offset:telldir获取的偏移量

5.4. 目录遍历逻辑

目录遍历是目录操作最核心的应用场景,分为单层目录遍历(仅遍历当前目录,不进入子目录)和递归目录遍历(遍历当前目录+所有子目录), 核心流程一致,递归遍历需额外处理子目录的打开与遍历。

5.4.1. 单层目录遍历流程

  1. 参数校验:检查传入的目录路径是否合法,是否为空;

  2. 打开目录:调用opendir打开目录,校验返回值,失败则打印错误退出;

  3. 循环读取:循环调用readdir读取目录项,直至返回NULL;

  4. 过滤特殊项:跳过“.”和“..”两个特殊目录项,避免无效遍历;

  5. 处理目录项:解析目录项的文件名、文件类型,按需打印或执行后续逻辑;

  6. 关闭目录:调用closedir关闭目录流,释放资源。

5.4.2. 递归目录遍历流程

在单层遍历的基础上,读取目录项时判断文件类型,若为子目录(排除“.”和“..”), 则拼接子目录全路径,递归调用遍历函数,直至遍历完所有层级的目录与文件,该方式适用于全盘扫描、批量文件检索等场景。

5.5. 目录操作示例

5.5.1. 单层目录遍历实验

5.5.1.1. 程序源码

本节我们通过一个实验,来讲解如何实现单层目录遍历。

单层目录遍历实验(base_linux/file_io/dir_traversal1/dir_traversal.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include <string.h>


const char *get_file_type(unsigned char d_type);

int main(int argc, char *argv[]) {
    // 校验命令行参数
    if (argc != 2) {
        fprintf(stderr, "用法:%s <目标目录路径>\n", argv[0]);
        return -1;
    }

    // 1. 打开目录
    DIR *dir = opendir(argv[1]);
    if (dir == NULL) {
        perror("opendir 打开目录失败");
        return -1;
    }
    printf("========== 开始遍历目录:%s ==========\n", argv[1]);

    struct dirent *entry;
    // 2. 循环读取目录项
    while ((entry = readdir(dir)) != NULL) {
        // 过滤.和..特殊目录项
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }
        // 打印目录项信息
        printf("文件名:%-25s | inode号:%-10ld | 文件类型:%s\n",
               entry->d_name, entry->d_ino, get_file_type(entry->d_type));
    }

    // 区分读取完毕和读取失败
    if (errno != 0) {
        perror("readdir 读取目录项失败");
        closedir(dir);
        return -1;
    }

    // 3. 关闭目录
    if (closedir(dir) == -1) {
        perror("closedir 关闭目录失败");
        return -1;
    }
    printf("========== 目录遍历完成 ==========\n");
    return 0;
}

// 根据d_type打印文件类型
const char *get_file_type(unsigned char d_type) {
    switch (d_type) {
        case DT_REG: return "普通文件";
        case DT_DIR: return "目录文件";
        case DT_LNK: return "软链接文件";
        case DT_CHR: return "字符设备";
        case DT_BLK: return "块设备";
        case DT_FIFO: return "管道文件";
        case DT_SOCK: return "套接字文件";
        default: return "未知类型";
    }
}

以上程序实现指定目录的单层遍历,打印目录内所有文件/子目录的名称、inode号、文件类型,过滤特殊目录项,适配基础遍历需求。

5.5.1.2. Makefile说明

Makefile是跟工程目录匹配的,本实验仅有一个C文件,且与Makefile处于同级目录。

Makefile内容如下所示:

单层目录遍历实验(base_linux/file_io/dir_traversal1/Makefile文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#定义变量
TARGET = dir_traversal
#定义编译器
CC = gcc
#定义头文件的位置()
CFLAGS = -I.
#定义头文件
DEPS = 
#定义目标文件
OBJS = $(TARGET).o

#目标文件
$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(CFLAGS)

#*.o文件的生成规则
%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

#伪目标
.PHONY: clean
#make clean清除编译结果
clean:
	rm -f $(TARGET) $(TARGET).o

5.5.1.3. 编译及测试

使用如下步骤进行编译测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#在主机的实验代码Makefile目录下编译
make

#运行
./dir_traversal /etc/apt/

#信息输出如下
========== 开始遍历目录:/etc/apt/ ==========
 文件名:sources.list.d            | inode号:8650973    | 文件类型:目录文件
 文件名:auth.conf.d               | inode号:8650971    | 文件类型:目录文件
 文件名:apt.conf.d                | inode号:8650970    | 文件类型:目录文件
 文件名:sources.list              | inode号:8668437    | 文件类型:普通文件
 文件名:trusted.gpg.d             | inode号:8650974    | 文件类型:目录文件
 文件名:preferences.d             | inode号:8650972    | 文件类型:目录文件
 文件名:sources.list.curtin.old   | inode号:8651749    | 文件类型:普通文件
 文件名:keyrings                  | inode号:8672267    | 文件类型:目录文件
 ========== 目录遍历完成 ==========

5.5.2. 递归目录遍历实验

5.5.2.1. 程序源码

本节我们通过一个实验,来讲解如何实现递归目录遍历。

递归目录遍历实验(base_linux/file_io/dir_traversal2/dir_traversal.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

void traverse_dir(const char *dir_path);

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

    printf("========== 递归遍历目录:%s ==========\n", argv[1]);
    traverse_dir(argv[1]);
    printf("========== 递归遍历完成 ==========\n");
    return 0;
}

// 递归遍历目录函数
void traverse_dir(const char *dir_path) {
    DIR *dir = opendir(dir_path);
    if (dir == NULL) {
        perror("opendir 失败");
        return;
    }

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        // 过滤.和..
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        // 拼接文件/子目录全路径
        char full_path[PATH_MAX];
        snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name);

        // 打印全路径
        printf("%s\n", full_path);

        // 判断是否为子目录,若是则递归遍历
        if (entry->d_type == DT_DIR) {
            traverse_dir(full_path);
        }
    }

    // 读取错误判断
    if (errno != 0) {
        perror("readdir 失败");
    }

    // 关闭目录
    closedir(dir);
}

以上程序实现指定目录的递归遍历,遍历所有层级的子目录与文件,打印全路径信息, 适配深度文件扫描场景,加入路径拼接、递归调用、资源释放等逻辑。

5.5.2.2. Makefile说明

Makefile与systemcall1示例的Makefile一致,此处不重复说明。

5.5.2.3. 编译及测试

使用如下步骤进行编译测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#在主机的实验代码Makefile目录下编译
make

#运行
./dir_traversal /etc/apt/

#信息输出如下
========== 递归遍历目录:/etc/apt/ ==========
/etc/apt//sources.list.d
/etc/apt//sources.list.d/docker.list
/etc/apt//auth.conf.d
/etc/apt//apt.conf.d
.....
/etc/apt//trusted.gpg.d/debian-archive-trixie-automatic.asc
/etc/apt//trusted.gpg.d/debian-archive-bookworm-stable.asc
/etc/apt//trusted.gpg.d/ubuntu-keyring-2018-archive.gpg
/etc/apt//preferences.d
/etc/apt//sources.list.curtin.old
/etc/apt//keyrings
/etc/apt//keyrings/docker.gpg
========== 递归遍历完成 ==========

5.6. 目录操作注意事项

  1. 严格校验返回值:opendir、readdir、closedir均需校验返回值,尤其readdir要区分读取完毕和失败,避免遗漏错误、程序崩溃。

  2. 需过滤特殊目录项:遍历目录时必须跳过“.”和“..”,否则会陷入无限循环,尤其是递归遍历,极易导致程序卡死。

  3. 路径拼接规范:操作子目录/文件时,需拼接目录路径与文件名,不能直接使用d_name(仅为文件名),避免路径无效。

  4. 杜绝资源泄漏:opendir成功后,无论后续操作是否异常,都必须调用closedir关闭目录流,递归场景下更要注意逐级关闭。

  5. 权限问题处理:部分系统目录、权限受限目录无法访问,opendir/readdir会失败,需做好容错,避免程序直接退出。

  6. d_type兼容性:部分老旧文件系统不支持d_type成员,此时需结合stat函数的st_mode判断文件类型,保证兼容性。

  7. 野指针规避:目录关闭后,将DIR*指针置为NULL,避免重复关闭、误操作。

  8. 递归深度限制:递归遍历目录时,系统栈空间有限,过深的目录层级可能导致栈溢出,极端场景建议改用非递归遍历。