# etvd — 第二阶段:内存段 Diff(COW append-only 后端) > 对应 `docs/DESIGN.md` 第二阶段。在第一阶段 FUSE 拦截通路基础上,把透传后端替换为 COW append-only 后端:写入只追加到 `data.raw` 尾部、以 Segment 拼接还原视图,验证五种写入场景并从底层观测每一次 write 的 Segment。 ## 这一步做了什么 - 第一阶段的 FUSE 拦截层保留,后端由「透传真实目录」改为 **COW append-only**: - 物理存储是单个稀疏文件 `data.raw`,唯一写接口是 Append(顺序追加,绝不原地改字节)。 - 每次 write 把新字节追加到 `data.raw` 尾部,得到一个物理段 Segment(`phys_off` / `phys_len` / `log_off`)。 - 一个虚拟文件的当前内容由一组 Segment 按逻辑顺序拼接;段间 gap 即空洞,读返回零。 - 读取按当前视图段链表从 `data.raw` 取字节拼接。 - **全自管内存 VFS**:目录树 + 文件元数据全部内存维护,`data.raw` 只存文件字节。`getattr` / `readdir` / `mkdir` / `unlink` / `rename` 全走内存。 - **即时写 + 当前视图**:write 立即 Append + 生成 Segment/VersionDiff + 增量更新当前视图;read 走当前视图;fsync 对 `data.raw` 落盘并标记版本边界。事务暂存圈定留到第六阶段。 - **底层观测**:每次 write 打印 `[etvd] write op= phys=+ off=.. size=.. data=`,满足「从底层观察每一次写入产生的 Segment」。 - 元数据纯内存,进程重启丢失(第五阶段 KV 持久化);`data.raw` 残留旧字节待后续 GC。 ### 三层结构 ``` 上层程序 (bash / cat / dd / sqlite3) │ 标准 Linux 文件 API ▼ FUSE 拦截层 (src/fuse_ops.cpp) ── 每次调用先记日志,再走 COW 后端 │ ▼ 内存元数据层 (src/meta.cpp) ── 段链表(当前视图)+ 目录树 + history,shared_mutex 保护 │ ▼ 物理存储层 (src/store.cpp) ── data.raw,Append-only / 按偏移读 ``` ## 依赖 Linux + `/dev/fuse`、`libfuse3-dev`、`pkg-config`、`g++`(C++17)、`make`、`sqlite3`(测试用)。 ```bash sudo apt-get install -y libfuse3-dev pkg-config g++ make sqlite3 ``` ## 构建 ```bash make ``` 产出可执行文件 `etvd_fuse`。 ## 运行 ```bash ./etvd_fuse mnt -o backend=./backend ``` - `-o backend=DIR`:存放 `data.raw` 的目录(默认 `./backend`,不存在会自动创建)。 - `-f` 前台运行(**已强制开启**);`-o default_permissions`(**已强制开启**)。 - 日志输出到 stderr。 ### 卸载 ```bash fusermount3 -u mnt ``` ## 验收测试 ```bash bash tests/phase2.sh ``` 脚本覆盖五种写入场景 + SQLite + 目录操作,并校验底层日志的 Segment 观测,末尾打印 `RESULT: pass=N fail=M`。 ### 五种写入语义 | 场景 | 行为 | 日志可见 | |------|------|----------| | 追加写 | `echo >>` | `write op=Append phys=<尾部off>+` | | 随机覆盖写 | `dd seek=` / SQLite UPDATE 改页 | `write op=Overwrite phys=<新off>+`(旧字节仍在 `data.raw`,仅视图摘出) | | 截断缩小 | `truncate -s N` | `truncate size=N`,视图裁剪,不产生新物理字节 | | 截断扩大 / 扩展写 | `truncate -s M` / `dd seek=超末尾` | `write op=Extend`,中间空洞读返回零 | | Fsync 边界 | SQLite COMMIT | `fsync datasync=..`,对 `data.raw` 落盘 | ## 实现要点 - **分层**:`store`(物理 Append/Read/Sync)+ `meta`(段链表算法 + 目录树 + history)+ `fuse_ops`(回调)。`meta` 方法不加锁,由 `fuse_ops` 经 `Meta::Lock()`(`shared_mutex`)在外层界定读写锁;write 持写锁跨 `Store::Append`,保证段落盘与视图更新原子。 - **段链表更新**:write 区间 `[off,off+len)` 把相交旧段拆成前缀/后缀片段、丢弃被覆盖中段、插入新段,保持升序不重叠,gap 即空洞。truncate 缩小裁剪段、扩大只增长 `total_size`。 - **即时写 + 当前视图**:当前视图(`FileView`)是 cur 的朴素前身;第三阶段才在其上做 LRU 缓存 / 聚合版本 / 多 Block 分块。`history`(`VersionDiff` 列表)保留供观测与未来 MVCC,不参与读路径。 - **全自管内存 VFS**:根 `/` 在 `Meta` 构造时建立;路径经 `Parent`/`Base` 拆分管理父子;rename 目录时遍历子树改 key 前缀。 - **fsync 真落盘**:fsync 回调 `::fsync(data.raw fd)`,SQLite 依赖其持久性。 - **锁**:`lock` 回调对 `F_GETLK` 返回 `F_UNLCK`、`F_SETLK` 返回 0,SQLite fcntl 建议锁单进程正常运行。真实互斥留第十一阶段。 - **default_permissions**:getattr 返回内存元数据的 mode/uid/gid,内核据此校验;以当前用户身份运行,文件 0644 / 目录 0755。 - **重启语义**:`data.raw` 的 append 游标从文件大小恢复(继续追加、不覆写旧字节),但内存元数据丢失,旧字节成孤儿待 GC。 - **未实现 op**(readlink / symlink / fallocate / xattr)保持 NULL,FUSE 返回 `-ENOSYS`。 ## 第二阶段边界(明确不做) cur 的 LRU 缓存 / 聚合版本 / 多 Block 分块(第三阶段)、KV 持久化(第五阶段)、事务 TxnBegin/End(第六阶段)、Prefix 策略(第七阶段)、MVCC 恢复(第八阶段)、etcd 协议(第九阶段)、QUIC mTLS Raft(第十阶段)、分布式租约(第十一阶段)、GC 空洞回收、冷数据归档、symlink / 设备文件。 ## 文件 ``` src/store.h/.cpp 物理存储:data.raw Append / Read / Sync src/meta.h/.cpp 内存元数据:Segment/VersionDiff/FileView/FileMeta + 段链表算法 + 目录树 src/fuse_ops.h/.cpp FUSE3 回调:日志 + 走 COW 后端 + ops 表 src/main.cpp 参数解析、绝对路径、打开 data.raw、初始化 Meta、挂载入口 src/logger.h 线程安全操作日志 + hex 预览 tests/phase2.sh 五种场景 + SQLite 验收脚本 Makefile pkg-config fuse3 ```