﻿# FUSE —— 用户态文件系统

> 前置阅读 [vfs-overview.md](/concepts/vfs/vfs-overview.md)（四大对象模型）、[vfs-file-operations.md](/concepts/vfs/vfs-file-operations.md)（`file_operations` 多态分发 + `private_data` 设计模式）。本文聚焦 FUSE 如何利用 VFS 接口把文件系统实现搬到用户态，以及多挂载点的隔离机制。

## 零、一句话认知

**FUSE（Filesystem in Userspace）不是一种文件系统格式——它是一种"代理架构"**。内核中的 `fuse.ko` 模块不存储任何数据，它只是一个**转发器**：把 VFS 下发的 `read/write/open/mkdir` 等操作封装成 FUSE 协议消息，通过 `/dev/fuse` 发给用户态守护进程，由守护进程处理后再返回结果。用户态程序可以实现任意后端（S3、HTTP、加密、压缩），内核只负责把 VFS 接口和 `/dev/fuse` 字符设备对接起来。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<user>> #C8E6C9
  BorderColor<<user>>     #388E3C
  BackgroundColor<<vfs>>  #BBDEFB
  BorderColor<<vfs>>      #1976D2
  BackgroundColor<<fuse>> #FFE0B2
  BorderColor<<fuse>>     #E65100
}
rectangle "用户应用\ncat /mnt/fuse/file.txt" <<user>> as APP
rectangle "glibc\nopen / read / write" <<user>> as GLIBC
rectangle "VFS 层\n─────────────\nsuper_block\ninode\ndentry\nfile\nfile_operations" <<vfs>> as VFS
rectangle "FUSE 内核模块\nfuse.ko\n─────────────\nfuse_conn\nfuse_file_info\n请求队列" <<fuse>> as FUSE
rectangle "/dev/fuse\n字符设备" <<fuse>> as DEVFUSE
rectangle "用户态 FUSE 守护进程\n─────────────\nlibfuse / fuse3\n事件循环\n→ 读 /dev/fuse\n→ 处理请求\n→ 写回响应" <<user>> as DAEMON
rectangle "后端存储\n─────────────\n本地目录 / S3 / HTTP\n/ 数据库 / 加密层" <<user>> as BACKEND
APP --> GLIBC
GLIBC --> VFS : syscall
VFS --> FUSE : file->f_op = fuse_file_operations
FUSE --> DEVFUSE : 请求入队 → 等待用户态读取
DEVFUSE --> DAEMON : read(dev_fuse_fd) 取出请求
DAEMON --> BACKEND : 操作实际存储
BACKEND --> DAEMON : 返回数据/状态
DAEMON --> DEVFUSE : write(dev_fuse_fd) 写回响应
DEVFUSE --> FUSE : 响应出队 → 唤醒等待的 VFS 操作
FUSE --> VFS : 返回结果
VFS --> APP : read/write 返回
@enduml
```

## 一、FUSE 的关键内核数据结构

### 1.1 `struct fuse_conn` —— 挂载会话的"连接上下文"

每个 FUSE 挂载点在内核中对应一个独立的 `fuse_conn`，它是整个挂载会话的核心——保存请求队列、inode 缓存、配置参数、通信状态等。访问路径：

```bash
用户访问 /mnt/fuse1/some/file
  → VFS 找到该路径对应的 super_block
  → super_block->s_fs_info = fuse_conn  ← 这就是本挂载点的会话上下文
