04-文件属性与目录.md 31 KB


title: 文件属性与目录 tags: [嵌入式Linux, Linux应用编程, 文件属性, stat, 目录, 权限, 硬链接, 软链接, IMX6ULL] created: 2026-09-18 updated: 2026-09-18

pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第五章 文件属性与目录"

文件属性与目录

💡 关联知识:[[01-Linux应用编程基础/02-文件IO基础与深入探究]]、[[01-Linux应用编程基础/03-标准IO库]]、[[01-Linux应用编程基础/05-字符串处理与系统信息]]、[[01-Linux应用编程基础/面试-Linux应用编程基础]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/4. 目录操作与文件属性]]

前几章讨论的都是围绕普通文件的打开、读写、关闭等 I/O 操作。本章转向 Linux 文件系统的其它特性:文件类型、stat 系列、属主、权限、时间戳、软硬链接、目录、删除与重命名。学完本章,你能用 C 代码实现 ls -lstatchmodlnrmmvmkdir 等命令的核心功能。


1. Linux 系统中的文件类型

Windows 通过后缀识别文件类型;Linux 不会根据后缀识别文件类型,后缀只是给人看的,仍应按内容规范命名(.c / .h / .sh)。Linux 下一切皆文件,共 7 种类型,由 ls -l 输出第 1 个字符或 stat 命令标记:

字符 文件类型 说明
- 普通文件 regular file 文本文件、二进制文件、.o、可执行文件等
d 目录文件 directory 文件夹也是一种文件
c 字符设备 character 对应硬件设备,按字节流访问,如串口、按键、LED
b 块设备 block 对应硬件设备,按块访问,如硬盘、loop 设备
l 符号链接 symbolic link 类似 Windows 快捷方式
s 套接字 socket 用于网络通信(进程间通信)
p 管道 pipe 用于进程间通信

普通文件分两类:文本文件(内容为 ASCII 字符,方便人阅读,如 .c/.h/.sh/.txt)和二进制文件(存真正的数字,如可执行文件、.o.bin)。

设备文件对应硬件设备,但不占用磁盘,由文件系统虚拟出来、一般由内存维护,关机即消失;字符设备通常在 /dev/ 下,/dev 因此也称虚拟文件系统 devfs。符号链接文件内容指向另一个文件路径,对它的操作会被转移到所指向的文件。管道与套接字都用于进程间通信,后者可跨主机(网络通信)。

graph TD
    F["Linux 文件类型"] --> R["- 普通文件"]
    F --> D["d 目录"]
    F --> C["c 字符设备"]
    F --> B["b 块设备"]
    F --> L["l 符号链接"]
    F --> S["s 套接字"]
    F --> P["p 管道"]
    R --> R1["文本: .c .h .sh .txt"]
    R --> R2["二进制: 可执行文件 .o .bin"]
    C --> DV["/dev 虚拟设备文件, 不占磁盘, 关机消失"]
    B --> DV

2. stat / fstat / lstat

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
函数 起点 说明
stat 文件路径 无需先打开;若 pathname 是符号链接,解引用所指向的文件
fstat 文件描述符 使用前先 open;若 fd 指向符号链接,得到所指文件属性
lstat 文件路径 不解引用,查阅链接文件本身的属性

选择原则:文件已打开用 fstat;不想打开文件用 stat;要查链接本身用 lstat。参数:pathname 文件路径(或 fd 文件描述符),buf 是传出参数,指向 struct stat。返回值:成功 0,失败 -1 并设置 errno

常见 errno 含义 常见 errno 含义
ENOENT 文件不存在 ELOOP 符号链接层级过多
EACCES 路径中某目录无搜索权限 ENAMETOOLONG 路径名过长
ENOTDIR 路径中某段不是目录 EFAULT buf 非法地址
EPERM 权限不足 EBADF fstatfd 无效

2.1 struct stat 结构体

