﻿# fs_struct —— 进程的文件系统上下文

> 这是 [../task-struct.md](/concepts/process/task-struct.md) 里 `task->fs` 那一项的展开。`fs_struct` 是进程看到文件系统视图的"立足点"——当前目录在哪、根目录在哪、创建文件时的默认权限掩码。`chdir`/`chroot`/`umask` 改的都是它。

## 零、一句话认知：fs_struct = "我在文件系统里的位置"

每次进程用**相对路径**（`open("data/config.json")`）访问文件，内核必须知道"从哪个目录出发解析"。`fs_struct` 就是这个出发点。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<fs>> #E3F2FD
  BorderColor<<fs>> #1976D2
  BackgroundColor<<dc>> #C8E6C9
  BorderColor<<dc>> #388E3C
  BackgroundColor<<rf>> #FFE0B2
  BorderColor<<rf>> #EF6C00
  BackgroundColor<<um>> #F3E5F5
  BorderColor<<um>> #7B1FA2
}
rectangle "fs_struct" <<fs>> as FS {
  rectangle "pwd (struct path)\n当前工作目录\nchdir 改这里" <<dc>> as PWD
  rectangle "root (struct path)\n根目录\nchroot 改这里" <<rf>> as ROOT
  rectangle "umask\n(umode_t)\n默认权限掩码\numask() 改这里" <<um>> as UMASK
  rectangle "users (int)\n引用计数\n多少 task 在用这个 fs_struct" as USERS
  rectangle "lock (spinlock_t)\n保护并发修改" as LOCK
}
note bottom of PWD : 相对路径解析的起点\nopen("a/b") = openat(AT_FDCWD, "a/b")\n→ 从 pwd 开始解析
note bottom of ROOT : 这个进程的"文件系统根"\nchroot 后, / 指的就是这里\n(容器隔离文件系统视图的关键)
note bottom of UMASK : open(path, O_CREAT, 0666)\n实际权限 = 0666 & ~umask
@enduml
```

**`fs_struct` 核心数据结构**（各字段深入见下文 §一~§三）：

```c++
struct fs_struct {
    int          users;       // 引用计数：多少个 task 共用此 fs_struct
    spinlock_t   lock;        // 自旋锁：保护并发修改（如多线程 chdir/chroot）
    umode_t      umask;       // 文件创建权限掩码：实际权限 = mode & ~umask，详见 §三
    struct path  pwd;         // 当前工作目录（{vfsmount*, dentry*}），相对路径解析起点，详见 §一
    struct path  root;        // 进程根目录，chroot 把 "/" 重新锚定到此子树，详见 §二
};
```

其中 `struct path` 内部结构为 `{vfsmount *mnt; dentry *dentry;}`（见 [§一 1.1](#11-pwd-的类型struct-path)）。五个字段分工：`users` 控制生命周期、`lock` 保证并发安全、`umask` 管新建权限、`pwd` 和 `root` 分别锚定进程在文件系统中的"当前位置"和"根边界"。

## 一、pwd —— 当前工作目录

### 1.1 pwd 的类型：struct path

`pwd` 不是字符串路径，而是 `struct path` —— 一对指针精确锁定文件系统中的某个目录：

```c++
struct path {
    struct vfsmount *mnt;   // 指向哪个挂载实例（同一文件系统可 mount 到多个位置）
    struct dentry  *dentry; // 指向哪个目录项（已缓存的目录结点，含 inode、名称、父子关系）
};
```

### path → dentry → inode：VFS 的三层命名体系

在分析 `struct path` 的两个成员之前，需要先搞清三个概念的层次关系——它们是整个 VFS 路径解析的基石：

| 层 | 存什么 | 生命周期 | 类比 |
|----|-------|---------|------|
| **path 字符串** | 用户态路径（`"/home/alice/work"`） | 栈上临时变量，`open()` 返回后立即释放 | 问路的问题 |
| **dentry** | 路径分量到 inode 的映射缓存 | 内存缓存（dcache），压力下可回收，下次访问时重建 | 记住的路 |
| **inode** | 文件元数据（权限、大小、磁盘块位置） | 持久存在（磁盘有对应记录），inode cache 中常驻 | 到达的目的地 |

```c++
// ── dentry：namespace 层，一个路径分量 → 一个 dentry 对象 ──
struct dentry {
    struct qstr         d_name;      // 分量名称（"home"），含预计算的哈希值
    struct inode       *d_inode;     // → 指向对应的 inode（文件本体）
    struct dentry      *d_parent;    // → 父目录 dentry（向上回溯）
    struct hlist_bl_node d_hash;     // 挂在父 dentry 的 d_subdirs 哈希链中
    struct list_head    d_child;     // 挂在父 dentry 子目录链表中的兄弟节点
    struct list_head    d_subdirs;   // 本 dentry 下所有子目录的哈希表头（查找子项的入口）
};
// ── inode：数据层，一个磁盘文件/目录 → 一个 inode 对象 ──
struct inode {
    umode_t          i_mode;      // 文件类型（S_IFREG/S_IFDIR/...）+ 权限位（rwx）
    kuid_t           i_uid;       // 所有者 UID
    kgid_t           i_gid;       // 所有者 GID
    loff_t           i_size;      // 文件大小（字节）
    unsigned long    i_ino;       // inode 编号（ls -i 看到的值，文件系统内唯一）
    struct super_block *i_sb;     // 所属超级块（标识 ext4 / xfs / tmpfs ……）
};
```

两者的树状组织——以 `"/home/alice"` 为例：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<dentry>> #E3F2FD
  BorderColor<<dentry>> #1976D2
  BackgroundColor<<inode>> #C8E6C9
  BorderColor<<inode>> #388E3C
}
rectangle "root dentry\nd_name = \"/\"" <<dentry>> as RD
rectangle "home dentry\nd_name = \"home\"" <<dentry>> as HD
rectangle "alice dentry\nd_name = \"alice\"" <<dentry>> as AD
rectangle "root inode\ni_ino=2\nS_IFDIR | 0755" <<inode>> as RI
rectangle "home inode\ni_ino=25w\nS_IFDIR | 0755" <<inode>> as HI
rectangle "alice inode\ni_ino=51w\nS_IFDIR | 0755" <<inode>> as AI
RD -down-> RI : d_inode
HD -down-> HI : d_inode
AD -down-> AI : d_inode
RD -right-> HD : d_subdirs["home"]
HD -right-> AD : d_subdirs["alice"]
AD -left-> HD : d_parent
HD -left-> RD : d_parent
note bottom of RD : dentry 是纯缓存：内存不够时可回收\ninode 才代表磁盘上的真实文件
note right of AD : 路径解析 = 按 d_name 哈希查找\n沿 d_subdirs 链条一级级往下走
@enduml
```