```

```c
// fs/fuse/fuse_i.h (简化)
struct fuse_conn {
    struct list_head    devices;          // 连接到本 conn 的 /dev/fuse fd 列表
    struct list_head    queue;            // 待处理的请求队列（pending）
    wait_queue_head_t   waitq;            // 等待队列——用户态守护进程阻塞在这里
    struct list_head    bg_queue;         // 后台请求队列
    u64                 reqctr;           // 请求计数器
    struct inode        *ctl_inode;       // 控制 inode
    unsigned int        max_pages;        // 单次请求最大页面数
    // ... 挂载选项、配置、统计 ...
};
```

> **关键**：`fuse_conn` 不是通过 `file->private_data` 访问的，而是通过 `super_block->s_fs_info`。这是区分"这个请求属于哪个挂载点"的核心机制。

### 1.2 `struct fuse_file_info` —— 单次打开文件的会话状态

每次 `open()` 一个 FUSE 文件，内核分配一个 `fuse_file_info`，挂到 `file->private_data`：

```c
struct fuse_file_info {
    u64     fh;          // FUSE 文件句柄（由用户态守护进程分配，替代内核的 fd）
    u32     open_flags;  // 打开标志（O_RDONLY / O_RDWR 等）
    u64     nodeid;      // 该文件在 FUSE 守护进程中的 inode 号
    // ...
};
```

**这里体现了 VFS 的 `private_data` 模式**：同一套 `fuse_file_operations`（全局唯一），通过 `file->private_data` 区分不同的打开会话。详见 [vfs-file-operations.md](/concepts/vfs/vfs-file-operations.md) §四。

## 二、多挂载点隔离：`super_block->s_fs_info` 是关键

### 2.1 隔离机制

```bash
┌─────────────────────────────────────────────────────┐
│  挂载点 A: /mnt/fuse1                                │
│  super_block_A                                       │
│    → s_fs_info = fuse_conn_A                         │
│      → 独立的请求队列                                 │
│      → 独立的 inode 缓存                              │
│      → 绑定 /dev/fuse fd_A（用户态守护进程 A 持有）    │
├─────────────────────────────────────────────────────┤
│  挂载点 B: /mnt/fuse2                                │
│  super_block_B                                       │
│    → s_fs_info = fuse_conn_B                         │
│      → 独立的请求队列                                 │
│      → 独立的 inode 缓存                              │
│      → 绑定 /dev/fuse fd_B（用户态守护进程 B 持有）    │
└─────────────────────────────────────────────────────┘
所有 FUSE 文件共用一套 fuse_file_operations（f_op 全局唯一）
挂载点之间的隔离全靠 super_block → fuse_conn 的不同实例
```

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<vfs>>  #BBDEFB
  BorderColor<<vfs>>      #1976D2
  BackgroundColor<<fuse>> #FFE0B2
  BorderColor<<fuse>>     #E65100
}
together {
rectangle "fuse_file_operations\n(全局唯一, 所有挂载点共用)" <<vfs>> as FOPS
}
rectangle "super_block_A\n/mnt/fuse1" <<vfs>> as SBA
rectangle "super_block_B\n/mnt/fuse2" <<vfs>> as SBB
rectangle "fuse_conn_A\n· 请求队列\n· inode 缓存\n· 配置" <<fuse>> as CONNA
rectangle "fuse_conn_B\n· 请求队列\n· inode 缓存\n· 配置" <<fuse>> as CONNB
rectangle "/dev/fuse fd_A" <<fuse>> as FDA
rectangle "/dev/fuse fd_B" <<fuse>> as FDB
FOPS -down-> SBA : VFS 路由
FOPS -down-> SBB : VFS 路由
SBA --> CONNA : s_fs_info
SBB --> CONNB : s_fs_info
CONNA --> FDA : 守护进程 A 持有
CONNB --> FDB : 守护进程 B 持有
note bottom of CONNA
  同一个对象存储桶
  挂载到两个路径：
  /mnt/bucket1/a.txt
  /mnt/bucket2/a.txt
  → 两个独立 inode
  → 两套独立缓存
  → 内核不会混淆
end note
@enduml
```

### 2.2 `/dev/fuse` 绑定规则

**一个 `/dev/fuse` fd 只能绑定一个挂载会话**。每新增一个挂载点，必须重新 `open("/dev/fuse")` 获取新的 fd：

