3b72884d9f303c9d3eb8826cad996ae54744bbca
etvd — 第一阶段:FUSE 拦截验证
对应
docs/DESIGN.md第一阶段。目标:让 FUSE Daemon 挂载目录、拦截所有文件系统调用,上层程序能正常 open / read / write / close,并能在底层结构化观察到每一次写入,证明通路打通。
这一步做了什么
- 一个基于 libfuse3 的 C++ FUSE Daemon(
etvd_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-config、g++(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-journal → unlink 每事务一次 |
| 刷盘 | SQLite COMMIT |
fsync |
实现要点
- 绝对路径:libfuse3 在跑事件循环前会
chdir("/"),因此 daemon 在请求时解析的所有路径(后端根目录)必须是绝对路径。main在挂载前把backend与mountpoint都realpath成绝对路径。 - 文件句柄:
open/create打开真实 fd 存入fuse_file_info->fh;read/write用pread/pwrite;release关闭。 - 锁:
lock回调对F_GETLK返回F_UNLCK、对F_SETLK返回 0,使 SQLite 的 fcntl 建议锁在单进程下正常运行。真实互斥留到第十一阶段的分布式写租约。 - 未实现的 op(xattr / fallocate 等)保持 NULL,FUSE 返回
-ENOSYS,SQLite 等会自动回退。
第一阶段边界(明确不做)
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
Description
Languages
C++
72.9%
Python
15.9%
Shell
10%
C
0.6%
Makefile
0.6%