三层分工的关键差异：

1. **path → dentry 转换只做一次**：`chdir("/home/alice")` 触发 `path_lookup()`，把字符串 "home" → "alice" 逐分量转成 dentry 链并缓存；后续 `open("data.txt")` 直接从 pwd dentry 出发，不再碰路径字符串。
2. **dentry ≠ inode，且非一对一**：一个 inode 可以有多个 dentry 指向它——最典型的是硬链接：`/home/a/file` 和 `/backup/file` 是两个 dentry，但 `d_inode` 指向同一个 inode（`i_ino` 相同）。反过来，一个 dentry 也可能没有 inode（`d_inode == NULL`），即 **negative dentry**——用于缓存"这个文件不存在"的查询结果，避免反复穿透磁盘。
3. **dentry 可回收，inode 是常驻的**：dcache 是一个内存缓存层，`shrink_dcache()` 在内存紧张时回收长期未用的 dentry。但 inode 只要文件未被删除 + 无进程持有引用，就保留在 inode cache 中。dentry 回收后再次访问时，由父 dentry + `d_name` 重新通过文件系统的 `lookup()` 创建。

> **一句话**：**path 是问路，dentry 是记路，inode 是到站。** VFS 路径解析 = 把 path 拆成分量 → 沿 dentry 的 `d_subdirs` 哈希表逐级查找 → 最终落到 inode。

两个成员缺一不可，各自回答不同的问题：

