在Unix/Linux系统编程中,获取文件元数据是文件操作的基础需求。stat()和fstat()这两个系统调用就像文件的"体检报告单",能够让我们在不打开文件内容的情况下,获取文件类型、权限、大小、时间戳等关键信息。它们都定义在<sys/stat.h>头文件中,但使用场景有所不同:
这两个调用返回的信息存储在struct stat结构中,这个结构体就像是一个包含20多项属性的文件信息表单。在实际开发中,我经常用它们来做文件存在性检查、权限验证、大小监控等操作。比如备份工具需要确认文件最后修改时间,或者Web服务器需要检查静态文件的权限,都离不开这些基础API。
注意:虽然lstat()也属于这个家族,但它处理符号链接的方式不同,本文聚焦stat()/fstat()的核心用法。
这个结构体是信息存储的核心容器,不同系统版本字段可能略有差异,但基本包含以下关键字段(以Linux 5.x内核为例):
c复制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 */
dev_t st_rdev; /* 特殊文件设备ID */
off_t st_size; /* 文件大小(字节) */
blksize_t st_blksize; /* 文件系统I/O块大小 */
blkcnt_t st_blocks; /* 分配的512B块数量 */
struct timespec st_atim; /* 最后访问时间 */
struct timespec st_mtim; /* 最后修改时间 */
struct timespec st_ctim; /* 最后状态变更时间 */
};
时间戳字段从timespec结构获取纳秒级精度,这是现代系统的改进。在实际项目中,我常用st_size做文件传输进度计算,用st_mtim判断文件是否被修改过。
c复制#include <sys/stat.h>
int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
返回值:成功返回0,失败返回-1并设置errno。我在实际编码中总会检查返回值,因为权限不足、路径错误等情况很常见。
st_mode字段通过位掩码表示文件类型和权限。判断文件类型时应该使用以下宏:
c复制S_ISREG(m) /* 常规文件 */
S_ISDIR(m) /* 目录 */
S_ISCHR(m) /* 字符设备 */
S_ISBLK(m) /* 块设备 */
S_ISFIFO(m) /* 管道或FIFO */
S_ISLNK(m) /* 符号链接 */
S_ISSOCK(m) /* 套接字 */
典型使用示例:
c复制struct stat sb;
if (stat("example.txt", &sb) == -1) {
perror("stat");
exit(EXIT_FAILURE);
}
printf("File type: ");
switch (sb.st_mode & S_IFMT) {
case S_IFREG: printf("regular file\n"); break;
case S_IFDIR: printf("directory\n"); break;
default: printf("other\n"); break;
}
踩坑提醒:不要直接比较st_mode的数值,而应该用位掩码操作。我曾遇到过直接比较导致设备文件误判的bug。
现代系统使用timespec结构提供纳秒级精度:
c复制struct timespec {
time_t tv_sec; /* 秒 */
long tv_nsec; /* 纳秒 */
};
时间转换示例:
c复制char timestr[100];
struct tm *tm_info;
tm_info = localtime(&sb.st_mtim.tv_sec);
strftime(timestr, sizeof(timestr), "%Y-%m-%d %H:%M:%S", tm_info);
printf("Last modified: %s.%09ld\n", timestr, sb.st_mtim.tv_nsec);
在开发文件同步工具时,我通过比较两个文件的st_mtim.tv_sec和tv_nsec来精确判断哪个版本更新。
每次stat调用都涉及从用户态到内核态的切换,在高频操作中会成为性能瓶颈。我的优化经验:
| 错误码 | 含义 | 典型处理方式 |
|---|---|---|
| ENOENT | 路径不存在 | 检查路径拼写或父目录权限 |
| EACCES | 权限不足 | 检查文件权限或改用特权运行 |
| ELOOP | 符号链接循环 | 检查符号链接引用链 |
| ENOMEM | 内存不足 | 减少并发操作或优化程序 |
健壮性处理示例:
c复制if (stat(path, &sb) == -1) {
switch (errno) {
case ENOENT:
fprintf(stderr, "%s does not exist\n", path);
break;
case EACCES:
fprintf(stderr, "No permission to access %s\n", path);
break;
default:
perror("stat error");
}
exit(EXIT_FAILURE);
}
下面是我在一个日志监控项目中使用的核心代码片段,展示stat()的实际应用:
c复制#define MONITOR_INTERVAL 5
void monitor_file(const char *path) {
struct stat prev_sb, curr_sb;
time_t last_mod = 0;
off_t last_size = 0;
if (stat(path, &prev_sb) == -1) {
perror("Initial stat failed");
return;
}
last_mod = prev_sb.st_mtim.tv_sec;
last_size = prev_sb.st_size;
while (1) {
sleep(MONITOR_INTERVAL);
if (stat(path, &curr_sb) == -1) {
perror("Monitor stat failed");
continue;
}
if (curr_sb.st_mtim.tv_sec != last_mod ||
curr_sb.st_size != last_size) {
printf("[%.*s] File changed! Size: %ld -> %ld\n",
(int)sizeof(time_t), ctime(&curr_sb.st_mtim.tv_sec),
last_size, curr_sb.st_size);
last_mod = curr_sb.st_mtim.tv_sec;
last_size = curr_sb.st_size;
}
}
}
这个实现有几个优化点值得注意:
不同UNIX-like系统对struct stat的定义存在差异:
| 字段/平台 | Linux | macOS | FreeBSD |
|---|---|---|---|
| 时间精度 | 纳秒 | 纳秒 | 纳秒 |
| 时间字段 | st_atim | st_atimespec | st_atimespec |
| 块大小 | st_blksize | st_blksize | st_blksize |
| 设备号 | st_dev | st_dev | st_dev |
我在跨平台项目中常用以下宏来统一访问时间字段:
c复制#if defined(__APPLE__)
#define ST_ATIME(st) ((st).st_atimespec.tv_sec)
#define ST_MTIME(st) ((st).st_mtimespec.tv_sec)
#elif defined(__linux__)
#define ST_ATIME(st) ((st).st_atim.tv_sec)
#define ST_MTIME(st) ((st).st_mtim.tv_sec)
#else
#define ST_ATIME(st) ((st).st_atime)
#define ST_MTIME(st) ((st).st_mtime)
#endif
使用示例:
c复制time_t mtime = ST_MTIME(sb);
Time-of-Check to Time-of-Use (TOCTOU)是stat相关操作常见的安全问题。典型场景:
防御方案:
stat()会跟随符号链接,这可能不是预期行为。安全敏感场景应该:
c复制struct stat sb;
if (lstat(path, &sb) == -1) {
/* 错误处理 */
}
if (S_ISLNK(sb.st_mode)) {
/* 处理符号链接情况 */
} else {
if (stat(path, &sb) == -1) {
/* 错误处理 */
}
/* 处理常规文件 */
}
bash复制strace -e trace=file your_program
这会显示所有文件相关系统调用,包括stat/fstat的调用参数和返回值。
我常用的调试打印函数:
c复制void print_stat(const struct stat *sb) {
printf("Device: %lu\n", (unsigned long)sb->st_dev);
printf("Inode: %lu\n", (unsigned long)sb->st_ino);
printf("Mode: %o\n", sb->st_mode);
printf("Links: %lu\n", (unsigned long)sb->st_nlink);
printf("Size: %ld\n", (long)sb->st_size);
printf("Blocks: %ld\n", (long)sb->st_blocks);
char timestr[100];
strftime(timestr, sizeof(timestr), "%F %T",
localtime(&sb->st_mtim.tv_sec));
printf("Modify: %s.%09ld\n", timestr, sb->st_mtim.tv_nsec);
}
建议测试以下特殊情况:
c复制void file_stats(const char *dirpath) {
DIR *dir;
struct dirent *entry;
struct stat sb;
int reg=0, dir=0, other=0;
if (!(dir = opendir(dirpath))) {
perror("opendir");
return;
}
while ((entry = readdir(dir)) != NULL) {
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s", dirpath, entry->d_name);
if (stat(path, &sb) == -1) {
perror("stat");
continue;
}
if (S_ISREG(sb.st_mode)) reg++;
else if (S_ISDIR(sb.st_mode)) dir++;
else other++;
}
closedir(dir);
printf("Regular: %d\nDirectories: %d\nOther: %d\n", reg, dir, other);
}
结合stat()和inotify可以实现高效的文件监控:
这种组合避免了纯轮询的性能开销,又能获取详细的变化数据。
在已打开文件的情况下,fstat()通常比stat()更快:
| 操作 | 平均耗时(纳秒) | 系统调用次数 |
|---|---|---|
| stat() | 1200 | 2-3 |
| fstat() | 800 | 1 |
这是因为:
在需要频繁检查文件属性的场景(如日志轮转监控),保持文件打开并使用fstat()是更好的选择。