```c
// 挂载点 A
fd_A = open("/dev/fuse", O_RDWR);
mount("/dev/fuse", "/mnt/fuse1", "fuse", flags, fd_A);
// 内核: fd_A → 创建 fuse_conn_A → sb_A->s_fs_info = fuse_conn_A
// 挂载点 B（必须用新 fd！）
fd_B = open("/dev/fuse", O_RDWR);
mount("/dev/fuse", "/mnt/fuse2", "fuse", flags, fd_B);
// 内核: fd_B → 创建 fuse_conn_B → sb_B->s_fs_info = fuse_conn_B
```

> **如果用同一个 fd 执行两次 mount，内核会报错**——因为 fd 已经绑定到第一个 `fuse_conn`，不能复用给第二个挂载点。

## 三、VFS 请求在 FUSE 中的完整流转

以 `read("/mnt/fuse/file")` 为例：

```bash
1. 用户态: read(fd, buf, 4096)
     │
2. VFS: ksys_read(fd)
     │  file = fdget(fd)
     │  file->f_op->read_iter(file, ...)  ← fuse_file_operations 中的函数
     ▼
3. FUSE 内核模块: fuse_read_iter()
     │  从 file->private_data 取出 fuse_file_info
     │  从 file->f_inode->i_sb->s_fs_info 取出 fuse_conn
     │  构造 FUSE 请求（FUSE_READ, 带上 nodeid + fh + offset + size）
     │  请求入队 → fuse_conn.queue
     │  唤醒等待在 /dev/fuse 上的守护进程
     │  当前进程阻塞（TASK_INTERRUPTIBLE）
     ▼
4. 用户态守护进程:
     │  read(dev_fuse_fd, buf, sizeof(fuse_in_header) + payload)
     │  解析请求 → 调用对应后端（读 S3 / HTTP / 本地磁盘）
     │  拿到数据后 → 构造 FUSE 响应
     ▼
5. 守护进程写回:
     │  write(dev_fuse_fd, response, sizeof(fuse_out_header) + data)
     ▼
6. FUSE 内核模块:
     │  响应出队 → 匹配原始请求
     │  拷贝数据到用户态 buf
     │  唤醒阻塞的 VFS read 调用
     ▼
7. 返回用户态: read() 返回读取的字节数
```

### 3.1 调度分析：进内核态必然触发调度吗？

**进内核态本身不必然触发调度**。普通 syscall（如 `getpid()`）执行完直接 `iretq` 返回，全程不碰调度器。

**但 FUSE 场景下必定触发调度**——因为内核自己完成不了请求（数据在用户态守护进程手里），内核必须把调用者**挂起睡眠**、把 CPU 让给守护进程。具体涉及的调度点：

```bash
时间线
═══════
                    syscall 入口
                         │
  调用者进程              ▼
  (cat /mnt/fuse/f)   VFS → fuse_read_iter()
                         │
                         │  ① 请求入队后，调用者主动睡眠
                         ▼  set_current_state(TASK_INTERRUPTIBLE)
                      调度点 1: schedule()          ← 让出 CPU，切到其他进程
                         ║
                         ║  上下文切换（调用者 → 守护进程）
                         ║
  守护进程                ║
  (之前阻塞在             ▼
   read(/dev/fuse))   唤醒：wake_up(&fud->waitq)
                         │
                         │  守护进程从 read(/dev/fuse) 返回
                         │  拿到 FUSE 请求 → 访问后端存储
                         │
                         │  后端返回数据 → 构造响应
                         │  write(dev_fuse_fd, response)
                         │     ↓
                         │  ② 响应入队，唤醒调用者
                         │  wake_up(fuse_conn->waitq)
                         │
                         │  ③ 守护进程继续循环：
                         │  read(dev_fuse_fd) ← 无新请求，阻塞
                         ▼
                      调度点 2: schedule()          ← 守护进程睡眠，让出 CPU
                         ║
                         ║  上下文切换（守护进程 → 调用者 或 其他进程）
                         ║
  调用者进程              ║
                         ▼
                      被唤醒 → 从 schedule() 返回
                         │  fuse_read_iter() 拷贝数据 → 返回到 VFS
                         │  ksys_read() → iretq → 回到用户态
```

