14、目录操作:opendir()、readdir()、closedir()、mkdir()、rmdir() 函数的使用
文件操作我们聊了不少,但实际项目中,光读写单个文件往往不够。你想想看,一个嵌入式设备里,日志文件是按日期分目录存的,配置文件是放在特定路径下的,升级包需要先创建临时目录再解压——这些都离不开目录操作。
今天我就带你把 Linux 下最常用的五个目录操作函数捋一遍:opendir()、readdir()、closedir()、mkdir()、rmdir()。说白了,就是怎么打开一个目录、怎么遍历里面的内容、怎么创建和删除目录。这套接口我几乎每个项目都会用到,尤其是做文件系统相关的底层开发时。
核心要点:目录在 Linux 中也被当作一种文件来对待,但它有专门的 API 族。操作目录前必须先打开,操作完后必须关闭——这和文件操作是一个道理。
14.1 目录操作的三件套:opendir、readdir、closedir
先看最基础的一组函数。它们负责遍历一个目录下的所有条目(包括文件和子目录)。
14.1.1 opendir() —— 打开目录
原型很简单:
#include <dirent.h>
DIR *opendir(const char *name);
参数就是目录路径字符串。返回值是一个 DIR * 指针,你可以把它理解成目录的“句柄”。如果打开失败,返回 NULL,并设置 errno。
我个人习惯在调用后立刻检查返回值:
DIR *dp = opendir("/tmp/logs");
if (dp == NULL) {
perror("opendir failed");
return -1;
}
嗯,这里要注意:DIR 这个结构体是系统定义的,你不需要关心它的内部细节,只要知道它是一个不透明指针就行。
14.1.2 readdir() —— 读取目录条目
打开目录后,就可以用 readdir() 逐个读取里面的内容了。
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
每次调用返回一个 struct dirent * 指针,指向当前条目。读取完所有条目后,返回 NULL。
struct dirent 里我们最常用的字段就两个:
| 字段 | 说明 |
|---|---|
d_name |
文件名(不含路径),以 '\0' 结尾 |
d_type |
文件类型(DT_REG 普通文件,DT_DIR 目录等) |
我在项目中遇到过一个问题:很多新手只关注 d_name,却忽略了 d_type。结果遍历目录时把 "." 和 ".." 也当普通文件处理了,导致逻辑出错。所以我的建议是——遍历时一定要过滤掉这两个特殊条目。
来看一个完整的遍历示例:
#include <stdio.h>
#include <dirent.h>
#include <errno.h>
void list_dir(const char *path) {
DIR *dp = opendir(path);
if (dp == NULL) {
perror("opendir");
return;
}
struct dirent *entry;
while ((entry = readdir(dp)) != NULL) {
// 跳过当前目录和父目录
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0) {
continue;
}
if (entry->d_type == DT_DIR) {
printf("[DIR] %s\n", entry->d_name);
} else if (entry->d_type == DT_REG) {
printf("[FILE] %s\n", entry->d_name);
} else {
printf("[OTHER]%s\n", entry->d_name);
}
}
closedir(dp);
}
小技巧:有些文件系统(比如某些嵌入式用的 FUSE 或 RAMFS)可能不支持 d_type,这时 d_type 的值会是 DT_UNKNOWN。遇到这种情况,你需要用 stat() 系统调用来获取文件类型。我一般会写一个兼容函数,先查 d_type,如果无效再 fallback 到 stat()。
14.1.3 closedir() —— 关闭目录
用完目录后,一定要关闭它。这跟 fclose() 是一个道理。
#include <dirent.h>
int closedir(DIR *dirp);
成功返回 0,失败返回 -1。我曾经见过一个同事,在循环里反复 opendir() 却不 closedir(),结果跑了一天后进程打开的文件描述符耗尽,整个系统都卡住了。嗯,这种坑踩过一次就再也不敢忘了。
14.2 创建与删除目录:mkdir() 和 rmdir()
遍历目录只是基本功,真正干活的时候,你还需要能创建和删除目录。
14.2.1 mkdir() —— 创建目录
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
第一个参数是路径,第二个参数是权限位(类似 chmod 的数值)。
举个例子:
if (mkdir("/tmp/myapp/logs", 0755) == -1) {
perror("mkdir failed");
return -1;
}
这里有个容易忽略的点:mkdir() 只能创建单级目录。如果你想创建 /a/b/c 这种多级目录,而 /a 或 /a/b 不存在,那就会失败。
避坑指南:我曾经在一个自动化部署脚本里直接调 mkdir("/data/logs/2025/04", 0755),结果因为 /data/logs 不存在而失败。后来我写了一个递归创建目录的辅助函数,逐级检查并创建,才彻底解决这个问题。
递归创建目录的参考实现:
#include <string.h>
#include <errno.h>
int mkdir_p(const char *path, mode_t mode) {
char tmp[256];
char *p = NULL;
size_t len;
snprintf(tmp, sizeof(tmp), "%s", path);
len = strlen(tmp);
// 去掉末尾的 '/'
if (tmp[len - 1] == '/')
tmp[len - 1] = '\0';
for (p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = '\0';
if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
return -1;
}
*p = '/';
}
}
// 创建最后一级目录
if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
return -1;
}
return 0;
}
14.2.2 rmdir() —— 删除目录
#include <unistd.h>
int rmdir(const char *pathname);
这个函数只能删除空目录。如果目录里还有文件或子目录,调用就会失败,errno 设置为 ENOTEMPTY。
你想想看,这其实是一种保护机制——防止你误删整个目录树。但在实际项目中,我们经常需要递归删除一个非空目录。这时候就得自己写递归逻辑了:先遍历目录,删除所有文件,再删除子目录,最后删除自身。
重要提醒:千万不要在生产环境里用 system("rm -rf /path") 来删除目录!这样做既不安全(路径里如果有特殊字符会出大问题),也不可移植。我见过有人因为路径拼接错误,把 /usr 给删了的惨案……
14.3 知识体系总览
下面这张图把今天讲的五个函数以及它们之间的关系梳理清楚了。你可以把它当作一个快速参考。
14.4 综合实战:一个简易的目录清理工具
光说不练假把式。我把今天讲的函数串起来,写一个实用的小工具:遍历指定目录,删除所有超过 7 天的日志文件(以 .log 结尾),并删除空目录。
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
void clean_old_logs(const char *path, int max_days) {
DIR *dp = opendir(path);
if (dp == NULL) {
perror("opendir");
return;
}
struct dirent *entry;
char fullpath[512];
struct stat st;
time_t now = time(NULL);
while ((entry = readdir(dp)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0) {
continue;
}
snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name);
if (stat(fullpath, &st) == -1) {
perror("stat");
continue;
}
if (S_ISDIR(st.st_mode)) {
// 递归处理子目录
clean_old_logs(fullpath, max_days);
// 尝试删除空目录
if (rmdir(fullpath) == 0) {
printf("Removed empty dir: %s\n", fullpath);
}
} else if (S_ISREG(st.st_mode)) {
// 检查是否为 .log 文件
char *ext = strrchr(entry->d_name, '.');
if (ext && strcmp(ext, ".log") == 0) {
double days_old = difftime(now, st.st_mtime) / (60 * 60 * 24);
if (days_old > max_days) {
if (unlink(fullpath) == 0) {
printf("Deleted old log: %s (%.1f days old)\n",
fullpath, days_old);
} else {
perror("unlink");
}
}
}
}
}
closedir(dp);
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <directory> <max_days>\n", argv[0]);
return 1;
}
clean_old_logs(argv[1], atoi(argv[2]));
return 0;
}
这个例子把 opendir、readdir、closedir、rmdir、stat、unlink 全用上了。你想想看,实际项目中的日志清理、临时文件回收、缓存目录维护,本质上都是这个套路。
个人建议:在嵌入式系统里,目录操作往往伴随着权限检查和路径合法性校验。我一般会在函数入口处加一个 access(path, R_OK | W_OK) 检查,确保当前进程有足够的权限,避免在遍历到一半时因为权限不足而报错退出。
好了,目录操作的核心内容就这些。记住三个关键点:打开后必须关闭、遍历时过滤特殊条目、创建和删除目录时注意层级和空条件。把这些基本功练扎实了,后面遇到更复杂的文件系统操作(比如 ftw()、nftw() 树遍历)时,你就能快速上手了。