背景与现象

在龙芯 LS2K0300 平台上运行 OpenHarmony 6.1(LiteOS-M 内核)的 KV Store 功能测试套件 KvStoreFuncTestSuite 时,出现大量测试用例失败。典型输出如下:

testKvStoreSetValue001:FAIL: Expected 0 Was -1
testKvStoreSetValue002:FAIL: Expected 0 Was -1
testKvStoreSetValue003:FAIL: Expected 0 Was -1
testKvStoreSetValue004:FAIL: Expected 0 Was -1
testKvStoreSetValue005:PASS   ← 空值测试,不涉及文件操作
testKvStoreGetValue001:FAIL: Expected 0 Was -1
testKvStoreClearCache001:FAIL: Expected 0 Was -1
testKvStoreCacheSize001:FAIL: Expected 0 Was -1
testKvStoreMaxSize001:FAIL: Expected 0 Was -1

所有涉及文件读写的用例均返回 -1EC_FAILURE),只有不操作文件的用例通过。后续调试过程中,又出现了另外几类错误:

LfsLowLevelInit: mount fs on '/' failed
LfsLowLevelInit: RAM disk allocation failed

以及修复上述问题后仍然存在的测试失败:

testKvStoreClearCache001:FAIL: Expected -1 to be greater than 0
testKvStoreMaxSize001:FAIL: Expected -1 to be greater than 0
...

本文完整记录了从现象定位到最终解决的调试全过程,涉及文件系统挂载、内存布局、HAL 接口契约等多个层面的问题。


第一阶段:文件系统与路径问题

1.1 调用链分析

从测试代码到硬件层的完整调用链如下:

测试代码: UtilsSetValue("test_key_1", "value_1")
    │
    ▼
kv_store.c: SetValueToFile("test_key_1", "value_1")
    │  注意:key 直接作为文件名使用
    ▼
kv_store.c: UtilsFileOpen("test_key_1", O_RDWR_FS | O_CREAT_FS | O_TRUNC_FS, 0)
    │
    ▼
file.c: HalFileOpen("test_key_1", oflag, mode)
    │
    ▼
hal_file.c: open("test_key_1", oflag)    ← ❌ 路径不以 "/" 开头
    │
    ▼
vfs_fs.c: VfsOpen("test_key_1", flags)
    │
    ▼
vfs_mount.c: VfsMpFind("test_key_1", &pathInMp)
    │  ❌ VfsMpFind 要求路径以 "/" 开头才能匹配挂载点
    │  "test_key_1" 不以 "/" 开头 → mp = NULL → 返回 -1
    ▼
返回 EC_FAILURE (-1)

1.2 二个叠加的基础问题

问题一:/data 目录未创建

KV Store 测试将数据路径设置为 /data

#define DATA_PATH "/data"
static BOOL KvStoreFuncTestSuiteSetUp(void)
{
    UtilsSetEnv(DATA_PATH);  // 设置路径为 /data
    return TRUE;
}

UtilsSetEnv() 是一个空函数:

int UtilsSetEnv(const char* path)
{
    (void)path;
    return EC_SUCCESS;  // 什么都没做
}

即使文件系统挂载成功,/data 目录也未被创建,导致后续以 /data/ 前缀写文件时找不到路径。

问题二:HAL 层未处理非绝对路径

默认 HAL 实现 commonlibrary/utils_lite/hals/file/hal_file.c 直接将传入的路径透传给 POSIX open()

int HalFileOpen(const char *path, int oflag, int mode)
{
    (void)mode;
    return open(path, oflag);  // path = "test_key_1"
}

而 LiteOS-M 的 VFS 层要求路径以 / 开头才能正确匹配挂载点。对于 "test_key_1" 这种非绝对路径,VfsMpFind 会直接返回 NULL

注意:在 Hi3861 等平台上,HAL 层会自动为相对路径添加 /data/ 前缀,但龙芯平台使用的是通用实现,没有这一逻辑。

1.3 第一阶段修复

修复1:创建 /data 目录

fs_init.cLfsLowLevelInit() 末尾添加:

mkdir("/data", 0777);
printf("%s: Created /data directory\n", __func__);
修复2:平台特定 HAL 层添加路径前缀

device/soc/loongson/ls2k300/adapter/hals/utils/file/hal_file.c 中实现路径自动补全:

#define DATA_PATH_PREFIX "/data/"

