﻿# Linux 进程如何与 VFS 绑定 —— 从 `task_struct` 到文件树的两条全链路

> 本篇聚焦**进程和 VFS 之间的绑定关系**——进程描述符通过哪几条指针链接入全局 VFS 挂载树、路径名怎么一步步解析成 inode、fork/exec/exit 各个阶段绑定关系如何变化。
> 前置知识：[task-struct.md](/concepts/process/task-struct.md)（`task_struct` 字段全景）、[task-resources/fs-struct.md](/concepts/process/task-resources/fs-struct.md)（`fs_struct` 各字段深入）、[task-resources/files-struct.md](/concepts/process/task-resources/files-struct.md)（`files_struct` 三层模型）。VFS 本身的架构见 [vfs-overview.md](/concepts/vfs/vfs-overview.md)（四大对象与多态分发），进程视角的 VFS 交互见 [vfs-from-process.md](/concepts/vfs/vfs-from-process.md)（五跳指针链）。
> 本篇不重复讲 `fs_struct` / `files_struct` / VFS 对象各自的字段细节——那些是上述三篇的职责。本篇只讲**它们之间的连接关系**：进程凭什么能 `open("/a/b.txt")`、内核怎么把路径字符串变成 dentry / inode、fork 后父子怎么共享这些绑定。

## 零、一句话先给结论

**每个 `task_struct` 通过两条指针链绑到全局 VFS 文件树：`task->fs`（`fs_struct`）锚定进程在文件系统中的"视角"——根目录在哪、当前目录在哪；`task->files`（`files_struct`）管理进程打开的所有文件——每个 fd 是一个数组下标，指向的 `struct file` 再通过 `f_path = {vfsmount, dentry}` 锁定 VFS 树上的具体节点。两条链路共同构成"进程能访问文件"的完整基础设施。**

---

## 一、数据结构全景：两条绑定链路

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<proc>> #E3F2FD
  BorderColor<<proc>>     #1565C0
  BackgroundColor<<ctx>>  #C8E6C9
  BorderColor<<ctx>>      #2E7D32
  BackgroundColor<<vfs>>  #FFE0B2
  BorderColor<<vfs>>      #EF6C00
}
rectangle "**task_struct**\n(进程描述符)\n─────────────\n· pid / tgid\n· fs → fs_struct *\n· files → files_struct *" <<proc>> as TASK
rectangle "**fs_struct**\n(文件系统上下文)\n─────────────\n· root: 进程根目录 dentry\n· pwd:  当前工作目录 dentry\n· umask: 权限掩码\n· users: 引用计数" <<ctx>> as FSCTX
rectangle "**files_struct**\n(打开文件表)\n─────────────\n· fdt → fdtable *\n· next_fd\n· count (引用计数)" <<ctx>> as FILES
rectangle "**fdtable**\n(fd 数组本体)\n─────────────\nfd[0] → stdin\nfd[1] → stdout\nfd[2] → stderr\nfd[3] → file * ①\nfd[4] → file * ②\n..." <<ctx>> as FDT
rectangle "**struct file**\n(打开文件描述)\n─────────────\n· f_path = {mnt, dentry}\n· f_pos (当前偏移)\n· f_op → file_operations\n· f_count (引用计数)\n· private_data" <<vfs>> as SFF
rectangle "**全局 VFS 挂载树**\n(一棵树，所有进程共享)\n─────────────\n· mount tree (链表)\n· dcache (dentry 缓存)\n· inode cache" <<vfs>> as VFS_TREE
' 链路一：fs_struct → VFS 挂载树
TASK --> FSCTX : fs (链路一)
FSCTX --> VFS_TREE : pwd.dentry / root.dentry\n锚定进程文件系统"视角"
' 链路二：files_struct → struct file → VFS 节点
TASK --> FILES : files (链路二)
FILES --> FDT : fdt
FDT --> SFF : fd[3]
SFF --> VFS_TREE : f_path.dentry → 锁定具体文件节点
note right of FSCTX
  **链路一（上下文绑定）**
  fs_struct 回答两个问题：
  · "绝对路径从哪开始解析？" → root
  · "相对路径从哪开始解析？" → pwd
  改 pwd(chdir) / root(chroot) 只改这里
  不影响全局 VFS 挂载树
