# kvfs **Repository Path**: logeexpluoqi/kvfs ## Basic Information - **Project Name**: kvfs - **Description**: 一个小巧的键值文件系统库,可以非常容易集成到小容量单片机中,且实现与硬件平台无关,储存介质的读取和写入由外部函数提供 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-22 - **Last Updated**: 2026-09-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # KVFS — 嵌入式设备键值文件系统 KVFS 是一个专为资源受限的单片机(MCU)设计的轻量级键值文件系统。键值对数量仅受储存容量限制,无固定上限,无需预分配索引表。 > **介质要求(重要)**:KVFS 默认(不提供 `ops->erase`)通过写入 `0xFF` 实现“擦除”(见 `erase_range_`),适用于**字节可任意重写 / 写入即擦除**的介质:RAM、FRAM/MRAM、AT24Cxx 等字节可写 EEPROM。可选地,`KvfsOps` 提供 `erase` 回调把整段擦除委托给介质(如 NOR 扇区擦除),见[第 7.3 节](#73-可选擦除回调kvfsopserase)。裸 NOR/NAND Flash 仍需要回调背后的 FTL/擦写管理层,详见[第 15 节](#15-介质支持与限制),可用 `nor_probe` 工具实测对照。 --- ## 1. 设计理念 | 需求 | 实现方式 | |------|---------| | 最小 RAM/ROM 占用 | 零动态内存分配;所有状态保存在单个 `Kvfs` 结构体中。I/O 缓存由用户提供(≥ `KVFS_BLOCK_SIZE`,默认 32 字节) | | 硬件无关 | 所有储存访问通过 `KvfsOps` 回调(`read`/`write`/`lock`),无芯片特定依赖 | | 数据完整性 | 三重 CRC 保护:CRC32 校验超块头、CRC8 校验条目描述符、CRC16 校验 key+value 数据。损坏条目自动检测并标记删除(自愈) | | 碎片整理 | 空间不足时自动压缩有效条目,回收已删除条目占用的空间 | | 页边界安全 | 回调可返回少于请求量的字节数(真实 EEPROM 行为),库会自动重试直到传输完成 | | 磨损友好 | 采用顺序追加写入(Append-Only),避免对同一位置的反复擦写 | --- ## 2. 核心数据结构 ### 2.1 超块头 — `KvfsHeader`(24 字节,`__attribute__((packed))`) 超块头位于储存介质偏移 0 处,是文件系统的"根"。 ``` Byte 0 1 2 3 4 5 6 7 8 9 10 11 ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐ │ K │ V │ F │ S │ ver │ ver │ flg │ flg │ capacity │ ├────┴────┴────┴────┼────┴────┼────┴────┼────┴────┴────┴────┤ │ magic[4] │version │ flags │ capacity (LE) │ └───────────────────┴─────────┴─────────┴──────────────────┘ Byte 12 13 14 15 16 17 18 19 20 21 22 23 ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐ │pairs│pairs│ res │ res │ used_bytes │ crc32 │ ├────┴────┼────┴────┼────┴────┴────┴────┴────┼────┴────┴────┤ │pair_cnt │reserved │ used_bytes (LE) │ CRC32 (LE) │ └─────────┴─────────┴────────────────────────┴──────────────┘ ``` | 字段 | 偏移 | 大小 | 说明 | |------|------|------|------| | `magic` | 0 | 4 | 固定为 `'K' 'V' 'F' 'S'` (0x4B 0x56 0x46 0x53),用于识别有效文件系统 | | `version` | 4 | 2 | 格式版本号,当前为 1,小端序 | | `flags` | 6 | 2 | 保留,当前为 0 | | `capacity` | 8 | 4 | 储存区总容量(字节),小端序。由 `kvfs_mount` 参数指定 | | `pair_count` | 12 | 2 | 当前有效的键值对数量,小端序 | | `reserved` | 14 | 2 | 保留 | | `used_bytes` | 16 | 4 | 所有条目占用的总字节数(从偏移 24 起算),小端序。也是下次写入的起始偏移 | | `crc32` | 20 | 4 | CRC-32/IEEE 802.3 校验字节 0..19,小端序 | > **CRC32 计算方法**:对超块头前 20 字节(magic 到 used_bytes)计算 CRC-32,结果写入 `crc32` 字段。校验时先将 `crc32` 清零,计算前 20 字节的 CRC-32,与存储值比对。 ### 2.2 条目描述符 — `KvfsPair`(8 字节,`__attribute__((packed))`) 每个键值对条目前 8 字节为此描述符,包含该条目的所有元信息。 ``` Byte 0 1 2 3 4 5 6 7 ┌────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐ │ flags │key_size│val_sz_l│val_sz_h│use_blk │blk_id_l│blk_id_h│ crc8 │ └────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘ ``` | 字节 | 字段 | 说明 | |------|------|------| | 0 | `flags` | `0x00` = 有效 (VALID), `0xFE` = 已删除 (DELETED), `0xFF` = 未使用 (FREE/ERASED) | | 1 | `key_size` | Key 的长度(字节),最大 255 | | 2-3 | `val_size` | Value 的长度(字节),小端序,最大 65535 | | 4 | `use_blk` | 条目占用的 KVFS_BLOCK_SIZE 字节块数,**必须 ≥ 1**(为 0 时扫描会死循环,库已做防护)。受 `uint8_t` 限制最大 255 块 | | 5-6 | `blk_idx` | 数据块起始偏移索引(= 条目起始偏移 / KVFS_BLOCK_SIZE),小端序,仅供参考 | | 7 | `crc8` | CRC-8/MAXIM 校验字节 0..6 | > **CRC8 计算方法**:多项式 `0x8C`(即 x⁸ + x⁵ + x⁴ + 1 的反射形式),初始值 0。对描述符前 7 字节计算。 ### 2.3 条目数据载荷布局 ``` 偏移 大小 内容 ─────────────────────────────── 0 8 KvfsPair 描述符(含 flags, key_size, ...) 8 2 CRC-16/XMODEM(覆盖 key_len + val_len + key + value) 10 ks Key 的原始字节 10+ks vs Value 的原始字节 10+ks+vs pad 用 0xFF 填充至 KVFS_BLOCK_SIZE 字节对齐 ─────────────────────────────── 条目总大小 = ceil((8 + 2 + ks + vs) / KVFS_BLOCK_SIZE) × KVFS_BLOCK_SIZE ``` > **CRC16 计算方法**:多项式 `0x1021`(XMODEM),初始值 0。覆盖顺序为:`key_size`(1字节)→ `val_size`(2字节,LE)→ `key`(ks字节)→ `value`(vs字节)。 --- ## 3. 顺序追加写入(Append-Only Log) KVFS 最核心的设计决策是 **Append-Only**:新数据总是追加到文件系统末尾,从不原地修改。 ### 3.1 写入流程 ``` kvfs_write("mykey", "hello", 5) │ ├─ 1. find_pair_("mykey") ──→ 找到旧条目在 offset=88 │ ├─ 2. 检查空间:HDR_SIZE(24) + used_bytes + new_entry_size ≤ capacity? │ ├─ 不足 → defrag_() 碎片整理 │ └─ 仍不足 → 返回 KVFS_ERR_NOSPACE(旧数据完好!) │ ├─ 3. 标记旧条目为 DELETED(写入 flags=0xFE 到 offset=88) │ pair_count-- │ ├─ 4. 在 used_bytes 处追加新条目 │ ├─ 写 KvfsPair 描述符(8 字节) │ ├─ 写 CRC16(2 字节) │ ├─ 写 key("mykey",5 字节) │ └─ 写 value("hello",5 字节) │ used_bytes += entry_size │ pair_count++ │ └─ 5. sync_hdr_() 将更新后的超块头写回介质 ``` **关键安全保障**:步骤 2(空间检查)在步骤 3(标记删除)**之前**执行。如果空间不足,旧数据完好无损。 ### 3.2 设计优势 | 优势 | 原理 | |------|------| | **写入性能高** | 追加 = 只需一次顺序写入,无需先擦除旧位置 | | **磨损均衡** | 数据在介质上"流动",避免反复擦写同一物理位置 | | **断电安全** | 最坏情况:丢失最后一个未完成写入的条目。超块头在每次写入后更新,保证了元数据一致性 | | **无需垃圾回收调度** | 只有空间不足时才触发 defrag,写入路径可预测 | ### 3.3 更新语义 更新一个已存在的 key 时: 1. 旧条目标记为 `DELETED`(flags 改为 `0xFE`) 2. 新条目追加到末尾 因此更新操作**不会回收旧条目空间**——旧空间在下次 `defrag_()` 时才被回收。这也是 NOSPACE 保护能生效的原因。 --- ## 4. 查找流程(顺序扫描) KVFS 不维护内存中的索引表,查找 key 时从偏移 24(HDR_SIZE)开始顺序扫描。 ``` find_pair_("target_key") │ └─ scan_entries_() │ │ offset = 24 │ while offset < 24 + used_bytes: │ │ │ ├─ read_pair_(offset) → 读 KvfsPair 描述符 │ │ │ ├─ flags != VALID? → 跳过(DELETED 或 FREE) │ │ │ ├─ crc8 校验描述符 → 失败? → 跳过(静默忽略损坏条目) │ │ │ ├─ use_blk == 0? → break(死循环防护) │ │ │ ├─ 回调 find_cb_: │ │ ├─ key_size ≠ target_len? → 不匹配,继续 │ │ ├─ 逐块读取 key 字节 → 逐一比对 │ │ └─ 完全匹配? → found_off = offset,return 1 │ │ │ └─ offset += use_blk × KVFS_BLOCK_SIZE │ └─ found_off > 0 → 成功;否则 → KVFS_ERR_NOENT ``` ### 4.1 复杂度分析 - **时间复杂度**:O(n),n = 条目总数(有效+已删除) - **读放大**:每次查找需读取每个条目的 KvfsPair(8 字节)+ key 字节;数据量约 `n × (8 + avg_key_len)` 字节 - **适用场景**:条目数在数十到数百量级,完全适合嵌入式 MCU 场景 ### 4.2 已删除条目的处理 已删除条目(flags=0xFE)在扫描时被**跳过**,但其占用的空间仍然计入 `used_bytes`,直到 `defrag_()` 整理时才回收。这也是为什么频繁更新同一 key 会产生"空间碎片"——需要 `defrag_()` 回收。 --- ## 5. 碎片整理(Defragmentation) 当追加写入发现空间不足时,自动触发 `defrag_()`。整理过程: ``` defrag_() │ │ read_off = 24, write_off = 24, valid_cnt = 0 │ │ while read_off < 24 + used_bytes: │ │ │ ├─ read_pair_(read_off) → KvfsPair │ ├─ flags == VALID && crc8 OK && use_blk > 0? │ │ ├─ verify_entry_crc_() → CRC16 校验 │ │ │ │ │ ├─ CRC16 OK? │ │ │ ├─ read_off ≠ write_off? │ │ │ │ ├─ 逐块复制条目数据从 read_off → write_off │ │ │ │ ├─ 更新复制后条目的 blk_idx │ │ │ │ └─ 重新计算并写入 crc8 │ │ │ ├─ write_off += entry_size │ │ │ └─ valid_cnt++ │ │ │ │ │ └─ CRC16 失败? → 静默丢弃(垃圾回收) │ │ │ └─ use_blk == 0? → break(死循环防护) │ read_off += use_blk × KVFS_BLOCK_SIZE │ ├─ write_off < read_off? → erase_range_(write_off, read_off - write_off) │ (擦除被移动/删除条目遗留的旧数据) │ └─ 更新 header: used_bytes = write_off - 24, pair_count = valid_cnt sync_hdr_() ``` ### 5.1 视觉示意 ``` 整理前: ┌──────┬─────────┬───────────┬─────────┬───────────┬─────────┐ │Header│ Entry A │ Entry B │ Entry C │ Entry D │ FREE │ │ │ (VALID) │ (DELETED) │ (VALID) │ (DELETED) │ │ └──────┴─────────┴───────────┴─────────┴───────────┴─────────┘ 整理后: ┌──────┬─────────┬─────────┬──────────────────────────────────┐ │Header│ Entry A │ Entry C │ ERASED (0xFF) │ │ │ (VALID) │ (VALID) │ │ └──────┴─────────┴─────────┴──────────────────────────────────┘ ↑ ↑ ↑ read_off= write_off= read_off= write_off write_off+esz old_end (未移动) (从后面搬来) ``` ### 5.2 设计要点 | 要点 | 说明 | |------|------| | **原地紧凑** | 有效条目向前搬移,已删除条目被覆盖 | | **CRC16 二次校验** | 移动前重新校验数据完整性,损坏条目自动丢弃 | | **尾部擦除** | 移动完毕后,擦除尾部被释放的旧区域 | | **blk_idx 更新** | 移动后条目的 `blk_idx` 重算为新的位置索引 | | **crc8 重算** | 因为 `blk_idx` 变了,描述符的 crc8 必须重新计算 | | **搬移缓冲** | 逐块搬移使用独立局部缓冲,**不可借用 `fs->cache`**:`read_range_()/write_range_()` 内部会把整块暂存进 `fs->cache`,而条目偏移(24+块对齐)非块对齐,会覆盖尚未搬出的载荷字节 | | **I/O 错误传播** | 整理过程中任何 I/O 错误立即终止并返回 | --- ## 6. 三层 CRC 保护体系 ``` ┌──────────────────────────────────────────────────────────────┐ │ KvfsHeader (24 bytes) │ │ ┌──────────┬──────┬──────┬──────────┬────┬───────┬───────┐ │ │ │ magic │ ver │flags │ capacity │... │ used │ CRC32 │ │ │ └──────────┴──────┴──────┴──────────┴────┴───────┴───────┘ │ │ ◄──────────── CRC-32 over bytes 0..19 ──────────────► │ └──────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ Entry (N × KVFS_BLOCK_SIZE bytes) │ │ ┌──────┬──────┬──────┬──────┬────┬────┬────┬───┬───┬─────┐ │ │ │flags │ ks │ vs │ blk │blk│blk│CRC8│CRC│CRC│ key │ │ │ │ │ │ │ │idx│idx│ │16 │16 │+val │ │ │ └──────┴──────┴──────┴──────┴────┴────┴────┴───┴───┴─────┘ │ │ ◄── CRC-8 ──► ◄────────── CRC-16 ──────────────────► │ │ (descriptor) (key_size + val_size + key + value) │ └──────────────────────────────────────────────────────────────┘ ``` | 层级 | 算法 | 覆盖范围 | 检测能力 | |------|------|---------|---------| | **CRC32** | IEEE 802.3 | 超块头前 20 字节 | 检测超块头损坏 → 视为文件系统不存在 | | **CRC8** | MAXIM (0x8C) | KvfsPair 前 7 字节 | 检测描述符损坏 → 跳过该条目 | | **CRC16** | XMODEM (0x1021) | key_len + val_len + key + value | 检测数据载荷损坏 → 标记条目为 DELETED(自愈) | ### 6.1 自愈机制 当 CRC16 校验失败时,库自动将该条目标记为 `DELETED`,并递减 `pair_count`。读取该 key 时返回 `KVFS_ERR_NOENT`(视为不存在)。这在以下函数中均生效: - `kvfs_read` — 读时发现损坏,标记删除 - `kvfs_has` — 查询时发现损坏,视为不存在 - `kvfs_size` — 查询大小时发现损坏,视为不存在 - `kvfs_dump` — dump 时发现损坏,标记删除 - `count_valid_pairs_` — 挂载时扫描,CRC 损坏条目自动清理 > **注意**:自愈操作仅在 `KVFS_ERR_CRC` 时触发。如果发生 I/O 错误(`KVFS_ERR_FORMAT`),不会标记删除,错误直接向上传播。 --- ## 7. I/O 抽象层 ### 7.1 分块读写 所有 I/O 以 **KVFS_BLOCK_SIZE(默认 32)字节块**为单位进行。库维护一个用户提供的 `cache` 缓冲区(≥ `KVFS_BLOCK_SIZE` 字节),用于读-改-写操作。 ``` write_range_(stor_addr=14, buf, sz=12) │ ├─ 块0:stor_addr 14..31 (18 bytes) │ ├─ read_block_(0) → 读取 block 0 (offset 0..31) 到 cache │ ├─ memcpy(cache+14, buf+0, 12) → 写入用户数据 │ └─ write_block_(0) → 写回 block 0 │ └─ pos=12 → 12 == sz → 完成 ``` ### 7.2 页边界重试 `read_block_` 和 `write_block_` 循环调用用户回调,每次可能只传输部分数据(模拟 EEPROM 页边界): ```c static int write_block_(Kvfs *fs, size_t stor_addr) { const uint8_t *src = fs->cache; size_t done = 0; while (done < KVFS_BLOCK_SIZE) { int rc = fs->ops->write( (uint32_t)(stor_addr + done), src + done, KVFS_BLOCK_SIZE - done ); if (rc <= 0) return KVFS_ERR_FORMAT; // I/O 错误 done += (size_t)rc; // 可能只写了部分! } return KVFS_OK; } ``` 用户回调只需保证"写入不超过当前页边界",库会自动循环直到 KVFS_BLOCK_SIZE 字节全部传输完成。 ### 7.3 可选擦除回调(`KvfsOps.erase`) `KvfsOps` 还提供一个**可选**的擦除回调(位于结构体末尾,可为 `NULL`): ```c typedef struct { void (*lock)(bool set); // 可选 int (*write)(size_t addr, const void *data, size_t sz); // 必选 int (*read)(size_t addr, void *data, size_t sz); // 必选 int (*erase)(size_t addr, size_t sz); // 可选,NULL 兼容 } KvfsOps; ``` > 地址参数使用 `size_t`,与库内部寻址一致;在 32 位 MCU(如 STM32)上等价于 `uint32_t`,无 ABI 变化。 - `erase == NULL`(默认):库保持原行为——通过 `write` 写入 `0xFF` 来"擦除",适合 RAM / EEPROM / FRAM 等字节可重写介质。**现有 API 与行为完全不变。** - `erase != NULL`:凡是库需要把某段区域恢复为 `0xFF` 的操作(`kvfs_format`、全新挂载初始化、`kvfs_clear`、defrag 尾部擦除、挂载时尾部清理)都会一次性委托给该回调。库保证传入的范围只含**可丢弃字节**,因此实现可按自身擦除粒度对齐。 --- ## 8. 架构分层 ``` ┌──────────────────────────────────────────────────────────┐ │ 用户应用程序 │ │ kvfs_write / kvfs_read / kvfs_del / ... │ ← 公共 API ├──────────────────────────────────────────────────────────┤ │ find_pair_ scan_entries_ defrag_ count_valid_... │ ← 内部逻辑层 │ (顺序扫描) (通用扫描器) (碎片整理) (挂载时统计) │ ├──────────────────────────────────────────────────────────┤ │ read_block_ write_block_ read_range_ write_range_ │ ← I/O 抽象层 │ erase_range_ │ (块级读写, RMW) ├──────────────────────────────────────────────────────────┤ │ ops->read() / ops->write() / ops->lock() / │ ← 用户回调 │ ops->erase()(可选) │ (硬件适配) ├──────────────────────────────────────────────────────────┤ │ RAM / EEPROM(AT24Cxx) / FRAM-MRAM / ... │ ← 物理介质 └──────────────────────────────────────────────────────────┘ ``` > ⚠️ 仅提供 `ops->erase` 并不能把 KVFS 变成完整的裸 NOR/NAND 文件系统:日志模型仍会对删除标记与超块做**逐字节覆写**,这类 0→1 变化必须先擦后写,需要擦写感知层(如微型 FTL)在回调背后处理,见[第 15 节](#15-介质支持与限制)。 --- ## 9. 运行时状态 — `Kvfs` 结构体 ```c typedef struct { KvfsOps *ops; // I/O 回调指针 uint8_t *cache; // 工作缓存(用户提供,≥ KVFS_BLOCK_SIZE 字节) size_t base_addr; // 介质上的绝对起始地址 size_t capacity; // 储存区总容量(字节) KvfsHeader header; // 内存中的超块头(运行时缓存) struct { bool mounted : 1; // 是否已挂载 } flags; } Kvfs; ``` **零动态内存设计**:所有状态保存在此结构体中,无需 `malloc`。用户只需提供一个 ≥ `KVFS_BLOCK_SIZE`(默认 32)字节的缓存即可。 --- ## 10. 挂载流程 ``` kvfs_mount(fs, ops, cache, KVFS_BLOCK_SIZE, 0, 8192) │ ├─ 1. 参数校验:fs/ops/cache/read/write 非空,cache ≥ KVFS_BLOCK_SIZE, │ capacity ≥ HDR_SIZE + KVFS_BLOCK_SIZE(24 + BLOCK_SIZE), │ base_addr 与 capacity 均为 KVFS_BLOCK_SIZE 整数倍(块级 RMW 需要, │ 否则区域边缘整块访问会越界),capacity ≤ UINT32_MAX(超块 32 位字段) │ ├─ 2. 初始化 Kvfs 结构体,设置 ops、cache、capacity 等 │ ├─ 3. load_hdr_() → 读取偏移 0 处的 24 字节 │ ├─ magic == "KVFS" && version == 1 && CRC32 正确? │ │ ├─ 是:有效文件系统 │ │ │ ├─ count_valid_pairs_() 扫描所有条目 │ │ │ │ ├─ 每个条目做 CRC16 校验 │ │ │ │ ├─ CRC16 失败 → 标记 DELETED(自愈) │ │ │ │ └─ 统计有效条目数和 used_bytes │ │ │ ├─ 有效条目为 0 但 header.pair_count > 0? │ │ │ │ └─ 全部损坏 → 擦除条目区 + 重建(长度钳制在区域内) │ │ │ ├─ 尾部清理:used_bytes 大于实际扫描末尾? │ │ │ │ └─ 擦除多余尾部(非正常卸载/半写残留,防止旧数据被误当有效) │ │ │ └─ 用实际统计值更新 header 并同步 │ │ │ │ │ └─ 否:空白介质或损坏 → 擦除条目区 → init_hdr_() │ │ └─ 写入新的超块头(KVFS magic, version=1, used_bytes=0, pair_count=0) │ └─ 4. 设置 flags.mounted = true → 挂载完成 ``` --- ## 11. 错误码 | 宏 | 值 | 含义 | |----|----|------| | `KVFS_OK` | 0 | 操作成功 | | `KVFS_ERR_PARAM` | -1 | 无效参数(NULL 指针、超限 key/value 等) | | `KVFS_ERR_NOMEM` | -2 | 用户缓冲区太小,用 `kvfs_size()` 查询所需大小 | | `KVFS_ERR_NOENT` | -3 | Key 未找到(或 CRC 校验失败,自愈删除) | | `KVFS_ERR_EXIST` | -4 | 预留:Key 已存在 | | `KVFS_ERR_NOSPACE` | -5 | 储存空间不足(碎片整理后仍不足) | | `KVFS_ERR_CRC` | -6 | CRC 不匹配(内部使用,API 层转为 NOENT) | | `KVFS_ERR_FORMAT` | -7 | 格式无效或 I/O 错误 | | `KVFS_ERR_NOTMNT` | -8 | 文件系统未挂载 | | `KVFS_ERR_BUSY` | -9 | 文件系统已挂载(重复挂载) | --- ## 12. API 参考 | 函数 | 成功返回值 | 说明 | |------|-----------|------| | `kvfs_mount(fs, ops, cache, cache_sz, base, cap)` | `0` | 挂载(或自动创建)文件系统 | | `kvfs_unmount(fs)` | `0` | 卸载(写入超块头) | | `kvfs_format(fs)` | `0` | 格式化(擦除全部并重建超块头) | | `kvfs_write(fs, key, value, sz)` | 写入字节数 `>0` | 写入/更新键值对 | | `kvfs_read(fs, key, buf, sz)` | 读出字节数 `>0` | 读取键值对 | | `kvfs_del(fs, key)` | `0` | 删除键值对(标记 DELETED) | | `kvfs_has(fs, key)` | `true`/`false` | 检查 key 是否存在 | | `kvfs_pairs(fs, &count)` | `0` | 获取有效键值对数量 | | `kvfs_clear(fs)` | `0` | 快速清空所有条目 | | `kvfs_size(fs, key)` | value 字节数 `≥0` | 查询 value 大小(不读数据) | | `kvfs_dump(fs, callback)` | 有效条目数 | 调试输出(格式化打印) | ### 12.1 返回值约定 > **正数 = 成功(字节数),零/负数 = 错误码**。`kvfs_write`/`kvfs_read`/`kvfs_size` 返回正数表示传输的字节数,负数为 `KVFS_ERR_*`。`kvfs_del`/`kvfs_mount`/`kvfs_unmount` 等返回 `0`(`KVFS_OK`)表示成功。 --- ## 13. 硬件集成示例 ```c #define EEPROM_PAGE_SIZE 32 #define EEPROM_CAPACITY 32768 static int eeprom_write(size_t addr, const void *data, size_t sz) { const uint8_t *p = (const uint8_t *)data; size_t done = 0; while (done < sz) { size_t n = sz - done; size_t page_end = ((addr + done) + EEPROM_PAGE_SIZE) & ~(EEPROM_PAGE_SIZE - 1); if (addr + done + n > page_end) n = page_end - (addr + done); int ret = i2c_write_block(I2C1, 0xA0, (uint16_t)(addr + done), I2C_MEMADDR_16BIT, p + done, n, 100); if (ret <= 0 && done == 0) return ret; if (ret > 0) done += (size_t)ret; } return (int)done; } static int eeprom_read(size_t addr, void *data, size_t sz) { return i2c_read_block(I2C1, 0xA0, (uint16_t)addr, I2C_MEMADDR_16BIT, data, sz, 100); } static KvfsOps ops = { .read = eeprom_read, .write = eeprom_write }; int main(void) { uint8_t cache[KVFS_BLOCK_SIZE]; Kvfs fs = {0}; kvfs_mount(&fs, &ops, cache, sizeof(cache), 0, EEPROM_CAPACITY); kvfs_write(&fs, "device", "ESP32", 5); // ... kvfs_unmount(&fs); } ``` --- ## 14. 可配置宏 | 宏 | 默认值 | 说明 | |----|-------|------| | `KVFS_BLOCK_SIZE` | 32 | I/O 块大小(字节),必须是 32 的倍数,≥ 32 | | `KVFS_KEY_SIZE_MAX` | 255 | 最大 key 长度(字节),可通过 `-D` 覆盖 | > **选择 `KVFS_BLOCK_SIZE` 的注意事项**:每个条目的块数存储为 `uint8_t`(最大 255)。确保 `ceil((10 + KEY_SIZE_MAX + 最大value) / BLOCK_SIZE) ≤ 255`。默认 32 字节块可容纳约 8KB 的 value(实际受 `capacity` 限制);若需支持更大的 value,可配置为 64、128 等。 --- ## 15. 介质支持与限制 ### 15.1 支持介质(字节可重写) KVFS 在逻辑上把整块区域当作"可随时原地改写"的存储,其**擦除操作就是写入 `0xFF`**: - ✅ **RAM**、`FRAM`/`MRAM`(写任意字节无限制) - ✅ **AT24Cxx 等字节/页写 EEPROM**(编程前自动擦除,写 `0xFF` 即等效擦除) - ✅ 任何 `write()` 能自由改写字节的介质(含仿真存储、SD 卡文件模拟等) ### 15.2 关于 NOR / NAND Flash 的说明 NOR/NAND 的编程**只能把 1 写成 0**,恢复为 1 必须执行独立的(扇区级)擦除命令。KVFS 针对这类介质提供了**可选**的 `KvfsOps.erase` 回调(见[第 7.3 节](#73-可选擦除回调kvfsopserase)): - **提供 `ops->erase` 后**:`format / clear / 挂载初始化 / defrag 尾部 / 挂载尾部清理` 这些"整段置 0xFF"的操作会真正擦除介质; - **仍然不适用裸 NOR/NAND 的地方**:KVFS 的日志模型会对**删除标记**(flags 字节 0x00→0xFE)与**超块**做逐字节覆写,这些写入需要把 0 改回 1,必须先擦后写;而条目的块粒度(默认 32B)远小于 flash 扇区(通常 4KB),无法逐块擦除。 因此,在裸 NOR/NAND 上可靠使用 KVFS 需要在其回调背后再包一层**擦写感知/FTL**(记录哪些逻辑块位于已擦除物理区、按需扇区擦除、磨损均衡),这属于介质驱动层的工作,超出库本身的职责。仓库附带的 `demo/nor_probe.c`(构建产物 `nor_probe`)用 1→0-only 仿真直观演示了"无擦除管理层"时数据被破坏的过程,可用于对照验证。 > 若需求是直接在裸 Flash 上跑文件系统,建议评估专为 Flash 设计的方案(LittleFS、SPIFFS、ESP-IDF NVS 等),它们原生管理扇区擦除与磨损均衡。 ### 15.3 其它固有约束 | 约束 | 说明 | |------|------| | `base_addr`/`capacity` 须为 `KVFS_BLOCK_SIZE` 整数倍 | 所有 I/O 均按整块读改写,未对齐区域会越界整块访问;mount 会返回 `KVFS_ERR_PARAM` | | `capacity` ≥ `24 + KVFS_BLOCK_SIZE` | mount 参数校验 | | 掉电安全 | 无事务日志:写/删中途掉电可能丢失最后一次操作(CRC 保证不会读到半写数据,残留由下次 mount 的尾部清理回收) | | 磨损热点 | header(块 0)每次写/删都会同步更新;Append-Only 让数据区"流动",但超块存在固定磨损 | | 单实例 | 同一区域不可并发 mount;`lock` 回调用于线程互斥(可选) | --- ## 16. 项目结构 ``` kvfs/ ├── kvfs.h # 公开 API 头文件 + 数据结构定义 ├── kvfs.c # 核心实现 ├── CMakeLists.txt # CMake 构建(含 ctest 注册) ├── demo/ │ ├── demo.c # 使用示例 │ └── nor_probe.c # NOR-flash 介质限制实测探针 ├── test/ │ ├── sim_storage.h # 仿真 EEPROM(模拟页边界写入) │ ├── sim_storage.c # 仿真 EEPROM 实现 │ ├── test_kvfs.c # 单元/回归测试(81 个用例) │ ├── fuzz_kvfs.c # 模型校验的随机 fuzz(写/删/格式化/重挂载) │ └── corrupt_fuzz.c # 随机字节损坏 + 反复重挂载韧性测试 ├── README.md # 本文件 └── build/ # 构建输出 ``` --- ## 17. 测试覆盖 ### 17.1 单元测试(`test/test_kvfs.c`,81 用例) | 测试组 | 用例数 | 覆盖内容 | |--------|-------|---------| | 参数校验 | 12 | NULL、未挂载、超限参数等非法输入 | | 格式化+魔数 | 1 | 介质字节级验证 `"KVFS"` | | 挂载/卸载 | 1 | 挂载→卸载→重新挂载循环 | | CRUD | 5 | write→read→update→delete→noent 完整流程 | | has/pairs/clear | 3 | 存在检查、计数、快速清空 | | 碎片整理 | 2 | 删除后整理(真实触发 defrag)、空间不足 NOSPACE | | 碎片整理正确性 | 3 | defrag 搬移后幸存条目完好;defrag+update 读到新值;尾部删除条目经重挂载回收 | | CRC 保护 | 1 | 数据损坏检测+自愈 | | 持久化 | 4 | 写入→卸载→重新挂载→验证 | | Dump | 2 | 输出格式、已删除条目排除 | | 边界情况 | 7 | 空值、最大 key、多 key、二进制数据、零长度等 | | 生命周期 | 1 | clear→format→写入 完整流程 | | 超块损坏 | 1 | 损坏超块→重新挂载→自动重建 | | 压力测试 | 1 | 同一 key 连续更新 30 次 | | 完整 CRUD+重挂载 | 1 | 全流程+持久化验证 | | 格式化后写入 | 1 | format→魔数保留→正常写入 | | 页边界 | 1 | 跨页写入(SIM_PAGE_SIZE=16) | | 填满槽位 | 1 | 60 个 key 批量写入 | | 幽灵条目检测 | 2 | CRC 损坏条目自动清理 | | 二进制数据 | 7 | 结构体、跨块大小、全零/全FF | | kvfs_size API | 3 | 尺寸查询+参数校验 | | 返回值正确性 | 5 | 正数=字节数,负数=错误码 | | 结构体大小 | 1 | 运行时验证 sizeof | | 抗腐蚀 | 1 | use_blk=0 不死循环 | | NOSPACE 保护 | 2 | 空间不足时旧数据保留 | | 多轮碎片整理 | 1 | 5 轮写入-删除-defrag 循环 | | 已用字节准确性 | 1 | 各操作后 pair_count 正确 | | Key 边界 | 3 | 大小写、单字符、超长 key | | 最大 value | 1 | KVFS_BLOCK_SIZE=32 时约 7.8KB(受 use_blk uint8_t 限制) | | 空文件系统 | 1 | 所有只读操作的行为 | | Clear 持久化 | 1 | clear→重挂载→确认清空 | | Format 持久化 | 1 | format→重挂载→确认清空 | | 可选 erase 回调 | 3 | 提供 `ops->erase` 时:clear 真正擦除介质、CRUD/format/重挂载正常、defrag 后数据完好(`erase == NULL` 的兼容性由其余全部用例覆盖) | ### 17.2 鲁棒性 / Fuzz 测试(长期资产) | 程序 | 作用 | |------|------| | `test/fuzz_kvfs.c` | 在内存模型中随机执行 写/更新/删除/format/clear,周期性核对磁盘与模型完全一致(键存在性、值内容),并周期重挂载验证持久化与 mount 时对账逻辑。可配置 `FUZZ_CAP/FUZZ_VMAX/FUZZ_SEED/FUZZ_ITERS` | | `test/corrupt_fuzz.c` | 反复"写满→随机翻转若干字节→重挂载→随机读写删除",验证任何位损坏都不会导致挂死、崩溃或越界,挂载后仍可继续使用 | | `demo/nor_probe.c` | NOR 1→0-only 仿真,演示**未提供擦除管理**(`ops.erase==NULL`)时删除静默失败、重挂载数据全丢、空间无法回收;对照说明为何裸 Flash 仍需 FTL/擦写层 | 构建与运行: ```bash mkdir -p build && cd build cmake .. # 默认构建全部目标 cmake --build . ./test_kvfs # 单元/回归测试 ctest # 全部测试(单元 + fuzz + corrupt-fuzz) # 使用内存消毒器(ASan/UBSan)构建并测试 cmake .. -DKVFS_ASAN=ON -B build-asan cmake --build build-asan ctest --test-dir build-asan # 独立 fuzz 调参(示例) gcc -std=c11 -g -fsanitize=address,undefined -fno-sanitize-recover=all \ -DFUZZ_CAP=8192 -DFUZZ_VMAX=1500 -DFUZZ_ITERS=60000 \ test/fuzz_kvfs.c test/sim_storage.c kvfs.c -I. -Itest -o fuzz && ./fuzz ``` > 可选 CMake 开关:`KVFS_BUILD_TESTS`(默认 ON)、`KVFS_BUILD_FUZZ`(默认 ON)、`KVFS_BUILD_DEMO`(默认 ON)、`KVFS_ASAN`(默认 OFF)。 > 跨平台:CMake 构建,GCC / MinGW / MSVC 兼容;`KVFS_ASAN` 仅对 GCC/Clang 生效。可通过 `-DKVFS_BLOCK_SIZE=64` 等参数自定义块大小。 --- ## 18. 资源占用参考 KVFS 为**零动态内存 / 零全局变量**设计:库自身没有 `.data/.bss`(无静态 RAM),全部状态由调用者提供的 `Kvfs` 实例与工作缓存承载。 ### 18.1 实测(本机 x86-64, GCC 13.3, `-ffreestanding`, 仅 `kvfs.c`,不含 libc 的 memcpy 等) | 优化档 | .text + .rodata | .data / .bss | |--------|-----------------|--------------| | `-Os` | **约 5.4 KB**(5 360 B) | 0 | | `-O2` | 约 7.9 KB(7 887 B) | 0 | | `-O0` | 约 9.6 KB(9 573 B) | 0 | ### 18.2 STM32(Cortex-M3/M4,Thumb-2)估算 按指针与 `size_t` 为 32 位、Thumb-2 指令密度通常优于 x86-64(经验系数约 0.65–0.8)换算: | 项目 | 估算值 | |------|--------| | **ROM(Flash,`-Os`)** | **约 3.5 – 4.5 KB**(核心库;不含介质驱动与 `memcpy/memset/strlen` 实现) | | **RAM - Kvfs 实例** | ≈ **44 B**(32 位 ARM:4 指针/长度 + 24 B 超块 + 标志,64 位主机实测 `sizeof(Kvfs)=64`) | | **RAM - 工作缓存** | 用户提供 ≥ `KVFS_BLOCK_SIZE`(默认 32 B),不入库 | | **运行栈峰值** | 最深处为 `kvfs_write` 路径(`-fstack-usage`:最大单帧 256 B、`kvfs_dump` 384 B,均含 256 B 的 key 缓冲,x86-64 口径);STM32 上更小,任务栈预留 **0.5–1 KB** 足够(介质驱动另计) | > 编译建议:MCU 工程用 `-Os -ffunction-sections -fdata-sections` 配合链接器 `--gc-sections`,可进一步裁剪未用 API。 > 测量命令:`gcc -std=c11 -Os -c kvfs.c && size kvfs.o`。