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变量
|
使用要点:
打开目录后必须校验返回值,严禁直接使用NULL指针操作;
目录使用完毕后,必须调用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值
|
使用要点:
循环调用readdir时,需先判断返回值是否为NULL,再区分是读取完毕还是失败;
目录项包含“.”和“..”,遍历时常需过滤这两个特殊项;
返回的struct dirent*指针指向的内存由系统分配,无需用户手动free;
多次调用readdir会覆盖上一次的目录项数据,如需保存需自行拷贝;
子目录需单独调用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变量
|
使用要点:
目录关闭后,对应的DIR*指针变为野指针,建议置为NULL;
无论目录操作是否成功,只要opendir成功,就必须执行closedir;
程序退出前,需确保所有打开的目录流均已关闭。
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.4. 目录遍历逻辑¶
目录遍历是目录操作最核心的应用场景,分为单层目录遍历(仅遍历当前目录,不进入子目录)和递归目录遍历(遍历当前目录+所有子目录), 核心流程一致,递归遍历需额外处理子目录的打开与遍历。
5.4.1. 单层目录遍历流程¶
参数校验:检查传入的目录路径是否合法,是否为空;
打开目录:调用opendir打开目录,校验返回值,失败则打印错误退出;
循环读取:循环调用readdir读取目录项,直至返回NULL;
过滤特殊项:跳过“.”和“..”两个特殊目录项,避免无效遍历;
处理目录项:解析目录项的文件名、文件类型,按需打印或执行后续逻辑;
关闭目录:调用closedir关闭目录流,释放资源。
5.4.2. 递归目录遍历流程¶
在单层遍历的基础上,读取目录项时判断文件类型,若为子目录(排除“.”和“..”), 则拼接子目录全路径,递归调用遍历函数,直至遍历完所有层级的目录与文件,该方式适用于全盘扫描、批量文件检索等场景。
5.5. 目录操作示例¶
5.5.1. 单层目录遍历实验¶
5.5.1.1. 程序源码¶
本节我们通过一个实验,来讲解如何实现单层目录遍历。
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内容如下所示:
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. 程序源码¶
本节我们通过一个实验,来讲解如何实现递归目录遍历。
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. 目录操作注意事项¶
严格校验返回值:opendir、readdir、closedir均需校验返回值,尤其readdir要区分读取完毕和失败,避免遗漏错误、程序崩溃。
需过滤特殊目录项:遍历目录时必须跳过“.”和“..”,否则会陷入无限循环,尤其是递归遍历,极易导致程序卡死。
路径拼接规范:操作子目录/文件时,需拼接目录路径与文件名,不能直接使用d_name(仅为文件名),避免路径无效。
杜绝资源泄漏:opendir成功后,无论后续操作是否异常,都必须调用closedir关闭目录流,递归场景下更要注意逐级关闭。
权限问题处理:部分系统目录、权限受限目录无法访问,opendir/readdir会失败,需做好容错,避免程序直接退出。
d_type兼容性:部分老旧文件系统不支持d_type成员,此时需结合stat函数的st_mode判断文件类型,保证兼容性。
野指针规避:目录关闭后,将DIR*指针置为NULL,避免重复关闭、误操作。
递归深度限制:递归遍历目录时,系统栈空间有限,过深的目录层级可能导致栈溢出,极端场景建议改用非递归遍历。