end note
note bottom of SFF
  **链路二（文件绑定）**
  files_struct 回答：
  · "我打开了哪些文件？"
  fd → file → f_path.dentry → inode
  open() 创建此链，close() 断开
end note
@enduml
```

> **图析**：两个链路分工明确。**链路一（`fs->root` / `fs->pwd`）**是进程在 VFS 树上的"出发点"——决定 `open("/a")` 从哪开始解析、`open("b")` 从哪个目录出发。这条链路通过 chdir / chroot / pivot_root 修改，与具体的打开文件无关。**链路二（`files->fdtable` → `file` → `f_path`）**是进程与具体文件的"连接线"——每次 `open()` 创建一条，`close()` 断开。两条链路互不干扰：你可以 chdir 到 `/tmp`，但之前 open 的 fd 对应的文件仍然可读——因为 file 里存的是 `{mnt, dentry}`，不依赖 pwd。

### 两条链路速查

| 链路 | 起 | 经 | 终 | 回答的问题 | 修改接口 |
|------|----|----|----|-----------|---------|
| 链路一 | `task->fs` | `fs_struct.root` / `fs_struct.pwd` | VFS 树上的 dentry 节点 | "路径解析从哪出发？" | `chdir` / `chroot` / `pivot_root` |
| 链路二 | `task->files` | `fdtable[fd]` → `struct file` → `f_path` | VFS 树上的 dentry + inode | "fd 对应哪个文件？" | `open` / `close` / `dup` |

---

## 二、链路一：`fs_struct` —— 进程在 VFS 挂载树的"立足点"

> `fs_struct` 各字段（pwd / root / umask / lock / users）的深入拆解见 [task-resources/fs-struct.md](/concepts/process/task-resources/fs-struct.md)。这里聚焦它**如何把进程锚定到 VFS 挂载树上**。

### 2.1 struct path 的双坐标体系

`fs_struct` 里最关键的设计决策：**`pwd` 和 `root` 不是存字符串路径，而是存 `struct path = {vfsmount *mnt, dentry *dentry}`**。

```c
struct path {
    struct vfsmount *mnt;   // 从哪条 mount 路线过来的
    struct dentry  *dentry; // 在哪个目录节点上
};
```

这两个成员缺一不可：

- **`dentry`**：回答"在哪个目录"。dentry 是 VFS 的目录项缓存，内含 `d_inode`（指向 inode）和 `d_subdirs`（子目录哈希表）。知道 dentry，后续路径查找就能直接从它出发。
- **`vfsmount`**：回答"从哪条路过来的"。Linux 的文件树不是一棵天然的树——是通过多次 `mount` 操作把不同文件系统"嫁接"到一起的。两棵原本独立的 dentry 树通过 `struct mount` 链表连接，`vfsmount` 记录了这条嫁接路径。如果只存 dentry 不存 mnt，遇到 `..` 返回父目录时就不知道是否该穿越 mount 边界回到上一级文件系统。

> **为什么不能只存字符串路径**：存 `"/home/alice/work"` 意味着每次 `open("data.txt")` 都要拼接成 `"/home/alice/work/data.txt"`，然后从根开始逐级查找 dentry（5 级哈希查找）。而 `struct path` 的 dentry 已经直接指向 work 目录的 dentry 节点——`open("data.txt")` 只需做 1 级查找，省掉 80% 的重复劳动。详见 [fs-struct.md §1.1](/concepts/process/task-resources/fs-struct.md#11-pwd-的类型struct-path)。

### 2.2 绝对路径解析：从 root 出发的一级级 dentry 穿越

进程调用 `open("/home/alice/work/data.txt")`，路径以 "/" 开头，内核从 `fs->root.dentry` 出发：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor #1976D2
}
participant "open()" as O
participant "VFS path_lookup()" as PL
participant "dcache" as DC
participant "mount 链表" as ML
O -> PL : 路径以 "/" 开头\n→ 起点 = fs->root = {mnt, dentry}
== 第 1 级: "/" ==
PL -> PL : 已拿到 root dentry，出发
== 第 2 级: "home" ==
PL -> DC : 在 root dentry 子目录哈希表\n(d_subdirs) 中查找 "home"
DC --> PL : home dentry
note over PL : 检查 d_flags & DCACHE_MOUNTED？
note over PL : 未置位，继续下一级
== 第 3 级: "alice" ==
PL -> DC : 在 home dentry 下查找 "alice"
DC --> PL : alice dentry
== 第 4 级: "work" ==
PL -> DC : 在 alice dentry 下查找 "work"
DC --> PL : work dentry
== 第 5 级: "data.txt" ==
PL -> DC : 在 work dentry 下查找 "data.txt"
DC --> PL : data.txt dentry → d_inode
PL --> O : path = {mnt, data.txt dentry}
@enduml
```

