15、获取文件属性:stat()、fstat()、lstat() 函数详解,以及 struct stat 结构体
文件操作里,除了读写数据,还有一个很常见的需求——获取文件本身的“身世信息”。
比如这个文件多大?什么时候修改的?是普通文件还是目录?权限是什么?
这些信息,在 Linux 系统里都藏在文件的 inode 里。而我们要做的,就是通过三个系统调用把它们挖出来:stat()、fstat() 和 lstat()。
说实话,这三个函数我几乎每天都会用到。不管是写日志系统、做文件监控,还是实现一个简单的 ls -l,都离不开它们。
15.1 struct stat:文件信息的“容器”
在调用 stat 系列函数之前,你得先准备一个“收纳盒”——也就是 struct stat 结构体。所有获取到的文件属性,都会填到这个结构体里。
#include <sys/stat.h>
struct stat {
dev_t st_dev; // 文件所在设备的 ID
ino_t st_ino; // inode 编号
mode_t st_mode; // 文件类型 + 权限位
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 所有者用户 ID
gid_t st_gid; // 所有者组 ID
dev_t st_rdev; // 设备文件对应的设备号
off_t st_size; // 文件大小(字节)
blksize_t st_blksize; // 文件系统 I/O 块大小
blkcnt_t st_blocks; // 占用 512 字节块的数量
struct timespec st_atim; // 最后访问时间
struct timespec st_mtim; // 最后修改时间
struct timespec st_ctim; // 最后状态变更时间
};
这里我重点说几个我经常用的字段:
- st_mode:这个字段最复杂,它同时编码了文件类型和权限。判断文件类型要用宏,比如
S_ISREG(m)判断是否普通文件,S_ISDIR(m)判断是否目录。 - st_size:文件大小。注意,对目录来说这个值通常是 0 或很小的数,别拿它当目录里文件的总大小。
- st_mtim:最后修改时间。我写增量备份工具时,就是靠这个字段判断文件是否被改过。
- st_blocks:文件占用的磁盘块数。你想想看,一个空洞文件(sparse file)的 st_size 可能很大,但 st_blocks 很小,这就是它的“真实占用”。
st_atim、st_mtim、st_ctim 在 POSIX.1-2008 标准里是 struct timespec 类型,精度到纳秒。老代码里可能看到 st_atime、st_mtime、st_ctime,那是秒级精度,注意区分。
15.2 stat():通过路径名获取属性
stat() 是最常用的一个。你给它一个文件路径,它就把属性填到结构体里。
#include <sys/stat.h>
int stat(const char *pathname, struct stat *statbuf);
成功返回 0,失败返回 -1 并设置 errno。
来看一个实际例子:
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
int main() {
struct stat sb;
if (stat("/etc/passwd", &sb) == -1) {
perror("stat");
return 1;
}
printf("文件大小: %ld 字节\n", sb.st_size);
printf("占用块数: %ld\n", sb.st_blocks);
printf("最后修改时间: %s", ctime(&sb.st_mtime));
if (S_ISREG(sb.st_mode)) {
printf("这是一个普通文件\n");
} else if (S_ISDIR(sb.st_mode)) {
printf("这是一个目录\n");
}
return 0;
}
这段代码我经常用来做快速诊断。比如某个日志文件突然变大,我先 stat 一下看看大小和时间,心里就有数了。
15.3 fstat():通过文件描述符获取属性
如果你已经打开了一个文件,手里有文件描述符 fd,那就用 fstat()。它不需要路径名,直接从内核里拿信息。
#include <sys/stat.h>
int fstat(int fd, struct stat *statbuf);
我个人的习惯是:如果文件已经打开了,就别再用 stat() 传路径了。为什么?因为路径可能被篡改(TOCTOU 竞态条件),而 fd 是确定的。
举个例子:
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
int main() {
int fd = open("data.bin", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("fstat");
close(fd);
return 1;
}
printf("文件大小: %ld 字节\n", sb.st_size);
printf("文件所在设备: major=%d minor=%d\n",
major(sb.st_dev), minor(sb.st_dev));
close(fd);
return 0;
}
这里有个细节:st_dev 是文件所在设备的 ID,可以用 major() 和 minor() 宏拆出主次设备号。我在做磁盘空间统计时,经常用这个字段来区分文件在哪个分区上。
15.4 lstat():绕过符号链接
这个函数跟 stat() 几乎一样,唯一的区别是:如果路径是一个符号链接,lstat() 返回的是 符号链接本身 的属性,而不是它指向的目标文件。
#include <sys/stat.h>
int lstat(const char *pathname, struct stat *statbuf);
为什么要单独搞一个 lstat?
你想想看,如果你用 stat() 去查一个符号链接,它自动解引用,你永远拿不到链接文件自己的信息。而 lstat() 就是专门干这个的。
我曾经在写一个文件同步工具时踩过这个坑。当时我想判断一个文件是不是符号链接,用 stat() 怎么都判断不出来,后来才发现要用 lstat() 配合 S_ISLNK() 宏。
#include <stdio.h>
#include <sys/stat.h>
int main() {
struct stat sb;
if (lstat("mylink", &sb) == -1) {
perror("lstat");
return 1;
}
if (S_ISLNK(sb.st_mode)) {
printf("这是一个符号链接\n");
printf("链接文件大小: %ld 字节\n", sb.st_size);
// 注意:符号链接的 st_size 是目标路径的字符串长度
} else {
printf("这不是符号链接\n");
}
return 0;
}
嗯,这里要注意:符号链接的 st_size 不是目标文件的大小,而是目标路径字符串的字节数。比如链接指向 /usr/bin/python3,那 st_size 就是 16。
15.5 三个函数的对比
| 函数 | 参数 | 符号链接处理 | 典型场景 |
|---|---|---|---|
| stat() | 路径名 | 解引用 | 获取任意路径的文件属性 |
| fstat() | 文件描述符 | 不适用(已打开) | 已打开文件的属性查询 |
| lstat() | 路径名 | 不解引用 | 检查符号链接本身 |
简单总结一下:
- 有路径,想看目标文件 → 用
stat() - 有 fd,不想走路径 → 用
fstat() - 有路径,想看链接本身 → 用
lstat()
15.6 实战:实现一个简易的 ls -l
光说不练假把式。我们来写一个简化版的 ls -l,把文件类型、权限、大小、修改时间都打印出来。
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
void print_file_type(mode_t mode) {
if (S_ISREG(mode)) putchar('-');
else if (S_ISDIR(mode)) putchar('d');
else if (S_ISLNK(mode)) putchar('l');
else if (S_ISCHR(mode)) putchar('c');
else if (S_ISBLK(mode)) putchar('b');
else if (S_ISFIFO(mode)) putchar('p');
else if (S_ISSOCK(mode)) putchar('s');
else putchar('?');
}
void print_perms(mode_t mode) {
char perms[10] = "---------";
if (mode & S_IRUSR) perms[0] = 'r';
if (mode & S_IWUSR) perms[1] = 'w';
if (mode & S_IXUSR) perms[2] = 'x';
if (mode & S_IRGRP) perms[3] = 'r';
if (mode & S_IWGRP) perms[4] = 'w';
if (mode & S_IXGRP) perms[5] = 'x';
if (mode & S_IROTH) perms[6] = 'r';
if (mode & S_IWOTH) perms[7] = 'w';
if (mode & S_IXOTH) perms[8] = 'x';
printf("%s", perms);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "用法: %s <文件路径>\n", argv[0]);
return 1;
}
struct stat sb;
if (lstat(argv[1], &sb) == -1) {
perror("lstat");
return 1;
}
// 文件类型 + 权限
print_file_type(sb.st_mode);
print_perms(sb.st_mode);
// 硬链接数
printf(" %ld", sb.st_nlink);
// 所有者
struct passwd *pw = getpwuid(sb.st_uid);
struct group *gr = getgrgid(sb.st_gid);
printf(" %s %s", pw ? pw->pw_name : "?", gr ? gr->gr_name : "?");
// 大小
printf(" %8ld", sb.st_size);
// 修改时间
char timebuf[64];
strftime(timebuf, sizeof(timebuf), "%b %d %H:%M",
localtime(&sb.st_mtime));
printf(" %s", timebuf);
// 文件名
printf(" %s\n", argv[1]);
return 0;
}
这段代码我稍微解释一下:
- 用
lstat()而不是stat(),这样能正确显示符号链接本身。 - 用
getpwuid()和getgrgid()把 uid/gid 转成用户名和组名。 - 用
strftime()格式化时间,比ctime()更灵活。
getpwuid() 和 getgrgid() 返回的指针指向静态缓冲区,在多线程环境下要用 getpwuid_r() 和 getgrgid_r() 可重入版本。
15.7 知识体系图
下面这张图帮你理清 stat 系列函数的核心逻辑:
15.8 避坑指南
最后,分享几个我踩过的坑:
- TOCTOU 竞态:先 stat 判断文件存在,再 open 打开。这中间文件可能被替换。正确做法是 open 后用 fstat 验证。
- 符号链接大小:lstat 得到的 st_size 是目标路径字符串长度,不是目标文件大小。别搞混了。
- st_blocks 的单位:它是 512 字节块的数量,不是 st_blksize 的块数。很多新手在这里算错磁盘占用。
- 时间精度:老代码用 st_mtime(秒级),新代码用 st_mtim(纳秒级)。跨平台移植时要注意。
我曾经在生产环境里遇到过一个问题:一个监控脚本用 stat 检查日志文件大小,结果日志文件被删了又重建,stat 拿到的是新文件的属性,但监控脚本还拿着旧 fd 在写。后来改成 open 后统一用 fstat,问题就解决了。
嗯,文件属性获取这块,说白了就是三个函数加一个结构体。但用好了,能解决很多实际问题。你写代码的时候,多想想「我到底要查什么?用哪个函数最合适?」就行了。
核心要点回顾:
- stat() 查路径,解引用符号链接
- fstat() 查 fd,适合已打开文件
- lstat() 查路径,不解引用符号链接
- struct stat 包含文件所有元数据
- 用 S_ISxxx 宏判断文件类型
公众号:蓝海资料掘金营,微信deep3321