15、文件属性获取:stat函数详解、文件类型判断、文件权限与时间信息

文件操作里,读写数据只是基本功。真正让我觉得「嗯,这才叫懂系统」的,是能随手拿到文件的元信息——它是什么类型?谁有权限?什么时候改的?

这些信息,全靠 stat 家族搞定。今天咱们就把 stat 扒个底朝天。

stat 函数家族:三兄弟各有所长

Linux 下获取文件属性有三个函数:statfstatlstat。原型都在 <sys/stat.h> 里。

#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);

三个函数返回的都是同一个结构体 struct stat,区别在于怎么找到文件:

  • stat:给路径名,直接查。如果路径是符号链接,它会追踪到目标文件。
  • lstat:也给路径名,但不追踪符号链接。查的就是链接本身。
  • fstat:给文件描述符。你已经在用 open 拿到 fd 了,顺手查一下属性。

核心区别一句话:stat 看目标,lstat 看链接,fstat 看已打开的文件。

我个人习惯是:处理普通文件用 stat,处理符号链接用 lstat,处理已打开的文件用 fstat。有一次我在项目里用 stat 去查一个软链接的权限,结果拿到的是目标文件的权限——排查了半天才发现问题。嗯,从那以后我养成了「先判断是不是链接,再决定用哪个」的习惯。

struct stat 结构体:文件属性的全家桶

这个结构体里装的东西,比你想象的多。我挑最常用的几个字段说:

struct stat {
    dev_t     st_dev;     // 设备ID
    ino_t     st_ino;     // inode编号
    mode_t    st_mode;    // 文件类型 + 权限位
    nlink_t   st_nlink;   // 硬链接数
    uid_t     st_uid;     // 所有者UID
    gid_t     st_gid;     // 所属组GID
    off_t     st_size;    // 文件大小(字节)
    time_t    st_atime;   // 最后访问时间
    time_t    st_mtime;   // 最后修改时间
    time_t    st_ctime;   // 最后状态改变时间
    blksize_t st_blksize; // 块大小(I/O 建议值)
    blkcnt_t  st_blocks;  // 占用块数(512字节为单位)
};

你看,从设备号到块大小,全都有。但日常开发里,我们最关心的其实是三个维度:类型、权限、时间

文件类型判断:别被 st_mode 吓到

st_mode 是个位图,既包含文件类型,又包含权限位。新手一看就懵。其实拆开看很简单:

判断文件类型,用系统提供的宏:

if (S_ISREG(st.st_mode)) {
    printf("普通文件\n");
} else if (S_ISDIR(st.st_mode)) {
    printf("目录\n");
} else if (S_ISLNK(st.st_mode)) {
    printf("符号链接\n");
} else if (S_ISCHR(st.st_mode)) {
    printf("字符设备\n");
} else if (S_ISBLK(st.st_mode)) {
    printf("块设备\n");
} else if (S_ISFIFO(st.st_mode)) {
    printf("管道/FIFO\n");
} else if (S_ISSOCK(st.st_mode)) {
    printf("套接字\n");
}

这些宏本质上就是 (st_mode & S_IFMT) == S_IFREG 的封装。你想想看,Linux 下一切皆文件,但文件类型有七种。用宏判断,代码可读性高,也不容易出错。

小技巧:如果你用 lstat 查到一个符号链接,再用 stat 查一次,就能对比出「链接本身」和「目标文件」的属性差异。我在做备份工具时就用这个逻辑来判断是否需要递归处理。

文件权限:rwx 背后的数字

权限位藏在 st_mode 的低 12 位里。分别是:

  • 9 位基本权限(owner / group / other 的 rwx)
  • 3 位特殊权限(SUID、SGID、Sticky Bit)

提取权限位,用掩码 S_IRWXU | S_IRWXG | S_IRWXO,或者直接用 st_mode & 07777

// 打印权限的字符串形式
char perms[11] = {0};
perms[0] = (st.st_mode & S_IRUSR) ? 'r' : '-';
perms[1] = (st.st_mode & S_IWUSR) ? 'w' : '-';
perms[2] = (st.st_mode & S_IXUSR) ? 'x' : '-';
// ... 以此类推
printf("权限: %s\n", perms);

