UNIX下C语言编程与实践22-UNIX 文件其他属性获取:stat 结构与 localtime 函数的使用

从 stat 结构解析到时间戳转换,掌握 UNIX 文件全属性的获取与格式化

一、核心基础:stat 结构中的文件其他属性

在 UNIX 系统中,struct stat 结构体不仅包含文件类型(st_mode)和权限信息,还存储了一系列关键的文件属性,如链接数、所有者 ID、文件大小、时间戳等。这些属性是文件管理和系统运维的重要依据,通过 statlstat 函数可完整获取。

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_sizest_size 恒为 0,符号链接的 st_size 是目标路径字符串的长度(不含终止符 \0);
  • 三个时间戳的区别:st_atime 对应"读操作",st_mtime 对应"写内容",st_ctime 对应"改属性",三者独立更新,需根据需求选择使用。

二、C 语言实战:GetFileOtherAttr 函数实现

通过编写 GetFileOtherAttr 函数,结合 lstat 函数获取 struct stat 结构,再通过用户/组 ID 转换、时间戳格式化等操作,可生成类似 ls -l 命令的文件属性输出(如链接数、所有者、修改时间)。以下是完整实现流程。

1. 完整程序实现:获取并格式化文件属性

程序功能:接收文件路径,获取文件的硬链接数、所有者/组、文件大小、修改时间等属性,将 UID/GID 转换为用户名/组名,将时间戳转换为可读时间,最终输出格式化结果。

c 复制代码
#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_linkst_size 为 11(目标路径 /etc/passwd 的长度),设备文件 /dev/ttyst_size 为 0,所有者为 root、组为 tty,与设备文件特性完全一致。

三、时间戳转换核心:localtime 函数详解

UNIX 系统中,文件的时间属性(st_atimest_mtimest_ctime)均以 time_t 类型的时间戳存储(自 1970-01-01 00:00:00 UTC 起的秒数),无法直接阅读。localtime 函数是将时间戳转换为"年、月、日、时、分、秒"可读格式的核心工具,其使用逻辑和细节需重点掌握。

1. localtime 函数的使用流程

localtime 函数的核心作用是"将 UTC 时间戳转换为本地时区的结构化时间",具体使用流程如下:

c 复制代码
// 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_yeartm_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_rlocaltime 的线程安全版本,需手动传入用户定义的 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 还提供了 ctimemktimestrptime 等时间函数,分别用于快速获取可读时间、时间戳反向转换、字符串解析为时间结构,满足不同场景的时间处理需求。

函数原型 核心功能 使用场景 示例代码 输出结果
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(获取当前时间戳)、mktimedifftime(计算时间戳差值)函数,可计算文件修改时间距当前的时间差:

c 复制代码
#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);
}
 
c 复制代码
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 文件属性的全面管控,为文件管理工具开发、系统运维脚本编写提供基础。

建议结合实际需求多做实践(如编写文件属性统计脚本、时间差计算工具),加深对函数使用细节和属性特性的理解,避免因细节疏忽导致的错误。

相关推荐
傻童:CPU1 天前
C语言需要掌握的基础知识点之前缀和
java·c语言·算法
degen_1 天前
第一次进入 PEICORE 流程
c语言·笔记
我是大咖1 天前
C语言-贪吃蛇项目开发工具篇---ncursee库安装
c语言·开发语言
czy87874751 天前
用C语言实现单例模式
c语言·单例模式
czy87874751 天前
用C语言实现适配器模式
c语言·适配器模式
La Pulga1 天前
【STM32】RTC实时时钟
c语言·stm32·单片机·嵌入式硬件·mcu·实时音视频
Kevin Wang7271 天前
解除chrome中http无法录音问题,权限
前端·chrome
坚持编程的菜鸟1 天前
LeetCode每日一题——二进制求和
c语言·算法·leetcode
Dontla1 天前
(临时解决)Chrome调试避免跳入第三方源码(设置Blackbox Scripts、将目录添加到忽略列表、向忽略列表添加脚本)
前端·chrome
熙xi.1 天前
Linux I²C 总线驱动开发:从架构到实战的完整指南
linux·c语言·驱动开发