- **`vfsmount *mnt`**：对用户来说，文件系统确实只有从 "/" 出发的一棵树。但在内核内部，这棵树是通过多次 `mount` 操作**拼装**出来的——每次 `mount` 把一个独立的文件系统实例（ext4 分区、tmpfs、procfs……）"嫁接"到树上的某个节点。`vfsmount` 记录了这个嫁接关系：谁被接到了哪个位置。

  以系统启动时挂载 `/home` 分区为例：

  ```bash
  mount /dev/sda2 /home   # 把 sda2 分区挂到根文件系统的 /home 目录上
  ```

  根文件系统上 `/home` 原来只是一个空 dentry，执行 `mount /dev/sda2 /home` 后，内核的工作是把两棵原本独立的 dentry 树"焊接"成一棵。

  **mount 前的状态——两棵独立的 dentry 树：**

```bash
  ┌──── 根文件系统 (sda1) ────┐    ┌─── sda2 文件系统 ───┐
  │       "/" dentry           │    │    "/" dentry         │
  │      /    |     \          │    │    /      \           │
  │  "bin"  "home"  "etc" …   │    │ "alice"  "bob" …     │
  │            |               │    │   |                   │
  │     (空目录, 无子dentry)    │    │ "work" "docs" …      │
  └────────────────────────────┘    └───────────────────────┘
  ```

  两棵树各自独立——sda1 的 `home` dentry 下面没东西，sda2 有自己的 dentry 整树。`mount` 的任务是让 sda2 的根取代 sda1 的 `home` 在命名空间中的"下半身"。

  **关键数据结构——`struct mount` 如何记录嫁接关系：**

  ```c++
  // struct mount 是内核内部表示（struct vfsmount 是其内嵌成员，对外暴露的公共接口）
  struct mount {
      struct mount      *mnt_parent;     // 父 mount：我被挂在了谁的树上
      struct dentry     *mnt_mountpoint; // 接合点：父文件系统中哪个 dentry 上接了我
      struct list_head   mnt_child;      // 挂在父 mount 的子挂载链表中（兄弟 mount 的链表节点）
      struct list_head   mnt_mounts;     // 我上面的子挂载链表头（别人接在我身上）
      struct vfsmount    mnt;            // 对外可见的 vfsmount（内含 mnt_root、mnt_sb 等字段）
  };
  ```

  四组字段的分工：

  | 字段 | 回答的问题 | 本例中的值 |
  |------|-----------|-----------|
  | `mnt_mountpoint` | 接在父树的哪个 dentry 上？ | sda1 的 `home` dentry |
  | `mnt.mnt_root` | 我的树根在哪？ | sda2 的 `"/"` dentry |
  | `mnt_parent` | 我挂在谁身上？ | sda1 对应的 `struct mount` |
  | `mnt_child` / `mnt_mounts` | 父子 mount 链表 | 把 sda2_mount 串入 sda1_mount 的子链表 |

  此外，`dentry` 上还有一个关键标志位 **`DCACHE_MOUNTED`**：dentry 被用作 mountpoint 时置位。路径遍历时内核看到这个标志，就知道这里不是普通子目录，而是 mount 边界——后面跟着另一棵 dentry 树。

  **mount 过程——时序图：**

  ```plantuml

  @startuml
  skinparam shadowing false
  skinparam sequence {
    ParticipantBackgroundColor #E3F2FD
    ParticipantBorderColor #1976D2
  }
  participant "用户态" as U
  participant "VFS do_mount()" as V
  participant "sda1 根文件系统" as FS1
  participant "sda2 文件系统" as FS2

  U -> V : mount("/dev/sda2", "/home", "ext4", ...)

  == 1. 锁定接合点：在 sda1 上找到 /home 的 dentry ==
  V -> FS1 : path_lookup("/home")
  FS1 --> V : 返回 home dentry\n(属于 sda1 的 dentry 树)

  == 2. 打开被挂载设备，读超级块，获取其根 dentry ==
  V -> FS2 : get_tree_bdev() → 读超级块\n→ 获取 sda2 的 "/" dentry
  FS2 --> V : sda2 根 dentry

  == 3. 创建 struct mount，填入嫁接字段 ==
  V -> V : new_mount = alloc_vfsmnt("sda2")\nnew_mount->mnt_parent = sda1_mount\nnew_mount->mnt_mountpoint = home dentry\nnew_mount->mnt.mnt_root = sda2 根 dentry

  == 4. 将新 mount 挂入父 mount 的 mnt_mounts 链表 ==
  V -> V : list_add(&new_mount->mnt_child,\n&sda1_mount->mnt_mounts)

  == 5. 在接合点 dentry 上打标记 ==
  V -> V : home_dentry->d_flags |= DCACHE_MOUNTED
  note over V : 此后路径遍历走到 home dentry\n就会感知这里有 mount

  V --> U : 返回 0 (成功)
  @enduml

  ```

  **mount 后的结果——组件图（两棵树通过 struct mount 焊接）：**

  ```plantuml

  @startuml
  skinparam shadowing false
  skinparam rectangle {
    BackgroundColor<<dentry>> #E3F2FD
    BorderColor<<dentry>> #1976D2
    BackgroundColor<<mnt>> #FFE0B2
    BorderColor<<mnt>> #EF6C00
  }
  rectangle "struct mount (sda1)\nmnt.mnt_root = sda1 "/"\nmnt_mounts → [sda2_mount, ...]" <<mnt>> as M1
  rectangle "struct mount (sda2 → /home)\nmnt_parent = sda1_mount\nmnt_mountpoint = home dentry\nmnt.mnt_root = sda2 "/" dentry\nmnt_child → 挂在 sda1_mount 子链表" <<mnt>> as M2

  rectangle "sda1 "/"\ndentry" <<dentry>> as RD
  rectangle "home dentry\n(接合点)\nd_flags |= DCACHE_MOUNTED" <<dentry>> as HD
  rectangle "sda2 "/"\ndentry\n(mnt.mnt_root)" <<dentry>> as SRD
  rectangle "alice dentry" <<dentry>> as AD
  rectangle "work dentry" <<dentry>> as WD

  M1 --> RD : mnt_root
  M2 --> SRD : mnt_root
  M2 --> HD : mnt_mountpoint

  RD --> HD : d_subdirs
  SRD --> AD : d_subdirs
  AD --> WD : d_subdirs

  note right of HD
    关键：home dentry 的 d_subdirs 里没有 alice
    alice/work 全在 sda2 的 dentry 树下
    穿越靠 mount 链表，不靠 dentry 父子链
  end note
  @enduml

  ```

  **路径遍历如何穿越 mount 边界——时序图：**

  ```plantuml

  @startuml
  skinparam shadowing false
  skinparam sequence {
    ParticipantBackgroundColor #E3F2FD
    ParticipantBorderColor #1976D2
  }
  participant "open()" as O
  participant "path_lookup()" as PL
  participant "sda1 dcache" as D1
  participant "mount 链表" as ML
  participant "sda2 dcache" as D2

  O -> PL : lookup("/home/alice/work/data.txt")

  == 在 sda1 内：" /" → "home" ==
  PL -> D1 : 从 mnt_root (sda1 "/") 出发\n查找子目录 "home"
  D1 --> PL : home dentry

  == 检测到 mountpoint ==
  PL -> PL : d_flags & DCACHE_MOUNTED ?
  note over PL : 置位！home dentry 是接合点

  PL -> ML : lookup_mnt(home dentry)\n在 sda1_mount->mnt_mounts 中\n查找 mnt_mountpoint == home 的 mount
  ML --> PL : 找到 sda2_mount\n返回 &sda2_mount->mnt

  PL -> PL : 切换当前路径\nmnt = &sda2_mount->mnt\ndentry = sda2_mount->mnt.mnt_root

  == 在 sda2 内："alice" → "work" → "data.txt" ==
  PL -> D2 : 从 sda2 "/" dentry 出发\n查找 "alice"
  D2 --> PL : alice dentry
  PL -> D2 : 查找 "work"
  D2 --> PL : work dentry
  PL -> D2 : 查找 "data.txt"
  D2 --> PL : data.txt dentry

  PL --> O : path = {vfsmount(sda2), data.txt dentry}
  @enduml

  ```

  三个要点总结：
  1. **mount 不修改 dentry 树**：`home` dentry 的 `d_subdirs` 始终只有 sda1 原有的子目录。sda1 和 sda2 的 dentry 树**各自独立**，连接靠额外的 `struct mount` 链表，不靠 dentry 父子指针。
  2. **穿越由 `DCACHE_MOUNTED` + `lookup_mnt()` 完成**：路径解析走到 dentry 时先检查标志位，有挂载就沿 `mnt_mounts` 链表找到 `struct mount`，跳到其 `mnt.mnt_root` 继续。跳转由 mount 层控制，dentry 无感知。
  3. **这就是 `struct path` 必须包含 `mnt` 的根因**：路径解析结果用 `{mnt, dentry}` 表示。data.txt 的 dentry 属于 sda2 的树，但它被解析到的前提是穿过了 sda1→sda2 的 mount 边界。后续任何操作（如 `..` 返回父目录）都需要知道"当前在哪个 mnt 下"，否则无法正确穿越回 sda1 的 `home`。

