20、文件状态与目录操作:stat函数、文件类型判断、文件权限、目录遍历
文件操作,说白了就两件事:读内容和查状态。很多新手只盯着读写函数看,却忽略了文件状态查询这个基本功。我早年在一个嵌入式项目里吃过亏——程序跑着跑着突然崩溃,查了半天才发现是试图对一个目录执行了读取操作。从那以后,我养成了一个习惯:操作文件之前,先拿 stat 问一句「你到底是什么东西」。
20.1 stat 函数:文件状态的「身份证」
stat 函数是 POSIX 标准里查询文件元信息的核心接口。它的原型长这样:
#include <sys/stat.h>
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
三个函数的区别我简单说一下:
- stat:通过路径名查询,如果路径是符号链接,它返回的是目标文件的信息。
- lstat:和 stat 类似,但遇到符号链接时,它返回的是链接本身的信息,而不是目标文件。
- fstat:通过已打开的文件描述符查询,效率更高,适合你已经打开文件后的场景。
返回的 struct stat 结构体里装了一大堆东西,我挑几个最常用的字段给你看:
| 字段 | 类型 | 说明 |
|---|---|---|
| st_mode | mode_t | 文件类型 + 权限位,最核心的字段 |
| st_size | off_t | 文件大小(字节),普通文件才有意义 |
| st_atime | time_t | 最后访问时间 |
| st_mtime | time_t | 最后修改时间 |
| st_ctime | time_t | 状态改变时间(比如权限变了) |
| st_uid / st_gid | uid_t / gid_t | 文件所有者的用户ID和组ID |
20.2 文件类型判断:别把目录当文件读
拿到 st_mode 之后,怎么判断文件类型?POSIX 提供了一套宏,你直接用就行:
#include <sys/stat.h>
struct stat sb;
stat("some_path", &sb);
if (S_ISREG(sb.st_mode)) {
printf("这是一个普通文件\n");
} else if (S_ISDIR(sb.st_mode)) {
printf("这是一个目录\n");
} else if (S_ISLNK(sb.st_mode)) {
printf("这是一个符号链接\n");
} else if (S_ISFIFO(sb.st_mode)) {
printf("这是一个管道(FIFO)\n");
} else if (S_ISSOCK(sb.st_mode)) {
printf("这是一个套接字\n");
} else if (S_ISBLK(sb.st_mode)) {
printf("这是一个块设备\n");
} else if (S_ISCHR(sb.st_mode)) {
printf("这是一个字符设备\n");
}
嗯,这里要注意:S_ISLNK 只有在使用 lstat 时才有意义。如果你用 stat 查一个符号链接,它直接穿透到目标文件,你永远看不到 S_ISLNK 这个结果。
我曾经在写一个备份工具时踩过这个坑——遍历目录时想跳过符号链接,结果用 stat 判断死活不生效。后来换成 lstat 才搞定。说白了,查什么就用什么函数,别混着用。
20.3 文件权限:rwx 背后的数字
Linux 文件权限用 12 位二进制表示,但咱们平时接触最多的是低 9 位:所有者、所属组、其他人各三位(rwx)。st_mode 里还包含了 setuid、setgid 和 sticky 位,不过日常用得少。
提取权限位可以用掩码 S_IRWXU | S_IRWXG | S_IRWXO,或者直接用 0777 与运算:
mode_t perm = sb.st_mode & 0777;
printf("权限:%o\n", perm); // 八进制输出,比如 0755
判断某个具体权限,用对应的宏:
S_IRUSR— 所有者读S_IWUSR— 所有者写S_IXUSR— 所有者执行S_IRGRP、S_IWGRP、S_IXGRP— 组权限S_IROTH、S_IWOTH、S_IXOTH— 其他人权限
access() 来判断文件是否存在,然后用 stat 读权限——这中间存在 TOCTOU(Time of Check, Time of Use)竞态条件。正确的做法是直接尝试打开文件,根据 errno 判断失败原因。
20.4 目录遍历:opendir / readdir / closedir
遍历目录是文件操作里的家常便饭。标准做法是用 opendir 打开目录,然后用 readdir 一条一条读条目,最后 closedir 关闭。来看代码:
#include <dirent.h>
#include <stdio.h>
void list_dir(const char *path) {
DIR *dir = opendir(path);
if (!dir) {
perror("opendir");
return;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
// 跳过 . 和 ..
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0) {
continue;
}
printf("%s\n", entry->d_name);
}
closedir(dir);
}
struct dirent 里最重要的字段是 d_name(文件名)和 d_type(文件类型)。d_type 是个好东西,它可以直接告诉你这个条目是文件、目录还是链接,省得你再调一次 stat。不过要注意:d_type 不是所有文件系统都支持(比如某些老旧的 NFS 就不支持),所以别完全依赖它。
我个人的做法是:先用 d_type 快速判断,如果遇到 DT_UNKNOWN,再 fallback 到 lstat。这样既快又稳。
20.5 知识体系总览
下面这张图把本章的核心知识点串起来了,你一看就明白:
20.6 避坑指南与实战建议
最后,我把自己这些年踩过的坑总结一下,你写代码时多留个心眼:
- 符号链接穿透问题:前面说过了,用 stat 还是 lstat,想清楚再选。
- 路径拼接:遍历目录时,子文件的完整路径需要自己拼。别直接用
d_name去调 stat,它只是文件名,不是完整路径。我习惯用snprintf拼路径:snprintf(fullpath, sizeof(fullpath), "%s/%s", dirpath, entry->d_name)。 - 错误处理:opendir 可能因为权限不足返回 NULL,readdir 也可能失败(虽然概率低)。别偷懒不检查返回值。
- 性能考量:如果你要遍历一个大目录(比如几万个文件),每次 readdir 后都调一次 stat 会很慢。这时候
d_type就派上用场了——能省一次系统调用就是一次。 - 线程安全:
readdir不是线程安全的,多线程环境下用readdir_r(虽然 POSIX 标记它为废弃,但实际中还是比裸用 readdir 安全)。
一句话总结:stat 是文件操作的「侦察兵」,目录遍历是文件系统的「扫街」。两者配合,你就能在 Linux 文件系统里来去自如。
公众号:蓝海资料掘金营,微信deep3321