**最少触发 2 次 schedule()：**

| 调度点 | 谁触发 | 原因 |
|--------|-------|------|
| ① `schedule()` | 调用者（cat 进程） | `TASK_INTERRUPTIBLE` 睡眠，等待守护进程响应 |
| ② `schedule()` | FUSE 守护进程 | `read(/dev/fuse)` 无新请求，阻塞等待 |

**最少 2 次 context switch：**
1. 调用者 → 守护进程（调度点 ① 之后）
2. 守护进程 → 调用者（调度点 ② 之后，或中间经过其他进程）

> **对比**：普通磁盘 I/O 也阻塞也调度，但区别在于 **FUSE 的等待链多了一个用户态环节**——普通 read(ext4) 是：调用者睡眠 → 磁盘中断唤醒 → 返回。FUSE read 是：调用者睡眠 → 守护进程被唤醒（调度）→ 守护进程访问后端（可能再次睡眠/调度）→ 守护进程 write /dev/fuse → 调用者被唤醒（再次调度）。每个简单的 `read()` 在 FUSE 上至少产生 **2 次调度 + 2 次上下文切换**，这是 FUSE 性能开销的重要来源之一。

### 3.2 守护进程侧：事件循环也依赖调度

```c
// libfuse 事件循环（简化）
while (1) {
    struct fuse_buf buf;
    ssize_t res = read(dev_fuse_fd, buf, bufsize);
    //     ↑ 无请求时，守护进程在这里 TASK_INTERRUPTIBLE 睡眠
    //       内核调度器会把它切走，CPU 给别人用
    //       有新请求到来 → wake_up → 守护进程变为 TASK_RUNNING
    //       → 调度器下次选中时 → read() 返回 → 开始处理
    fuse_session_process_buf(se, &buf);  // 解析请求、调 handler
    // ... handler 完成后 write(dev_fuse_fd, response)
}
```

守护进程大部分时间都在 `read(/dev/fuse)` 上**阻塞睡眠**——这是设计意图：没有请求时不消耗 CPU。有请求时才被内核唤醒。

> **一句话总结**：FUSE 的每一次 I/O 都伴随 **2+ 次调度**——这不是 bug，是 FUSE 把文件系统搬到用户态的必然代价。普通文件系统在内核态一条龙完成，FUSE 必须在用户态和内核态之间来回踢皮球，每次踢球都意味着一次 context switch。

## 四、两种实现方案

### 方案 1：多进程多挂载点（标准方案，libfuse 默认）

每个挂载点一个独立进程 + 独立 `/dev/fuse` fd：

```bash
进程A                           进程B
  │                               │
  ├─ open("/dev/fuse") → fd_A     ├─ open("/dev/fuse") → fd_B
  ├─ mount → /mnt/fuse1           ├─ mount → /mnt/fuse2
  ├─ fuse_loop(fd_A)              ├─ fuse_loop(fd_B)
  │   while(1) {                  │   while(1) {
  │     read(fd_A, req);          │     read(fd_B, req);
  │     process(req);             │     process(req);
  │     write(fd_A, resp);        │     write(fd_B, resp);
  │   }                           │   }
```

- **优点**：简单、天然隔离、崩溃互不影响、所有开源 FUSE 工具默认方式（s3fs、mergerfs、zipfs、sshfs）
- **缺点**：多进程，资源开销略高

### 方案 2：单进程多会话（高级用法，需 libfuse3）

单个进程管理多个挂载点，内部维护多个 `fuse_session`：

```c
// libfuse3 单进程多挂载点简化示例
struct fuse_session *se1 = fuse_session_new(&ops1, &se1_data);
struct fuse_session *se2 = fuse_session_new(&ops2, &se2_data);
fuse_session_mount(se1, "/mnt/fuse1");
fuse_session_mount(se2, "/mnt/fuse2");
// 同一个事件循环处理多个 session
// 每个 session 对应内核中独立的 fuse_conn
fuse_loop_mt(se1);  // 或使用 fuse_session_loop_mt 同时驱动多个
```