> **图析**：绝对路径解析 = **路径分量数 × 单次 dentry 哈希查找**。每遇到一个 "/" 分隔的分量（"home"、"alice"、"work"、"data.txt"），就在当前 dentry 的 `d_subdirs` 哈希表中查一次。命中 dcache → 纯内存操作（极快）；未命中 → 调具体文件系统的 `lookup()` 读磁盘。全程不涉及字符串拼接。

**穿越 mount 边界**：路径解析走到某个 dentry 时，如果 `d_flags & DCACHE_MOUNTED` 置位（说明这里有子挂载），内核调用 `lookup_mnt()` 在父 mount 的 `mnt_mounts` 链表中找到对应的子 mount，切换到子 mount 的 `mnt.mnt_root` 继续。这就是为什么 `/home` 可以是独立挂载的分区——路径解析穿越了 sda1 → sda2 的 mount 边界，但用户感知到的仍然是一棵无缝的树。

### 2.3 相对路径解析：pwd 免去前段重复遍历

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor #1976D2
}
participant "用户态" as U
participant "glibc" as G
participant "VFS" as V
U -> G : open("data/config.json", O_RDONLY)
G -> G : 转换为 openat(AT_FDCWD, "data/config.json")
G -> V : openat(AT_FDCWD, "data/config.json")
V -> V : dirfd == AT_FDCWD(-100)\n→ 读取 current->fs->pwd\n得到 {mnt work, dentry work}
V -> V : 从 work dentry 出发\n① 在 d_subdirs 中查 "data"\n② 在 data dentry 下查 "config.json"
V -> V : 拿到 config.json dentry → 创建 struct file → 分配 fd
V --> U : fd=3
note bottom of V
  pwd 存 {mnt, dentry} 的价值：
  省掉 "/home/alice/work" 这段
  4 级 dentry 查找（/ → home → alice → work）
  只做 2 级（data → config.json）
end note
@enduml
```

> **图析**：`open("data/config.json")` 在 glibc 层转换成 `openat(AT_FDCWD, ...)`。内核遇到 `AT_FDCWD`（值为 -100），直接读取 `current->fs->pwd` 拿到 {mnt, dentry}。这个 dentry 是之前 `chdir` 时 `path_lookup` 缓存下来的——已经指向 work 目录的 dentry 节点，后续只需从该节点出发向下查两级（"data" → "config.json"），完全跳过 "/" → "home" → "alice" → "work" 这 4 级。

### 2.4 root：绝对路径的解析起点与 chroot 隔离

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<real>> #E3F2FD
  BorderColor<<real>>     #1565C0
  BackgroundColor<<jail>> #FFE0B2
  BorderColor<<jail>>     #EF6C00
}
rectangle "系统真实文件树" <<real>> {
  rectangle "/" as ROOT
  rectangle "/etc" as ETC
  rectangle "/home" as HOME
  rectangle "/home/jail" as JAIL {
    rectangle "bin/" as BIN
    rectangle "lib/" as LIB
    rectangle "etc/" as JETC
  }
  rectangle "/usr" as USR
  rectangle "/var" as VAR
}
rectangle "chroot /home/jail\n后进程视角" <<jail>> {
  rectangle "/ = /home/jail" as JROOT {
    rectangle "bin/" as JBIN
    rectangle "lib/" as JLIB
    rectangle "etc/" as JJETC
  }
}
note bottom of JAIL : 进程看不到 /etc /usr /home\n全局 VFS 树没有变化\n只是 fs->root 被改成了 jail 的 dentry
@enduml
```

> **图析**：`chroot("/home/jail")` 做的事情极其简单——把当前进程 `fs->root` 的 dentry 从系统 "/" 换成 `"/home/jail"` 的 dentry。此后该进程的所有绝对路径解析都从 jail 的 dentry 出发，看不到 jail 之外的任何文件。**全局 VFS 挂载树完全不受影响**——其他进程的 `fs->root` 仍指向系统 "/"，它们眼中的文件树没有变化。chroot 只是进程级的"视觉障碍"，不是文件系统的物理切割。