- **`dentry *dentry`**：我在**哪个目录结点**？dentry 是 VFS 的目录项缓存对象，包含指向 `inode` 的指针、目录名（如 `"work"`）、父目录 dentry 和子目录链表。它把路径查找的结果缓存下来——知道 dentry 就知道这个目录对应的文件实体是什么、父目录是谁、下面有哪些子目录。

> **一句话**：`struct path = {vfsmount, dentry}` 等价于"文件系统命名空间中的唯一坐标"——dentry 回答"哪个文件"，`mnt` 回答"从哪条路过来的"。

### 内核如何从 "/" 逐级查找文件——dentry 的链式遍历

有了上面对 dentry/inode 结构的理解，现在看内核如何解析一个**绝对路径**。以 `open("/home/alice/work/data.txt")` 为例：路径以 "/" 开头，内核从 `fs->root` 出发，**每遇到一个路径分量就做一次 dentry 子树哈希查找**，一级一级往下走：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor #1976D2
}
participant "进程" as P
participant "VFS" as V
participant "dcache" as DC
participant "磁盘文件系统\n(ext4/xfs/...)" as FS
P -> V : open("/home/alice/work/data.txt")
note over V
路径以 "/" 开头\n→ 从 fs->root 出发
end note
== 第 1 级: "/" — 进程根目录 ==
V -> V : 起点 = fs->root.dentry\n(正常进程指向系统 "/" 的 dentry,\n chroot 后指向 jail 目录的 dentry)
== 第 2 级: "home" ==
V -> DC : 在 root dentry 子目录哈希表中查找 "home"
DC --> V : home dentry (已缓存)
== 第 3 级: "alice" ==
V -> DC : 在 home dentry 子目录哈希表中查找 "alice"
alt 未命中 dcache
  DC -> FS : 调 ext4_lookup(), 从磁盘读出
  FS --> DC : 创建 alice dentry, 加入 dcache