struct stat 由内核定义,在 <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;    /* 文件内容存储的块大小 */
    blkcnt_t st_blocks;      /* 文件内容所占块数 */
    struct timespec st_atim;  /* 文件最后被访问的时间 */
    struct timespec st_mtim;  /* 文件内容最后被修改的时间 */
    struct timespec st_ctim;  /* 文件状态最后被改变的时间 */
};
字段 含义 字段 含义
st_dev 文件所在设备,不常用 st_rdev 设备号,仅设备文件使用
st_ino inode 编号 st_size 文件逻辑大小,字节
st_mode 文件类型 + 权限(核心) st_blksize 存储块大小
st_nlink 硬链接数 st_blocks 占用块数
st_uid/st_gid 所有者用户 ID / 组 ID st_atim/st_mtim/st_ctim 访问/内容修改/状态改变时间

2.2 st_mode 变量

st_mode 是 32 位无符号整数,记录文件类型(高 4 bit)与权限(低 12 bit);权限部分与 openmode 参数一致(S/U/G/O)。

| 权限宏 | 值 | 含义 | 权限宏 | 值 | 含义 | | ------ | -- | ---- | ------ | -- | ---- | | S_IRWXU | 00700 | 所有者 读写执行 | S_IRGRP | 00040 | 同组 读 | | S_IRUSR | 00400 | 所有者 读 | S_IWGRP | 00020 | 同组 写 | | S_IWUSR | 00200 | 所有者 写 | S_IXGRP | 00010 | 同组 执行 | | S_IXUSR | 00100 | 所有者 执行 | S_IROTH | 00004 | 其它 读 | | S_IRWXG | 00070 | 同组 读写执行 | S_IWOTH | 00002 | 其它 写 | | S_IRWXO | 00007 | 其它 读写执行 | S_IXOTH | 00001 | 其它 执行 |

| 类型宏 | 值 | 类型 | 类型宏 | 值 | 类型 | | ------ | -- | ---- | ------ | -- | ---- | | S_IFSOCK | 0140000 | 套接字 | S_IFDIR | 0040000 | 目录 | | S_IFLNK | 0120000 | 符号链接 | S_IFCHR | 0020000 | 字符设备 | | S_IFREG | 0100000 | 普通文件 | S_IFIFO | 0010000 | 管道 | | S_IFBLK | 0060000 | 块设备 | S_IFMT | 0170000 | 类型字段位掩码 |

if (st.st_mode & S_IXUSR) { /* 所有者具有执行权限 */ }
if ((st.st_mode & S_IFMT) == S_IFREG) { /* 普通文件 */ }   /* 写法 1 */
if (S_ISREG(st.st_mode)) { /* 普通文件 */ }               /* 写法 2(推荐) */

判断宏全集:S_ISREG / S_ISDIR / S_ISCHR / S_ISBLK / S_ISFIFO / S_ISLNK / S_ISSOCK

2.3 struct timespec

定义在 <time.h>time_t 实质是 long int,表示从基准时间点经过的秒数(日历时间)。

struct timespec { time_t tv_sec; syscall_slong_t tv_nsec; };  /* 秒 / 纳秒 */
graph LR
    A["stat(pathname, &st)"] --> B["struct stat"]
    B --> C["st_mode: 类型+权限"]
    B --> D["st_uid/st_gid: 属主"]
    B --> E["st_size/st_ino: 大小/inode"]
    B --> F["st_atim/mtim/ctim: 时间戳"]
    B --> G["st_nlink: 链接数"]
    C --> H["S_ISREG/S_ISDIR..."]
    C --> I["S_IRUSR/S_IWOTH..."]

3. 文件属主

Linux 是多用户系统,每个文件关联一个用户(所有者)和一个用户组;系统通过 UID / GID 识别,而非用户名。

ID 类型 作用
实际用户 ID / 实际组 ID 标识进程究竟是谁(执行该程序的用户)
有效用户 ID / 有效组 ID / 附属组 ID 用于文件访问权限检查