### 2.5 链路一的观测命令

```bash
# 进程当前工作目录
ls -l /proc/<pid>/cwd       # 符号链接 → 当前目录
readlink /proc/<pid>/cwd    # 解析出字符串路径
# 进程根目录
ls -l /proc/<pid>/root      # 符号链接 → 根目录
readlink /proc/<pid>/root   # 若 chroot 过，这里指向 jail 目录
# 进程内读取
#include <unistd.h>
char *getcwd(buf, size);    # 返回当前工作目录字符串
```

> **pwd 可能"不可读" **：如果 pwd 所在的目录已被 `rm -rf` 删除，`/proc/<pid>/cwd` 符号链接的目标变成 `(deleted)`，`getcwd()` 可能返回错误。但 `fs->pwd` 里的 dentry 仍然有效——因为还有引用计数，dentry 不会被真正释放，进程仍可正常使用相对路径和 `..`。

---

## 三、链路二：`files_struct` —— 进程打开文件与 VFS 节点的绑定

> `files_struct` 的三层模型（fd 表 → `struct file` → inode）完整拆解见 [task-resources/files-struct.md](/concepts/process/task-resources/files-struct.md)。这里聚焦**这条链路如何与 VFS 文件树节点建立绑定**。

### 3.1 `open()` 全过程：从空 fd 到 VFS 节点的完整绑定

```plantuml
@startuml
skinparam shadowing false
skinparam participant {
  BackgroundColor #C8E6C9
  BorderColor     #388E3C
}
participant "用户进程\nfd = open(\"/tmp/a.txt\", O_RDONLY)" as U
participant "系统调用层\nsys_open → do_sys_open" as SYS
participant "VFS 路径查找\npath_openat()" as PATH
participant "file 分配\nget_empty_filp()" as ALLOC
participant "fd 分配\nget_unused_fd_flags()" as FD_ALLOC
participant "fd 绑定\nfd_install()" as INSTALL
U -> SYS : open("/tmp/a.txt", O_RDONLY)
SYS -> SYS : 从 flags 解析 O_RDONLY
== 阶段 1: 路径查找 ==
SYS -> PATH : path_openat(AT_FDCWD, "/tmp/a.txt", ...)
PATH -> PATH : 路径以 "/" 开头，从 fs->root 出发\n逐级解析:  / → tmp → a.txt
PATH -> PATH : 查 dcache → 读磁盘 inode → 权限检查
PATH -> PATH : 得到 a.txt 的 dentry + inode\n且拿到对应的 vfsmount
== 阶段 2: 分配 struct file ==
PATH -> ALLOC : get_empty_filp()
ALLOC -> ALLOC : 分配 struct file 并初始化:\n· f_mode = FMODE_READ\n· f_flags = O_RDONLY\n· f_pos = 0\n· f_count = 1\n· f_path = {vfsmount, a.txt dentry} ← 绑定 VFS 节点!\n· f_op = ext4_file_operations ← 确定后续所有操作的分发路径
ALLOC --> PATH : struct file *
== 阶段 3: 分配 fd ==
PATH -> FD_ALLOC : get_unused_fd_flags()
FD_ALLOC -> FD_ALLOC : 在 current->files->fdt 中\n找最小编号的空闲位 → fd=3
FD_ALLOC --> PATH : fd=3
== 阶段 4: 绑定 fd → file ==
PATH -> INSTALL : fd_install(3, file)
INSTALL -> INSTALL : fdtable.fd[3] = file
PATH --> U : 返回 fd=3
@enduml
```

> **图析**：`open()` 做四件事建立链路二的绑定——**(1) 路径查找**：从 `fs->root` 出发，逐级 dentry 哈希查找，最终定位到目标文件的 dentry + inode，并获得对应的 vfsmount（用于处理 mount 边界穿越）；**(2) 分配 `struct file`**：初始化偏移为 0、引用计数为 1，**把 `f_path` 设为 `{vfsmount, dentry}`**——这条路径一旦设好就固化，后续 `read`/`write` 不再依赖 `fs->pwd` 或 `fs->root`；**(3) 分配 fd**：在 `fdtable` 中找空闲位，得到一个整数；**(4) 绑定**：`fdtable.fd[fd] = file`——此时 `fd=3` 这个整数和 VFS 文件树上的 `a.txt` 节点通过 `struct file` 牢牢绑定。

