文件属性获取:stat函数、fstat函数、文件类型判断、文件权限与时间戳

文件操作里,读写数据只是基本功。真正让我觉得「嗯,这才叫懂文件」的,是能拿到文件的各种属性。比如这个文件多大?什么时候改的?是普通文件还是目录?权限对不对?

我记得刚做嵌入式那会儿,调试一个SD卡日志系统,死活写不进去。后来一查,是文件系统只读挂载了。用stat一看,权限位全是0,瞬间破案。从那以后,我写文件操作代码,第一件事就是先查属性。

stat 函数:获取文件属性的主力

stat函数是POSIX标准里的老将。原型长这样:

#include <sys/stat.h>

int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);

两个函数干的事差不多,区别在于:stat传路径,fstat传文件描述符。我个人习惯用fstat,因为很多时候我已经打开了文件,手里拿着fd,没必要再传一次路径。

返回的struct stat结构体,里面信息很丰富:

成员 含义 类型
st_dev 设备ID dev_t
st_ino inode编号 ino_t
st_mode 文件类型 + 权限位 mode_t
st_nlink 硬链接数 nlink_t
st_uid 所有者用户ID uid_t
st_gid 所有者组ID gid_t
st_size 文件大小(字节) off_t
st_blksize I/O块大小 blksize_t
st_blocks 占用块数(512字节为单位) blkcnt_t
st_atime 最后访问时间 time_t
st_mtime 最后修改时间 time_t
st_ctime 状态最后改变时间 time_t
小提示:st_ctime不是创建时间,是属性修改时间。比如你改了权限,ctime就会更新。很多新手在这里栽过跟头。

文件类型判断:用宏,别自己算

st_mode这个字段,既包含了文件类型,又包含了权限位。怎么拆开?别自己写位运算,系统提供了现成的宏:

#include <sys/stat.h>

struct stat sb;
stat("test.txt", &sb);

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

我在项目中遇到过一个问题:嵌入式设备上有个文件,用ls -l看是普通文件,但程序里用S_ISREG判断却返回假。后来发现是文件系统损坏,inode标记异常。嗯,这种情况用stat反而能帮你提前发现问题。

核心要点:判断文件类型,永远用S_ISxxx宏,不要手动解析st_mode。不同系统位布局可能不同,宏帮你跨平台。

文件权限:rwx 是怎么存的?

权限位也藏在st_mode里。Linux用12位表示权限,分成三组:

  • 低9位:所有者、所属组、其他用户的 rwx 权限
  • 高3位:setuid、setgid、sticky bit

提取权限的常用方法:

// 获取文件权限(低9位)
mode_t perm = sb.st_mode & 0777;
printf("权限: %o\n", perm);

// 检查特定权限
if (sb.st_mode & S_IRUSR) printf("所有者可读\n");
if (sb.st_mode & S_IWUSR) printf("所有者可写\n");
if (sb.st_mode & S_IXUSR) printf("所有者可执行\n");

你想想看,为什么0777是八进制?因为每3位一组,八进制正好对应。我刚开始学的时候,用十进制算权限,算得头大。后来发现八进制才是正道。

注意:不要直接用sb.st_mode & 0x1FF这种写法。虽然结果一样,但可读性差。用0777,一眼就知道是权限掩码。

时间戳:三个时间,各司其职

文件有三个时间戳,很多人搞混:

字段 含义 触发条件
st_atime 访问时间 读文件(cat、less、read等)
st_mtime 修改时间 内容改变(write、truncate等)
st_ctime 状态改变时间 权限/所有者/链接数改变

格式化输出时间戳:

#include <time.h>

struct stat sb;
stat("test.txt", &sb);

char buf[64];
struct tm *tm_info;

tm_info = localtime(&sb.st_mtime);
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm_info);
printf("最后修改时间: %s\n", buf);

我曾经调试一个日志轮转程序,发现日志文件总是不按预期删除。查了半天,原来我比较的是st_ctime而不是st_mtime。日志内容没变,但权限被其他进程改了,ctime更新了,导致判断逻辑出错。所以,用哪个时间戳,一定要想清楚业务场景。

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

把上面这些串起来,写一个实用的小工具:

#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 sb;
    
    if (stat(path, &sb) == -1) {
        perror("stat");
        return;
    }
    
    // 文件类型
    printf("类型: ");
    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 printf("其他\n");
    
    // 大小
    printf("大小: %ld 字节\n", sb.st_size);
    printf("占用块: %ld (512字节/块)\n", sb.st_blocks);
    
    // 权限(八进制 + 符号形式)
    printf("权限: %o\n", sb.st_mode & 0777);
    printf("权限位: ");
    printf( (sb.st_mode & S_IRUSR) ? "r" : "-");
    printf( (sb.st_mode & S_IWUSR) ? "w" : "-");
    printf( (sb.st_mode & S_IXUSR) ? "x" : "-");
    printf( (sb.st_mode & S_IRGRP) ? "r" : "-");
    printf( (sb.st_mode & S_IWGRP) ? "w" : "-");
    printf( (sb.st_mode & S_IXGRP) ? "x" : "-");
    printf( (sb.st_mode & S_IROTH) ? "r" : "-");
    printf( (sb.st_mode & S_IWOTH) ? "w" : "-");
    printf( (sb.st_mode & S_IXOTH) ? "x" : "-");
    printf("\n");
    
    // 时间戳
    char buf[64];
    struct tm *tm;
    
    tm = localtime(&sb.st_atime);
    strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm);
    printf("访问时间: %s\n", buf);
    
    tm = localtime(&sb.st_mtime);
    strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm);
    printf("修改时间: %s\n", buf);
    
    tm = localtime(&sb.st_ctime);
    strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm);
    printf("状态改变时间: %s\n", buf);
    
    // 硬链接数
    printf("硬链接数: %lu\n", sb.st_nlink);
    
    // 所有者和组(需要pwd.h和grp.h)
    struct passwd *pw = getpwuid(sb.st_uid);
    struct group *gr = getgrgid(sb.st_gid);
    printf("所有者: %s (UID %d)\n", pw ? pw->pw_name : "?", sb.st_uid);
    printf("所属组: %s (GID %d)\n", gr ? gr->gr_name : "?", sb.st_gid);
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "用法: %s <文件名>\n", argv[0]);
        return 1;
    }
    print_file_info(argv[1]);
    return 0;
}
实用技巧:嵌入式系统里,很多板子没有getpwuidgetgrgid。这时候直接打印UID/GID数字就行,别强求用户名。

知识体系图

下面这张图,把文件属性获取的核心逻辑串起来了:

文件属性获取核心逻辑 stat / fstat 调用 struct stat 结构体 文件类型判断 S_ISREG / S_ISDIR / S_ISLNK 权限解析 st_mode & 0777 / S_IRUSR 时间戳 atime / mtime / ctime 应用层:日志分析 / 文件监控 / 权限校验

说白了,stat就是文件系统的「户口本」。你想知道文件的一切,都得通过它。我个人的习惯是:只要涉及文件操作,先调一次stat确认文件状态,再决定后续动作。这个习惯帮我避免了很多线上事故。

总结:
  • stat/fstat 是获取文件属性的标准接口
  • 文件类型用 S_ISxxx 宏判断,别手动解析
  • 权限位用八进制掩码提取,可读性强
  • 三个时间戳各有用处,选对场景再用
  • 嵌入式环境注意跨平台兼容性

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