UNIX下C语言编程与实践22-UNIX 文件其他属性获取:stat 结构与 localtime 函数的使用
从 stat 结构解析到时间戳转换,掌握 UNIX 文件全属性的获取与格式化
一、核心基础:stat 结构中的文件其他属性
在 UNIX 系统中,struct stat
结构体不仅包含文件类型(st_mode
)和权限信息,还存储了一系列关键的文件属性,如链接数、所有者 ID、文件大小、时间戳等。这些属性是文件管理和系统运维的重要依据,通过 stat
或 lstat
函数可完整获取。
1. stat 结构核心属性解析
以下是除文件类型和权限外,struct stat
中最常用的文件属性字段,定义在 <sys/stat.h>
头文件中:
属性字段 | 数据类型 | 核心含义 | 单位/格式 | 示例值 |
---|---|---|---|---|
st_nlink | nlink_t | 文件的硬链接数(目录默认 2 个:. 和 .. ) | 整数 | 1(普通文件,无额外硬链接)、2(空目录) |
st_uid | uid_t | 文件所有者的用户 ID(UID),对应 /etc/passwd 中的用户 | 整数(用户唯一标识) | 1000(普通用户 bill)、0(root 用户) |
st_gid | gid_t | 文件所属组的组 ID(GID),对应 /etc/group 中的组 | 整数(组唯一标识) | 1000(组 bill)、20(组 dialout) |
st_size | off_t | 文件大小(普通文件:字节数;设备文件:0;符号链接:目标路径长度) | 字节(64 位整数) | 1234(普通文件,1234 字节)、0(设备文件)、4(符号链接,目标路径 "bash") |
st_atime | time_t | 文件最后访问时间(Access Time):读取文件内容时更新 | 时间戳(自 1970-01-01 00:00:00 UTC 起的秒数) | 1727500800(对应 2024-09-28 10:00:00) |
st_mtime | time_t | 文件最后修改时间(Modify Time):修改文件内容时更新(ls -l 默认显示) | 时间戳 | 1727499000(对应 2024-09-28 09:30:00) |
st_ctime | time_t | 文件最后状态变更时间(Change Time):修改文件属性(如权限、所有者)时更新 | 时间戳 | 1727499000(与修改时间同步,若仅改权限则单独更新) |
st_blocks | blkcnt_t | 文件占用的数据块数(每块默认 512 字节,与 ls -s 输出一致) | 块数 | 8(对应 8×512=4096 字节,即 1 个 4KB 数据块) |
关键认知:
st_nlink
对目录的特殊意义:空目录的硬链接数为 2(.
指向自身,..
指向父目录),每新增一个子目录,父目录的st_nlink
增加 1(子目录的..
指向父目录);st_size
st_size 恒为 0,符号链接的st_size
是目标路径字符串的长度(不含终止符\0
);- 三个时间戳的区别:
st_atime
对应“读操作”,st_mtime
对应“写内容”,st_ctime
对应“改属性”,三者独立更新,需根据需求选择使用。
二、C 语言实战:GetFileOtherAttr 函数实现
通过编写 GetFileOtherAttr
函数,结合 lstat
函数获取 struct stat
结构,再通过用户/组 ID 转换、时间戳格式化等操作,可生成类似 ls -l
命令的文件属性输出(如链接数、所有者、修改时间)。以下是完整实现流程。
1. 完整程序实现:获取并格式化文件属性
程序功能:接收文件路径,获取文件的硬链接数、所有者/组、文件大小、修改时间等属性,将 UID/GID 转换为用户名/组名,将时间戳转换为可读时间,最终输出格式化结果。
#include <sys/stat.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 函数:将 UID 转换为用户名(如 1000 → "bill")
const char* UidToName(uid_t uid) {struct passwd *pwd = getpwuid(uid);return (pwd != NULL) ? pwd->pw_name : "unknown";
}// 函数:将 GID 转换为组名(如 1000 → "bill")
const char* GidToName(gid_t gid) {struct group *grp = getgrgid(gid);return (grp != NULL) ? grp->gr_name : "unknown";
}// 函数:将 time_t 时间戳转换为可读时间字符串(格式:YYYY-MM-DD HH:MM:SS)
void TimeToStr(time_t timestamp, char *time_str, size_t max_len) {struct tm* local_tm = localtime(×tamp);if (local_tm == NULL) {strncpy(time_str, "invalid time", max_len);time_str[max_len - 1] = '\0';return;}// 格式化时间:年(tm_year+1900)、月(tm_mon+1)、日、时、分、秒strftime(time_str, max_len, "%Y-%m-%d %H:%M:%S", local_tm);
}// 核心函数:获取文件其他属性并格式化输出
void GetFileOtherAttr(const char *file_path) {struct stat file_stat;if (lstat(file_path, &file_stat) == -1) {perror("lstat error");return;}// 1. 转换 UID/GID 为用户名/组名const char *owner = UidToName(file_stat.st_uid);const char *group = GidToName(file_stat.st_gid);// 2. 转换修改时间戳为可读字符串char mtime_str[32];TimeToStr(file_stat.st_mtime, mtime_str, sizeof(mtime_str));// 3. 格式化输出(模仿 ls -l 格式)printf(" Links: %-3ld Owner: %-8s Group: %-8s Size: %-8lld Modify Time: %s File: %s\n",(long)file_stat.st_nlink,owner,group,(long long)file_stat.st_size,mtime_str,file_path);
}int main(int argc, char *argv[]) {if (argc < 2) {fprintf(stderr, "Usage: %s <file_path1> [file_path2 ...]\n", argv[0]);exit(EXIT_FAILURE);}// 遍历所有命令行参数,输出每个文件的属性printf("属性格式: Links Owner Group Size Modify Time File\n");printf("--------------------------------------------------------------------------\n");for (int i = 1; i < argc; i++) {GetFileOtherAttr(argv[i]);}return EXIT_SUCCESS;
}
2. 关键函数解析
getpwuid(uid_t uid)
:定义在
<pwd.h>
,通过 UID 获取struct passwd
结构体,其中pw_name
字段为用户名。若 UID 不存在(如自定义无效 UID),返回NULL
。getgrgid(gid_t gid)
:定义在
<grp.h>
,通过 GID 获取struct group
结构体,其中gr_name
字段为组名。与getpwuid
类似,无效 GID 返回NULL
。localtime(const time_t *timer)
:定义在
<time.h>
,将time_t
类型的时间戳转换为本地时区的struct tm
结构体(包含年、月、日等字段)。核心用于时间戳的“可读化”转换,下文将详细讲解。strftime(char *s, size_t maxsize, const char *format, const struct tm *tm)
:定义在
<time.h>
,按指定格式将struct tm
结构体格式化为字符串(如%Y-%m-%d %H:%M:%S
对应“2024-09-28 09:30:00”),避免手动拼接时间字段的繁琐操作。
3. 程序编译与多场景测试
将代码保存为 fileattr.c
,编译后对不同类型的文件(普通文件、目录、符号链接、设备文件)进行测试,验证属性获取的正确性。
步骤 1:编译程序
gcc fileattr.c -o fileattr
步骤 2:测试 1:普通文件(/etc/passwd)
./fileattr /etc/passwd
属性格式
Links Owner Group Size Modify Time File
-----------------------------------------------------------------------------
1 root root 2345 2024-09-27 18:00:00 /etc/passwd
结果验证
普通文件 /etc/passwd
的硬链接数为 1
,所有者和组均为 root
,大小 2345
字节,修改时间与系统实际一致,符合预期。
步骤 3:测试 2:目录文件(空目录与非空目录)
创建空目录和含子目录的目录
mkdir empty_dir
mkdir -p non_empty_dir/sub_dir
测试两个目录的属性
./fileattr empty_dir non_empty_dir
属性格式
Links Owner Group Size Modify Time File
--------------------------------------------------------------------------
Links: 2 Owner: bill Group: bill Size: 4096 2024-09-28 10:10:00 File: empty_dir
Links: 3 Owner: bill Group: bill Size: 4096 2024-09-28 10:11:00 File: non_empty_dir
结果验证
空目录 empty_dir
的硬链接数为 2(.和..),含子目录的 non_empty_dir
硬链接数为 3(子目录 sub_dir
的 .. 增加 1 个链接),正确反映目录链接数的特性。
步骤 4:测试 3:符号链接与设备文件
创建符号链接(指向 /etc/passwd)
ln -s /etc/passwd passwd_link
测试符号链接和字符设备文件 /dev/tty
./fileattr passwd_link /dev/tty
属性格式
Links Owner Group Size Modify Time File
--------------------------------------------------------------------------
Links: 1 Owner: bill Group: bill Size: 11 2024-09-28 10:15:00 File: passwd_link
Links: 1 Owner: root Group: tty Size: 0 2024-09-28 08:00:00 File: /dev/tty
结果验证
符号链接 passwd_link
的 st_size
为 11(目标路径 /etc/passwd
的长度),设备文件 /dev/tty
的 st_size
为 0,所有者为 root
、组为 tty
,与设备文件特性完全一致。
三、时间戳转换核心:localtime 函数详解
UNIX 系统中,文件的时间属性(st_atime
、st_mtime
、st_ctime
)均以 time_t
类型的时间戳存储(自 1970-01-01 00:00:00 UTC 起的秒数),无法直接阅读。localtime
函数是将时间戳转换为“年、月、日、时、分、秒”可读格式的核心工具,其使用逻辑和细节需重点掌握。
1. localtime 函数的使用流程
localtime
函数的核心作用是“将 UTC 时间戳转换为本地时区的结构化时间”,具体使用流程如下:
// 1. 定义变量:时间戳、结构化时间、时间字符串
time_t timestamp = file_stat.st_mtime; // 从 stat 结构获取文件修改时间戳
struct tm* local_tm; // 存储结构化时间的指针
char time_str[32]; // 存储最终的可读时间字符串// 2. 调用 localtime 转换时间戳
local_tm = localtime(×tamp);
if (local_tm == NULL) {perror("localtime error");return;
}// 3. 提取 struct tm 中的时间字段(注意:部分字段需调整)
int year = local_tm->tm_year + 1900; // tm_year:自 1900 年起的年数 → 需加 1900
int month = local_tm->tm_mon + 1; // tm_mon:0-11(1 月为 0)→ 需加 1
int day = local_tm->tm_mday; // tm_mday:1-31(无需调整)
int hour = local_tm->tm_hour; // tm_hour:0-23(24 小时制)
int minute = local_tm->tm_min; // tm_min:0-59
int second = local_tm->tm_sec; // tm_sec:0-60(60 为闰秒)// 4. 格式化输出(手动拼接或用 strftime)
snprintf(time_str, sizeof(time_str), "%d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second);
printf("Modify Time: %s\n", time_str); // 输出:Modify Time: 2024-09-28 09:30:00
2. struct tm 结构体字段解析
struct tm 结构体
定义在 <time.h>
中,包含完整的时间字段,部分字段需调整后才能直接使用,具体解析如下:
- tm_year:自 1900 年起的年数(2024 → 124)
- tm_mon:月份(0-11)(9 月 → 8)
- tm_mday:月内天数(1-31)(28 日 → 28)
- tm_hour:小时(0-23)(9 点 → 9)
- tm_min:分钟(0-59)(30 分 → 30)
- tm_sec:秒(0-60)(15 秒 → 15)
- tm_wday:周内天数(0-6,周日为 0)(周六 → 6)
- tm_yday:年内天数(0-365)(9 月 28 日 → 271)
- tm_isdst:夏令时标识(1=启用,0=禁用,-1=未知)(未启用 → 0)
常见错误点:
- 忘记调整
tm_year
和tm_mon
:直接使用tm_year
会得到“124”(2024-1900),直接使用tm_mon
会得到“8”(9 月),导致时间显示错误; - 忽略
localtime
的返回值检查:当时间戳无效(如负数)时,localtime
返回NULL
,若未检查会导致空指针访问,程序崩溃; - 混淆本地时区与 UTC 时区:
localtime
转换的是本地时区时间,若需 UTC 时间,应使用gmtime
函数(用法与localtime
一致)。
3. localtime 函数的线程安全问题
localtime
函数存在一个关键缺陷:其返回的 struct tm
指针指向静态全局变量,多个线程同时调用时会导致数据竞争,出现时间错乱(如线程 A 的时间被线程 B 覆盖)。
线程安全解决方案:
- 方案 1:使用 localtime_r(推荐,POSIX 标准):
localtime_r
是localtime
的线程安全版本,需手动传入用户定义的struct tm
变量地址,避免静态变量竞争,用法如下:struct tm local_tm; localtime_r(×tamp, &local_tm); // 结果存储在用户提供的 local_tm 中 int year = local_tm.tm_year + 1900; // 直接访问结构体成员(非指针)
- 方案 2:使用互斥锁(兼容非 POSIX 系统):
若系统不支持
localtime_r
(如部分嵌入式系统),可通过互斥锁(pthread_mutex_t
)确保同一时间只有一个线程调用localtime
,避免竞争。
四、其他时间相关函数拓展
除 localtime
外,UNIX 还提供了 ctime
、mktime
、strptime
等时间函数,分别用于快速获取可读时间、时间戳反向转换、字符串解析为时间结构,满足不同场景的时间处理需求。
函数原型 | 核心功能 | 使用场景 | 示例代码 | 输出结果 |
---|---|---|---|---|
char *ctime(const time_t *timer); | 将时间戳直接转换为本地时区的可读字符串(格式:Wed Sep 28 09:30:00 2024\n ),无需手动处理 struct tm | 快速打印时间戳,无需自定义格式 | time_t t = file_stat.st_mtime; printf("Modify Time: %s", ctime(&t)); | Modify Time: Sat Sep 28 09:30:00 2024 |
time_t mktime(struct tm *tm); | 将 struct tm 结构体(本地时区)反向转换为 time_t 时间戳,支持自定义时间的时间戳计算 | 计算特定时间(如“2024-10-01 00:00:00”)的时间戳,用于时间比较 | struct tm t = {0}; t.tm_year = 2024-1900; t.tm_mon = 10-1; t.tm_mday = 1; time_t timestamp = mktime(&t); printf("Timestamp: %ld", (long)timestamp); | Timestamp: 1727750400 |
char *strptime(const char *s, const char *format, struct tm *tm); | 将自定义格式的时间字符串(如“2024-09-28 09:30:00”)解析为 struct tm 结构体,与 strftime 功能相反 | 解析用户输入的时间字符串,转换为时间戳进行计算 | char time_str[] = "2024-09-28 09:30:00"; struct tm t; strptime(time_str, "%Y-%m-%d %H:%M:%S", &t); time_t ts = mktime(&t); printf("Timestamp: %ld", (long)ts); | Timestamp: 1727499000 |
struct tm *gmtime(const time_t *timer); | 将时间戳转换为 UTC 时区的 struct tm 结构体,与 localtime 唯一区别是时区 | 需要统一 UTC 时间的场景(如日志同步、跨时区数据对比) | struct tm *utc_tm = gmtime(&t); strftime(buf, 32, "%Y-%m-%d %H:%M:%S UTC", utc_tm); printf("UTC Time: %s", buf); | UTC Time: 2024-09-28 01:30:00 UTC |
实战:计算文件修改时间与当前时间的差值
结合 time
(获取当前时间戳)、mktime
、difftime
(计算时间戳差值)函数,可计算文件修改时间距当前的时间差:
#include <time.h>
#include <stdio.h>void CalcTimeDiff(time_t file_mtime)
{time_t now = time(NULL);double diff_sec = difftime(now, file_mtime);int days = diff_sec / (24 * 3600);int hours = (diff_sec % (24 * 3600)) / 3600;int mins = (diff_sec % 3600) / 60;int secs = diff_sec % 60;printf("文件最后修改时间距现在:%d天 %d时 %d分 %d秒\n", days, hours, mins, secs);
}
struct stat file_stat;
stat("filename.txt", &file_stat);
CalcTimeDiff(file_stat.st_mtime);
五、常见问题与解决方法
在获取文件其他属性和处理时间戳的过程中,常因函数使用不当、权限不足等导致问题。以下是高频问题及对应的解决方法:
常见问题 | 问题现象 | 原因分析 | 解决方法 |
---|---|---|---|
getpwuid/getgrgid 返回 NULL | 用户名/组名显示为“unknown”,但 UID/GID 实际存在(如 1000) | 1. 系统 /etc/passwd 或 /etc/group 文件损坏或权限不足(无法读取);2. 程序运行在 chroot 环境中,未挂载 /etc 目录,导致无法读取用户/组配置 | 1. 检查 /etc/passwd 权限:ls -l /etc/passwd ,确保有读权限(如 644);2. chroot 环境:挂载 /etc/passwd 和 /etc/group 到 chroot 目录;3. 降级处理:若无法修复,直接输出 UID/GID(如“UID=1000”) |
localtime 转换时间错误(年份为 1970) | 时间显示为“1970-01-01 08:00:00”,与实际时间不符 | 1. 时间戳为 0 或无效值(如负数),localtime 无法正确转换;2. 文件的 st_mtime 未初始化(如 stat 函数调用失败后未检查,直接使用 file_stat.st_mtime ) | 1. 检查时间戳有效性:确保 st_mtime 为正数(正常时间戳自 1970 年起,最小值为 0);2. 强制检查 stat /lstat 返回值,失败时不进行时间转换 |
线程安全问题导致时间错乱 | 多线程程序中,不同线程输出的文件时间相互覆盖(如线程 A 的时间显示为线程 B 的时间) | localtime 返回静态全局变量的指针,多线程并发访问时存在数据竞争 | 1. 替换为线程安全函数 localtime_r (推荐);2. 若不支持 localtime_r ,使用互斥锁(pthread_mutex_lock/unlock )保护 localtime 调用 |
符号链接的属性与目标文件不一致 | 获取符号链接的 st_uid /st_size 时,得到的是目标文件的属性,而非链接本身 | 使用了 stat 函数而非 lstat 函数——stat 会自动跟随符号链接,获取目标文件的属性 | 始终使用 lstat 函数获取符号链接本身的属性;若需目标文件属性,再对符号链接的目标路径调用 stat |
本文从 stat
结构的文件属性解析入手,详细讲解了文件链接数、所有者、时间戳等属性的获取方法,重点剖析了 localtime
函数的时间戳转换逻辑,并拓展了其他时间相关函数的使用。掌握这些知识,可实现对 UNIX 文件属性的全面管控,为文件管理工具开发、系统运维脚本编写提供基础。
建议结合实际需求多做实践(如编写文件属性统计脚本、时间差计算工具),加深对函数使用细节和属性特性的理解,避免因细节疏忽导致的错误。