end
DC --> V : alice dentry
== 第 4 级: "work" ==
V -> DC : 在 alice dentry 子目录哈希表中查找 "work"
DC --> V : work dentry (已缓存)
== 第 5 级: "data.txt" ==
V -> DC : 在 work dentry 子目录哈希表中查找 "data.txt"
DC --> V : data.txt dentry → d_inode
V --> P : fd=3, file 对象
note over P, FS
  **每次 open 绝对路径都要从头走完**
  / → home → alice → work → data.txt 共 5 级 dentry 查找
  即使 dentry 全部命中缓存, 5 次哈希查找也不可避免
end note
@enduml
```

这个过程的三个关键事实：

1. **"/" 就是 `fs->root.dentry`**：绝对路径的第一个 "/" 告诉 VFS "从进程根目录 dentry 出发"。正常进程的 `fs->root` 指向系统 "/" 的 dentry；`chroot` 后指向 jail 目录的 dentry——根节点换了，后续所有绝对路径解析的基准跟着换，这就是 chroot 隔离文件系统的本质。
2. **dentry 是路径查找的最小粒度**：每个路径分量（"home"、"alice"……）都在其父 dentry 的哈希表 `d_subdirs` 中查找对应子 dentry。命中 dcache → 直接拿到（纯内存）；未命中 → 调具体文件系统的 `lookup()` 从磁盘读出并新建 dentry 入缓存。各 dentry 通过 `d_parent` / `d_child` 指针串联成树状结构。
3. **绝对路径的查找成本 = 分量数 × 单次 dentry 哈希查找**：即使全部命中缓存，`open("/home/alice/work/data.txt")` 也要做 5 次哈希查找。如果进程反复 open 同一深路径下的不同文件，前 N-1 级的查找完全是重复劳动。

**为什么 pwd 不存字符串？** 如果 pwd 存的是字符串 `"/home/alice/work"`，那每次 `open("data.txt")` 先要拼接成 `"/home/alice/work/data.txt"`，然后从 `fs->root` 出发重复上述 5 级 dentry 遍历——每次 I/O 都白花一遍路径解析的开销。而 `struct path` 的 pwd = `{mnt, dentry_of_work}` 直接跳到第 4 级，`open("data.txt")` 只需做 1 次查找：**5 级 → 1 级，省掉 80% 的无用功**。

存 `struct path` 的好处是 **一次性解析，永久复用**：`chdir` 时做一次 `path_lookup`，把结果（dentry + vfsmount）缓存到 `fs->pwd`；此后所有相对路径操作直接从已缓存的 dentry 出发向下查找，无需反复解析前段路径。下面的时序图展示了这个过程：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor #1976D2
}
participant "进程" as P
participant "VFS" as V
participant "dentry cache" as DC
== 阶段一：chdir — 字符串→dentry，只做一次 ==
P -> V : chdir("/home/alice/work")
V -> V : path_lookup("/home/alice/work")
V -> DC : 逐级查找并缓存 dentry\n( / → home → alice → work )
V -> P : fs->pwd = {mnt, dentry_of_work}
note over P
pwd 已缓存，后续相对路径\n无需再解析 "/home/alice/work"
end note
== 阶段二：后续 open — 直接从缓存 dentry 出发，O(目录深度) ==
P -> V : open("data.txt")
V -> DC : 从 pwd.dentry 下直接查找 "data.txt"\n(work 的目录项已在缓存中)
@enduml
```

