jabberwocky238 3b72884d9f DESIGN
2026-07-07 00:18:31 +08:00
2026-07-07 00:18:31 +08:00
2026-07-07 00:07:58 +08:00
2026-07-07 00:07:58 +08:00
2026-07-07 00:07:58 +08:00
2026-07-07 00:18:31 +08:00

etvd — 第一阶段:FUSE 拦截验证

对应 docs/DESIGN.md 第一阶段。目标:让 FUSE Daemon 挂载目录、拦截所有文件系统调用,上层程序能正常 open / read / write / close,并能在底层结构化观察到每一次写入,证明通路打通。

这一步做了什么

  • 一个基于 libfuse3 的 C++ FUSE Daemonetvd_fuse),挂载一个目录,拦截全部文件系统调用。
  • 透传后端(passthrough:所有操作转发到真实后端目录(默认 ./backend)。这是第一阶段的后端;第二阶段会替换为 COW append-only 后端(data.raw + Segment),第三阶段再引入 cur 快照加速读,FUSE 拦截层与日志层保持不变。
  • 操作日志层:每一次拦截到的调用都结构化打印到 stderr([etvd] <op> <path> <detail>),write 额外带 off/size/rc 与前 16 字节 hex 预览 —— 满足「从底层观察到每一次写入」。
  • 回调里直接 log + syscall 透传,没有额外抽象层FUSE3 自己的 struct fuse_operations 就是接口)。第二阶段做 COW 后端时再按需引入存储抽象 —— 届时 write 语义是 append+Segment,和现在的 pwrite 形状不同,现在猜接口多半对不上。

三层结构

上层程序 (bash / cat / dd / sqlite3)
        │  标准 Linux 文件 API
        ▼
FUSE 拦截层  (src/fuse_ops.cpp)   ── 每次调用先记日志,再直接 syscall 透传到 ./backend

依赖

  • Linux + /dev/fuse(内核 FUSE 模块)
  • libfuse3-dev(头文件 + fuse3.pc
  • pkg-configg++C++17)、make

安装依赖(Ubuntu/Debian):

sudo apt-get install -y libfuse3-dev pkg-config g++ make

构建

make

产出可执行文件 etvd_fuse

运行

# 挂载到 mnt/,后端目录 ./backend(两者不存在会自动创建)
./etvd_fuse mnt -o backend=./backend
  • -o backend=DIR:指定后端目录(默认 ./backend)。
  • -f:前台运行(已强制开启,便于日志输出与 Ctrl-C 卸载)。
  • -o default_permissions已强制开启,内核按透传的 mode 做权限校验。
  • 日志输出到 stderr。

卸载

fusermount3 -u mnt

验收测试

# 1. 启动 daemon(前台,日志重定向到文件)
./etvd_fuse mnt -o backend=./backend 2>etvd.log &

# 2. 基本读写
echo "hello world" > mnt/a.txt
cat mnt/a.txt                  # -> hello world
echo "second line" >> mnt/a.txt

# 3. 块写 / 目录 / 重命名 / 删除 / 截断
dd if=/dev/zero of=mnt/b.bin bs=4096 count=4 status=none
mkdir mnt/sub && ls -F mnt
mv mnt/a.txt mnt/a.log
rm mnt/b.bin
truncate -s 100 mnt/a.log

# 4. SQLite 冒烟(真实数据库负载,无感知运行)
sqlite3 mnt/test.db "CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT); \
  INSERT INTO t VALUES (1,'alice'); INSERT INTO t VALUES (2,'bob'); SELECT * FROM t;"
sqlite3 mnt/test.db "UPDATE t SET name='ALICE' WHERE id=1; SELECT * FROM t;"
sqlite3 mnt/test.db "PRAGMA integrity_check;"

# 5. 观察底层拦截日志
grep '\[etvd\] write' etvd.log          # 每次 write 的 off/size/hex 预览
grep '/test.db' etvd.log | grep write   # 随机页覆盖写(off=0 / off=4096
grep -E 'create|unlink' etvd.log | grep journal  # rollback journal 生命周期
grep -c fsync etvd.log                  # 事务刷盘调用

# 6. 卸载
fusermount3 -u mnt

已验证的写入语义

场景 行为 日志可见
追加写 echo >> write off=<文件末尾> size=N
块写 dd bs=4k count=4 4 次 write off=0/4096/8192/12288 size=4096
随机覆盖写 SQLite UPDATE 改页 write off=4096 size=4096(页 1 原地覆盖)
截断 truncate -s 100 truncate size=100
journal SQLite 事务 create /test.db-journalunlink 每事务一次
刷盘 SQLite COMMIT fsync

实现要点

  • 绝对路径libfuse3 在跑事件循环前会 chdir("/"),因此 daemon 在请求时解析的所有路径(后端根目录)必须是绝对路径。main 在挂载前把 backendmountpointrealpath 成绝对路径。
  • 文件句柄open/create 打开真实 fd 存入 fuse_file_info->fhread/writepread/pwriterelease 关闭。
  • lock 回调对 F_GETLK 返回 F_UNLCK、对 F_SETLK 返回 0,使 SQLite 的 fcntl 建议锁在单进程下正常运行。真实互斥留到第十一阶段的分布式写租约。
  • 未实现的 opxattr / fallocate 等)保持 NULLFUSE 返回 -ENOSYSSQLite 等会自动回退。

第一阶段边界(明确不做)

COW / append-only、Segment / VersionDiff、cur 快照、KV 持久化、前缀策略、MVCC、Raft、租约、多节点、QUIC —— 全部留到后续阶段。当前唯一目标是证明 FUSE 拦截通路打通,且能观测每一次写入。

文件

src/logger.h                 线程安全操作日志 + hex 预览
src/fuse_ops.h/.cpp          FUSE3 回调:日志 + 直接 syscall 透传 + ops 表
src/main.cpp                 参数解析(fuse_opt)、绝对路径处理、挂载入口
Makefile                     pkg-config fuse3
S
Description
No description provided
Readme 144 KiB
Languages
C++ 72.9%
Python 15.9%
Shell 10%
C 0.6%
Makefile 0.6%