- **适用场景**：需要统一管理多个挂载目录、不想启动大量进程的服务
- **限制**：libfuse3 才完善支持；多个 session 共用进程内存空间，需注意全局变量隔离（每个 session 绑定自己的配置数据，不要用全局变量保存挂载点相关配置）

## 五、FUSE 中的三层隔离设计

FUSE 完美体现了 VFS 的分层设计思想：

| 层级 | 存储位置 | 粒度 | 作用 |
|------|---------|------|------|
| **① `file_operations`** | 全局变量 | 类型级别 | 所有 FUSE 文件共用同一套函数代码 |
| **② `file->private_data`** | `struct file` | 单次打开 | 区分**同一挂载点内**不同 `open()` 的会话（偏移量、打开标志） |
| **③ `super_block->s_fs_info`** | `struct super_block` | 每个挂载点 | 区分**不同挂载点**本身（inode 编号空间、缓存、请求队列全部隔离） |

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<fuse>> #FFE0B2
  BorderColor<<fuse>>     #E65100
  BackgroundColor<<vfs>>  #BBDEFB
  BorderColor<<vfs>>      #1976D2
}
rectangle "fuse_file_operations\n(全局 1 份)" <<vfs>> as FOPS
rectangle "super_block_A\n/mnt/fuse1" <<vfs>> as SBA
rectangle "super_block_B\n/mnt/fuse2" <<vfs>> as SBB
rectangle "fuse_conn_A\n(第③层隔离)" <<fuse>> as CA
rectangle "fuse_conn_B\n(第③层隔离)" <<fuse>> as CB
rectangle "file_X → fuse_file_info_X\n(第②层隔离)" <<fuse>> as FFX
rectangle "file_Y → fuse_file_info_Y\n(第②层隔离)" <<fuse>> as FFY
FOPS --> SBA
FOPS --> SBB
SBA --> CA : s_fs_info
SBB --> CB : s_fs_info
note right of FFX
  /mnt/fuse1 内两次 open
  生成两个 file，两个 fuse_file_info
  f_op 相同，fuse_conn 相同
  private_data 不同 → 偏移量独立
end note
@enduml
```

## 六、容易踩坑的关键点

1. **一个 `/dev/fuse` fd 只能绑定一个挂载点** —— 复用 fd 执行第二次 mount 会失败
2. **单进程多挂载点时，避免全局变量** —— 所有挂载点的回调共用同一份代码，全局变量会导致会话混淆；每个挂载点的配置必须绑定到 `fuse_session` 的私有数据上
3. **挂载命名空间** —— 如果需要部分进程可见，可结合 Linux mount namespace 实现容器级别的挂载点隔离
4. **libfuse2 的多会话局限** —— 老版本不支持多 session，容易出现信号、全局变量冲突

## 七、延伸阅读

- [vfs-overview.md](/concepts/vfs/vfs-overview.md) — VFS 四大对象模型、多态分发机制
- [vfs-file-operations.md](/concepts/vfs/vfs-file-operations.md) — `file_operations` 逐字段拆解 + `f_op` vs `private_data` 设计模式
- [vfs-and-socket.md](/concepts/vfs/vfs-and-socket.md) — socket 如何利用 VFS 的 `private_data` 桥接网络协议栈
- [../process/task-resources/files-struct.md](/concepts/process/task-resources/files-struct.md) — 进程 fd 表 → `struct file` 的完整链路
- [../io/read-write-process.md](/concepts/io/read-write-process.md) — `read()` 从 syscall 到 DMA 的 10 层调用链

## 一句话总结

**FUSE 是 VFS 多态分发的典范应用——内核通过 `super_block->s_fs_info` 隔离挂载点、通过 `file->private_data` 隔离打开会话、通过全局 `fuse_file_operations` 复用函数代码；用户态守护进程只需 `read/write` 一个 `/dev/fuse` fd 就能实现完整文件系统的语义。**

