Linux 文件访问与路径解析:目录、文件名、dentry、路径缓冲、挂载与软硬链接全解析
一、为什么要搞懂文件访问与路径解析
在 Linux 中"一切皆文件"。
我们每天都在调用
open、read、stat、ls,但很少思考:当你写下
open("/home/alice/file.txt", O_RDONLY)时,内核到底做了什么?这一串字符是如何一步步变成内存中的文件对象、最终读到磁盘数据的?
理解这条链路,你才能真正理解:
- 为什么文件名"不属于文件",磁盘上甚至没有"目录"这个概念;
- 为什么打开文件后,原路径被删除/替换都不影响 fd;
- 为什么删除一个正在使用的日志文件,磁盘空间却不释放;
- Linux 有多个分区,路径解析时怎么知道自己在哪个分区;
- 为什么会有 TOCTOU 竞态,为什么要发明
openat、O_PATH;- 为什么
chroot不是安全边界。这正是**路径解析(Path Resolution)**要解决的问题,也是 Linux 系统编程和面试的核心考点。
二、全局观:VFS 四大对象
VFS(Virtual File System,虚拟文件系统)是内核中对所有文件系统的抽象层。
它定义了四个核心对象:
对象 对应关系 生命周期 作用 super_block一个已挂载的文件系统实例 挂载期间 管理整个文件系统、inode 分配 inode磁盘上一个文件(唯一) 内核缓存期间 文件元数据 + 数据块定位 dentry内存中一个路径分量 (如 home、alice) 内存,可被回收 把路径名和 inode 关联,组成 dcache file一个进程打开的文件实例 从 open 到 close 记录打开模式、当前读写偏移 关系要点:
- 一个 inode 可以对应多个 dentry(硬链接、不同路径访问同一文件);
- 一个 dentry 可以对应多个 file(多次 open、多进程打开同一文件);
- file 里存的是
struct path(dentry + vfsmount),fd 最终指向 file。
三、文件名与目录的本质
3.1 文件名不属于文件,而属于目录
一个非常关键的认知:文件名并不存在 inode 里,而是存在它父目录的"目录项"中。
- inode 只存文件的元数据(大小、权限、时间戳、数据块指针),不存名字;
- 目录里的每条目录项记录「文件名 → inode 号」的映射;
- 由此带来几个重要结论:
- 重命名(
rename)只改目录项,不移动数据,是原子的;- 硬链接 = 多个目录项指向同一个 inode;
- 同一个文件可以有多个名字。
3.2 磁盘上没有"目录",只有"文件"
这是必须建立的第二层认知:
- 磁盘上没有"目录"的概念,只有"文件属性 + 文件内容";
- 目录的本质也是一个文件,它有自己的 inode 和数据块;
- 区别只在于:普通文件的数据块存文件内容,目录的数据块存的是**"文件名 → inode 号"的映射关系****;**
- 目录不能像普通文件那样直接
write,只能通过mkdir/unlink/rename/link等系统调用修改。3.3 目录项的磁盘格式(以 ext 系列为例)
字段 长度 含义 inode 4B 该名字对应的 inode 号 rec_len 2B 本条目录项总长度(用于遍历) name_len 1B 文件名长度 file_type 1B 文件类型(普通/目录/符号链接) name 变长 文件名字节
ls底层用getdents64读取这些目录项;
ls -l还要对每个名字再查一次 inode,所以海量文件时ls -l明显变慢。3.4 验证:用 opendir/readdir 读取目录
cpp#include <stdio.h> #include <string.h> #include <stdlib.h> #include <dirent.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <directory>\n", argv[0]); exit(EXIT_FAILURE); } DIR *dir = opendir(argv[1]); if (!dir) { perror("opendir"); exit(EXIT_FAILURE); } struct dirent *entry; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino); } closedir(dir); return 0; }运行
./readdir /就能看到根目录下每个文件名和它的 inode 号(如mnt: 1048577、home: 786433),正好印证**"目录内容 = 文件名 + inode 号"****。**
四、dentry 结构与 dcache
4.1 磁盘目录项 vs 内存 dentry
用户态看到的目录项是磁盘上的持久记录,而内核在内存中用
struct dentry表示每一个被访问过的路径分量。两者务必区分:
- 磁盘目录项:持久、按文件系统格式存放;
- dentry:内存对象、非持久、由 VFS 维护。
4.2 struct dentry 关键字段
字段 作用 d_inode指向关联的 inode(负 dentry 时为 NULL) d_parent父目录 dentry d_name该分量的名字 d_hash链入 dcache 哈希表 d_lru链入 LRU 链表(可被回收) d_child / d_subdirs父目录的子链表 / 自己的子节点 d_count引用计数 d_mounted该路径是否是挂载点 d_opdentry 操作函数表 4.3 dcache 如何组织
- 哈希表
dentry_hashtable:用「父 dentry + 名字」做哈希,实现 O(1) 查找;- LRU 链表:引用计数为 0 的 dentry 挂在 LRU 上,内存紧张时被回收;
- 解析每个分量时先查哈希表,命中就无需读磁盘------这是路径解析最主要的加速手段;
- 新解析出的 dentry 会被加入 dcache。
4.4 负 dentry(negative dentry)
- 文件不存在时,内核也会缓存一个
d_inode = NULL的 dentry;- 再次访问该名字时可以立刻返回
ENOENT,不必反复读磁盘;- 在该路径创建文件时,负 dentry 会被"转正"。
五、路径的表示与限制
5.1 绝对路径与相对路径
类型 形式 解析起点 绝对路径 以 /开头,如/home/alice/f.txt根目录 /相对路径 不以 /开头,如alice/f.txt、../f.txt进程当前工作目录(cwd) 5.2 长度限制
常量 值 含义 超限错误 PATH_MAX4096 整条路径长度(含 \0)ENAMETOOLONG NAME_MAX255 单个文件名长度 ENAMETOOLONG 5.3 特殊规则
- 多个连续
/等价于一个/(/home//alice同/home/alice);- 文件名不能含
/和空字符\0;- 尾随
/表示最后一个分量必须是目录,否则返回 ENOTDIR;- 空路径名(
"")是非法的;- Linux 文件名区分大小写,按字节比较(可含 UTF-8 编码)。
六、进程的 fs_struct:root 与 pwd
6.1 fs_struct 结构
每个进程的
task_struct里有一个fs指针,指向struct fs_struct,保存两个关键"位置":
- root:进程的根目录(
/对应什么);- pwd:进程当前工作目录(cwd)。
它们都是
struct path(= dentry + 挂载点 vfsmount)。fork 时子进程复制 fs_struct,拥有独立的 cwd。
6.2 chdir / chroot / pivot_root
chdir(path):改变进程的 pwd;getcwd():读取当前工作目录;chroot(path):改变进程看到的根目录,但不是安全边界------若 cwd 还在 chroot 目录之外,或持有已打开的 fd,仍可能逃逸;- 容器真正使用的是 mount namespace + pivot_root;
/proc/self/cwd、/proc/self/root是指向这两个位置的符号链接。
七、路径解析完整流程(核心)
7.1 什么是路径解析
路径解析(Path Resolution / Lookup)就是:
把一个路径字符串(如
/home/alice/f.txt)转换成内核中的 dentry / inode 对象。它由内核
fs/namei.c实现,核心结构是nameidata(保存解析的中间状态)。7.2 路径解析的"递归"推导
顺着"访问文件要打开父目录"往下追问,就会得到路径解析的本质:
- 要访问
test.c,必须先打开它所在的目录test;- 要打开
test,就得先打开它的父目录code;- 如此类似"递归"一路向上,直到出口------根目录
/;- 根目录是递归的终点:根目录的文件名和 inode 号固定(ext 系列为 2),无需查找,开机即知。
7.3 内核函数调用链
cppopen() └─ do_sys_open() └─ do_filp_open() └─ path_openat() ├─ getname() 拷贝路径到内核 ├─ path_init() 确定起点(root/pwd) ├─ link_path_walk() 逐分量(除最后一个) │ └─ walk_component() │ ├─ "." / ".." │ ├─ lookup_fast() 查 dcache(RCU) │ ├─ lookup_slow() dcache miss → 读磁盘目录 │ ├─ follow_mount() 穿越挂载点 │ └─ may_lookup() 权限检查 ├─ do_last() / lookup_open() 处理最后分量 └─ complete_walk()7.4 逐分量解析细节
.:忽略,留在当前目录;..:回到父目录(d_parent),注意 chroot 下不能穿越根;- 普通分量:
- 先
lookup_fast(RCU 查 dcache);- 未命中走
lookup_slow:分配 dentry 并调用具体文件系统读目录;- 符号链接:
follow_link读取目标,递归解析;- 挂载点:
follow_mount切换到挂载文件系统的根。7.5 符号链接处理
- 符号链接内容是目标路径字符串,缓存在 page cache;
- 解析时递归,受两个限制:嵌套深度(约 8)、总链接数(约 40),超限返回
ELOOP;- 权限检查针对目标文件,不检查符号链接本身;
O_NOFOLLOW让 open 不跟随末尾符号链接。7.6 最后一个分量的特殊处理
stat:必须存在;open(O_CREAT):不存在则创建;O_CREAT|O_EXCL:保证原子创建,已存在则失败(防竞态);O_DIRECTORY:必须是目录;O_NOFOLLOW:末尾符号链接不跟随。7.7 路径由谁提供、最初路径从哪来
- 命令/工具访问本质是进程访问,进程有 CWD,相对路径由进程提供;
- 系统安装后有根目录和缺省目录,用户创建的文件天然处于某条路径中;
- 是系统 + 用户共同构建了 Linux 的路径结构。
八、路径缓冲与三种遍历模式
8.1 为什么要先把路径拷进内核
系统调用传进来的路径在用户态内存里,内核在解析过程中(可能含磁盘 I/O),用户线程可能修改它,造成混乱甚至安全问题。
所以解析前先用
getname()把路径完整拷到内核缓冲区,再基于这份固定副本解析。8.2 路径缓冲实现
getname():优先从 per-cpu 名称缓存分配,否则kmalloc;- 返回
struct filename(含 name、uptr、refcnt);putname()释放;- 这份内核副本就是路径解析的工作内存。
8.3 三种遍历模式
模式 特点 适用 RCU walk 无锁、最快,靠 RCU + seqlock 校验 dcache 全命中、无阻塞 ref walk 持有 dentry 引用计数和锁 需要读磁盘、符号链接 RCU → ref 回退 RCU 校验失败(ECHILD)自动降级 运行时动态切换
九、挂载分区与路径前缀
9.1 inode 不能跨分区
- inode 号、块号都是以分区为单位划分的,不能跨分区;
- 但 Linux 可以有多个分区/文件系统,那么路径解析时怎么知道自己在哪一个分区?
9.2 挂载:把文件系统和一个目录关联
分区写入文件系统后无法直接使用,必须挂载到某个目录(挂载点)才能访问。
实验流程:
cppdd if=/dev/zero of=./disk.img bs=1M count=5 # 造一个 5MB 磁盘文件 mkfs.ext4 disk.img # 建立 ext4 文件系统 mkdir /mnt/mydisk # 建立挂载点 sudo mount -t ext4 ./disk.img /mnt/mydisk # 挂载 df -h # 看到 /dev/loop0 挂到 /mnt/mydisk sudo umount /mnt/mydisk # 卸载9.3 loop 回环设备
- 挂载普通文件时,
df -h显示设备是/dev/loop0;- loop(回环/loopback)设备是一种伪设备,让一个普通文件被当作块设备使用,从而把文件(如 ISO 镜像、磁盘镜像)挂载成文件系统。
9.4 关键结论:靠"路径前缀"判断分区
挂载后,每个文件系统都挂在目录树的某个目录下,因此:
可以根据访问目标文件的"路径前缀",准确判断它属于哪个分区/文件系统;
路径解析走到挂载点时,就切换到对应文件系统继续解析。
十、权限检查与 sticky bit
10.1 路径上的权限
路径上的每一级目录都必须有执行(搜索)权限 x,最后再检查目标文件的读/写权限:
权限 对目录的含义 r 能列出目录里的文件名(ls) w 能在目录里增删改名目录项(创建/删除文件) x 能进入/穿越目录,访问其中的文件
- 只有 r 没有 x:能看到文件名,却无法访问文件;
- 只有 x 没有 r:能访问知道名字的文件,但不能列出全部;
- 删除文件需要父目录的 w 权限(配合 x),而不是文件本身的写权限------因为删除本质是修改父目录的目录项。
10.2 sticky bit
- sticky bit(t):在
/tmp这类公共可写目录中,只有文件属主、目录属主、root 能删除/改名别人的文件;- 防止公共目录里互相删除文件。
十一、软硬链接
11.1 硬链接:多个名字对应同一个 inode
cpptouch abc ln abc def # 建立硬链接 def,指向 abc 的 inode ls -li abc def # 两者 inode 相同,硬链接计数为 2
- abc 和 def 地位完全相同,都是指向同一 inode 的目录项;
- 内核在 inode 中记录硬链接计数,此时为 2。
11.2 软链接:通过名字引用,是独立文件
cppln -s abc abc.s # 创建软链接 abc.s,指向 abc ls -li # abc 普通文件,inode 263563,链接数 2 # abc.s -> abc 软链接,独立 inode 261678,链接数 1
- 软链接是独立文件,有自己的 inode,数据块里存的是目标文件的路径字符串;
- 路径解析遇到软链接时,读出目标路径再递归解析。
11.3 软硬链接对比
对比项 硬链接 软链接 本质 文件名与 inode 的映射(目录项) 独立文件,存目标路径 inode 与目标相同 与目标不同 跨文件系统/分区 不能(inode 不跨分区) 能 链接目录 不能(普通用户) 能 目标删除后 仍可访问(链接数还在) 失效(悬空链接) 典型例子 .、..快捷方式 11.4 删除文件的两步操作
删除一个文件时内核做两件事:
- 在父目录中删除该目录项;
- 将 inode 的硬链接计数 −1;只有计数减到 0、且没有进程打开它时,才真正释放 inode 和数据块。
11.5 文件的三个时间
时间 含义 Access(atime) 文件内容最后被访问的时间 Modify(mtime) 文件内容最后被修改的时间 Change(ctime) 文件属性(inode)最后被改变的时间
十二、TOCTOU 与安全机制
12.1 TOCTOU 问题
经典的 TOCTOU(Time Of Check To Time Of Use):
先
access/stat检查文件,再open,两步之间文件可能被替换(符号链接攻击),造成越权访问。12.2 解决方案
*at系列(openat、fstatat、mkdirat、unlinkat、renameat):相对路径基于dirfd,dirfd可传AT_FDCWD;- 打开后用
fstat检查 fd,而不是再检查路径;- O_NOFOLLOW:不跟随末尾符号链接;
- O_PATH:只获得一个路径级 fd(不真正打开),专门用作
*at的 dirfd;- O_CREAT|O_EXCL:原子创建;
- O_TMPFILE:创建无目录项的临时文件,从根本上避免路径竞态。
十三、getcwd 与 realpath
- getcwd(buf, size):内核从 pwd 的 dentry 沿
d_parent上溯,用prepend_path逐段前插,直到根,再反向拼出完整路径;- realpath(path, buf):库函数,解析路径中所有符号链接、
.、..,返回规范化绝对路径;- canonicalize_file_name(path):等价于自动 malloc 结果的 realpath;
- 底层可借助
readlink("/proc/self/fd/...")。
十四、常见错误码汇总
错误码 含义 ENOENT 路径某分量不存在 ENAMETOOLONG 路径超 4096 或文件名超 255 EACCES 某级目录无 x 权限或目标无 r/w 权限 ELOOP 符号链接嵌套过多 ENOTDIR 路径中间分量不是目录 EEXIST O_CREAT ENOTEMPTY 删除非空目录
十五、面试题精讲(44 题)
概念与原理
1. 文件名存在哪里?为什么说文件名不属于文件?
存在父目录的目录项里,inode 不存名字。
文件名只是 "目录项 → inode 号" 的映射,所以重命名只改目录项、硬链接可以让一个文件有多个名字。
2. 磁盘上有"目录"这种东西吗?
没有,磁盘上只有"文件属性 + 文件内容"。
目录也是文件,只是它的数据块存的是"文件名 → inode 号"。
3. 目录是什么?里面存的是什么?
目录是特殊文件,有 inode 和数据块,数据块里存目录项(文件名 → inode),还含
.和..。
4. VFS 四大对象及关系?
super_block(已挂载文件系统)、inode(文件)、dentry(路径分量)、file(打开实例)。
dentry 指向 inode,file 持有 path(dentry+mnt),多个 dentry/file 可对应同一 inode。
5. dentry 和磁盘目录项的区别?
dentry 是内存对象、非持久、组成 dcache,含父子关系和挂载点信息;
磁盘目录项是持久的、按文件系统格式存储。
6. 为什么需要 dcache?
路径解析每分量都要查目录,dcache 用哈希表缓存已解析的 dentry,命中即可不读磁盘,大幅加速路径解析。
7. 什么是负 dentry?有什么用?
d_inode 为 NULL 的 dentry,表示该名字查过但不存在。
缓存它可以快速返回 ENOENT,避免反复读磁盘;
创建文件时转正。
8. 一个 inode 可以对应几个 dentry?为什么?
多个。硬链接会产生多个目录项,再加上不同路径、挂载点访问,都可能产生多个 dentry 指向同一 inode。
9. 硬链接和符号链接在路径解析中的区别?
硬链接就是普通目录项,直接指向同一 inode,无额外解析;
符号链接的内容是目标路径,需要
follow_link递归解析并可能返回 ELOOP。
10. PATH_MAX 和 NAME_MAX 是多少?
PATH_MAX 4096(整条路径),NAME_MAX 255(单文件名),
超限返回 ENAMETOOLONG。
11. 为什么单文件名限制是 255?
历史上 ext 文件系统目录项用 1 字节 name_len(最大 255),成为通用上限。
12. 目录项在磁盘上的格式?
inode(4B)、rec_len(2B)、name_len(1B)、file_type(1B)、name(变长),ls 底层用 getdents64 读取。
路径解析内核机制
13. 描述路径解析的完整流程。
getname 拷贝路径 → path_init 确定起点(root/pwd)→ link_path_walk 逐分量(查 dcache、miss 读磁盘、符号链接递归、穿越挂载点、每级查 x 权限)→ do_last 处理最后分量 → complete_walk 完成。
14. 为什么路径解析能一路递归到根?根目录为什么特殊?
打开任何文件都要先打开父目录,父目录又要打开它的父目录,递归到根目录为止;
根目录的文件名和 inode 号(ext 为 2)固定,开机即知,无需查找。
15. 为什么路径解析前要把路径拷进内核?
防止解析过程中用户态修改路径,保证一致性并避免安全漏洞(TOCTOU)。
16. RCU walk 是什么?为什么快?
在 RCU 保护下无锁遍历 dcache,配合 seqlock 检测 dentry 是否被修改,无需拿引用计数和锁,所以最快;
遇到阻塞或校验失败回退到 ref walk。
17. 路径解析中每一级需要什么权限?
路径上每一级目录都需要 x(搜索)权限,最后目标文件再检查 r/w 权限。
18. 为什么目录需要 x 权限?
x 表示可以"进入/穿越"目录、按名字访问其中的文件。
没有 x,即使有 r 也只能看到文件名而无法访问。
19. 目录 r、w、x 分别表示什么?
r 列出文件名;
w 增删改名目录项;
x 进入/穿越并访问文件。
20. 符号链接如何处理?权限检查谁?
读取目标路径递归解析,嵌套/总数超限返回 ELOOP;
权限检查针对目标文件,不检查符号链接本身。
21. ELOOP 在什么情况下出现?
符号链接自指、互相指或嵌套过深,超过内核限制(约 8 层/40 次)。
22. 路径解析遇到挂载点怎么处理?
follow_mount 检测 dentry 上的挂载,把当前 path 切换到挂载文件系统的根,再继续解析。
23. nameidata 是什么?
路径解析的状态结构,保存当前 path、起点 root、last_type、flags、链接计数(link_count/total_link_count)等。
24. dcache 如何组织与回收?dentry 引用计数有何作用?
哈希表(父 dentry+名字)用于查找,LRU 链表挂载引用计数为 0 的 dentry,内存紧张时回收;
count>0 不会被回收,降为 0 进入 LRU。
挂载与文件系统
25. inode 能跨分区吗?怎么确定在哪个分区?
不能,inode 以分区为单位编号;
文件系统挂载到目录树后,根据路径前缀判断分区,走到挂载点就切换文件系统。
26. 挂载的本质是什么?
分区写入文件系统后无法直接使用,必须挂载(mount)把文件系统和某个目录关联,之后通过该目录访问。
27. loop 设备是什么?
伪设备,让普通文件被当作块设备,从而把磁盘镜像/ISO 文件挂载成文件系统(如 /dev/loop0)。
28. 为什么硬链接不能跨文件系统、不能链接目录?
硬链接靠 inode 关联,而 inode 只在本文件系统内有效;
目录被限制硬链接是为了防止破坏目录树层级(
./..一致性)。
29. 路径由谁提供?最初路径从哪来?
进程有 CWD,相对路径由进程提供;
系统提供根目录和缺省目录,用户新建文件天然处于路径中,由系统+用户共同构建。
系统调用与场景
30. getcwd 如何实现?
从 pwd dentry 沿 d_parent 上溯到根,逐段前插(prepend_path)拼出路径。
31. realpath 做了什么?
解析所有符号链接、
.、..,返回规范化绝对路径。
32. openat 解决什么问题?
相对路径基于指定 dirfd 而非全局 cwd,避免多线程 cwd 竞态和 TOCTOU;
dirfd 为 AT_FDCWD 时等价于 open。
33. 什么是 TOCTOU?如何彻底解决?
检查与使用之间文件被替换。
用
*at系列、打开后 fstat、O_NOFOLLOW、O_PATH、O_TMPFILE 等手段。
34. O_PATH 是什么?用途?
只获得路径级 fd,不真正打开文件,不能读写,但可作为
*at的 dirfd 或配合 fstat/dup 使用,适合安全的路径操作。
35. O_NOFOLLOW 的作用?
让 open 不跟随末尾符号链接,若末尾是符号链接则失败,防止符号链接攻击。
36. chroot 为什么不是安全边界?
chroot 只改 root,若 cwd 仍在外部、持有已打开 fd 或进程有特权,仍可能逃逸;
容器应使用 mount namespace + pivot_root。
37. 删除一个正在被打开使用的文件会发生什么?
unlink 只删目录项并使 inode 链接数减 1;
只要有 file 打开,inode 和数据块不释放,fd 仍可读写;
全部关闭后才真正释放空间。
这也是日志文件被删后磁盘空间不立即释放的原因,可通过
/proc/pid/fd恢复。
38. 文件的三个时间分别是什么?
Access(atime)最后访问、Modify(mtime)内容最后修改、Change(ctime)属性最后改变。
进阶场景
39. 为什么 open 之后,原路径文件被替换/删除不影响已打开的 fd?
d 直接绑定 file → inode,路径解析只在 open 时发生一次;
之后文件按 inode 访问,与路径无关。
40. 为什么 rename 是原子的?如何利用它做原子更新?
rename 只改目录项,由文件系统保证原子性和崩溃一致性**。**
写配置时先写临时文件再 rename 覆盖目标,读取方要么看到旧内容要么看到新内容,不会读到半截。
41. 为什么 rm -rf 大目录很慢?
每个文件都要单独 unlink、每个目录 rmdir,每次都要路径解析、改目录项、改位图、更新元数据,是大量元数据操作而非顺序 I/O。
42. 如何安全地创建临时文件?
用 mkstemp(内部 O_CREAT|O_EXCL 原子创建)或 O_TMPFILE(无目录项);
不要用 tmpnam/mktemp 等有竞态的函数。
43. 为什么删除文件需要父目录的写权限,而不是文件本身?
删除动作本质是修改父目录的目录项,所以需要父目录的 w 权限(配合 x);
文件本身的权限不决定能否被删除(sticky bit 另有限制)。
44. sticky bit 解决什么问题?
在
/tmp等公共可写目录中,防止用户删除/改名别人的文件,只有文件属主、目录属主、root 可操作。
十六、总结
把整条链路串起来:
- 路径字符串先经
getname拷入路径缓冲;- 从 root(绝对)或 pwd(相对) 开始;
- 逐分量在 dcache 中查找(命中走 RCU,未命中读磁盘),处理符号链接、挂载点和每级 x 权限;
- 走到挂载点时,根据路径前缀切换到对应分区/文件系统;
- 找到 dentry → inode,open 后由 fd → file 持有,此后不再依赖路径;
- 软硬链接、删除计数、三个时间刻画了文件的"名字---inode"关系;
- 安全问题(TOCTOU、临时文件、公共目录)由
*at、O_PATH、O_NOFOLLOW、O_TMPFILE、sticky bit 等机制解决。核心一句话:
路径解析是"字符串 → dentry → inode"的查找过程,
而文件描述符是"解析完成后与路径无关的稳定句柄"。