### 1.2 相对路径解析全过程

用户代码调用 `open("data/config.json", O_RDONLY)`，glibc 在进入内核前做一层转换：**相对路径 → `openat(AT_FDCWD, ...)`**。这是 Linux 2.6.16 引入的 `*at` 系统调用族（`openat` / `fstatat` / `unlinkat` / `renameat`……），统一用 `dirfd` 参数指定"从哪个目录出发解析"，彻底取代了老式 `open()` 内核里硬编码 `current->fs->pwd` 的做法。时序如下：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor #1976D2
}
participant "用户代码" as U
participant "glibc" as G
participant "VFS" as V
participant "pwd (fs->pwd)" as PWD
participant "inode" as I
U -> G : open("data/config.json", O_RDONLY)
note over G
相对路径 → 转为 openat(AT_FDCWD, ...)
end note
G -> V : openat(AT_FDCWD, "data/config.json")
V -> PWD : 获取当前 task->fs->pwd
PWD -> V : 返回 (mnt, dentry)
V -> V : 从 pwd.dentry 出发\n逐级解析 "data"/"config.json"
V -> I : 找到 inode, 权限检查
I -> V : file 对象
V -> U : fd=3
@enduml
```

流程拆解：

1. **用户态 glibc 转换**：`open("data/config.json")` 在 glibc 内部被重写为 `openat(AT_FDCWD, "data/config.json")`。glibc 不碰路径字符串本身，只是把"用哪个起点"的信息从隐式（当前目录）变成显式（`AT_FDCWD` 参数）。
2. **内核获取 pwd**：`openat` 系统调用进入 VFS 层，遇到 `dirfd == AT_FDCWD`（即 `-100`），内核读 `current->fs->pwd`，拿到 `{mnt, dentry}`。这个 dentry 就是 §1.1 中 `chdir` 时缓存下来的 work 目录项——**省掉了从 "/" 到 work 的 4 级 dentry 遍历**（回顾上文绝对路径解析的 5 级成本）。
3. **逐级解析残余分量**：以 pwd.dentry 为起点，对 "data" / "config.json" 各做一次 dentry 哈希查找（2 级），找到目标文件的 inode，权限检查后返回 file 对象和 fd。

> **`AT_FDCWD = -100`**（定义在 `<fcntl.h>`）：是发给内核的信号——"用当前工作目录作为起点"。如果传入真实 fd（如先 `open("备份目录", O_RDONLY)` 拿到 `dirfd=3`，再 `openat(3, "file", ...)`），路径解析就从那个具体的目录 fd 出发。`*at` 系统调用族的真正价值在于：**用 dirfd 替代隐式 pwd，避免多线程 chdir 竞态**（一个线程拿着 dirfd 永远不受其他线程 chdir 影响），这正是 §四要讲的多线程共享陷阱的解法。

### 1.3 如何观测 pwd

```bash
ls -l /proc/<pid>/cwd     # 符号链接, 指向当前目录
readlink /proc/<pid>/cwd  # 人类可读的路径
# 启动时改 cwd
cd /tmp && ./myapp &      # 进程的 cwd = /tmp
# 进程内读取
getcwd(buf, size);        # glibc → getcwd 系统调用
```

> **pwd 不一定能"读出来" **：如果 pwd 所在的目录已被删除（`rm -rf`），`getcwd` 可能返回 `(unreachable)` 或被删除的路径。但 `struct path` 里的 `dentry` 仍然有效——因为还有引用计数，dentry 不会被真正释放。

## 二、root —— 进程的根目录

### 2.1 正常情况

对于普通进程，`fs->root` 和系统的 "/" 一样。`open("/etc/hosts")` 就是从 `fs->root` 出发解析 `/etc/hosts`。

### 2.2 chroot —— 把进程关在一个子目录里

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<out>> #E3F2FD
  BorderColor<<out>> #1976D2
  BackgroundColor<<in>> #FFE0B2
  BorderColor<<in>> #EF6C00
}
rectangle "系统真实文件系统" <<out>> as REAL {
  rectangle "/" as ROOTFS
  rectangle "/home" as HOME
  rectangle "/home/jail" as JAIL {
    rectangle "bin/" as BIN
    rectangle "etc/" as ETC
    rectangle "lib/" as LIB
  }
  rectangle "/etc" as REALETC
  rectangle "/usr" as USR
}
rectangle "chroot 后进程视角" <<in>> as JAILED {
  rectangle "/ = /home/jail" as JROOT {
    rectangle "bin/" as JBIN
    rectangle "etc/" as JETC
    rectangle "lib/" as JLIB
  }
}
note bottom of JAILED : 进程看不到 /etc /usr /home\n只能访问 jail 子树
@enduml
```

