文件属性获取: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_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反而能帮你提前发现问题。
文件权限: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;
}
getpwuid和getgrgid。这时候直接打印UID/GID数字就行,别强求用户名。
知识体系图
下面这张图,把文件属性获取的核心逻辑串起来了:
说白了,stat就是文件系统的「户口本」。你想知道文件的一切,都得通过它。我个人的习惯是:只要涉及文件操作,先调一次stat确认文件状态,再决定后续动作。这个习惯帮我避免了很多线上事故。
- stat/fstat 是获取文件属性的标准接口
- 文件类型用 S_ISxxx 宏判断,别手动解析
- 权限位用八进制掩码提取,可读性强
- 三个时间戳各有用处,选对场景再用
- 嵌入式环境注意跨平台兼容性
公众号:蓝海资料掘金营,微信deep3321