Linux 基础体系 · 第 5/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。
Linux 文件、Inode 与链接:描述符、硬链接、符号链接和删除语义
在 Linux 中,“文件”不是一个单一对象。一个路径名、目录项、inode、打开文件描述和文件描述符,分别属于不同层次:
进程
└─ 文件描述符 fd
└─ 打开文件描述(open file description)
├─ 当前文件偏移
├─ 文件状态标志
└─ inode
├─ 文件类型与权限
├─ 所有者、大小、时间
└─ 数据块或目录内容
目录
└─ 目录项(directory entry)
├─ 名字
└─ inode 号
理解这些对象之间的关系,才能解释以下看似反直觉的现象:
- 两个不同路径可以指向同一个文件;
- 删除文件名后,已经打开的文件仍然可以读写;
cp、硬链接和符号链接的行为完全不同;ls -l看到的是路径解析后的结果,而不是所有内核对象;- 文件名消失并不必然意味着数据立即释放;
rename()可以替换一个已存在的路径,但已经打开该路径的进程仍可能继续访问旧内容。
本文以本地 Linux 文件系统为主要范围。具体文件系统对磁盘布局、日志和崩溃恢复的实现不同,但下面关于路径、链接、文件描述符和删除的基本语义,是 Linux VFS 与常见本地文件系统共同提供的抽象。
一、路径名不是文件本身
1. 路径解析的基本过程
/var/log/app.log 首先是一个字符串。内核执行 open("/var/log/app.log", ...) 时,并不是直接从字符串中取出“文件对象”,而是从根目录开始逐级查找:
- 找到根目录
/; - 在根目录中查找名字
var; - 得到
var对应的目录 inode; - 在该目录中查找名字
log; - 得到
log对应的目录 inode; - 在
log中查找名字app.log; - 最终得到目标 inode;
- 按照打开方式检查权限并建立打开状态;
- 将一个整数文件描述符返回给进程。
目录本身也是一种特殊文件。它的数据内容逻辑上是一组“名字到 inode”的映射:
例如:
目录 /tmp 的内容:
"a.txt" ───────> inode 12001
"b.txt" ───────> inode 12001
"link" ───────> inode 12050(符号链接自身)
这里 a.txt 和 b.txt 是两个目录项,但它们可以指向同一个 inode。link 则通常指向一个专门表示符号链接的 inode。
目录项通常被称为 dentry。dentry 是 VFS 用于表示路径组件的内核对象,并且会被缓存;它不是用户可以直接读取的独立“文件内容”。磁盘上的目录格式由 ext4、XFS 等文件系统分别实现,VFS 在其上提供统一的路径解析语义。
2. 相对路径和符号链接中的相对目标
相对路径默认相对于进程当前工作目录:
cat ./config/app.conf
但符号链接中的相对目标不是相对于执行命令时的当前目录,而是相对于符号链接所在的目录。
mkdir -p /tmp/link-demo/real /tmp/link-demo/dir
printf 'ok\n' > /tmp/link-demo/real/app.conf
ln -s ../real/app.conf /tmp/link-demo/dir/app.conf
cat /tmp/link-demo/dir/app.conf
# ok
这里符号链接保存的目标字符串是 ../real/app.conf。解析它时,内核以 /tmp/link-demo/dir 作为起点,因此得到 /tmp/link-demo/real/app.conf。
如果错误地把它理解为相对于当前工作目录,就会在部署目录、归档解包和软件安装中生成大量失效链接。
二、inode:文件元数据和数据定位的核心对象
1. inode 保存什么
在 Linux 文件系统中,inode 通常保存一个文件对象的元数据,例如:
- 文件类型:普通文件、目录、符号链接、设备文件、套接字等;
- 权限位;
- 用户 ID 和组 ID;
- 文件大小;
- inode 的时间戳;
- 硬链接计数;
- 文件数据所在位置或间接索引信息;
- 扩展属性、ACL 或其他文件系统特有信息。
inode 通常不保存文件名。文件名属于目录项。正因为文件名和 inode 分离,多个文件名才能指向同一个 inode。
文件系统还可能拥有 inode 号,但 inode 号的有效范围通常局限于该文件系统。两个不同挂载文件系统中的 inode 号相同,并不表示它们是同一个文件对象。
可以用 stat 观察路径对应的 inode:
mkdir -p /tmp/inode-demo
printf 'hello\n' > /tmp/inode-demo/a
ln /tmp/inode-demo/a /tmp/inode-demo/b
ln -s a /tmp/inode-demo/c
stat -c 'name=%n inode=%i type=%F links=%h size=%s' \
/tmp/inode-demo/a \
/tmp/inode-demo/b \
/tmp/inode-demo/c
典型输出类似:
name=/tmp/inode-demo/a inode=123456 type=regular file links=2 size=6
name=/tmp/inode-demo/b inode=123456 type=regular file links=2 size=6
name=/tmp/inode-demo/c inode=123457 type=symbolic link links=1 size=1
其中:
a和b的 inode 号相同;- 普通文件 inode 的硬链接计数为 2;
c的 inode 号不同,它是符号链接自身;- 符号链接的大小通常是目标字符串长度,这里目标
a长度为 1。
输出中的 stat 默认会跟随符号链接;若要查看符号链接自身,应使用:
stat -c 'name=%n inode=%i type=%F links=%h size=%s' -L /tmp/inode-demo/c
不同 stat 实现和选项组合容易造成混淆。GNU/Linux 上更直接的区分方式是:
stat /tmp/inode-demo/c # 默认跟随链接
stat -L /tmp/inode-demo/c # -L 也表示跟随链接
若希望明确查看链接自身,可使用 lstat 系统调用,或使用 ls -l 查看其 -> 目标;在脚本中应确认所用工具的选项语义,而不要仅凭命令名称猜测。
2. inode 不等于磁盘上的全部内容
inode 主要描述对象和数据位置。目录内容由目录 inode 引用的数据块保存;普通文件的实际内容也由 inode 所描述的数据块保存;符号链接则保存一个目标路径字符串。
短符号链接在一些文件系统中可能直接存放在 inode 内部,而不占用单独数据块。这是实现优化,不是应用程序可以依赖的通用接口。
三、文件描述符、打开文件描述和 inode
1. 文件描述符是进程内的整数句柄
文件描述符(file descriptor,简称 fd)是一个进程文件描述符表中的整数。例如:
0 标准输入
1 标准输出
2 标准错误
3 程序后来打开的某个文件
open() 返回的是 fd,而不是 inode 号,也不是路径:
int fd = open("/tmp/data", O_RDONLY);
同一个进程中的 fd 只是访问内核打开状态的入口。可以通过 /proc 观察:
printf 'data\n' > /tmp/fd-demo
exec 3</tmp/fd-demo
readlink /proc/$$/fd/3
# /tmp/fd-demo
ls -l /proc/$$/fd/3
# ... 3 -> /tmp/fd-demo
/proc/$$/fd/3 是一个 procfs 中的特殊符号链接,展示该 fd 当前引用的打开对象。它不是普通文件系统中用户创建的硬链接,也不能据此推断路径仍然存在。
2. 打开文件描述保存“打开状态”
Linux 内核中的 打开文件描述(open file description)通常保存:
- 所引用的文件对象;
- 当前文件偏移;
- 文件状态标志,如
O_APPEND、O_NONBLOCK; - 与该次打开相关的其他状态。
文件描述符表中的一个 fd 指向一个打开文件描述:
进程 A 的 fd 3 ─┐
├─> 打开文件描述 X ─> inode I
进程 A 的 fd 4 ─┘
执行:
int fd2 = dup(fd);
得到的两个 fd 指向同一个打开文件描述,因此共享当前偏移:
char a[4], b[4];
read(fd, a, 3); // 假设读到 "abc",偏移从 0 变为 3
read(fd2, b, 3); // 从偏移 3 继续读取,而不是重新从 0 开始
fork() 后,父子进程继承的 fd 也通常指向相同的打开文件描述,因此它们会共享偏移和部分打开状态。
但是,以下两次独立的 open() 通常建立两个不同的打开文件描述:
int a = open("data", O_RDONLY);
int b = open("data", O_RDONLY);
它们虽然可以引用同一个 inode,却拥有各自独立的文件偏移。于是:
fd a ─> open description A ─> inode I
fd b ─> open description B ─> inode I
这一区分可以解释并发读写时的差异:共享同一打开文件描述的线程可能共享偏移;分别调用 open() 的线程则通常不共享偏移。使用 pread() 和 pwrite() 可以按显式偏移读写,避免依赖共享的当前偏移。
3. fd 的生命周期
一个典型的打开流程如下:
open()
│
├─ 路径解析
├─ 权限检查
├─ 创建打开文件描述
└─ 返回进程内 fd
dup()/fork()
└─ 增加对已有打开状态的引用
close()
└─ 删除一个 fd;若不再有引用,打开状态可释放
close(fd) 只关闭当前进程的这个 fd。它不等于删除路径,也不一定立即释放 inode。
程序必须检查 open()、read()、write() 和 close() 的返回值。特别是在网络文件系统或某些存储错误场景下,写入错误可能延迟到 write()、fsync() 或 close() 阶段才暴露;close() 返回错误时不能简单忽略。
四、硬链接:多个目录项指向同一个 inode
1. 硬链接的定义
硬链接是一个新的目录项,它直接指向已有 inode:
printf 'version 1\n' > /tmp/inode-demo/original
ln /tmp/inode-demo/original /tmp/inode-demo/alias
ls -li /tmp/inode-demo/original /tmp/inode-demo/alias
典型结果:
123500 -rw-r--r-- 2 user user 10 ... alias
123500 -rw-r--r-- 2 user user 10 ... original
-i 显示 inode 号。两个路径的 inode 号相同,所以它们是同一个文件对象的两个名字。
通过任一名字写入,另一名字都能看到修改:
printf 'changed\n' > /tmp/inode-demo/alias
cat /tmp/inode-demo/original
# changed
这不是文件内容复制。若使用:
cp /tmp/inode-demo/original /tmp/inode-demo/copy
copy 默认拥有新的 inode;以后修改 copy 不会修改 original。
2. 硬链接的形式化条件
对已有非目录 inode ,创建硬链接相当于在目录 中增加:
并使 inode 的硬链接计数增加:
其中 N 是新名字。硬链接操作不复制数据块,也不创建新的普通文件 inode。
普通用户通常不能对目录创建硬链接,原因是任意创建目录硬链接会破坏目录树结构,可能造成目录环和 .. 关系不一致。mkdir 创建的目录还会自动产生:
父目录中的名字
目录内的 "."
父目录中的子目录关系对应的 ".."
目录硬链接的管理由内核和文件系统共同约束。应用程序不应把目录硬链接当作普通文件链接使用。
3. 跨文件系统限制
硬链接目标和新名字必须位于同一个文件系统中。否则:
ln /home/user/file /mnt/otherfs/file
# ln: ... Invalid cross-device link
原因是硬链接只保存 inode 号,而 inode 号只在对应文件系统内有意义。跨文件系统无法让一个目录项直接引用另一个文件系统的 inode。
符号链接没有这个限制,因为它保存的是路径字符串;但目标路径在目标环境中必须可解析。
现代 Linux 还可能因为权限和安全策略拒绝硬链接,例如受 fs.protected_hardlinks 等内核策略影响。即使用户对目录有写权限,也不应假定一定能成功创建任意硬链接。
五、符号链接:保存目标路径的独立文件对象
1. 符号链接的结构
创建符号链接:
ln -s /etc/hosts /tmp/inode-demo/hosts-link
ls -l /tmp/inode-demo/hosts-link
# ... /tmp/inode-demo/hosts-link -> /etc/hosts
符号链接自身有一个 inode,其内容是目标路径 /etc/hosts。当路径解析遇到符号链接时,内核把目标字符串重新加入解析过程:
/tmp/inode-demo/hosts-link
│
└─> /etc/hosts
因此:
- 符号链接自身可以被删除;
- 符号链接目标可以不存在;
- 符号链接可以跨文件系统;
- 符号链接可以指向目录;
- 符号链接可以形成循环。
例如:
ln -s does-not-exist /tmp/inode-demo/broken
ls -l /tmp/inode-demo/broken
# ... -> does-not-exist
cat /tmp/inode-demo/broken
# cat: ... No such file or directory
“链接文件存在”与“链接目标存在”是两个不同问题。
2. stat、lstat 与 fstat
同一个路径涉及两种观察对象:
stat(path):通常跟随符号链接,查看目标;lstat(path):查看符号链接自身;fstat(fd):通过已打开的 fd 查看 fd 当前引用的对象。
在 C 中:
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
struct stat st;
if (lstat("link", &st) == -1) {
perror("lstat");
return 1;
}
if (S_ISLNK(st.st_mode)) {
puts("link is a symbolic link");
}
return 0;
}
程序处理不可信路径时,不能只依赖字符串拼接。符号链接可能把访问引向预期目录之外。Linux 提供 openat()、目录 fd,以及较新的 openat2() 约束路径解析行为;具体使用时应根据内核和 libc 版本确认可用的标志与语义,并检查所有错误返回。
六、删除:删除名字,不一定立即删除数据
1. unlink() 的真实含义
对普通文件执行:
rm /tmp/inode-demo/original
通常对应 unlink() 系统调用。它的主要语义是从父目录中删除名为 original 的目录项,并减少目标 inode 的硬链接计数:
如果该 inode 仍有其他目录项:
alias ─> inode I
original ─> inode I
删除 original 后:
alias ─> inode I
数据仍然可以通过 alias 访问。
如果删除后硬链接计数变为 0,是否可以真正回收数据,还要看是否存在内核引用,例如打开文件描述、内存映射等。可以近似表示为:
这里的 r_open 表示仍有打开状态引用,r_map 表示仍有映射等引用。实际内核对象生命周期还涉及 inode、页缓存和文件系统内部引用计数,公式是帮助理解语义的抽象,而不是文件系统内部唯一计数器。
2. 现场验证:删除后仍能读写
下面的 shell 示例使用一个额外 fd 保持文件打开:
set -eu
f=$(mktemp /tmp/unlink-demo.XXXXXX)
printf 'before\n' > "$f"
exec 3<>"$f"
rm "$f"
printf 'path exists after rm: '
if [ -e "$f" ]; then
echo yes
else
echo no
fi
printf 'read through fd: '
IFS= read -r line <&3
printf '%s\n' "$line"
readlink "/proc/$$/fd/3"
典型结果:
path exists after rm: no
read through fd: before
/tmp/unlink-demo.xxxxx (deleted)
路径已经不存在,但 fd 仍然引用原来的打开对象。(deleted) 是 /proc 对该状态的展示,不是一个可直接访问的普通路径后缀。
如果打开方式允许写入,即使路径被删除,也可以继续写入:
f=$(mktemp /tmp/unlink-write.XXXXXX)
exec 3<>"$f"
rm "$f"
printf 'still reachable\n' >&3
当最后一个引用关闭后,若没有其他目录项,文件数据才会成为文件系统回收对象。磁盘空间因此可能被“已删除但仍打开”的进程占用。诊断这类问题常用:
lsof +L1
它通常列出硬链接计数小于 1 但仍被进程打开的文件。命令输出依赖 procfs 和 lsof 权限;容器、不同 mount namespace 或权限限制可能导致观察不完整。
3. 删除符号链接删除的是链接自身
执行:
ln -s /etc/hosts /tmp/inode-demo/hosts-link
rm /tmp/inode-demo/hosts-link
删除的是 /tmp/inode-demo/hosts-link 这个符号链接 inode,不是 /etc/hosts。
rm 通常不会跟随命令行中最后一个符号链接去删除其目标;但递归删除、脚本拼接路径、通配符展开和特殊工具的选项可能改变风险。对不可信目录树执行递归删除时尤其不能把“符号链接不会被跟随”扩展成“整个脚本绝对不会越界”:脚本后续若对链接目标执行 cd、cp、find -exec 或自行拼接路径,仍可能出现路径穿越。
七、打开、删除和重命名之间的状态关系
可以把关键对象和操作表示为以下关系:
flowchart LR
P[进程 fd] --> O[打开文件描述]
O --> I[inode]
D[目录项: 名字] --> I
U[unlink 名字] --> D
R[rename 名字] --> D
C[close fd] --> O
D -.硬链接计数为零.-> X{仍有打开或映射引用?}
O -.最后一个引用关闭.-> X
X -->|否| F[文件数据可回收]
X -->|是| K[继续保留,路径可能已不存在]
关键路径如下:
open()通过路径找到 inode,并创建打开文件描述;unlink()只修改目录项和硬链接计数;- 已有 fd 不依赖目录项继续存在;
close()减少对打开状态的引用;- 目录项和打开引用都消失后,文件系统才可能回收空间。
rename() 的特殊性
rename(old, new) 主要修改目录项关系。对同一文件系统中的普通文件,替换已有 new 通常是一个原子的命名空间操作:
操作前:
old ─> inode A
new ─> inode B
rename(old, new) 后:
new ─> inode A
inode B 的目录项引用减少
已经打开 old 的进程仍引用 inode A;已经打开原 new 的进程仍可能引用 inode B。打开路径的进程不会因为路径后来被替换而自动切换到新 inode。
这就是常见安全更新模式的基础:
printf 'new content\n' > /tmp/app.conf.tmp
mv /tmp/app.conf.tmp /tmp/app.conf
在同一文件系统内,mv 通常使用 rename(),读者看到的路径要么指向旧文件,要么指向新文件,而不会看到“半个新文件”。但这只说明命名空间替换具有原子性,不自动保证:
- 新文件内容已经持久化到介质;
- 目录项更新已经持久化;
- 断电后一定保留更新;
- 跨文件系统移动仍是原子操作。
跨文件系统时,rename() 会失败并返回 EXDEV,用户空间工具通常退化为复制后删除。这一过程可能暴露部分文件、丢失元数据或留下源文件。
八、写入、缓存和持久化:可见不等于已落盘
Linux 普通文件写入通常经过页缓存。成功返回 write() 表示内核已经接受了数据,但不等于数据已经稳定写入持久介质。
若应用需要较强的崩溃持久性,通常需要考虑:
write(fd, data, len);
fsync(fd);
如果还创建、删除或重命名了目录项,往往还需要对父目录 fd 执行 fsync(),以持久化目录结构变化:
int dirfd = open("/tmp", O_RDONLY | O_DIRECTORY);
fsync(dirfd);
完整的持久化协议依赖更新方式、文件系统和存储设备。日志文件系统可以在崩溃后恢复元数据一致性,但“恢复后一定存在最新业务内容”仍取决于应用是否正确执行同步操作以及硬件是否遵守写入保证。
因此:
rename() 的原子可见性
≠
数据已持久化
这是文件替换程序中经常被忽略的边界。
九、匿名临时文件与“先打开、再删除”
一种常见临时文件模式是:
- 创建文件;
- 立即
unlink(); - 通过 fd 使用它;
- 关闭 fd 后自动释放。
int fd = open("/tmp/work", O_RDWR | O_CREAT | O_EXCL, 0600);
if (fd == -1) {
perror("open");
return 1;
}
if (unlink("/tmp/work") == -1) {
perror("unlink");
close(fd);
return 1;
}
/* 继续通过 fd 读写 */
这样其他进程无法通过路径重新打开该文件,进程退出或关闭 fd 后文件也会自然消失。
现代 Linux 和部分文件系统还支持 O_TMPFILE:
int fd = open("/tmp", O_RDWR | O_TMPFILE, 0600);
它创建没有目录项的临时 inode。该能力依赖内核、libc 和底层文件系统支持,失败时可能返回 EOPNOTSUPP 或其他错误。若需要将其最终公开为路径,可使用 linkat() 将打开的匿名文件链接到目录;这属于需要严格检查权限、目录和错误处理的底层接口,不应把 O_TMPFILE 当作所有挂载点都支持的通用能力。
十、常见误解与反例
误解一:文件名就是文件
反例:
printf x > /tmp/a
ln /tmp/a /tmp/b
rm /tmp/a
cat /tmp/b
# x
删除 a 只删除了一个目录项;inode 仍由 b 引用。
误解二:inode 号相同就代表可以跨目录或跨系统任意访问
inode 号只能在对应文件系统语境下解释。不同挂载点中即使显示相同数字,也可能是完全不同的 inode。判断两个路径是否是同一对象,应结合设备号和 inode 号,例如:
stat -c '%d:%i %n' /path/one /path/two
设备号和 inode 号组合通常用于识别同一文件系统中的对象,但网络文件系统、容器命名空间和特殊文件系统下仍应结合具体语义判断。
误解三:删除打开文件会立即释放空间
反例是前面的 rm 后继续通过 fd 读写。日志轮转中经常出现:
路径上的日志已删除
进程仍持有旧日志 fd
磁盘空间仍未释放
诊断时应查找已删除但仍打开的文件,而不是只看目录中的文件名。
误解四:符号链接和硬链接只是两种写法
它们的对象关系不同:
硬链接:
name-a ─────┐
└─> inode I
符号链接:
name-b ─> inode S ──保存字符串──> "name-a"
硬链接直接引用目标 inode;符号链接引用的是另一个 inode,而该 inode 的内容是路径字符串。因此目标被替换、移动或删除时,两者表现不同:
printf old > /tmp/target
ln /tmp/target /tmp/hard
ln -s /tmp/target /tmp/sym
printf new > /tmp/target
cat /tmp/hard # old
cat /tmp/sym # new
这里 printf new > /tmp/target 是通过截断原 inode 写入,因此硬链接仍看到 old 被替换为 new 才是预期?注意上例实际结果是 hard 也会看到 new,因为重定向 > 打开原路径并截断同一个 inode。为了展示差异,必须使用 rename 替换:
printf old > /tmp/target
ln /tmp/target /tmp/hard
ln -s /tmp/target /tmp/sym
printf new > /tmp/new-target
mv /tmp/new-target /tmp/target
cat /tmp/hard # old
cat /tmp/sym # new
mv 在同一文件系统内通常执行原子重命名,使路径 /tmp/target 改为指向新 inode;硬链接 hard 仍指向旧 inode,符号链接 sym 重新按路径解析,因此跟随到新 inode。这个例子也说明“修改文件内容”和“替换路径指向的 inode”不是一回事。
误解五:ls -l 显示的链接数是引用次数的全部含义
普通文件的链接数通常对应硬链接目录项数量,但目录的链接数还受 .、.. 和子目录关系影响;特殊文件系统可能有不同实现。链接数适合诊断常见情况,但不能作为跨所有文件系统推断内部生命周期的唯一证据。
十一、权限、竞态和生产风险
访问路径至少涉及两类权限:
- 对路径中每个父目录需要搜索(执行)权限;
- 对最终对象执行读、写等操作时需要相应权限。
删除文件主要需要父目录的写权限和搜索权限,而不是目标普通文件本身的写权限。目录的粘滞位(如 /tmp 常见的 1777)会进一步限制用户删除他人文件。
路径检查和使用之间存在竞态:
检查 /tmp/job/output 确认安全
↓
攻击者替换某个目录组件或符号链接
↓
程序真正 open() 到另一个对象
因此,特权程序不应依赖“先 stat() 检查,再 open() 使用”的非原子流程。更可靠的做法包括:
- 使用目录 fd 和
openat()限定解析起点; - 对不应跟随的组件使用相应的
O_NOFOLLOW或openat2()解析约束; - 使用
fstat()检查已经打开的对象; - 对临时文件使用排他创建,如
O_CREAT | O_EXCL; - 不把用户可控路径直接拼接到高权限删除或覆盖命令中。
符号链接保护还受到内核配置、挂载选项、用户命名空间和程序自身逻辑影响。realpath() 可以把路径规范化为绝对路径,但它不能单独消除并发替换竞态,也不应被当作安全检查与实际打开之间的锁。
十二、诊断时应分别问四个问题
遇到“文件消失”“磁盘空间未释放”“链接内容不一致”等问题,可以按对象层次定位:
路径是否存在?
test -e /path/to/file
test -L /path/to/file
-e 通常检查目标是否存在;对悬空符号链接,-e 可能为假,而 -L 可确认链接自身存在。
路径最终解析到哪个 inode?
stat /path/to/file
必要时分别比较:
stat -c '%d:%i %F %n' path1 path2
是否有其他硬链接?
find /some/mount -xdev -samefile /path/to/file -print
该操作可能遍历大量目录,生产环境使用时要评估开销,并注意权限和挂载边界。
是否仍被进程打开?
lsof +L1
ls -l /proc/<pid>/fd
在容器环境中,宿主机和容器可能拥有不同的进程、挂载和 PID 命名空间;从容器内部观察到的信息不一定覆盖宿主机全部引用。
十三、把几个层次合并起来看
对路径 /srv/app/current/config,一次访问可能经历:
路径字符串
↓
目录项 current
↓
符号链接目标字符串,或直接得到 inode
↓
目标 inode
↓
权限检查与打开
↓
进程 fd
↓
打开文件描述:偏移、状态标志、引用
↓
数据块、页缓存和文件系统持久化机制
而删除操作只从其中一部分开始:
unlink("/srv/app/current/config")
↓
删除父目录中的名字 config
↓
减少 inode 硬链接计数
↓
若仍有 fd 或映射,则数据继续存在
↓
最后引用消失后,文件系统才可回收空间
硬链接改变的是“有多少目录项直接指向 inode”;符号链接增加的是一个保存路径字符串的独立 inode;文件描述符保持的是一次打开状态;rename() 改变的是目录项到 inode 的映射。它们都可能被口语化为“文件”,但其生命周期和并发行为完全不同。
掌握这种分层后,以下结论可以直接从机制推出:
- 想共享同一份内容并让两个名字始终指向同一 inode,可以使用硬链接,但不能跨文件系统;
- 想让一个名字跟随目标路径的替换,可以使用符号链接;
- 想让已打开内容不受路径删除或替换影响,应依赖 fd,而不是反复按路径重新打开;
- 想原子替换一个文件名,应在同一文件系统内准备新文件后执行
rename(); - 想确认磁盘空间为什么没有释放,应检查已删除但仍打开的对象;
- 想保证断电后更新仍存在,必须把命名空间原子性和数据持久化分别设计,不能只依赖
rename()。
系列导航与关联阅读
- 系列入口:Linux 完整学习路线:从内核与文件系统到网络、性能和生产运维
- 上一篇:Linux 目录与文件系统层次:FHS、挂载点、proc、sysfs 与设备
- 下一篇:Linux 权限完整指南:UID/GID、mode、umask、ACL 与 capabilities
- 延伸:Linux 文件系统:ext4、XFS、日志、缓存、一致性和损坏恢复
官方资料
本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。

评论
0 条讨论