### 3.2 `read()` 如何沿链路二到达 VFS 节点

用户调 `read(fd, buf, len)` 时内核的路径：

| 步骤 | 操作 | 涉及结构 |
|------|------|---------|
| ① | `fdget(fd)` → 从 `current->files->fdt->fd[fd]` 取出 `struct file *` | `files_struct` → `fdtable` |
| ② | 检查 `file->f_mode & FMODE_READ`，不通过返回 `-EBADF` | `struct file` |
| ③ | `file->f_op->read(file, buf, len, &file->f_pos)` | `file_operations` vtable 分发 |
| ④ | 具体实现读取数据（ext4 走 page cache、pipe 走环形缓冲区、socket 走 tcp_recvmsg） | 具体文件系统 |
| ⑤ | 返回读取字节数 | — |

> **关键点**：`read()` 不需要路径查找——`struct file.f_path` 里已经锁定了 {vfsmount, dentry}，内核直接拿着 inode 读数据。读完后 `f_pos` 自动前移，下一次 `read` 从新位置接着读。不管之后进程 chdir 去了哪里、甚至源文件所在的目录被删了，只要 `file` 没 close，`read` 照用不误。

### 3.3 `close()`：解绑与引用计数

```plantuml
@startuml
skinparam shadowing false
skinparam participant {
  BackgroundColor #C8E6C9
  BorderColor     #388E3C
}
participant "进程\nclose(3)" as P
participant "VFS\n__close_fd()" as VFS
participant "fdtable" as FDT
participant "struct file\n(f_count)" as FILE
P -> VFS : close(3)
VFS -> FDT : fdtable.fd[3] = NULL（断开 fd 层）
VFS -> FILE : fput(file) → f_count--
alt f_count > 0
  FILE --> VFS : 还有其他引用，暂不释放
  note right : 场景：fork 后父子共享\nfile，一方 close 只减计数
else f_count == 0
  FILE -> FILE : ① file->f_op->flush(file)（若有）
  FILE -> FILE : ② file->f_op->release(inode, file)
  FILE -> FILE : ③ 释放 struct file 内存
  FILE -> FILE : ④ inode 引用计数 -1
  note right : 全部引用释放才真正解绑
end
VFS --> P : 0
@enduml
```

> **图析**：`close(fd)` 分两层。**fd 层**：`fdtable.fd[fd] = NULL`，进程立即失去通过该 fd 访问文件的能力。**VFS 层**：`fput(file)` 递减 `f_count`，**只有归零才触发真正的 release**。fork 后父子共享 `struct file`（`f_count=2`），一方 close 后另一方仍可通过自己的 fd 正常读写——这是"引用计数"给进程的假象保护。

### 3.4 链路二的观测命令

```bash
# 查看进程当前开了多少个 fd
ls /proc/<pid>/fd | wc -l
# 查看每个 fd 指向什么文件
ls -l /proc/<pid>/fd/
# 输出示例: 3 -> /tmp/a.txt
#          4 -> socket:[12345]
#          5 -> pipe:[67890]
# 用 lsof 看详情（含偏移量、文件大小）
lsof -p <pid>
# strace 抓 open/close
strace -e open,openat,close -p <pid>
```

---

## 四、进程生命周期中的 VFS 绑定变化

### 4.1 fork / clone：继承与引用计数

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<parent>> #E3F2FD
  BorderColor<<parent>>     #1565C0
  BackgroundColor<<child>>  #BBDEFB
  BorderColor<<child>>      #0D47A1
  BackgroundColor<<shared>> #C8E6C9
  BorderColor<<shared>>     #2E7D32
}
rectangle "父进程 task_struct\nfiles → files_struct_A\nfs → fs_struct_A" <<parent>> as PT
rectangle "子进程 task_struct\nfiles → files_struct_B\nfs → fs_struct_B" <<child>> as CT
rectangle "fs_struct_A\nroot = "/" dentry\npwd = "/home" dentry" <<shared>> as FSA
rectangle "files_struct_A\nfdt_A\nfd[3] → file_X" <<shared>> as FLSA
rectangle "struct file_X\nf_path = {mnt, dentry}\nf_count = 2" <<shared>> as FILEX
PT --> FSA : fs
PT --> FLSA : files
CT --> FSA : fs(复制, 同值)
CT --> FLSA : files(复制, 同值)
FLSA --> FILEX : fd[3]
FLSA --> FILEX : fd[3]
note bottom of FILEX
  **fork 关键行为**：
  · fs_struct 被**复制**出新实例
    （初始值同父，各自独立修改）
  · files_struct 被**复制**出新实例
    （fdtable 指针数组复制，但
    每个 file* 指向**同一个** struct file）
  · 所有共享的 struct file 的
    f_count 递增