```bash
# chroot 操作
mkdir -p /home/jail/{bin,lib,etc}
cp /bin/bash /home/jail/bin/
# 把依赖库也拷进去...
chroot /home/jail /bin/bash    # 现在 / = /home/jail
```

### 2.3 chroot 的安全局限
| 可以隔离 | 不能隔离 |
|---------|---------|
| 文件系统视图 | 网络（同一个 IP 端口） |
| `/proc` 的部分内容 | 进程列表（同一个 pid namespace） |
| 用户看到的 "/" | 真正的 root 用户仍能逃逸（有 CAP_SYS_CHROOT 就能 `chroot` 出去） |

> 这也是为什么容器不用 chroot 而用 **pivot_root + mount namespace**：前者有逃逸风险，后者结合 user namespace 即使容器内是 root 也逃不出去。

### 2.4 观测 root

```bash
ls -l /proc/<pid>/root     # 符号链接, 指向进程的根目录
readlink /proc/<pid>/root  # 系统上真实路径
```

## 三、umask —— 新建文件的默认权限掩码

### 3.1 权限计算公式

```bash
实际权限 = 请求权限 & ~umask
```

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<req>> #E3F2FD
  BorderColor<<req>> #1976D2
  BackgroundColor<<mask>> #C8E6C9
  BorderColor<<mask>> #388E3C
  BackgroundColor<<res>> #FFE0B2
  BorderColor<<res>> #EF6C00
}
rectangle "请求权限" <<req>> as REQ
rectangle "umask" <<mask>> as MASK
rectangle "最终权限" <<res>> as RES
note bottom of REQ
open("a", O_CREAT, 0666)\n→ 请求 rw-rw-rw-
end note
note bottom of MASK
默认通常 0022\n→ ----w--w-
end note
note bottom of RES
最终 rw-r--r-- (0644)\n→ 即 0666 & ~0022
end note
note bottom
常见 umask\n0022: 新建文件 644, 目录 755 (推荐)\n0002: 组有写权限\n0077: 只有自己能访问(私密服务)
end note
@enduml
```

### 3.2 umask 的进程继承

- shell 的 `umask` 命令 → 调 `umask()` 系统调用 → 改当前 shell 的 `fs->umask`
- fork 出的子进程**继承**父的 umask
- exec 后**保持** umask（不变）
- 线程**共享**同一个 fs_struct → 同进程的线程 umask 一致

> **Dockerfile 的坑**：`RUN umask 0027` 只影响当前 RUN 层；建议用 `ENV` 或在 ENTRYPOINT 脚本里设。

### 3.3 程序内设置 umask

```c
#include <sys/stat.h>
mode_t old = umask(0027);  // 设新值, 返回旧值
// 此后 open/create 的文件权限 = mode & ~0027
umask(old);                // 恢复
```

## 四、共享语义 —— 什么时候共 fs_struct

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<t>> #C8E6C9
  BorderColor<<t>> #388E3C
  BackgroundColor<<p>> #FFE0B2
  BorderColor<<p>> #EF6C00
}
rectangle "同进程多线程\nCLONE_FS" <<t>> as TH {
  rectangle "task1->fs ──┐" as T1F
  rectangle "task2->fs ──┤" as T2F
  rectangle "task3->fs ──┘" as T3F
}
rectangle "同一个 fs_struct\npwd/root/umask" as SAME
T1F --> SAME
T2F --> SAME
T3F --> SAME
note bottom of TH : 一个线程 chdir →\n所有线程 cwd 都变!\n(多线程里 chdir 是大忌)
rectangle "fork 父子进程\n复制 fs_struct" <<p>> as PROC {
  rectangle "parent->fs → fs_A" as PF
  rectangle "child->fs → fs_B(复制)" as CF
}
PF --> CF : 初始值相同, 但独立\n一个 chdir 不影响另一个
@enduml
```

