# kvfs **Repository Path**: logeexpluoqi/kvfs ## Basic Information - **Project Name**: kvfs - **Description**: 一个小巧的键值文件系统库,可以非常容易集成到小容量单片机中,且实现与硬件平台无关,储存介质的读取和写入由外部函数提供 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-22 - **Last Updated**: 2026-06-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # KVFS — 嵌入式设备键值文件系统 KVFS 是一个专为资源受限的单片机(MCU)设计的轻量级键值文件系统,适用于 EEPROM 或 Flash 作为储存介质。键值对数量仅受储存容量限制,无固定上限,无需预分配索引表。 --- ## 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 必须重新计算 | | **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 字节全部传输完成。 --- ## 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() │ ← 用户回调 │ │ (硬件适配) ├──────────────────────────────────────────────────────────┤ │ I2C EEPROM / SPI Flash / ... │ ← 物理介质 └──────────────────────────────────────────────────────────┘ ``` --- ## 9. 运行时状态 — `Kvfs` 结构体 ```c typedef struct { KvfsOps *ops; // I/O 回调指针 uint8_t *cache; // 工作缓存(用户提供,≥ KVFS_BLOCK_SIZE 字节) size_t base_addr; // 介质上的绝对起始地址 size_t capacity; // 储存区总容量(字节) size_t cache_size; // 缓存大小 KvfsHeader header; // 内存中的超块头(运行时缓存) struct { bool mounted : 1; // 是否已挂载 bool has_write: 1; // 是否有写回调 bool has_read : 1; // 是否有读回调 bool locked : 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) │ ├─ 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? │ │ │ │ └─ 全部损坏 → 擦除+重建 │ │ │ └─ 用实际统计值更新 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(uint32_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(uint32_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. 项目结构 ``` kvfs/ ├── kvfs.h # 公开 API 头文件 + 数据结构定义 ├── kvfs.c # 核心实现(~1000 行) ├── CMakeLists.txt # CMake 构建 ├── demo/ │ └── demo.c # 使用示例 ├── test/ │ ├── sim_storage.h # 仿真 EEPROM(模拟页边界写入) │ ├── sim_storage.c # 仿真 EEPROM 实现 │ └── test_kvfs.c # 75 个测试用例(31 组) ├── README.md # 本文件 └── build/ # 构建输出 ``` --- ## 16. 测试覆盖 **75 个测试用例,31 个测试组**: | 测试组 | 用例数 | 覆盖内容 | |--------|-------|---------| | 参数校验 | 12 | NULL、未挂载、超限参数等非法输入 | | 格式化+魔数 | 1 | 介质字节级验证 `"KVFS"` | | 挂载/卸载 | 1 | 挂载→卸载→重新挂载循环 | | CRUD | 5 | write→read→update→delete→noent 完整流程 | | has/pairs/clear | 3 | 存在检查、计数、快速清空 | | 碎片整理 | 2 | 删除后整理、空间不足触发 | | 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→重挂载→确认清空 | 构建和运行: ```bash mkdir build && cd build cmake .. -G "MinGW Makefiles" cmake --build . ./test_kvfs ``` > 跨平台:CMake 构建,GCC / MinGW / MSVC 兼容。可通过 `-DKVFS_BLOCK_SIZE=64` 等参数自定义块大小。