end note
note right of FSA
  **写时复制**：
  刚 fork 的子进程和父共用同一 fs_struct
  (users=2)
  子进程第一次 chdir/chroot 时才
  真正拷贝一份
end note
@enduml
```

> **图析**：fork 对两条链路的行为不同。**链路一（fs_struct）**：父子初始时共享同一 `fs_struct`（`users=2`），触发写时复制——子进程第一次执行 `chdir` / `chroot` 时才拷贝一份独立的。**链路二（files_struct）**：`files_struct` 立即复制出新实例，但 `fdtable` 里存的 `file *` 指针指向的**还是父进程那批 `struct file`**——所有共享的 `file` 的 `f_count` 递增。这意味着父子共享读写偏移，一个进程 `read()` 移动了 `f_pos`，另一个的 `read()` 接着往下读。

### 4.2 共享语义对照表

| 场景 | fs_struct | files_struct | struct file（f_pos） | close 行为 |
|------|-----------|-------------|---------------------|-----------|
| **fork** | 先共享(CoW)，子首次 chdir 时复制 | 复制 fd 表，指针指向同一批 file | **共享 file，共享偏移** | 各自 close 只减 f_count，全关才 release |
| **线程**（`CLONE_FILES \| CLONE_FS`） | **共享**同一份 | **共享**同一份 | 同一份 | 一个线程 close → 全线程丢失该 fd |
| **clone 无 CLONE_FS** | 复制（同 fork） | — | — | — |
| **clone 无 CLONE_FILES** | — | 复制（同 fork） | — | — |
| **execve** | **保留** | **保留**（除了 `FD_CLOEXEC` 标记的 fd） | 保留 | — |
| **进程 exit** | `put_fs_struct()` 减引用，归零释放 | 遍历 fdt，逐个 fput | f_count 归零 → release → 解绑 inode | 自动 close 全部 |

> **多线程 chdir 是大忌**：线程共享 `fs_struct`，一个线程 chdir → 所有线程的 pwd 同时变化。线程 A 正在读 `./data/file`，线程 B 做了 `chdir("/tmp")` → 线程 A 的相对路径解析立即出错。解法：用 `openat(dirfd, ...)` 锁定目录 fd，不依赖隐式 pwd。

### 4.3 进程退出时的解绑全过程

进程调用 `exit()` 或收到致命信号后，内核执行以下与 VFS 解绑相关的步骤：

1. **关闭所有打开文件**：遍历 `files_struct->fdt`，对每个非空 fd 调 `filp_close()` → `fput()`。每个 `struct file` 的 `f_count` 递减。
2. **触发 release**：`f_count` 归零的 file 调 `file->f_op->release(inode, file)`。对普通文件：inode 引用计数减 1；对 socket：触发四次挥手；对 pipe：唤醒阻塞的 reader/writer。
3. **释放 files_struct**：所有 fd 关闭完毕，释放 `files_struct` 和 `fdtable`。
4. **释放 fs_struct**：`put_fs_struct()` 递减引用计数，归零后释放 pwd/root 的 dentry 引用，最后释放 `fs_struct` 本身。

> 全局 VFS 挂载树不受单个进程退出影响——dentry 和 inode 有独立的引用计数系统，只在该节点不再被任何进程引用时才会被回收。

---

## 五、容器隔离：chroot / pivot_root / mount namespace

两条链路也是容器文件系统隔离的基础：

| 操作 | 改什么 | 影响范围 | 安全强度 | 典型场景 |
|------|--------|---------|---------|---------|
| `chdir` | `fs->pwd` | 共享 fs_struct 的所有线程 | 无 | 日常 cd |
| `chroot` | `fs->root` | 当前进程 | **弱**——root 可逃逸 | 传统 FTP 服务器 jail |
| `pivot_root` | 整个 mount namespace 的 "/" | 同 namespace 所有进程 | **强**——但需 mnt ns 配合 | 容器 init 进程 |
| Docker/K8s 容器 | `pivot_root` + mount ns + user ns + cgroup | 容器内所有进程 | **最强**——多层隔离 | 生产容器 |

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<host>> #E3F2FD
  BorderColor<<host>>     #1565C0
  BackgroundColor<<ctr>>  #C8E6C9
  BorderColor<<ctr>>      #2E7D32
}
rectangle "宿主机视角\nmount namespace A\n─────────────\n/ → sda1 根文件系统" <<host>> as HOST
rectangle "容器视角\nmount namespace B\n─────────────\n/ → 容器镜像层 (overlayfs)\n容器内看不到宿主机 /" <<ctr>> as CTR
HOST ..> CTR : 不同 mount namespace\n各自独立的 mount tree\n每个容器有自己的 "/"
note bottom of CTR
  **容器的三条绑定链**：
  ① fs->root → 容器内的 "/" dentry
     (pivot_root 重设，看不到宿主机根)
  ② fs->pwd → 容器内的 cwd
  ③ files->fdt → 进程打开的文件
     (若 bind mount 了宿主机路径进去，
      fd 就指向宿主机的文件节点)
@end note
@enduml
```

