文件持久化和OSAL接口封装架构

在嵌入式项目(如智能屏开发)中,随着系统复杂度的提升,我们经常会面临两个棘手的问题:第一,如何保证设备在突然掉电时,写入本地的配置数据不损坏?第二,如何保证我们的业务代码不被底层的操作系统(如Linux、FreeRTOS)强行“绑架”,实现轻松移植?

今天这篇笔记,就来分享如何通过**“文件+备份文件”的原子写入模式解决数据丢失问题,以及如何通过OSAL(操作系统抽象层)**实现代码与系统底层的解耦。

一、数据持久化:文件写入与读出

1. 写入流程:副本优先与原子切换

为了保证数据的完整性(Integrity),写入操作并不直接修改当前正在使用的“主文件”,而是采用“先副本后替换”的策略:

  • 隔离写入:所有新数据首先写入到一个独立的“备份文件”(或临时文件)中。此时,如果发生系统崩溃、断电或磁盘空间不足,受损的只会是这个临时文件。

  • 保护现场:在写入过程中,原有的“主文件”保持不动。这意味着即使写入失败,系统重启后依然可以读取到旧但完整的数据,避免了文件内容被“写一半”导致的格式乱码。

  • 原子重命名(Atomic Switch):当备份文件写入并校验成功后,通过系统调用(如 POSIX 的 rename)将备份文件更名为庶文件。

    核心原理:在现代文件系统中,重命名操作是原子性的。它只改变文件系统的元数据指向,而不移动实际数据块,因此速度极快且不可分割——文件要么是旧的,要么瞬间变成新的,不存在中间态。

2. 读取流程:容错与降级恢复

读取机制的设计核心在于可用性(Availability),即在主文件失效时提供兜底方案:

  • 标准路径:程序默认尝试读取“主文件”。
  • 异常捕获:如果主文件因为磁盘损坏、权限问题或意外丢失而无法读取,系统会立即触发异常处理。
  • 降级读取/恢复:此时系统会转向读取“备份文件”。在某些实现中,如果主文件损坏,备份文件不仅可以作为读取源,还可以用来重新生成主文件,实现自动故障恢复(Self-healing)
特性 说明
防损坏 避免了直接修改主文件可能导致的“空文件”或“断裂写入”风险。
高可用 双文件冗余设计,主文件异常时仍有备份可用。
高性能 利用原子操作减少了复杂的锁定机制。
数据一致性 确保了系统在任何时间点崩溃,重启后都能获得一个逻辑一致的文件版本。

写入实现:

int file_param_write(const char *name, void *data, int len)
{
    char file_name[PARAM_SAVE_PATH_MAX_LEN];
    char file_name_copy[PARAM_SAVE_PATH_MAX_LEN + 5];
    
    int fd = -1;
    int ret = -1;
    snprintf(file_name, sizeof(file_name), "%s%s", PARAM_SAVE_PATH, name);
    snprintf(file_name_copy, sizeof(file_name_copy), "%s%s_copy", PARAM_SAVE_PATH, name);

    if ((fd = open(file_name_copy, O_WRONLY | O_CREAT | O_TRUNC, 0666)) < 0)
    {
        printf("Failed to create %s\n", file_name_copy);
        return -1;
    }

    ssize_t btyes_written = write(fd, data, len);
    if (btyes_written != len)
    {
        printf("Failed to write to %s\n", file_name_copy);
        close(fd);
        return -1;
    }

    if (fsync(fd) != 0)
    {
        printf("Failed to fsync %s\n", file_name_copy);
        close(fd);
        return -1;
    }

    // 重命名备份文件为主文件(原子操作),什么是原子操作?要不成功,要不失败,没有其它状态
    if (rename(file_name_copy, file_name) != 0) 
    {
        printf("Failed to rename %s to %s\n", file_name_copy, file_name);
        close(fd);
        return -1;
    }

    return 0;
}

读出实现:

int file_param_read(const char *name, void *data, int len)
{
    char file_name[PARAM_SAVE_PATH_MAX_LEN];
    char file_name_copy[PARAM_SAVE_PATH_MAX_LEN + 5];
    
    int fd = -1;
    // 构建文件路径
    snprintf(file_name, sizeof(file_name), "%s%s", PARAM_SAVE_PATH, name);
    snprintf(file_name_copy, sizeof(file_name_copy), "%s%s_copy", PARAM_SAVE_PATH, name);
    // 检查主文件是否存在,因为文件操作不是原子操作,这里使用备份方法,避免写操作时异常导致内容丢失
    if (access(file_name, F_OK) != 0)
    {
        if (access(file_name_copy, F_OK) != 0)
        {
            printf("Neither %s nor %s exists\n", file_name, file_name_copy);
            return -1;
        }
        // 将备份文件重命名为主文件
        if (rename(file_name_copy, file_name) != 0)
        {
            printf("Failed to rename %s to %s\n", file_name_copy, file_name);
            return -1;
        }
    }

    // 打开文件进行读取 O_RDONLY只读,无需带0644类的权限参数
    if ((fd = open(file_name, O_RDONLY)) < 0)
    {
        printf("Failed to open %s\n", file_name);
        return -1;
    }

    ssize_t bytes_read = read(fd, data, len);
    if (bytes_read > 0) 
    {
        printf("Read %ld bytes from %s\n", bytes_read, file_name);
    } 

    close(fd);
    return 0;
}

二、 OSAL接口封装

1. 什么是 OSAL,为什么需要它?

OSAL 是操作系统抽象层(Operating System Abstract Layer)的缩写 。 在嵌入式开发中,系统可能会面临跨平台的需求。例如,当前项目在 Linux 系统下运行,创建线程使用的是 pthread_create 。但如果将来项目需要移植到 FreeRTOS 上,它的任务创建函数变成了 xTaskCreate

如果没有抽象层,我们就需要全局搜索并修改所有调用了 pthread_create 的业务代码,不仅工作量大而且极易引入 Bug 。因此,我们通过搭建 OSAL 层,让应用层统一调用类似 osal_thread_create 的抽象接口 。更换系统时,只需重写这个接口的底层实现即可,实现完美解耦 。

2. 线程创建的抽象封装实战

对于不同的操作系统,我们可以重写osal_thread_create进行实现,这样只需要改一个函数,就可以完成多操作系统的适配,后续再添加其它操作系统,也只需要重写这个函数即可。

抽象后的线程创建函数实现

类型隐藏(Opaque Pointer):通过定义 typedef void* osal_thread_t;,我们将底层操作系统特有的线程标识符(如 Linux 的 pthread_t)隐藏起来。上层业务代码只知道这是一个“句柄”(Handle),从而彻底阻断了业务层对 <pthread.h> 的直接依赖。后续再添加其它操作系统,也只需要重写这个底层函数即可

#ifndef _OSAL_CONF_H_
#define _OSAL_CONF_H_

typedef void* osal_queue_t;

typedef void* osal_thread_t;

typedef enum
{
    OSAL_SUCCESS = 0,
    OSAL_ERROR = -1,
    OSAL_INVALID_PARAM = -2,
    OSAL_MEMORY_ERROR = -3,
    OSAL_SYSTEM_ERROR = -4
}OSAL_RESULT_T;

#endif

观察上述代码的实现细节,一个健壮的 OSAL 抽象层不仅仅是简单地“套个函数壳”,还需要严谨地处理好资源生命周期的闭环

  • 安全创建与错误拦截:在 osal_thread_create 中,不仅执行了核心的 pthread_create,还提前做好了空指针校验,并安全地通过 malloc 在堆上分配了句柄内存。
  • 优雅的生命周期管理:代码不仅提供了线程的创建与休眠(osal_thread_sleep),还完整提供了线程的异步取消(osal_thread_cancel)、同步等待(osal_thread_join)以及自杀式销毁(osal_thread_delete)接口。
  • 防内存泄漏与野指针:在 osal_thread_joinosal_thread_delete 退出时,代码严格执行了 free(*thandle) 并将指针置为 NULL,这在长期运行的嵌入式设备中是防止内存泄漏和操作野指针的关键防御性编程操作。
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#include <errno.h>
#include "osal_thread.h"
#include "osal_conf.h"

