littlefs 源码分析
1. 概述
littlefs 是一个为微控制器设计的小型故障安全文件系统,具有以下特点:
- 掉电恢复能力:所有文件操作都有强写时复制保证,掉电后文件系统会回退到最后一个已知的良好状态
- 动态磨损均衡:专为闪存设计,提供动态块的磨损均衡,能检测并绕过坏块
- 有限的 RAM/ROM 使用:内存使用严格受限,不随文件系统大小增长而变化,无无限递归,动态内存限于可静态提供的可配置缓冲区
2. 代码分层结构
littlefs 采用清晰的分层设计,从底层到上层依次为:
+-------------------+
| API 层 |
+-------------------+
| 核心文件系统层 |
+-------------------+
| 块设备层 |
+-------------------+
| 物理存储介质 |
+-------------------+
2.1 块设备层
块设备层是文件系统与物理存储介质之间的接口,由用户实现,主要包括以下操作:
read:从块中读取数据prog:向块中编程数据(块必须先被擦除)erase:擦除块sync:同步底层块设备状态
这些操作在 lfs_config 结构体中定义,作为文件系统初始化的一部分。
2.2 核心文件系统层
核心文件系统层是 littlefs 的核心,实现了文件系统的主要功能:
- 元数据管理:使用元数据对(metadata pairs)实现原子更新
- 文件操作:实现文件的创建、读写、关闭等操作
- 目录操作:实现目录的创建、遍历等操作
- 块分配:实现动态磨损均衡的块分配器
- 垃圾回收:清理过时数据,优化存储空间
核心文件系统层的主要数据结构包括:
lfs_t:文件系统实例lfs_file_t:文件实例lfs_dir_t:目录实例lfs_cache_t:缓存实例lfs_mdir_t:元数据目录实例
2.3 API 层
API 层提供给用户的接口,模仿 POSIX 文件系统接口,主要包括:
- 文件系统操作 :
lfs_format、lfs_mount、lfs_unmount - 文件操作 :
lfs_file_open、lfs_file_read、lfs_file_write、lfs_file_close等 - 目录操作 :
lfs_mkdir、lfs_dir_open、lfs_dir_read等 - 通用操作 :
lfs_remove、lfs_rename、lfs_stat等
3. 核心数据结构
3.1 lfs_t
文件系统的主结构,包含文件系统的所有状态:
c
typedef struct lfs {
lfs_cache_t rcache; // 读缓存
lfs_cache_t pcache; // 写缓存
lfs_block_t root[2]; // 根目录的元数据对
struct lfs_mlist *mlist; // 元数据列表
uint32_t seed; // 随机种子
lfs_gstate_t gstate; // 全局状态
lfs_gstate_t gdisk; // 磁盘上的全局状态
lfs_gstate_t gdelta; // 全局状态增量
struct lfs_lookahead { // 预分配块的 lookahead 缓冲区
lfs_block_t start;
lfs_block_t size;
lfs_block_t next;
lfs_block_t ckpoint;
uint8_t *buffer;
} lookahead;
const struct lfs_config *cfg; // 配置
lfs_size_t block_count; // 块数量
lfs_size_t name_max; // 文件名最大长度
lfs_size_t file_max; // 文件最大大小
lfs_size_t attr_max; // 属性最大大小
lfs_size_t inline_max; // 内联文件最大大小
} lfs_t;
3.2 lfs_config
文件系统配置结构体,定义了文件系统的操作和参数:
c
struct lfs_config {
void *context; // 用户提供的上下文
// 块设备操作
int (*read)(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, void *buffer, lfs_size_t size);
int (*prog)(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, const void *buffer, lfs_size_t size);
int (*erase)(const struct lfs_config *c, lfs_block_t block);
int (*sync)(const struct lfs_config *c);
// 块设备配置
lfs_size_t read_size; // 读操作的最小大小
lfs_size_t prog_size; // 编程操作的最小大小
lfs_size_t block_size; // 块大小
lfs_size_t block_count; // 块数量
int32_t block_cycles; // 块循环计数
// 性能和内存配置
lfs_size_t cache_size; // 缓存大小
lfs_size_t lookahead_size; // 预分配缓冲区大小
lfs_size_t compact_thresh; // 元数据压缩阈值
// 静态缓冲区
void *read_buffer; // 静态读缓冲区
void *prog_buffer; // 静态编程缓冲区
void *lookahead_buffer; // 静态预分配缓冲区
// 限制
lfs_size_t name_max; // 文件名最大长度
lfs_size_t file_max; // 文件最大大小
lfs_size_t attr_max; // 属性最大大小
lfs_size_t metadata_max; // 元数据最大大小
lfs_size_t inline_max; // 内联文件最大大小
};
3.3 元数据对 (Metadata Pairs)
元数据对是 littlefs 的核心数据结构,用于实现原子更新:
c
typedef struct lfs_mdir {
lfs_block_t pair[2]; // 元数据对的两个块
uint32_t rev; // 修订号
lfs_off_t off; // 偏移量
uint32_t etag; // 标签
uint16_t count; // 计数
bool erased; // 是否已擦除
bool split; // 是否已分割
lfs_block_t tail[2]; // 尾部指针
} lfs_mdir_t;
3.4 文件和目录结构
c
// 文件结构
typedef struct lfs_file {
struct lfs_file *next; // 下一个文件
uint16_t id; // 文件 ID
uint8_t type; // 文件类型
lfs_mdir_t m; // 元数据目录
struct lfs_ctz { // CTZ 跳表
lfs_block_t head; // 头部块
lfs_size_t size; // 大小
} ctz;
uint32_t flags; // 标志
lfs_off_t pos; // 位置
lfs_block_t block; // 当前块
lfs_off_t off; // 当前偏移
lfs_cache_t cache; // 缓存
const struct lfs_file_config *cfg; // 文件配置
} lfs_file_t;
// 目录结构
typedef struct lfs_dir {
struct lfs_dir *next; // 下一个目录
uint16_t id; // 目录 ID
uint8_t type; // 目录类型
lfs_mdir_t m; // 元数据目录
lfs_off_t pos; // 位置
lfs_block_t head[2]; // 头部块
} lfs_dir_t;
4. 核心算法
4.1 元数据管理
littlefs 使用元数据对(两个块的日志)来实现原子更新:
- 元数据写入:将更新追加到日志中,使用校验和确保数据完整性
- 元数据压缩:当日志满时,清理过时数据并将有效数据复制到新块
- 元数据分割:当压缩后仍然满时,将元数据对分割为两个,形成链表
4.2 CTZ 跳表
CTZ(Count Trailing Zeros)跳表是 littlefs 用于存储文件数据的高效数据结构:
- 结构:多层链表,每个块包含指向前面块的指针
- 指针规则:对于第 n 个块,包含 ctz(n)+1 个指针
- 优点:支持高效的追加操作(O(1))和随机访问(O(log n))
- 存储开销:平均每个块仅需 2 个指针的开销
4.3 块分配
littlefs 实现了动态磨损均衡的块分配器:
- 随机分配:使用伪随机数生成器选择块,避免集中磨损
- 块循环计数:跟踪块的擦除次数,优先选择擦除次数少的块
- 坏块检测:检测并绕过坏块
4.4 垃圾回收
littlefs 实现了高效的垃圾回收机制:
- 元数据垃圾回收:通过压缩和分割元数据对清理过时数据
- 数据块垃圾回收:当需要新块时,回收包含过时数据的块
5. 移植方法
5.1 实现块设备操作
要移植 littlefs,首先需要实现块设备操作函数:
c
// 读操作
int user_provided_block_device_read(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, void *buffer, lfs_size_t size) {
// 从物理存储中读取数据
// ...
return LFS_ERR_OK;
}
// 编程操作
int user_provided_block_device_prog(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, const void *buffer, lfs_size_t size) {
// 向物理存储中写入数据
// ...
return LFS_ERR_OK;
}
// 擦除操作
int user_provided_block_device_erase(const struct lfs_config *c, lfs_block_t block) {
// 擦除物理存储中的块
// ...
return LFS_ERR_OK;
}
// 同步操作
int user_provided_block_device_sync(const struct lfs_config *c) {
// 同步物理存储
// ...
return LFS_ERR_OK;
}
5.2 配置文件系统
配置 lfs_config 结构体,设置块设备参数和内存配置:
c
const struct lfs_config cfg = {
// 块设备操作
.read = user_provided_block_device_read,
.prog = user_provided_block_device_prog,
.erase = user_provided_block_device_erase,
.sync = user_provided_block_device_sync,
// 块设备配置
.read_size = 16,
.prog_size = 16,
.block_size = 4096,
.block_count = 128,
.block_cycles = 500,
// 内存配置
.cache_size = 16,
.lookahead_size = 16,
};
5.3 初始化和使用文件系统
使用 lfs_mount 挂载文件系统,使用 lfs_format 格式化文件系统:
c
lfs_t lfs;
// 挂载文件系统
int err = lfs_mount(&lfs, &cfg);
// 如果挂载失败,格式化文件系统
if (err) {
lfs_format(&lfs, &cfg);
lfs_mount(&lfs, &cfg);
}
// 使用文件系统
// ...
// 卸载文件系统
lfs_unmount(&lfs);
6. 使用示例
6.1 基本文件操作
以下是一个完整的示例,展示如何使用 littlefs 进行基本的文件操作:
c
#include "lfs.h"
// 文件系统变量
lfs_t lfs;
lfs_file_t file;
// 块设备操作
int user_provided_block_device_read(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, void *buffer, lfs_size_t size) {
// 实现读操作
return LFS_ERR_OK;
}
int user_provided_block_device_prog(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, const void *buffer, lfs_size_t size) {
// 实现编程操作
return LFS_ERR_OK;
}
int user_provided_block_device_erase(const struct lfs_config *c, lfs_block_t block) {
// 实现擦除操作
return LFS_ERR_OK;
}
int user_provided_block_device_sync(const struct lfs_config *c) {
// 实现同步操作
return LFS_ERR_OK;
}
// 文件系统配置
const struct lfs_config cfg = {
// 块设备操作
.read = user_provided_block_device_read,
.prog = user_provided_block_device_prog,
.erase = user_provided_block_device_erase,
.sync = user_provided_block_device_sync,
// 块设备配置
.read_size = 16,
.prog_size = 16,
.block_size = 4096,
.block_count = 128,
.cache_size = 16,
.lookahead_size = 16,
.block_cycles = 500,
};
// 主函数
int main(void) {
// 挂载文件系统
int err = lfs_mount(&lfs, &cfg);
// 格式化文件系统(如果需要)
if (err) {
lfs_format(&lfs, &cfg);
lfs_mount(&lfs, &cfg);
}
// 读取启动计数
uint32_t boot_count = 0;
lfs_file_open(&lfs, &file, "boot_count", LFS_O_RDWR | LFS_O_CREAT);
lfs_file_read(&lfs, &file, &boot_count, sizeof(boot_count));
// 更新启动计数
boot_count += 1;
lfs_file_rewind(&lfs, &file);
lfs_file_write(&lfs, &file, &boot_count, sizeof(boot_count));
// 关闭文件(确保数据写入)
lfs_file_close(&lfs, &file);
// 卸载文件系统
lfs_unmount(&lfs);
// 打印启动计数
printf("boot_count: %d\n", boot_count);
}
6.2 目录操作
c
// 创建目录
lfs_mkdir(&lfs, "/mydir");
// 打开目录
lfs_dir_t dir;
lfs_dir_open(&lfs, &dir, "/mydir");
// 读取目录内容
struct lfs_info info;
while (lfs_dir_read(&lfs, &dir, &info) > 0) {
printf("%s\n", info.name);
}
// 关闭目录
lfs_dir_close(&lfs, &dir);
6.3 属性操作
c
// 设置属性
uint8_t data[] = {1, 2, 3, 4};
lfs_setattr(&lfs, "file.txt", 0x01, data, sizeof(data));
// 获取属性
uint8_t buffer[4];
lfs_getattr(&lfs, "file.txt", 0x01, buffer, sizeof(buffer));
7. 总结
littlefs 是一个专为微控制器设计的文件系统,具有以下优点:
- 掉电恢复能力:通过写时复制和校验和确保数据完整性
- 动态磨损均衡:延长闪存寿命
- 有限的内存使用:适合资源受限的微控制器
- 简单易用的 API:模仿 POSIX 接口,易于使用
- 高度可移植:只需实现块设备操作即可移植到不同平台
littlefs 的设计思想和实现技术对于理解嵌入式文件系统的工作原理具有重要参考价值,其代码结构清晰,算法设计巧妙,是学习嵌入式系统存储管理的优秀案例。