> **图析**：容器隔离的本质是给两条链路换上不同的值——`pivot_root` 把 `fs->root` 指向容器专属的文件树根（overlayfs），`mount namespace` 让容器内的 mount 操作不影响宿主机。但注意链路二（`files_struct`）不受 namespace 约束——如果容器启动前宿主机 open 了一个 fd 再传给容器，容器内的进程仍然可以通过这个 fd 访问宿主机文件。

---

## 六、延伸阅读

| 文档 | 关联点 | 说明 |
|------|--------|------|
| [task-struct.md](/concepts/process/task-struct.md) | `task_struct` 全体字段 | `fs` / `files` 在进程描述符中的位置和初始化 |
| [task-resources/fs-struct.md](/concepts/process/task-resources/fs-struct.md) | `fs_struct` 各字段深入 | pwd path 类型、dentry/inode 关系、mount 嫁接机制 |
| [task-resources/files-struct.md](/concepts/process/task-resources/files-struct.md) | `files_struct` 三层模型 | fd → file → inode，dup/fork 共享语义 |
| [task-resources/mm-struct.md](/concepts/process/task-resources/mm-struct.md) | 地址空间 | 和 file 的 mmap 绑定（`mm_struct` → `vm_area_struct` → `file`） |
| [../vfs/vfs-overview.md](/concepts/vfs/vfs-overview.md) | VFS 四大对象 | super_block / inode / dentry / file 的全景 |
| [../vfs/vfs-from-process.md](/concepts/vfs/vfs-from-process.md) | 进程视角的 VFS | 五跳指针链：`task_struct` → `files_struct` → `fdtable` → `file` → `f_op` |
| [fork-and-threads.md](/concepts/process/fork-and-threads.md) | 多线程 fork 陷坑 | 子进程只剩一个线程，但锁、fd 等共享状态全部继承 |
| [process-creation.md](/concepts/process/process-creation.md) | fork/clone 全流程 | CoW、fd 表复制、mm_struct 共享 |
| [../container/overview.md](/concepts/container/overview.md) | 容器隔离全景 | namespace / cgroup / pivot_root / overlayfs |

---

## 七、一句话总结

> **`task_struct` 通过两条指针链绑定到全局 VFS 文件树——链路一（`task->fs`）通过 `fs_struct.root` 和 `fs_struct.pwd` 锚定进程在文件系统中的"视角"，决定绝对路径和相对路径从哪个 dentry 出发解析（存 `struct path` 而非字符串，免去重复遍历）；链路二（`task->files`）通过 `fdtable[fd]` → `struct file` → `f_path = {mnt, dentry}` 把每个 fd 锁定到 VFS 树上的具体文件节点，`open()` 时建立绑定、`close()` 靠 `f_count` 引用计数推迟释放直到所有持有者松手。fork 时 fs_struct 写时复制、files_struct fd 表复制但共享底层 `struct file`（偏移共享）；chroot 只改 `fs->root` 不改全局 VFS 树；容器通过 pivot_root + mount namespace 给这两个链路换上隔离的值。**
