硬链接、软链接、unlink 与 rename
前面把目录项、inode、dentry、fd、page cache 这条主线先理了一遍之后,我发现文件系统里还有一组概念特别容易混在一起:
- 硬链接
- 软链接
- unlink
- rename
它们看起来都是在“改文件”,但实际上很多时候改的并不是文件内容,而是名字和目录项这一层的关系。把这部分看清之后,很多现象就会顺起来,比如:
- 为什么一个文件可以有多个名字
- 为什么删掉一个名字,文件不一定立刻消失
- 为什么改名通常很快
- 为什么软链接断了,但它自己还在
所以这一篇我想专门把这一层单独拎出来。
一、先把这一层放到整体框架里
如果只看这一组概念,我现在更愿意把它们放在“文件语义层”里理解。
这里最核心的一句话是:
文件名在目录项里,文件对象在 inode 里,所以很多操作改的是目录项,而不是文件内容本身。
也正因为目录项和 inode 分开了,文件系统才可以支持:
- 一个文件对象对应多个名字
- 删除一个名字但文件对象仍然存在
- 改名字时不一定需要搬动文件数据
所以这一层真正关心的是:
名字和文件对象之间,到底是什么关系。
二、硬链接是什么
硬链接最核心的理解就是:
多个目录项,指向同一个 inode。
例如:
a.txt -> inode 101
b.txt -> inode 101这里说明:
a.txt和b.txt是两个不同的名字- 但它们指向的是同一个 inode
- 也就是说,它们看到的是同一个文件对象
所以硬链接不是“复制一份文件”,而是:
给同一个 inode 再起一个名字。
1. 这意味着什么
这意味着:
- 修改内容时,两个名字看到的都是同一份数据
- 删除其中一个名字时,文件不一定消失
- inode 的链接计数会增加
也就是说,硬链接真正强调的是:
文件名和文件对象本来就不是一回事。
2. 硬链接为什么通常不能跨文件系统
因为 inode 号通常只在各自文件系统内部唯一。
所以目录项里记录的,也通常只是“本文件系统里的 inode 编号”。这样一来,硬链接天然更适合发生在同一个文件系统内部,而不是跨文件系统去直接引用另一个 inode。
3. 为什么目录通常不允许额外建立硬链接
普通文件建立硬链接问题不大,因为多个名字指向同一个 inode,不会破坏整体层级。
但目录如果允许随便建立新的硬链接,就可能让目录结构从树变成有环图,导致:
- 递归遍历变复杂
- 父子层级关系混乱
- 删除和回收更危险
所以通常不会允许用户随便给目录再额外创建硬链接。
不过目录里天然会有 . 和 .. 这两条特殊记录:
.指向当前目录自己..指向父目录
这两条可以理解成文件系统维护的特殊目录项,而不是普通用户额外创建出来的硬链接。
三、软链接是什么
软链接和硬链接的思路完全不同。
它更准确的理解是:
软链接本身是一个独立文件,它保存的不是目标文件的 inode,而是目标路径字符串。
例如:
ln -s /var/log/app.log current.log这里的 current.log 不是和 app.log 共用 inode,它自己就是一个单独的文件对象,只不过它的内容不是普通正文,而是一条路径,例如:
/var/log/app.log1. 访问软链接时发生了什么
当系统访问软链接时,大致过程是:
- 先找到软链接自己
- 发现它的类型是 symbolic link
- 读取它保存的目标路径字符串
- 再根据这个路径重新做一次路径查找
所以软链接更像是:
通过一个路径,再跳到另一个路径。
2. 软链接为什么可以跨文件系统
因为它存的是路径字符串,而不是目标 inode 本身。
路径本来就可以指向:
- 另一个目录
- 另一个分区
- 另一个挂载点
所以软链接天然就可以跨文件系统。
3. 软链接为什么会悬空
因为它只是保存了一条路径。如果目标文件后来被删了,软链接自己仍然还在,但它指向的路径已经找不到真正对象了。
这时它就变成了所谓的悬空链接。
四、硬链接和软链接最本质的区别
把两者放到一起看,最关键的差别其实就一句话:
- 硬链接连的是 inode
- 软链接连的是 路径
也就是说:
- 硬链接发生在目录项和 inode 这一层
- 软链接发生在路径解析这一层
所以虽然都叫“链接”,但它们其实不在一个层面上。
五、unlink 到底删掉了什么
unlink 常常会让人误以为它是“直接删除文件”。但如果从文件系统结构去看,更准确的说法是:
unlink 删除的是某个目录项,而不是立刻删除文件内容。
例如原来有:
a.txt -> inode 101执行 unlink("a.txt") 之后,本质上是把这条映射关系从父目录里删掉。
1. 为什么删了名字,文件不一定马上没了
因为 inode 和数据块是否真正回收,还要看两个条件:
- inode 的链接计数是否已经变成 0
- 是否还有进程通过 fd 持有这个文件的打开引用
所以删掉一个目录项之后,可能会发生:
- 路径已经找不到这个文件
- 但 inode 还在
- 文件数据也还在
- 进程甚至还能继续写这个文件
2. 为什么删除后进程还能继续写
因为进程真正访问文件走的是:
fd -> file -> inode -> page cache / 磁盘而不是再重新通过名字去查路径。
所以即使目录项没了,只要进程还持有打开引用,这条访问链就还在。
六、rename 改的又是什么
rename 看起来像“移动文件”或“改文件名”,但在很多情况下,它主要改的还是目录项这一层。
更准确地说:
rename 通常改的是名字,或者目录项所在的位置,而不是文件内容本身。
例如:
/dir1/a.txt改成:
/dir2/b.txt如果仍然在同一个文件系统里,那么很多时候本质上只是:
- 从旧目录中去掉旧目录项
- 在新目录中加入新目录项
- 指向的还是原来的 inode
所以 rename 这一层真正说明的是:
改名字通常不等于改文件对象,更不等于搬运文件数据。
这也是为什么在同一文件系统里,改名操作通常会非常快。
七、这些操作和 dentry、inode、目录页的关系
从运行时角度看,这些操作并不是“只改一个 dentry 缓存”。更准确地说,它们会同时影响几层东西:
1. 目录页
因为目录项真正存放在目录文件的数据内容里,所以:
linkunlinkrename
这些操作都会修改目录文件对应的目录页。目录页通常先体现在 page cache 里,之后再由文件系统回写磁盘。
2. inode 元数据
例如:
- 硬链接会增加链接计数
unlink会减少链接计数rename往往会更新某些时间戳- 软链接会创建一个新的 symlink inode
这些变化通常先体现在内存中的 inode 对象里,然后再回写到磁盘 inode。
3. dentry 缓存
dentry 是名字查找缓存,所以这些操作也会让相关 dentry 失效、更新或新建。
不过这里一定要分清:
dentry 只是内存中的缓存对象,不是磁盘上的目录项本体。
所以真正被修改的持久化数据,依然是目录文件中的目录项和相关 inode 元数据;dentry 只是运行时缓存,会跟着同步调整。
八、小结
把这一层重新收一下,我现在更愿意这样理解:
- 硬链接是在目录项层给同一个 inode 多起一个名字
- 软链接是创建一个新的特殊文件,里面存的是目标路径
- unlink 删除的是目录项,不一定立刻删除 inode 和数据
- rename 通常改的是目录项名字或位置,因此在同一文件系统里往往很快
- 这些操作真正改动的核心是目录项和 inode 元数据,dentry 只是内存中的路径查找缓存,也会跟着更新
到这里,文件系统里“名字”和“对象”的关系就会清楚很多。后面再去看为什么删除文件后进程还能继续写、为什么硬链接不能跨文件系统、为什么 rename 通常是原子的,会顺很多。