一般情况下进程的有效用户 ID = 实际用户 ID(绝大多数文件没有 set-user-ID 位)。st_uid / st_gid 记录文件的用户 ID 与组 ID。

#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int lchown(const char *pathname, uid_t owner, gid_t group);
uid_t getuid(void);
gid_t getgid(void);
参数 含义
pathname / fd 文件路径 / 文件描述符
owner / group 新的用户 ID / 组 ID

返回值:成功 0,失败 -1 并设置 errno(如 EPERM)。修改限制:只有超级用户进程能更改文件的用户 ID;普通用户可将组 ID 改为自己所从属的任意附属组 ID(前提有效用户 ID 等于文件用户 ID);超级用户可将组 ID 改为任意值。只改其中一个 ID 时,另一个传入文件当前值(先用 stat 获取)。getuid / getgid 返回当前进程的实际用户/组 ID,加 sudogetuid() 返回 0。


4. 文件访问权限

st_mode 记录访问权限。所有文件类型(目录、设备文件)都有访问权限,并非只有普通文件。

4.1 普通权限与特殊权限

9 个普通权限位按所有者(U)、同组(G)、其它(O)划分,每类含读(r)、写(w)、执行(x),宏见 2.2 节。3 个特殊权限位:

| 宏 | 值 | 含义 | | -- | -- | ---- | | S_ISUID | 04000 | set-user-ID 位 | | S_ISGID | 02000 | set-group-ID 位 | | S_ISVTX | 01000 | Sticky 位 |

  • 若文件设置了 set-user-ID 位,进程操作该文件时内核会把进程的有效用户 ID 设为该文件的用户 ID;
  • 若设置了 set-group-ID 位,内核会把进程的有效组 ID 设为该文件的组 ID;
  • Sticky 位等用途本书未展开。

4.2 权限检查规则

内核每次对文件读写执行都会做权限检查,参与检查的是进程的有效用户、有效组、附属组,对应文件的所有者、所属组、其它用户

  1. 进程有效用户 ID == 文件所有者 ID → 以所有者角色检查;
  2. 否则,进程有效组 ID 或任一附属组 ID == 文件组 ID → 以同组用户角色检查;
  3. 否则 → 以其它用户角色检查;
  4. 进程有效用户 ID == 0(root)→ 无需检查,拥有最高权限。

4.3 目录权限

目录拥有与普通文件相同的权限位,但含义不同:

目录权限 含义
读(r) 可列出目录内容(如 ls
写(w) 可在目录下创建、删除文件
执行(x) 可访问目录下的文件(读写执行其中的文件)

结论:只读无执行时只能看到文件名、无法访问文件属性;有执行无读时知道文件名可访问、但不能列出目录内容;创建/删除文件需要目录的写 + 执行权限;要对文件读写执行,除文件本身权限外还需拥有所在目录的执行权限

4.4 access / chmod / fchmod / umask

#include <unistd.h>
int access(const char *pathname, int mode);   /* F_OK / R_OK / W_OK / X_OK, 可 | 组合 */

#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);

#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
函数 说明 返回值
access 检查文件存在性/读/写/执行权限,任一不通过即失败 通过 0,否则 -1
chmod / fchmod 修改权限,mode 可用八进制或权限宏;链接文件改的是所指向文件 成功 0,失败 -1
umask 设置进程权限掩码,新建文件实际权限 = mode & ~umask 返回旧 umask

修改权限要么是 root 进程,要么进程有效用户 ID 与文件用户 ID 匹配。umask 是进程属性、不能屏蔽特殊权限位、通常继承自父进程。例如 mode=0777umask=0002 时实际权限为 0775


5. 文件的时间属性

字段 说明
st_atim 文件最后被访问的时间(如 read
st_mtim 文件内容最后被修改的时间(如 write
st_ctim 文件状态最后被改变的时间(inode 变更,如 chmodchown

5.1 utime / utimes

#include <sys/types.h>
#include <utime.h>
int utime(const char *filename, const struct utimbuf *times);
struct utimbuf { time_t actime; time_t modtime; };  /* 访问时间 / 内容修改时间 */

#include <sys/time.h>
int utimes(const char *filename, const struct timeval times[2]);
struct timeval { long tv_sec; long tv_usec; };      /* 秒 / 微秒 */

timesNULL 则都设为当前时间。返回值:成功 0,失败 -1 并设置 errnoutime 精度到秒,utimes 精度到微秒;可显式修改 atimemtime不能显式修改 ctime(内核在 inode 变更时自动更新)。修改权限:超级用户进程;有效用户 ID 与文件用户 ID 匹配的进程;times == NULL 且对文件有写权限的进程。

5.2 futimens / utimensat

相比 utime / utimes:可到纳秒精度;可单独设置某一个时间戳;可独立将任一时间戳设为当前时间。

#include <fcntl.h>
#include <sys/stat.h>
int futimens(int fd, const struct timespec times[2]);
int utimensat(int dirfd, const char *pathname,
              const struct timespec times[2], int flags);
tv_nsec 取值 效果
times == NULL 两个时间戳都设为当前时间
UTIME_NOW 该时间戳设为当前时间(忽略 tv_sec
UTIME_OMIT 该时间戳保持不变(忽略 tv_sec
其它值 设置为 tv_sec + tv_nsec 指定值

utimensatdirfd 可为目录 fd 或 AT_FDCWDflags 可为 0AT_SYMLINK_NOFOLLOW(修改链接本身时间戳)。


6. 软链接与硬链接

对比项 硬链接 软链接(符号链接)
inode 与源文件相同 与源文件不同
数据块 与源文件共享 存的是源文件路径
关系 完全平等 主从关系
源文件删除 仍可访问(链接数减 1) 变悬空链接,指向无效路径
链接数统计 计入 st_nlink 不计入
对目录 不能创建(root+底层支持除外) 可以
跨文件系统 要求同文件系统 可以
目标不存在 不允许 允许(悬空链接)

ln 源文件 链接文件 创建硬链接,ln -s 源文件 链接文件 创建软链接;源文件本身也算一个硬链接。

#include <unistd.h>
int link(const char *oldpath, const char *newpath);      /* 硬链接 */
int symlink(const char *target, const char *linkpath);   /* 软链接 */
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
函数 参数 返回值
link oldpath 源文件;newpath 新硬链接(已存在则报错) 成功 0,失败 -1
symlink target 被链接源路径;linkpath 软链接路径 成功 0,失败 -1
readlink pathname 必须是软链接;buf 缓冲区;bufsiz 大小 成功返回字节数,失败 -1

重要:不能对软链接用 open + read 读取其存储的路径——open 会打开所指向的文件,必须用 readlink

graph TD
    subgraph Hard["硬链接"]
        H1["test_file (inode=100)"] --- I1["inode 100 nlink=3"]
        H2["hard1 (inode=100)"] --- I1
        H3["hard2 (inode=100)"] --- I1
        I1 --- D1["数据块"]
    end
    subgraph Soft["软链接"]
        S1["soft (inode=200)"] --- I2["inode 200 nlink=1"]
        I2 --- D2["数据块: './test_file' 路径字符串"]
        D2 -. 解析路径 .-> I1
    end

7. 目录

  • 普通文件 = inode 节点 + 数据块;
  • 目录 = inode 节点 + 目录块;目录块由多个目录项组成,每个目录项记录某文件的文件名 + inode 编号

    #include <sys/stat.h>
    #include <sys/types.h>
    int mkdir(const char *pathname, mode_t mode);   /* 创建目录 */
    int rmdir(const char *pathname);                /* 删除空目录 */
    
    #include <sys/types.h>
    #include <dirent.h>
    DIR *opendir(const char *name);
    struct dirent *readdir(DIR *dirp);
    void rewinddir(DIR *dirp);
    int closedir(DIR *dirp);
    
    #include <unistd.h>
    char *getcwd(char *buf, size_t size);
    int chdir(const char *path);
    int fchdir(int fd);
    
函数 说明 返回值
mkdir mode 最终权限 mode & ~umask;已存在则失败 成功 0,失败 -1
rmdir 目录必须为空;不能是软链接 成功 0,失败 -1
opendir 打开目录得到 DIR * 句柄 成功 DIR *,失败 NULL
readdir 读取下一条目录项,按天然次序不排序 struct dirent * / NULL
rewinddir 目录流重置到起点
closedir 关闭目录释放资源 成功 0,失败 -1
getcwd 取当前工作目录绝对路径;buf=NULL,size=0 时内部按需分配需 free 成功 buf,失败 NULL
chdir / fchdir 以路径 / 文件描述符切换工作目录,目录须存在 成功 0,失败 -1
struct dirent {
    ino_t          d_ino;       /* inode 编号 */
    off_t          d_off;       /* not an offset; see NOTES */
    unsigned short d_reclen;    /* length of this record */
    unsigned char  d_type;      /* type of file; not supported by all filesystem types */
    char           d_name[256]; /* 文件名 */
};

一般只关注 d_inod_namereaddir 返回 NULL 时用 errno 区分到末尾还是出错:

errno = 0;
direntp = readdir(dirp);
if (NULL == direntp) {
    if (0 != errno) { /* 出错 */ } else { /* 已到目录末尾 */ }
}

进程的当前工作目录是解析相对路径的起点,一般继承自父进程。


8. 删除文件与重命名

#include <unistd.h>
int unlink(const char *pathname);      /* 删除文件(不含目录) */

#include <stdio.h>
int remove(const char *pathname);      /* 删除文件或空目录 */
int rename(const char *oldpath, const char *newpath);  /* 重命名 */

unlink 移除目录项并把 inode 链接计数减 1:若还有其它硬链接仍可访问数据;只有链接计数为 0 且无进程打开时,inode 与数据块才被回收;若 pathname 是软链接,删除链接本身、不解引用。返回值:成功 0,失败 -1 并设置 errno

remove 内部判断:非目录调用 unlink,目录调用 rmdir;对软链接不解引用,删除链接本身。

rename 只操作目录条目、不移动文件数据(inode 编号不变、数据块内容不移动),不影响指向该文件的其它硬链接,也不影响已打开该文件的进程。各种情况:

  • newpath 已存在则覆盖;
  • newpatholdpath 指向同一文件则不变化(且成功);
  • 对两个参数中的软链接均不解引用:oldpath 是软链接则重命名该链接;newpath 是软链接则移除并覆盖;
  • oldpath 是文件(非目录)时 newpath 不能是目录路径;移到某目录下必须包含新文件名;
  • oldpath 是目录时 newpath 要么不存在,要么是空目录;
  • 两者必须在同一文件系统;不能对 ... 重命名。

9. 完整可复制源码

以下综合例程先创建 test_file、打印完整属性(strftime 格式化三个时间戳),再演示 chmodaccesslink/symlink/readlink、遍历目录、renameunlink,可直接在 Ubuntu 上编译运行。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>

#define TEST_FILE "./test_file"
#define HARD_LINK "./hard_link"
#define SOFT_LINK "./soft_link"
#define NEW_NAME  "./new_file"

/* 根据 st_mode 打印文件类型 */
static void print_file_type(mode_t mode)
{
    if (S_ISREG(mode))       printf("普通文件");
    else if (S_ISDIR(mode))  printf("目录");
    else if (S_ISCHR(mode))  printf("字符设备");
    else if (S_ISBLK(mode))  printf("块设备");
    else if (S_ISLNK(mode))  printf("符号链接");
    else if (S_ISFIFO(mode)) printf("管道");
    else if (S_ISSOCK(mode)) printf("套接字");
    else                     printf("未知");
}

/* 把时间戳格式化为可读字符串 */
static void time_to_str(const struct timespec *ts, char *out, size_t len)
{
    struct tm tm_buf;
    localtime_r(&ts->tv_sec, &tm_buf);
    strftime(out, len, "%Y-%m-%d %H:%M:%S", &tm_buf);
}

/* 打印 stat 结构体中的关键属性 */
static void show_attr(const char *path)
{
    struct stat st;
    char at[64], mt[64], ct[64];

    if (-1 == stat(path, &st)) { perror("stat error"); return; }
    printf("===== %s =====\n", path);
    printf("inode=%ld  type=", (long)st.st_ino);
    print_file_type(st.st_mode);
    printf("  mode=0%o  nlink=%ld\n", st.st_mode & 07777, (long)st.st_nlink);
    printf("uid=%d gid=%d size=%ld blksize=%ld blocks=%ld\n",
           st.st_uid, st.st_gid, (long)st.st_size,
           (long)st.st_blksize, (long)st.st_blocks);
    time_to_str(&st.st_atim, at, sizeof(at));
    time_to_str(&st.st_mtim, mt, sizeof(mt));
    time_to_str(&st.st_ctim, ct, sizeof(ct));
    printf("atime=%s\nmtime=%s\nctime=%s\n", at, mt, ct);
}

/* 遍历目录,打印文件名与 inode */
static int list_dir(const char *dir_name)
{
    DIR *dirp = opendir(dir_name);
    struct dirent *dir;

    if (NULL == dirp) { perror("opendir error"); return -1; }
    printf("----- dir %s -----\n", dir_name);
    errno = 0;
    while (NULL != (dir = readdir(dirp)))
        printf("  %-20s inode=%ld\n", dir->d_name, (long)dir->d_ino);
    if (0 != errno) { perror("readdir error"); closedir(dirp); return -1; }
    printf("End of directory!\n");
    closedir(dirp);
    return 0;
}

int main(void)
{
    int fd;
    char buf[128];
    ssize_t n;
    struct utimbuf utm;
    time_t now;

    /* 1. 创建并写入 test_file */
    fd = open(TEST_FILE, O_RDWR | O_CREAT | O_TRUNC, 0664);
    if (-1 == fd) { perror("open error"); exit(-1); }
    if (write(fd, "Hello Linux file attribute\n", 27) < 0) perror("write error");
    close(fd);
    show_attr(TEST_FILE);

    /* 2. 检查权限并修改权限为 0777 */
    if (0 == access(TEST_FILE, R_OK | W_OK)) printf("access: 读写权限 OK\n");
    else                                     printf("access: 权限不足\n");
    if (-1 == chmod(TEST_FILE, 0777)) { perror("chmod error"); exit(-1); }

    /* 3. 显式修改访问时间与内容修改时间 */
    time(&now);
    utm.actime = now; utm.modtime = now;
    if (-1 == utime(TEST_FILE, &utm)) perror("utime error");

    /* 4. 创建硬链接与软链接 */
    if (-1 == link(TEST_FILE, HARD_LINK))    perror("link error");
    if (-1 == symlink(TEST_FILE, SOFT_LINK)) perror("symlink error");
    show_attr(TEST_FILE);
    show_attr(HARD_LINK);   /* inode 与 test_file 相同 */
    show_attr(SOFT_LINK);   /* inode 不同,大小为路径长度 */

    /* 5. 用 readlink 读取软链接内容 */
    memset(buf, 0x0, sizeof(buf));
    n = readlink(SOFT_LINK, buf, sizeof(buf) - 1);
    if (-1 == n) perror("readlink error");
    else { buf[n] = '\0'; printf("soft_link 指向: %s\n", buf); }

    /* 6. 遍历当前目录 */
    list_dir(".");

    /* 7. 重命名文件(inode 不变) */
    if (-1 == rename(TEST_FILE, NEW_NAME)) perror("rename error");
    show_attr(NEW_NAME);

    /* 8. 删除链接与文件 */
    if (-1 == unlink(SOFT_LINK)) perror("unlink soft error");
    if (-1 == unlink(HARD_LINK)) perror("unlink hard error");
    if (-1 == remove(NEW_NAME))  perror("remove error");

    printf("done.\n");
    exit(0);
}

逐段解释

  1. print_file_type:用 S_ISREG 等宏判断 st_mode 中的类型;比手工位运算可读性好。
  2. time_to_strlocaltime_rtime_t 分解为 struct tmstrftime%Y-%m-%d %H:%M:%S 格式化;_r 版本可重入、线程安全。
  3. show_attrstat 取属性,st_mode & 07777 取低 12 位权限,st_nlink 展示硬链接数。
  4. list_diropendir → 循环 readdirclosedir;用 errno = 0 + 返回值判断读到末尾还是出错。
  5. main 第 2 步:access 判权限,chmod 改权限(需所有者或 root)。
  6. main 第 3 步:utime 显式改 atime/mtime;注意 ctime 会被内核自动更新。
  7. main 第 4~5 步:linkst_nlink 加 1、inode 相同;symlink 后 inode 不同、st_size 为路径长度;readlink 才能读出软链接存储的路径。
  8. main 第 7 步:rename 后 inode 不变,验证只改目录条目。
  9. main 第 8 步:软链接用 unlink 删除链接本身;最后一个硬链接删除后数据块才回收。

10. 实验步骤与调试方法

gcc -Wall -o test_attr test_attr.c && ./test_attr
stat test_file                          # 对照程序打印的 inode、权限、时间戳
ls -li test_file hard_link soft_link    # 观察 inode 与链接数
readlink soft_link                      # 对照 readlink() 读出内容

观察点:硬链接与源文件 inode 相同、st_nlink 增加;软链接 inode 不同,ls -l 显示 l 且带 ->chmod 0777ls -l 权限变为 rwxrwxrwx

权限与属主实验:

./test_attr                     # 普通用户运行 chown 会 EPERM
sudo ./test_attr                # root 身份才能改属主
umask                           # 查看当前 shell 的 umask
umask 0000 && touch a && ls -l a   # 验证 umask 对新建文件权限的影响

常见问题:

现象 原因 处理
stat error: No such file or directory 路径不存在 / 相对路径基准不对 getcwd 打印当前工作目录
chown error: Operation not permitted 普通用户改属主 sudo,或只改组 ID
chmod error: Operation not permitted 既非 root 也非文件所有者 用所有者账户或 sudo
readlink error: Invalid argument 传入的不是软链接 先用 lstat/S_ISLNK 判断
rmdir error: Directory not empty 目录非空 先清空目录再删
时间戳格式化为 1970-... 结构体成员用错(如 st_atime 统一用 st_atim.tv_sec
link error: Invalid cross-device link 硬链接跨文件系统 改用软链接 symlink

调试技巧:出错立即 perror()strace -e trace=openat,stat,lstat,readlink,unlink ./test_attr 观察系统调用;ltrace 观察 C 库函数;对同一软链接分别 stat / lstat,观察 st_modest_ino 差异。


11. 跨平台对比:IMX6ULL vs STM32 vs RK3568

维度 I.MX6ULL(本教程) STM32(裸机/HAL) RK3568(Linux)
运行环境 Linux 用户态(Cortex-A7) 裸机 / RTOS(Cortex-M) Linux 用户态(Cortex-A55)
文件概念 一切皆文件,7 种类型 无文件系统(或 FatFS) 同 Linux,7 种类型
获取属性 stat/fstat/lstat + struct stat FatFS f_stat / FILINFO stat 系列
权限模型 UID/GID + rwx + 特殊位 无(或简单只读标志) 同 Linux(含 ACL/SELinux 可选)
链接 硬链接、软链接 硬链接、软链接
目录遍历 opendir/readdir f_readdir(FatFS) opendir/readdir
时间戳 atime/mtime/ctime + RTC 依赖 RTC,通常无文件时间 同 Linux
设备文件 /dev 虚拟文件系统,open 设备节点 直接寄存器/HAL 操作 /dev 设备节点、sysfs
典型场景 应用编程、驱动配合 实时控制、低功耗 高性能边缘计算、多媒体

要点:STM32 上的「文件」通常只是存储介质上的 FAT 记录,没有 inode、权限、链接概念;IMX6ULL/RK3568 是完整 Linux,文件属性体系完全一致,应用代码可无缝迁移。


12. 面试精选(5 题)

Q1:statfstatlstat 有什么区别?什么场景用哪个?

三者都获取文件属性并写入 struct statstat(pathname) 从路径出发、不需打开文件,若路径是符号链接会解引用得到所指文件属性;fstat(fd) 从文件描述符出发、需先 open,文件已打开时更自然,也能规避「检查后路径被替换」的 TOCTOU 风险;lstat(pathname)stat 类似但不解引用,得到链接文件本身属性。典型用法:判断是否软链接用 lstat + S_ISLNK;查看真实文件属性用 stat;对已打开的 fd 用 fstat

Q2:硬链接和软链接的本质区别是什么?为什么硬链接不能跨文件系统、不能指向目录?

硬链接是多个目录项指向同一个 inode,地位平等,st_nlink 计数,删除一个只是计数减 1,计数为 0 且无进程打开才真正回收数据块。软链接是独立文件(独立 inode),数据块存目标路径字符串,靠路径解析找到目标,目标删除后成悬空链接。

硬链接不能跨文件系统:inode 编号只在单个文件系统内唯一,跨文件系统无法用 inode 号定位同一文件。硬链接一般不能指向目录:若允许会形成目录环,导致 finddu 等遍历算法死循环,也会破坏目录树结构;只有超级用户在底层文件系统支持时才可能创建。

Q3:access() 检查权限有什么局限?为什么推荐用「打开再处理」替代?

access 使用进程的实际用户/组 ID(real UID/GID)判断权限,而文件访问时内核用有效用户/组 ID(effective UID/GID)。当程序设置了 set-user-ID 位(sudo 或 SUID 程序)时两者可能不同,access 结果就不等于真正 open 的结果。此外检查与实际操作之间存在时间窗口,文件可能被替换(TOCTOU 竞态)。因此更安全的做法是直接用目标权限 open,根据 errnoEACCES/EPERM)判断;access 适合做提示性、非安全关键的检查。

Q4:umask 的作用是什么?open(..., 0777) 新建的文件权限一定是 0777 吗?

umask进程属性,用于屏蔽新建文件/目录的权限位,最终权限为 mode & ~umask。所以 open(..., 0777) 的实际权限还取决于进程 umask,例如 umask=0002 时实际是 0775。umask 通常继承自父进程(如 shell),umask() 系统调用可修改并返回旧值;umask 不能屏蔽特殊权限位,也不影响已存在文件的权限(那是 chmod 的职责)。

Q5:文件有三个时间戳 atime/mtime/ctime,分别什么时候改变?能显式修改哪些?

atime(访问时间)在读取文件内容时更新(如 read);mtime(内容修改时间)在文件内容改变时更新(如 write);ctime(状态改变时间)在 inode 变更时更新(如 chmodchown、改链接数、rename),注意它不是「创建时间」。可显式修改的只有 atimemtimeutime(秒级)、utimes(微秒级)、futimens/utimensat(纳秒级,可单独设置、可用 UTIME_NOW/UTIME_OMIT)。ctime 无法显式修改,任何导致 inode 变更的操作都会让内核自动更新它——这正是「状态最后被改变」的含义。


内容来源:《I.MX6U嵌入式Linux C应用编程指南》第五章 文件属性与目录