| 场景 | fs_struct | 影响 |
|------|-----------|------|
| 同进程线程 | 共享（`CLONE_FS`） | **一个线程 chdir → 所有线程 cwd 变化**（多线程里别 chdir） |
| fork 子进程 | 复制新 fs_struct | 各自独立，改不干扰 |
| clone(无 CLONE_FS) | 复制新 fs_struct | 同 fork |
| execve | 保留 fs_struct | cwd/root 不变 |
| 内核线程 | `fs = NULL`（无 cwd/root） | 访问文件需绝对路径，或借用其他 task 的 fs（`set_fs(KERNEL_DS)` 旧接口已废弃） |

> **多线程里 chdir 是大忌**：线程 A 正在读 `./data/file`，线程 B 做 `chdir("/tmp")` → 线程 A 的 `./data/file` 解析失败或误读 `/tmp/data/file`。如果需要：用 `openat(dirfd, ...)` 锁定目录 fd，或保证只有一个线程改 cwd。

## 五、和 chroot/pivot_root/容器的关系

| 操作 | 改什么 | 谁受影响 | 安全强度 |
|------|--------|---------|---------|
| `chdir` | `fs->pwd` | 同线程组共享 fs 的所有线程 | 无安全含义 |
| `chroot` | `fs->root` | 单进程 | 弱——root 可逃逸 |
| `pivot_root` | 整个 mount namespace 的 "/" | 同 mnt namespace 的所有进程 | 强——需要 mount namespace 配合 |
| 容器（Docker） | `pivot_root` + mount ns + user ns | 容器内所有进程 | 强——多隔离层组合 |

## 六、观测

```bash
# 当前工作目录
ls -l /proc/<pid>/cwd
readlink /proc/<pid>/cwd
# 根目录
ls -l /proc/<pid>/root
readlink /proc/<pid>/root
# umask — 无法直接观测当前值（/proc 不导出）
# 间接方法：看进程新建文件的权限, 反推 umask
cat /proc/<pid>/status | grep Umask   # 不！这个不存在
# 近似办法：看子进程创建的文件
strace -f -e openat,creat -p <pid> 2>&1 | grep O_CREAT
```

> `/proc/<pid>/cwd` 是链接到真实目录的符号链接；`/proc/<pid>/root` 也是——如果进程被 chroot 了，`/proc/<pid>/root` 指向 jail 目录，你可以用 `ls -l /proc/<pid>/root/` 查看"进程眼里的 /"。

## 七、一句话总结

> **`fs_struct` 是进程在文件系统里的"立足点"——存着当前目录 pwd（相对路径解析的起点，存 dentry 而非字符串，即使目录被删也能工作）、根目录 root（chroot 把它设成子目录、容器用 pivot_root 换掉它）、umask（新建文件默认去掉的权限位）、引用计数。同进程线程共享 fs_struct（一个 chdir 全员变，多线程大忌），fork 复制。`/proc/<pid>/cwd`/`root` 可观测。**

