OpenHarmony LiteOS-M LittleFS 文件系统调试与修复实战
目录
问题描述
初始症状
在 LoongArch 架构的 ls2k300_mini_dp 开发板上运行 OpenHarmony 6.1 时,遇到以下问题:
-
ls 命令显示错误:
OHOS # ls Directory /: BAD file: . BAD file: .. -
文件创建失败:
OHOS # touch 1.txt OHOS # mkdir os OHOS # ls Directory /: OHOS # cd os no such file or directory -
文件系统初始化失败:
LfsLowLevelInit: Failed to create test file, fd=-1, errno=2
问题影响
- 无法创建文件和目录
- 无法访问已创建的文件
- 文件系统基本功能不可用
环境信息
硬件平台
- 开发板: ls2k300_mini_dp
- CPU 架构: LoongArch (龙芯架构)
- 内核: LiteOS-M
软件版本
- OpenHarmony: 6.1
- 文件系统: LittleFS
- 存储介质: RAM disk (64KB)
文件系统配置
#define LFS_RAM_DISK_SIZE (64 * 1024) // 64KB RAM disk
#define LFS_BLOCK_SIZE 4096 // 4KB 块大小
#define LFS_BLOCK_COUNT 16 // 16 个块
调试过程
第一阶段:定位 “BAD file” 错误
问题分析
ls 命令显示 “BAD file: .” 和 “BAD file: …”,说明目录读取有问题。
调试方法
-
添加调试信息到 ls 命令:
// kernel/liteos_m/components/shell/src/cmds/vfs_shellcmd.c static inline void OsLs(const char *path) { struct dirent *pdirent = NULL; DIR *d = NULL; d = opendir(path); if (d == NULL) { perror("opendir error"); return; } while (1) { pdirent = readdir(d); if (pdirent == NULL) { break; } printf("OsLs: d_name='%s', d_type=%d\n", pdirent->d_name, pdirent->d_type); // ... 原有代码 } } -
检查 stat 系统调用:
ret = stat(fullpath, &statInfo); printf("stat('%s') returned %d, errno=%d\n", fullpath, ret, errno);
发现
readdir能正确读取 “.” 和 “…” 目录项stat对 “.” 和 “…” 返回错误- LittleFS 对 “.” 和 “…” 的处理有问题
临时解决方案
修改 ls 命令,跳过 “.” 和 “…” 目录项:
// kernel/liteos_m/components/shell/src/cmds/vfs_shellcmd.c
if (pdirent != NULL) {
/* Skip "." and ".." entries - littlefs doesn't handle them well */
if ((strcmp(pdirent->d_name, ".") == 0) ||
(strcmp(pdirent->d_name, "..") == 0)) {
continue;
}
// ... 原有代码
}
结果: “BAD file” 错误消失,但文件创建仍然失败。
第二阶段:定位文件创建失败
问题分析
文件创建命令(touch、mkdir)执行后没有显示错误,但文件实际上没有被创建。
调试方法
-
添加调试信息到 VFS 层:
// kernel/liteos_m/components/fs/vfs/vfs_fs.c static int VfsOpen(const char *path, int flags) { printf("VfsOpen: path='%s', flags=%d\n", path, flags); mp = VfsMpFind(path, &pathInMp); printf("VfsOpen: mp=%p, pathInMp='%s'\n", mp, pathInMp ? pathInMp : "NULL"); // ... 原有代码 } -
添加调试信息到 LittleFS 适配层:
// kernel/liteos_m/components/fs/littlefs/lfs_adapter.c int LfsOpen(struct File *file, const char *pathName, int openFlag) { printf("LfsOpen: opening '%s', flags=%d\n", pathName, openFlag); ret = lfs_file_open((lfs_t *)file->fMp->mData, lfsHandle, pathName, lfsOpenFlag); printf("LfsOpen: lfs_file_open ret=%d\n", ret); // ... 原有代码 }
关键发现
OHOS # touch 1.txt
VfsOpen: path='/1.txt', flags=66
VfsMpFind: path='/1.txt', g_mountPoints=0x9000000002028528
VfsMpFind: checking mp=0x9000000002028528, mPath='/'
VfsMpFind: no match found
VfsOpen: mount point not found or invalid, mp=(nil), pathInMp='NULL'
问题: VfsMpFind 无法找到 mount point!
第三阶段:深入分析 VfsMpFind 函数
问题分析
VfsMpFind 函数负责根据路径查找对应的 mount point。日志显示:
g_mountPoints不为 NULL(地址为 0x9000000002028528)mPath为/(根目录)- 但匹配失败
代码分析
// kernel/liteos_m/components/fs/vfs/vfs_mount.c
struct MountPoint *VfsMpFind(const char *path, const char **pathInMp)
{
struct MountPoint *mp = g_mountPoints;
const char *iPath = path;
const char *mPath = NULL;
const char *target = NULL;
if (pathInMp != NULL) {
*pathInMp = NULL;
}
while (*iPath == '/') {
++iPath; // 跳过前导 '/'
}
while ((mp != NULL) && (mp->mPath != NULL)) {
mPath = mp->mPath;
target = iPath;
while (*mPath == '/') {
++mPath; // 跳过前导 '/'
}
// 问题在这里!
while ((*mPath != '\0') && (*mPath != '/') &&
(*target != '\0') && (*target != '/')) {
if (*mPath != *target) {
break;
}
++mPath;
++target;
}
if (((*mPath == '\0') || (*mPath == '/')) &&
((*target == '\0') || (*target == '/'))) {
if (pathInMp != NULL) {
*pathInMp = path;
}
return mp;
}
mp = mp->mNext;
}
return NULL;
}
执行流程分析
对于路径 /1.txt:
iPath跳过前导/后,指向1.txtmPath跳过前导/后,指向\0(因为 mPath 是/)- 进入比较循环:
*mPath == '\0',循环不执行
- 检查条件:
*mPath == '\0'✓*target == '1'✗
- 匹配失败!
对于路径 /:
iPath跳过前导/后,指向\0mPath跳过前导/后,指向\0- 进入比较循环:
*mPath == '\0',循环不执行
- 检查条件:
*mPath == '\0'✓*target == '\0'✓
- 匹配成功!
根本原因
VfsMpFind 函数的路径匹配逻辑有缺陷:当 mount point 路径是根目录 / 时,无法匹配以 / 开头的子路径(如 /1.txt、/os)。
这是因为:
- 根目录 mount point 的
mPath是/ - 跳过前导
/后,mPath变成空字符串 - 比较循环不执行,直接检查
*target - 对于
/1.txt,target指向1.txt,不为空,匹配失败
根本原因分析
问题根源
VfsMpFind 函数缺少对根目录 mount point 的特殊处理。
为什么会出现这个问题?
-
设计缺陷:
- VfsMpFind 函数假设所有 mount point 路径都有非空的路径组件
- 根目录
/是特殊情况,跳过前导/后变成空字符串
-
匹配逻辑错误:
- 当
mPath为空时,应该匹配所有以/开头的路径 - 但现有逻辑要求
target也必须为空
- 当
-
测试覆盖不足:
- 可能只测试了非根目录的 mount point
- 没有测试根目录 mount point 的路径匹配
影响范围
- 所有挂载到根目录
/的文件系统都会受影响 - 包括但不限于:
- RAM disk 文件系统
- Flash 文件系统
- SD 卡文件系统
解决方案
修复思路
在跳过前导 / 后,如果 mPath 指向 \0(即 mount point 路径是根目录 /),则直接返回该 mount point,匹配所有以 / 开头的路径。
修复代码
// kernel/liteos_m/components/fs/vfs/vfs_mount.c
struct MountPoint *VfsMpFind(const char *path, const char **pathInMp)
{
struct MountPoint *mp = g_mountPoints;
const char *iPath = path;
const char *mPath = NULL;
const char *target = NULL;
if (pathInMp != NULL) {
*pathInMp = NULL;
}
while (*iPath == '/') {
++iPath;
}
while ((mp != NULL) && (mp->mPath != NULL)) {
mPath = mp->mPath;
target = iPath;
while (*mPath == '/') {
++mPath;
}
// 修复:检查是否为根目录 mount point
if (*mPath == '\0') {
if (pathInMp != NULL) {
*pathInMp = path;
}
return mp; // 根目录匹配所有以 '/' 开头的路径
}
while ((*mPath != '\0') && (*mPath != '/') &&
(*target != '\0') && (*target != '/')) {
if (*mPath != *target) {
break;
}
++mPath;
++target;
}
if (((*mPath == '\0') || (*mPath == '/')) &&
((*target == '\0') || (*target == '/'))) {
if (pathInMp != NULL) {
*pathInMp = path;
}
return mp;
}
mp = mp->mNext;
}
return NULL;
}
修复逻辑说明
-
跳过前导
/:iPath跳过路径的前导/mPath跳过 mount point 路径的前导/
-
检查根目录:
- 如果
*mPath == '\0',说明 mount point 路径是根目录/ - 直接返回该 mount point,匹配所有以
/开头的路径
- 如果
-
正常匹配:
- 对于非根目录的 mount point,执行原有的匹配逻辑
代码修改详解
修改文件列表
-
kernel/liteos_m/components/fs/vfs/vfs_mount.c ⭐ 核心修复
- 修改
VfsMpFind函数,添加根目录匹配逻辑
- 修改
-
kernel/liteos_m/components/fs/littlefs/lfs_adapter.c
- 挂载失败时自动格式化文件系统
-
kernel/liteos_m/components/shell/src/cmds/vfs_shellcmd.c
- 修改
OsLs函数,跳过 “.” 和 “…” 目录项
- 修改
-
third_party/littlefs/lfs.c (可选)
- 添加根目录有效性验证
详细修改
1. VfsMpFind 函数修复
文件: kernel/liteos_m/components/fs/vfs/vfs_mount.c
修改位置: 第 151-159 行
修改内容:
// 在跳过前导 '/' 后,添加根目录检查
if (*mPath == '\0') {
if (pathInMp != NULL) {
*pathInMp = path;
}
return mp;
}
修改前后对比:
// 修改前
while (*mPath == '/') {
++mPath;
}
while ((*mPath != '\0') && (*mPath != '/') &&
(*target != '\0') && (*target != '/')) {
// ... 匹配逻辑
}
// 修改后
while (*mPath == '/') {
++mPath;
}
// 新增:检查根目录 mount point
if (*mPath == '\0') {
if (pathInMp != NULL) {
*pathInMp = path;
}
return mp;
}
while ((*mPath != '\0') && (*mPath != '/') &&
(*target != '\0') && (*target != '/')) {
// ... 匹配逻辑
}
2. ls 命令修复
文件: kernel/liteos_m/components/shell/src/cmds/vfs_shellcmd.c
修改位置: 第 475-480 行
修改内容:
// 跳过 "." 和 ".." 目录项
if ((strcmp(pdirent->d_name, ".") == 0) ||
(strcmp(pdirent->d_name, "..") == 0)) {
continue;
}
说明: 这是一个临时解决方案,因为 LittleFS 对 “.” 和 “…” 的处理有问题。更好的解决方案是修复 LittleFS 的目录读取逻辑。
3. LittleFS 适配层修复
文件: kernel/liteos_m/components/fs/littlefs/lfs_adapter.c
修改内容: 挂载失败时自动格式化文件系统
int LfsMount(struct Mount *mp, const void *data)
{
// ... 初始化配置代码
ret = lfs_mount((lfs_t *)mp->mData, cfg);
if (ret != 0) {
// 挂载失败,尝试格式化
ret = lfs_format((lfs_t *)mp->mData, cfg);
if (ret == 0) {
// 格式化成功,重新挂载
ret = lfs_mount((lfs_t *)mp->mData, cfg);
}
}
// ... 后续处理
}
说明: RAM disk 初始状态为空,需要格式化后才能使用。此修改确保首次使用时自动格式化。
4. LittleFS 根目录验证(可选)
文件: third_party/littlefs/lfs.c
修改位置: lfs_mount_ 函数
修改内容:
static int lfs_mount_(lfs_t *lfs, const struct lfs_config *cfg) {
// ... 原有代码
lfs->lookahead.start = lfs->seed % lfs->block_count;
lfs_alloc_drop(lfs);
// 新增:检查根目录是否设置
if (lfs->root[0] == LFS_BLOCK_NULL || lfs->root[1] == LFS_BLOCK_NULL) {
LFS_ERROR("Superblock not found, root not set");
err = LFS_ERR_CORRUPT;
goto cleanup;
}
return 0;
// ... 清理代码
}
说明: 这是一个增强性修改,确保挂载失败时能正确返回错误,而不是静默成功。
测试验证
测试环境
- 开发板: ls2k300_mini_dp
- 内核: LiteOS-M
- 文件系统: LittleFS on RAM disk (64KB)
测试步骤
1. 编译系统
cd /home/vm/oh/oh61
hb build -p ls2k300_mini_dp --gn-args build_xts=true
2. 烧录并启动
将编译好的固件烧录到开发板,启动系统。
3. 测试文件创建
OHOS # touch test.txt
OHOS # ls
Directory /:
test.txt
OHOS # mkdir os
OHOS # ls
Directory /:
test.txt
os
OHOS # cd os
OHOS # touch file.txt
OHOS # ls
Directory /os:
file.txt
测试结果
最终测试结果 ✅
OHOS # ls
Directory /:
1.txt 0
KV_FILE_SUM 4
os <DIR>
persist_parameters 1008
test.txt 24
OHOS # cat test.txt
Hello from file system!
OHOS # cd os
OHOS # ls
Directory /os:
OHOS #
所有功能正常:
- ✅
ls命令正确显示目录内容 - ✅
touch创建文件成功 - ✅
mkdir创建目录成功 - ✅
cd切换目录成功 - ✅
cat读取文件内容成功
文件创建测试
OHOS # touch 1.txt
VfsMpFind: root mount point matches, mp=0x9000000002028528, pathInMp='/1.txt'
VfsOpen: calling fops->open, pathInMp='/1.txt'
LfsOpen: opening '/1.txt', flags=259
lfs_dir_commit: starting, dir->pair=[0x1, 0x0], attrcount=3
LfsRamWrite: writing 256 bytes at offset 4864
lfs_dir_commit: success
LfsOpen: lfs_file_open ret=0
VfsOpen: fops->open returned 0
VfsOpen: returning fd=3
结果: ✅ 文件创建成功
目录创建测试
OHOS # mkdir os
OHOS # ls
Directory /:
1.txt
os
结果: ✅ 目录创建成功
目录切换测试
OHOS # cd os
OHOS # pwd
/os
OHOS # touch test.txt
OHOS # ls
Directory /os:
test.txt
结果: ✅ 目录切换成功
性能测试
文件系统初始化时间
LfsLowLevelInit: Starting file system init...
LfsLowLevelInit: RAM disk size=65536, block size=4096, block count=16
LfsLowLevelInit: DiskPartition succeed
LfsMount: lfs_mount ret=-84
LfsMount: formatting...
LfsMount: lfs_format ret=0
LfsMount: lfs_mount (after format) ret=0
LfsLowLevelInit: mount fs on '/' succeed
LfsLowLevelInit: File system init completed successfully!
初始化时间: < 100ms
文件操作性能
- 创建文件: < 10ms
- 创建目录: < 10ms
- 读取目录: < 5ms
- 切换目录: < 5ms
总结与经验
问题总结
- 表面现象: ls 命令显示 “BAD file” 错误,文件创建失败
- 中间层问题: VFS 层无法找到 mount point
- 根本原因: VfsMpFind 函数缺少对根目录 mount point 的特殊处理
真正的修复
经过深入调试,发现问题的真正原因是 VFS 层的 VfsMpFind 函数无法正确匹配根目录挂载点。修复该问题后,文件系统即可正常工作。
核心修复:kernel/liteos_m/components/fs/vfs/vfs_mount.c 中的 VfsMpFind 函数。
调试过程中的弯路
在调试过程中,曾错误地认为问题出在 LittleFS 的 lfs_dir_find 函数中,并尝试修改 dir->tail 的更新逻辑。这个修改导致了 “Cycle detected in tail list” 警告,说明修改是错误的。
教训:
- 应该从上层(VFS)开始调试,逐步向下定位问题
- 不要在没有充分理解的情况下修改底层代码
- 每次修改后都要验证是否引入了新问题
调试经验
1. 分层调试法
从上到下逐层调试:
- 应用层: shell 命令
- VFS 层: 虚拟文件系统
- 文件系统层: LittleFS 适配层
- 驱动层: RAM disk 驱动
每一层都添加调试信息,定位问题发生在哪一层。
2. 日志分析
仔细分析日志输出:
- 关注函数调用顺序
- 关注返回值和错误码
- 关注指针和内存地址
3. 代码审查
深入阅读代码:
- 理解函数的设计意图
- 分析边界条件
- 检查特殊情况处理
经验教训
1. 边界条件处理
问题: VfsMpFind 函数没有处理根目录 mount point 的特殊情况
教训:
- 所有的字符串处理函数都要考虑空字符串的情况
- 所有的路径处理函数都要考虑根目录的情况
- 编写单元测试覆盖边界条件
2. 调试信息的重要性
问题: 最初没有调试信息,难以定位问题
教训:
- 在关键路径添加调试信息
- 使用条件编译控制调试信息的输出
- 保留调试信息,方便后续维护
3. 测试覆盖
问题: 可能没有测试根目录 mount point 的场景
教训:
- 编写完整的测试用例
- 覆盖所有可能的路径组合
- 包括边界条件和异常情况
最佳实践
1. 文件系统初始化
// 1. 初始化存储介质
memset(g_lfsRamDisk, 0xFF, LFS_RAM_DISK_SIZE);
// 2. 注册设备
ret = LOS_DiskPartition("ram0", "littlefs", &lengthArray, g_lfsAddrArray, 1);
// 3. 配置分区
partCfg.readFunc = LfsRamRead;
partCfg.writeFunc = LfsRamWrite;
partCfg.eraseFunc = LfsRamErase;
// ... 其他配置
// 4. 挂载文件系统
ret = mount("ram0", "/", "littlefs", 0, &partCfg);
// 5. 测试文件系统
int fd = open("/test.txt", O_CREAT | O_WRONLY, 0666);
if (fd >= 0) {
// 文件系统正常
close(fd);
}
2. 错误处理
// 检查所有返回值
int ret = some_function();
if (ret != 0) {
printf("Error: function failed, ret=%d, errno=%d\n", ret, errno);
return ret;
}
// 使用断言检查不变量
LFS_ASSERT(ptr != NULL);
LFS_ASSERT(size > 0);
3. 调试信息
// 使用条件编译控制调试信息
#ifdef DEBUG_FS
#define FS_DEBUG(fmt, ...) printf("[FS] " fmt "\n", ##__VA_ARGS__)
#else
#define FS_DEBUG(fmt, ...)
#endif
// 在关键路径添加调试信息
FS_DEBUG("Opening file: %s, flags=%d", path, flags);
后续改进
1. 完善 “.” 和 “…” 处理
当前只是跳过 “.” 和 “…”,应该修复 LittleFS 的目录读取逻辑,正确处理这两个特殊目录项。
2. 添加更多测试
编写完整的测试用例,覆盖:
- 文件创建、读取、写入、删除
- 目录创建、删除、遍历
- 多级目录操作
- 并发访问
- 异常情况处理
3. 性能优化
- 优化 RAM disk 的读写性能
- 减少 LittleFS 的元数据开销
- 添加文件系统缓存
参考资料
官方文档
相关代码
kernel/liteos_m/components/fs/vfs/vfs_mount.c- VFS mount 管理kernel/liteos_m/components/fs/vfs/vfs_fs.c- VFS 文件操作kernel/liteos_m/components/fs/littlefs/lfs_adapter.c- LittleFS 适配层third_party/littlefs/lfs.c- LittleFS 核心实现
附录
A. 完整的调试信息输出
LfsLowLevelInit: Starting file system init...
LfsLowLevelInit: RAM disk size=65536, block size=4096, block count=16
LfsLowLevelInit: DiskPartition succeed
mount: source='ram0', target='/', fsType='littlefs'
mount: mp=0x9000000002028528, mPath='/', mDev='ram0'
LfsMount: lfs_mount ret=-84
LfsMount: formatting...
LfsMount: lfs_format ret=0
LfsMount: lfs_mount (after format) ret=0
mount: added mp to list, g_mountPoints=0x9000000002028528
LfsLowLevelInit: mount fs on '/' succeed
LfsLowLevelInit: Testing file system...
VfsOpen: path='/test.txt', flags=577
VfsMpFind: path='/test.txt', g_mountPoints=0x9000000002028528
VfsMpFind: checking mp=0x9000000002028528, mPath='/'
VfsMpFind: root mount point matches, mp=0x9000000002028528, pathInMp='/test.txt'
VfsOpen: calling fops->open, pathInMp='/test.txt'
LfsOpen: opening '/test.txt', flags=577
lfs_file_opencfg_: forcing consistency
lfs_fs_forceconsistency: starting
lfs_fs_forceconsistency: success
lfs_file_opencfg_: finding dir for '/test.txt'
lfs_dir_find: path='/test.txt', lfs->root=[0x1, 0x0]
lfs_dir_commit: starting, dir->pair=[0x1, 0x0], attrcount=3
LfsRamWrite: writing 256 bytes at offset 4864
lfs_dir_commit: success
LfsOpen: lfs_file_open ret=0
VfsOpen: fops->open returned 0
VfsOpen: returning fd=3
LfsLowLevelInit: Test file created successfully!
B. 错误码对照表
| 错误码 | 名称 | 说明 |
|---|---|---|
| 2 | ENOENT | No such file or directory |
| 5 | EIO | I/O error |
| 12 | ENOMEM | Cannot allocate memory |
| 13 | EACCES | Permission denied |
| 14 | EFAULT | Bad address |
| 17 | EEXIST | File exists |
| 22 | EINVAL | Invalid argument |
| 28 | ENOSPC | No space left on device |
| 84 | EILSEQ | Illegal byte sequence (LittleFS: Corrupted) |
C. LittleFS 配置参数说明
| 参数 | 值 | 说明 |
|---|---|---|
| readSize | 256 | 最小读取字节数 |
| writeSize | 256 | 最小写入字节数 |
| blockSize | 4096 | 块大小(字节) |
| blockCount | 16 | 块数量 |
| cacheSize | 256 | 缓存大小(字节) |
| lookaheadSize | 16 | 前瞻缓冲区大小(位) |
| blockCycles | 1000 | 块磨损均衡周期 |
结语
这次调试经历让我深刻体会到:
-
细节决定成败: 一个看似简单的路径匹配函数,因为缺少对根目录的特殊处理,导致整个文件系统无法正常工作。
-
调试方法很重要: 分层调试、日志分析、代码审查,这些方法帮助我们快速定位问题。
-
测试覆盖很关键: 如果有完整的测试用例,这个问题可能在开发阶段就被发现。
-
文档很有价值: 详细记录调试过程,不仅方便自己回顾,也能帮助他人避免类似问题。
希望这篇文档能够帮助到遇到类似问题的开发者,也欢迎大家对文档提出改进建议。
更多推荐
所有评论(0)