我曾经在嵌入式项目里遇到过一个问题:固件升级脚本总是报权限错误。查了半天,发现是打包时没保留可执行位,解压后文件是 644 而不是 755。从那以后,我写任何文件操作代码都会顺手检查一下权限位。

时间信息:三个时间戳各司其职

struct stat 里有三个时间字段,很多人搞混。我画个图帮你理清:

时间 atime 访问时间 cat / read 时更新 mtime 修改时间 内容改变时更新 ctime 状态改变时间 权限/所有者/链接数变化时更新 注意:ctime 不是创建时间!Linux 没有文件创建时间字段 rename、chmod、chown 都会更新 ctime

三个时间戳的区别,我用一句话总结:

  • atime:读文件时更新。频繁读盘会大量更新 atime,所以很多系统用 noatime 挂载选项来优化。
  • mtime:写文件内容时更新。这是最常用的,比如 make 就靠它判断是否需要重新编译。
  • ctime:文件元数据变化时更新。改权限、改所有者、改链接数,都会触发 ctime 更新。

注意:ctime 不是创建时间!Linux 下没有标准的「文件创建时间」字段。ctime 是「状态改变时间」(change time),不是「创建时间」(create time)。很多新手在这里栽过跟头。

完整示例:打印文件全部属性

下面这个函数,我在好几个项目里复用。它把 stat 拿到的信息格式化输出:

#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>

void print_file_info(const char *path) {
    struct stat st;
    if (lstat(path, &st) == -1) {
        perror("lstat");
        return;
    }

    // 文件类型
    if (S_ISREG(st.st_mode))  printf("类型: 普通文件\n");
    else if (S_ISDIR(st.st_mode)) printf("类型: 目录\n");
    else if (S_ISLNK(st.st_mode)) printf("类型: 符号链接\n");
    else printf("类型: 其他\n");

    // 权限(八进制 + 字符串)
    printf("权限: %o\n", st.st_mode & 07777);
    printf("硬链接数: %lu\n", (unsigned long)st.st_nlink);

    // 所有者和组
    struct passwd *pw = getpwuid(st.st_uid);
    struct group  *gr = getgrgid(st.st_gid);
    printf("所有者: %s (UID %u)\n", pw ? pw->pw_name : "?", st.st_uid);
    printf("所属组: %s (GID %u)\n", gr ? gr->gr_name : "?", st.st_gid);

    // 大小和块
    printf("大小: %ld 字节\n", (long)st.st_size);
    printf("占用块: %ld (512字节/块)\n", (long)st.st_blocks);

    // 时间
    printf("最后访问: %s", ctime(&st.st_atime));
    printf("最后修改: %s", ctime(&st.st_mtime));
    printf("状态改变: %s", ctime(&st.st_ctime));
}

这段代码里我用了 lstat 而不是 stat,因为我想看到符号链接本身的属性,而不是被它指向的目标。如果你需要追踪到目标文件,换成 stat 就行。

避坑指南:stat 的几个常见陷阱

我这些年踩过的坑,列出来给你提个醒:

  1. 符号链接追踪问题:用 stat 查链接,拿到的是目标文件的信息。如果你想知道链接本身的大小(通常是目标路径的字符串长度),必须用 lstat。
  2. ctime 误解:ctime 不是创建时间。我曾经在日志系统里用 ctime 做「文件创建时间」记录,结果文件被 chmod 一下,ctime 就变了——日志全乱套了。
  3. st_blocks 的单位:这个字段的单位是 512 字节,不是 st_blksize。很多人在计算磁盘占用时算错。
  4. 性能问题:stat 调用涉及系统调用,频繁调用会有性能开销。如果你在循环里对大量文件做 stat,考虑用 scandirfts 这类批量接口。

我的习惯:写文件操作代码时,先 stat 一把,确认类型和权限,再动手。这个习惯帮我避免了很多「文件不存在」「权限不足」的运行时错误。

总结一下

stat 函数族是文件元信息的入口。三个函数选哪个,取决于你要查的是路径、fd 还是链接本身。st_mode 里藏着类型和权限,用宏判断类型,用掩码提取权限。三个时间戳各管一摊,别把 ctime 当创建时间就行。

文件属性获取,说白了就是「问系统要一份文件的身份证」。读懂了 stat,你就真正理解了 Linux 文件系统的底层逻辑。


公众号:蓝海资料掘金营,微信deep3321