title: 文件属性与目录 tags: [嵌入式Linux, Linux应用编程, 文件属性, stat, 目录, 权限, 硬链接, 软链接, IMX6ULL] created: 2026-09-18 updated: 2026-09-18
💡 关联知识:[[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 等命令的核心功能。
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
#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 |
fstat 的 fd 无效 |
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 |
访问/内容修改/状态改变时间 |
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 | 类型字段位掩码 |
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。
定义在 <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..."]
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,加 sudo 时 getuid() 返回 0。
st_mode 记录访问权限。所有文件类型(目录、设备文件)都有访问权限,并非只有普通文件。
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 位 |
内核每次对文件读写执行都会做权限检查,参与检查的是进程的有效用户、有效组、附属组,对应文件的所有者、所属组、其它用户:
目录拥有与普通文件相同的权限位,但含义不同:
| 目录权限 | 含义 |
|---|---|
| 读(r) | 可列出目录内容(如 ls) |
| 写(w) | 可在目录下创建、删除文件 |
| 执行(x) | 可访问目录下的文件(读写执行其中的文件) |
结论:只读无执行时只能看到文件名、无法访问文件属性;有执行无读时知道文件名可访问、但不能列出目录内容;创建/删除文件需要目录的写 + 执行权限;要对文件读写执行,除文件本身权限外还需拥有所在目录的执行权限。
#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=0777、umask=0002 时实际权限为 0775。
| 字段 | 说明 |
|---|---|
st_atim |
文件最后被访问的时间(如 read) |
st_mtim |
文件内容最后被修改的时间(如 write) |
st_ctim |
文件状态最后被改变的时间(inode 变更,如 chmod、chown) |
#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; }; /* 秒 / 微秒 */
times 传 NULL 则都设为当前时间。返回值:成功 0,失败 -1 并设置 errno。utime 精度到秒,utimes 精度到微秒;可显式修改 atime 与 mtime,不能显式修改 ctime(内核在 inode 变更时自动更新)。修改权限:超级用户进程;有效用户 ID 与文件用户 ID 匹配的进程;times == NULL 且对文件有写权限的进程。
相比 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 指定值 |
utimensat 的 dirfd 可为目录 fd 或 AT_FDCWD;flags 可为 0 或 AT_SYMLINK_NOFOLLOW(修改链接本身时间戳)。
| 对比项 | 硬链接 | 软链接(符号链接) |
|---|---|---|
| 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
目录 = 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_ino 和 d_name。readdir 返回 NULL 时用 errno 区分到末尾还是出错:
errno = 0;
direntp = readdir(dirp);
if (NULL == direntp) {
if (0 != errno) { /* 出错 */ } else { /* 已到目录末尾 */ }
}
进程的当前工作目录是解析相对路径的起点,一般继承自父进程。
#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 已存在则覆盖;newpath 与 oldpath 指向同一文件则不变化(且成功);oldpath 是软链接则重命名该链接;newpath 是软链接则移除并覆盖;oldpath 是文件(非目录)时 newpath 不能是目录路径;移到某目录下必须包含新文件名;oldpath 是目录时 newpath 要么不存在,要么是空目录;. 和 .. 重命名。以下综合例程先创建 test_file、打印完整属性(strftime 格式化三个时间戳),再演示 chmod、access、link/symlink/readlink、遍历目录、rename、unlink,可直接在 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);
}
逐段解释:
print_file_type:用 S_ISREG 等宏判断 st_mode 中的类型;比手工位运算可读性好。time_to_str:localtime_r 把 time_t 分解为 struct tm,strftime 按 %Y-%m-%d %H:%M:%S 格式化;_r 版本可重入、线程安全。show_attr:stat 取属性,st_mode & 07777 取低 12 位权限,st_nlink 展示硬链接数。list_dir:opendir → 循环 readdir → closedir;用 errno = 0 + 返回值判断读到末尾还是出错。main 第 2 步:access 判权限,chmod 改权限(需所有者或 root)。main 第 3 步:utime 显式改 atime/mtime;注意 ctime 会被内核自动更新。main 第 4~5 步:link 后 st_nlink 加 1、inode 相同;symlink 后 inode 不同、st_size 为路径长度;readlink 才能读出软链接存储的路径。main 第 7 步:rename 后 inode 不变,验证只改目录条目。main 第 8 步:软链接用 unlink 删除链接本身;最后一个硬链接删除后数据块才回收。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。
权限与属主实验:
./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 差异。
| 维度 | 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,文件属性体系完全一致,应用代码可无缝迁移。
stat、fstat、lstat 有什么区别?什么场景用哪个?三者都获取文件属性并写入 struct stat。stat(pathname) 从路径出发、不需打开文件,若路径是符号链接会解引用得到所指文件属性;fstat(fd) 从文件描述符出发、需先 open,文件已打开时更自然,也能规避「检查后路径被替换」的 TOCTOU 风险;lstat(pathname) 与 stat 类似但不解引用,得到链接文件本身属性。典型用法:判断是否软链接用 lstat + S_ISLNK;查看真实文件属性用 stat;对已打开的 fd 用 fstat。
硬链接是多个目录项指向同一个 inode,地位平等,st_nlink 计数,删除一个只是计数减 1,计数为 0 且无进程打开才真正回收数据块。软链接是独立文件(独立 inode),数据块存目标路径字符串,靠路径解析找到目标,目标删除后成悬空链接。
硬链接不能跨文件系统:inode 编号只在单个文件系统内唯一,跨文件系统无法用 inode 号定位同一文件。硬链接一般不能指向目录:若允许会形成目录环,导致 find、du 等遍历算法死循环,也会破坏目录树结构;只有超级用户在底层文件系统支持时才可能创建。
access() 检查权限有什么局限?为什么推荐用「打开再处理」替代?access 使用进程的实际用户/组 ID(real UID/GID)判断权限,而文件访问时内核用有效用户/组 ID(effective UID/GID)。当程序设置了 set-user-ID 位(sudo 或 SUID 程序)时两者可能不同,access 结果就不等于真正 open 的结果。此外检查与实际操作之间存在时间窗口,文件可能被替换(TOCTOU 竞态)。因此更安全的做法是直接用目标权限 open,根据 errno(EACCES/EPERM)判断;access 适合做提示性、非安全关键的检查。
umask 的作用是什么?open(..., 0777) 新建的文件权限一定是 0777 吗?umask 是进程属性,用于屏蔽新建文件/目录的权限位,最终权限为 mode & ~umask。所以 open(..., 0777) 的实际权限还取决于进程 umask,例如 umask=0002 时实际是 0775。umask 通常继承自父进程(如 shell),umask() 系统调用可修改并返回旧值;umask 不能屏蔽特殊权限位,也不影响已存在文件的权限(那是 chmod 的职责)。
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应用编程指南》第五章 文件属性与目录