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
我的习惯:每次调用 stat 之前,先把 struct stat 用 memset 清零。虽然理论上 stat 会填满所有字段,但有些老旧系统或者特殊文件系统可能不会完全覆盖,清零能避免读到垃圾值。

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_IRGRPS_IWGRPS_IXGRP — 组权限
  • S_IROTHS_IWOTHS_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 知识体系总览

下面这张图把本章的核心知识点串起来了,你一看就明白:

文件状态与目录操作 stat / lstat / fstat struct stat 结构体 st_mode / st_size / st_*time 文件类型判断 S_ISREG / S_ISDIR / S_ISLNK S_ISFIFO / S_ISSOCK / S_ISBLK / S_ISCHR 文件权限 rwx 权限位 (0777) S_IRUSR / S_IWUSR / S_IXUSR 目录遍历 opendir → readdir → closedir struct dirent: d_name / d_type 综合应用 递归遍历目录树 文件查找 / 备份工具

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