--- 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 -l`、`stat`、`chmod`、`ln`、`rm`、`mv`、`mkdir` 等命令的核心功能。 --- ## 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。**符号链接文件**内容指向另一个文件路径,对它的操作会被转移到所指向的文件。管道与套接字都用于进程间通信,后者可跨主机(网络通信)。 ```mermaid 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 ```c #include #include #include 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` | `fstat` 的 `fd` 无效 | ### 2.1 struct stat 结构体 `struct stat` 由内核定义,在 `` 中声明,应用层可直接使用。 ```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; /* 文件所有者的用户 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);权限部分与 `open` 的 `mode` 参数一致(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` | 类型字段位掩码 | ```c 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_t` 实质是 `long int`,表示从基准时间点经过的秒数(日历时间)。 ```c struct timespec { time_t tv_sec; syscall_slong_t tv_nsec; }; /* 秒 / 纳秒 */ ``` ```mermaid 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。 ```c #include 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,加 `sudo` 时 `getuid()` 返回 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 ```c #include int access(const char *pathname, int mode); /* F_OK / R_OK / W_OK / X_OK, 可 | 组合 */ #include int chmod(const char *pathname, mode_t mode); int fchmod(int fd, mode_t mode); #include #include mode_t umask(mode_t mask); ``` | 函数 | 说明 | 返回值 | | ---- | ---- | ------ | | `access` | 检查文件存在性/读/写/执行权限,任一不通过即失败 | 通过 `0`,否则 `-1` | | `chmod` / `fchmod` | 修改权限,`mode` 可用八进制或权限宏;链接文件改的是所指向文件 | 成功 `0`,失败 `-1` | | `umask` | 设置进程权限掩码,新建文件实际权限 = `mode & ~umask` | 返回旧 umask | 修改权限要么是 root 进程,要么进程有效用户 ID 与文件用户 ID 匹配。umask 是**进程属性**、不能屏蔽特殊权限位、通常继承自父进程。例如 `mode=0777`、`umask=0002` 时实际权限为 `0775`。 --- ## 5. 文件的时间属性 | 字段 | 说明 | | ---- | ---- | | `st_atim` | 文件最后被访问的时间(如 `read`) | | `st_mtim` | 文件内容最后被修改的时间(如 `write`) | | `st_ctim` | 文件状态最后被改变的时间(inode 变更,如 `chmod`、`chown`) | ### 5.1 utime / utimes ```c #include #include int utime(const char *filename, const struct utimbuf *times); struct utimbuf { time_t actime; time_t modtime; }; /* 访问时间 / 内容修改时间 */ #include int utimes(const char *filename, const struct timeval times[2]); struct timeval { long tv_sec; long tv_usec; }; /* 秒 / 微秒 */ ``` `times` 传 `NULL` 则都设为当前时间。返回值:成功 `0`,失败 `-1` 并设置 `errno`。`utime` 精度到秒,`utimes` 精度到微秒;可显式修改 `atime` 与 `mtime`,**不能显式修改 `ctime`**(内核在 inode 变更时自动更新)。修改权限:超级用户进程;有效用户 ID 与文件用户 ID 匹配的进程;`times == NULL` 且对文件有写权限的进程。 ### 5.2 futimens / utimensat 相比 `utime` / `utimes`:可到纳秒精度;可单独设置某一个时间戳;可独立将任一时间戳设为当前时间。 ```c #include #include 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` 指定值 | `utimensat` 的 `dirfd` 可为目录 fd 或 `AT_FDCWD`;`flags` 可为 `0` 或 `AT_SYMLINK_NOFOLLOW`(修改链接本身时间戳)。 --- ## 6. 软链接与硬链接 | 对比项 | 硬链接 | 软链接(符号链接) | | ------ | ------ | ------------------ | | inode | 与源文件**相同** | 与源文件**不同** | | 数据块 | 与源文件共享 | 存的是源文件路径 | | 关系 | 完全平等 | 主从关系 | | 源文件删除 | 仍可访问(链接数减 1) | 变悬空链接,指向无效路径 | | 链接数统计 | 计入 `st_nlink` | 不计入 | | 对目录 | 不能创建(root+底层支持除外) | 可以 | | 跨文件系统 | 要求同文件系统 | 可以 | | 目标不存在 | 不允许 | 允许(悬空链接) | `ln 源文件 链接文件` 创建硬链接,`ln -s 源文件 链接文件` 创建软链接;源文件本身也算一个硬链接。 ```c #include 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`。 ```mermaid 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 编号**。 ```c #include #include int mkdir(const char *pathname, mode_t mode); /* 创建目录 */ int rmdir(const char *pathname); /* 删除空目录 */ #include #include DIR *opendir(const char *name); struct dirent *readdir(DIR *dirp); void rewinddir(DIR *dirp); int closedir(DIR *dirp); #include 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` | ```c 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_ino` 和 `d_name`。`readdir` 返回 `NULL` 时用 `errno` 区分到末尾还是出错: ```c errno = 0; direntp = readdir(dirp); if (NULL == direntp) { if (0 != errno) { /* 出错 */ } else { /* 已到目录末尾 */ } } ``` 进程的当前工作目录是解析**相对路径**的起点,一般继承自父进程。 --- ## 8. 删除文件与重命名 ```c #include int unlink(const char *pathname); /* 删除文件(不含目录) */ #include 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` 已存在则覆盖; - `newpath` 与 `oldpath` 指向同一文件则不变化(且成功); - 对两个参数中的软链接均不解引用:`oldpath` 是软链接则重命名该链接;`newpath` 是软链接则移除并覆盖; - `oldpath` 是文件(非目录)时 `newpath` 不能是目录路径;移到某目录下必须包含新文件名; - `oldpath` 是目录时 `newpath` 要么不存在,要么是空目录; - 两者必须在**同一文件系统**;不能对 `.` 和 `..` 重命名。 --- ## 9. 完整可复制源码 以下综合例程先创建 `test_file`、打印完整属性(`strftime` 格式化三个时间戳),再演示 `chmod`、`access`、`link`/`symlink`/`readlink`、遍历目录、`rename`、`unlink`,可直接在 Ubuntu 上编译运行。 ```c #include #include #include #include #include #include #include #include #include #include #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_str`:`localtime_r` 把 `time_t` 分解为 `struct tm`,`strftime` 按 `%Y-%m-%d %H:%M:%S` 格式化;`_r` 版本可重入、线程安全。 3. `show_attr`:`stat` 取属性,`st_mode & 07777` 取低 12 位权限,`st_nlink` 展示硬链接数。 4. `list_dir`:`opendir` → 循环 `readdir` → `closedir`;用 `errno = 0` + 返回值判断读到末尾还是出错。 5. `main` 第 2 步:`access` 判权限,`chmod` 改权限(需所有者或 root)。 6. `main` 第 3 步:`utime` 显式改 `atime`/`mtime`;注意 `ctime` 会被内核自动更新。 7. `main` 第 4~5 步:`link` 后 `st_nlink` 加 1、inode 相同;`symlink` 后 inode 不同、`st_size` 为路径长度;`readlink` 才能读出软链接存储的路径。 8. `main` 第 7 步:`rename` 后 inode 不变,验证只改目录条目。 9. `main` 第 8 步:软链接用 `unlink` 删除链接本身;最后一个硬链接删除后数据块才回收。 --- ## 10. 实验步骤与调试方法 ```bash 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 0777` 后 `ls -l` 权限变为 `rwxrwxrwx`。 权限与属主实验: ```bash ./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_mode` 与 `st_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:`stat`、`fstat`、`lstat` 有什么区别?什么场景用哪个? 三者都获取文件属性并写入 `struct stat`。`stat(pathname)` 从路径出发、不需打开文件,若路径是符号链接会**解引用**得到所指文件属性;`fstat(fd)` 从文件描述符出发、需先 `open`,文件已打开时更自然,也能规避「检查后路径被替换」的 TOCTOU 风险;`lstat(pathname)` 与 `stat` 类似但**不解引用**,得到链接文件本身属性。典型用法:判断是否软链接用 `lstat` + `S_ISLNK`;查看真实文件属性用 `stat`;对已打开的 fd 用 `fstat`。 ### Q2:硬链接和软链接的本质区别是什么?为什么硬链接不能跨文件系统、不能指向目录? 硬链接是多个目录项指向**同一个 inode**,地位平等,`st_nlink` 计数,删除一个只是计数减 1,计数为 0 且无进程打开才真正回收数据块。软链接是独立文件(独立 inode),数据块存目标路径字符串,靠路径解析找到目标,目标删除后成悬空链接。 硬链接不能跨文件系统:inode 编号只在单个文件系统内唯一,跨文件系统无法用 inode 号定位同一文件。硬链接一般不能指向目录:若允许会形成目录环,导致 `find`、`du` 等遍历算法死循环,也会破坏目录树结构;只有超级用户在底层文件系统支持时才可能创建。 ### Q3:`access()` 检查权限有什么局限?为什么推荐用「打开再处理」替代? `access` 使用进程的**实际用户/组 ID**(real UID/GID)判断权限,而文件访问时内核用**有效用户/组 ID**(effective UID/GID)。当程序设置了 set-user-ID 位(`sudo` 或 SUID 程序)时两者可能不同,`access` 结果就不等于真正 `open` 的结果。此外检查与实际操作之间存在时间窗口,文件可能被替换(TOCTOU 竞态)。因此更安全的做法是直接用目标权限 `open`,根据 `errno`(`EACCES`/`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 变更时更新(如 `chmod`、`chown`、改链接数、`rename`),注意它不是「创建时间」。可显式修改的只有 `atime` 和 `mtime`:`utime`(秒级)、`utimes`(微秒级)、`futimens`/`utimensat`(纳秒级,可单独设置、可用 `UTIME_NOW`/`UTIME_OMIT`)。`ctime` 无法显式修改,任何导致 inode 变更的操作都会让内核自动更新它——这正是「状态最后被改变」的含义。 --- **内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第五章 文件属性与目录