static void GetFullPath(const char *path, char *fullPath, unsigned int fullLen)
{
    if (path == NULL || fullPath == NULL || fullLen == 0) {
        return;
    }
    if (path[0] == '/') {
        snprintf(fullPath, fullLen, "%s", path);
    } else {
        snprintf(fullPath, fullLen, "%s%s", DATA_PATH_PREFIX, path);
    }
}

int HalFileOpen(const char *path, int oflag, int mode)
{
    (void)mode;
    char fullPath[PATH_MAX] = {0};
    GetFullPath(path, fullPath, sizeof(fullPath));
    return open(fullPath, oflag);
}

// HalFileDelete、HalFileStat 等函数同理处理

同时编写对应的 BUILD.gn,利用 OpenHarmony 的板级适配机制自动覆盖默认实现。


第二阶段:内存与存储空间不足

修复第一阶段问题后,测试依然失败,且启动日志中出现新的错误:

LfsLowLevelInit: mount fs on '/' failed

2.1 LittleFS RAM 磁盘空间不足

fs_init.c 中原本的 RAM disk 分配大小为 32KB,对于存储多组 KV 对(测试会写入 128 个键值对)来说远远不够。

解决方案:将 RAM disk 扩大至 256KB。

#define LFS_RAM_DISK_SIZE (256 * 1024)
#define LFS_BLOCK_SIZE    4096
#define LFS_BLOCK_COUNT   (LFS_RAM_DISK_SIZE / LFS_BLOCK_SIZE)

static uint8_t g_lfsRamDisk[LFS_RAM_DISK_SIZE];  // 静态分配在 BSS 段

2.2 堆空间不足

改为 256KB 静态 RAM disk 后,又出现新的错误:

LfsLowLevelInit: RAM disk allocation failed

原因分析:LittleFS 挂载过程中仍需要从 heap 中分配内部缓存(cacheSizelookaheadSize 等)和 VFS 结构体。默认的 heap 大小仅 64KB,无法满足需求。

解决方案:将系统堆扩大至 320KB。

kernel/liteos_m/kernel/include/los_config.h 中修改:

#ifndef LOSCFG_SYS_HEAP_SIZE
#define LOSCFG_SYS_HEAP_SIZE  (320 * 1024)
#endif

内存布局总结

项目 大小 说明
RAM 总大小 800KB Linker script 定义
Stack 64KB _STACK_SIZE = 0x10000
LittleFS RAM disk 256KB 静态分配(BSS 段)
Heap 320KB 动态分配

此时文件系统能够正常挂载,部分测试开始通过,但仍有几类用例失败。


第三阶段:HalFileStat 接口契约错误(根本原因)

3.1 现象

即使文件系统工作正常、heap 空间充足,以下用例仍然失败:

testKvStoreClearCache001:FAIL: Expected -1 to be greater than 0
testKvStoreClearCache002:FAIL: Expected -1 to be greater than 0
testKvStoreCacheSize003:FAIL: Expected -1 to be greater than 0
testKvStoreMaxSize001:FAIL: Expected -1 to be greater than 0
...

所有失败都表现为 UtilsGetValue 返回 -1,但期望返回值 > 0

3.2 深度调用链追踪

UtilsGetValue(key, temp, len)
    ↓
GetValueByFile(key, value, len)        [kv_store.c]
    ↓
UtilsFileStat(key, &valueLen)          [kv_store.c]
    ↓
HalFileStat(key, &valueLen)            [hal_file.c]  ← 问题发生点

GetValueByFile 中,判断逻辑为:

static int GetValueByFile(const char* key, char* value, unsigned int len)
{
    unsigned int valueLen = 0;
    if (UtilsFileStat(key, &valueLen) != EC_SUCCESS) {  // EC_SUCCESS = 0
        return EC_FAILURE;  // 只要返回值不是 0 就失败
    }
    // ... 后续读文件操作
}

3.3 错误的 HAL 实现

第一阶段我们添加的 HalFileStat 实现如下:

int HalFileStat(const char *path, unsigned int *fileSize)
{
    (void)fileSize;  // ❌ 忽略了输出参数
    char fullPath[PATH_MAX] = {0};
    GetFullPath(path, fullPath, sizeof(fullPath));
    struct stat info = { 0 };
    int ret = stat(fullPath, &info);
    if (ret < 0) {
        return ret;          // 失败返回 -1(正确)
    } else {
        return info.st_size; // ❌ 成功时返回文件大小(如 28),而不是 0
    }
}

根据 utils_file.h 的接口契约:

/**
 * @brief Obtains the file size.
 * @param path Indicates the file name.
 * @param fileSize Indicates the file size. This is an output parameter.
 * @return Returns <b>0</b> if the file size is obtained; returns <b>-1</b> otherwise.
 */
int UtilsFileStat(const char* path, unsigned int* fileSize);
  • 成功时应当返回 0,并将文件大小写入 *fileSize
  • 失败时返回 -1

我们错误地在成功时返回了文件大小(一个正整数),导致上层 if (UtilsFileStat(...) != 0) 条件成立,误认为操作失败。

3.4 正确修复

int HalFileStat(const char *path, unsigned int *fileSize)
{
    char fullPath[PATH_MAX] = {0};
    GetFullPath(path, fullPath, sizeof(fullPath));
    struct stat info = { 0 };
    int ret = stat(fullPath, &info);
    if (ret < 0) {
        return -1;
    }
    if (fileSize != NULL) {
        *fileSize = (unsigned int)info.st_size;  // 正确写回输出参数
    }
    return 0;  // 成功返回 0
}

同样的修正也需要应用到 HalFileDeleteHalFileOpen 等其他 HAL 函数中,确保返回值严格遵循 POSIX-like 接口约定(成功返回 0,失败返回 -1)。


完整修复方案汇总

1. 文件系统初始化修复

文件device/board/loongson/ls2k300_mini_dp/liteos_m/fs/fs_init.c

  • 扩大 RAM disk 至 256KB。
  • 在挂载成功后创建 /data 目录。
  • 修正 main.c 中的条件编译错误(或确保 LfsLowLevelInit 被正确调用)。

2. 堆大小配置

文件kernel/liteos_m/kernel/include/los_config.h

#define LOSCFG_SYS_HEAP_SIZE  (320 * 1024)

3. 平台 HAL 层适配

文件device/soc/loongson/ls2k300/adapter/hals/utils/file/hal_file.c

  • 实现 GetFullPath 为相对路径添加 /data/ 前缀。
  • 修正所有文件操作函数(HalFileOpenHalFileDeleteHalFileStat)的返回值和参数处理,严格遵循接口契约。

文件:同目录下的 BUILD.gn

ohos_static_library("hal_file_static") {
  sources = [ "hal_file.c" ]
  include_dirs = [
    "//commonlibrary/utils_lite/include",
    "//commonlibrary/utils_lite/hals/file",
  ]
}

利用 commonlibrary/utils_lite/file/BUILD.gn 中的板级适配逻辑自动覆盖默认实现。


测试验证

完成所有修复后,重新编译并烧录固件,运行 KV Store 功能测试套件:

testKvStoreSetValue001-017: PASS
testKvStoreGetValue001-005: PASS
testKvStoreDeleteValue: PASS
testKvStoreClearCache001-002: PASS
testKvStoreCacheSize001-003: PASS
testKvStoreMaxSize001-004: PASS

全部 30+ 个测试用例均通过。


经验总结

接口契约是铁律

HAL 层函数必须严格遵守上层定义的接口契约,包括:

  • 返回值的含义(0 表示成功,-1 表示失败)。
  • 输出参数的使用(不能忽略、不能为空)。
  • 路径处理的约定。

任何偏离都可能导致上层逻辑误判,且这类错误往往难以直接定位。

内存规划要全面

在资源受限的 LiteOS-M 平台上,静态分配(BSS/Data)和动态分配(Heap)需统筹考虑:

  • LittleFS RAM disk 若用静态数组,占用的是 BSS 段,不消耗 heap。
  • 但 LittleFS 内部缓存、VFS 结构体仍需 heap 空间。
  • 测试用例可能同时打开多个文件,需要预留足够的文件句柄和缓冲区。
  • 内存空间有限的情况下,为满足KvTest 可以将RAM disk配置为128KB(静态分配),heap 配置为64KB, stack 配置为64KB, 整个RAM 锁定为384KB

调试方法总结

  1. 从失败用例倒推调用链,逐层检查函数返回值与参数传递。
  2. 增加日志输出,尤其在 HAL 层和文件系统层打印路径和错误码。
  3. 检查编译配置与链接脚本,确认内存布局和宏定义是否符合预期。
  4. 参考其他成功平台的实现(如 Hi3861),对比 HAL 层差异。

适用性说明

本文所述问题及解决方案专门针对 龙芯 LS2K300 平台 + OpenHarmony 6.1 (LiteOS-M) 组合。不同平台的内存布局和 HAL 层实现可能有所不同,但排查思路和接口契约的遵守原则是通用的。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