/**
 * 创建线程
 * @param thandle 输出参数,用于存储线程句柄
 * @param thread_fun 线程函数
 * @param arg 传递给线程函数的参数
 * @return 操作结果
 */
OSAL_RESULT_T osal_thread_create(osal_thread_t *thandle, 
                               void* (*thread_fun)(void *), 
                               void *arg)
{
    // 参数有效性检查
    if (thandle == NULL || thread_fun == NULL) {
        printf("osal_thread_create: Invalid parameters\n");
        return OSAL_INVALID_PARAM;
    }

    // 分配线程ID存储内存
    pthread_t *tid = (pthread_t *)malloc(sizeof(pthread_t));
    if (tid == NULL) {
        printf("osal_thread_create: Memory allocation failed\n");
        return OSAL_MEMORY_ERROR;
    }

    // 创建线程
    int ret = pthread_create(tid, NULL, thread_fun, arg);
    if (ret != 0) {
        printf("osal_thread_create: pthread_create failed, errno=%d\n", ret);
        free(tid);
        return OSAL_SYSTEM_ERROR;
    }

    *thandle = tid;
    return OSAL_SUCCESS;
}

/**
 * 取消线程
 * @param thandle 线程句柄
 * @return 操作结果
 */
OSAL_RESULT_T osal_thread_cancel(osal_thread_t *thandle)
{
    // 参数有效性检查
    if (thandle == NULL || *thandle == NULL) {
        printf("osal_thread_cancel: Invalid thread handle\n");
        return OSAL_INVALID_PARAM;
    }

    pthread_t *tid = (pthread_t *)*thandle;
    int ret = pthread_cancel(*tid);
    if (ret != 0) {
        printf("osal_thread_cancel: pthread_cancel failed, errno=%d\n", ret);
        return OSAL_SYSTEM_ERROR;
    }

    return OSAL_SUCCESS;
}

/**
 * 等待线程结束并释放资源
 * @param thandle 线程句柄
 * @param thread_return 用于存储线程返回值
 */
void osal_thread_join(osal_thread_t *thandle, void **thread_return)
{
    // 参数有效性检查
    if (thandle == NULL || *thandle == NULL) {
        printf("osal_thread_join: Invalid thread handle\n");
        return;
    }

    pthread_t *tid = (pthread_t *)*thandle;
    int ret = pthread_join(*tid, thread_return);
    if (ret != 0) {
        printf("osal_thread_join: pthread_join failed, errno=%d\n", ret);
    }

    // 释放线程ID内存
    free(*thandle);
    *thandle = NULL;
}

/**
 * 销毁线程资源
 * @param thandle 线程句柄
 * @return 操作结果
 */
OSAL_RESULT_T osal_thread_delete(osal_thread_t *thandle)
{
    // 参数有效性检查
    if (thandle != NULL && *thandle != NULL) {
        free(*thandle);
        *thandle = NULL;
    }

    // 线程自身调用时退出
    pthread_exit(NULL);
    // 理论上不会执行到这里
    return OSAL_SUCCESS;
}

/**
 * 线程休眠
 * @param msecs 休眠时间,单位:毫秒
 */
void osal_thread_sleep(int32_t msecs)
{
    usleep(msecs*1000);
}

整体总结

无论是前文提到的“文件+备份文件”的双文件持久化机制,还是这里的 OSAL(操作系统抽象层)接口封装,其核心目的都是为了构建一个高可靠、高内聚、低耦合的嵌入式系统架构。

  • 数据持久化解决了物理环境的痛点:通过巧妙利用文件系统“重命名”的原子性,实现了掉电不丢数据的防损坏机制,并提供了降级恢复的高可用性设计。
  • OSAL 层解决了软件工程的痛点:通过隔离业务逻辑与底层系统 API,避免了业务代码被特定的实时操作系统(RTOS)或 Linux 发行版“绑架”,极大降低了跨平台移植的成本。

掌握并运用好这两项架构设计技巧,可以让我们的系统代码在面对各种复杂的硬件平台和严苛的异常断电环境时,依然具备极强的鲁棒性与可维护性。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