UTFS:类似TAR的嵌入式系统文件系统(2025)
摘要
UTFS是一个小巧的嵌入式文件系统,受TAR格式启发,专为微控制器设计,支持基于字符串的文件名存储数据,将数据存储与应用程序逻辑分离。
暂无内容
查看缓存全文
缓存时间: 2026/06/22 13:33
# UTFS:面向嵌入式系统的类Tar文件系统 | CLI Systems
来源:https://clisystems.com/article-UTFS-intro
在本文中,我们将介绍UTFS,即“微TAR文件系统”,这是由CLI Systems开发的一种小型嵌入式文件系统结构。请查看其GitHub仓库:https://github.com/clisystems/utfs/ UTFS 是一种在具有平面地址空间的存储介质上使用基于字符串的文件名来组织数据的方法,它将数据存储细节与应用层数据结构分离,并包含允许数据大小和位置在无数据丢失的情况下进行更改的特性。UTFS 专为读取、更新、写入操作而设计,对流式或追加操作的支持有限。
## 问题所在
几乎每个嵌入式系统都需要将数据存储到非易失性存储器中。常见示例包括序列号、配置参数和功能设置。微控制器可以轻松访问原始闪存/EEPROM,常见的解决方案是使用固定的数据结构,在启动时读取这些参数,并在参数更改时写入。这种解决方案有效,但存在一些严重影响固件结构的严重限制。以下是一个常见的数据存储结构示例,以及多个包含多个数据片段的源文件:
datastore.h:
``
typedef struct{
char myserial[8];
int subsystem1_settings;
int subsystem2_data;
int subsystem3_myval;
int subsystem2_data2;
}data_t;
extern data_t systemdata;
void loaddata();
void savedata();
``
datastore.c:
``
#include "datastore.h"
data_t systemdata;
void loaddata(){ ... }
void savedata(){ ... }
``
main.c:
``
#include "datastore.h"
loaddata();
printf("Serial: %s\n",systemdata.myserial);
``
subsystem1.c:
``
#include "datastore.h"
printf("Val: %d\n",systemdata.subsystem1_settings);
``
subsystem2.c:
``
#include "datastore.h"
printf("Val1: %d\n",systemdata.subsystem2_data);
printf("Val2: %d\n",systemdata.subsystem2_data2);
``
subsystem3.c:
``
#include "datastore.h"
printf("Val: %d\n",systemdata.subsystem3_myval);
``
这种方法存在几个问题:
1. 应用逻辑与数据结构紧密耦合
2. 不相关的代码可以修改数据
3. 结构非常僵化
4. 变量名非常僵化
## UTFS 解决方案
UTFS 的初衷是分离不同子系统的数据存储,使得一个子系统可以更新或更改,而系统中的其他源文件无需更新。隔离子系统需要一种方法来识别该子系统的正确数据块,这需要一个标识符,例如字符串;也就是文件的“名称”。在探索选项时,我们想起这个问题早在1970年代就已经用磁带驱动器解决了。磁带驱动器是平面内存地址空间的存储介质,没有随机访问写入的功能。TAR 文件格式是一种将这些平面内存空间上的数据存储到不同文件中的方法。TAR 存档格式有一个512字节的头部,其中存储了文件的所有信息,然后文件以512字节块的形式写入。后续文件依次附加一个头部和随后的数据。将 TAR 存档的基本概念与指向内存块的指针相结合,可以存储和检索任意数据,完全独立于源代码实现。
## UTFS 的独特特性
在开发 UTFS 解决方案时,我们重新评估了现代文件系统中常用的打开、读/写、关闭范式。文件的打开与关闭状态主要与数据流式操作或追加数据有关,并不理想地适用于加载-修改-保存的范式。因此,UTFS 没有打开和关闭函数,所有数据要么加载到 RAM 中,要么从 RAM 保存到存储介质上。虽然 TAR 格式简单,但其块和头部仍然是512字节,这可能占据微控制器的 RAM 或 EEPROM 大小的很大比例。因此,我们决定在 UTFS 头部中只存储最少量的信息。为了保持头部在八位字节边界上,决定将头部设为24字节,并允许12字节用于“文件名”,即最大文件名为11字节加上 C 字符串的空终止符(\0)。UTFS 头部包含一个16位的签名变量。该变量供应用程序设置,以管理不同的版本。签名值随数据自动加载和保存,因此无需在文件内部的数据结构中添加额外信息。
## 接口
UTFS 接口基于指向 RAM 数据块的指针。当加载 UTFS 文件系统时,如果某个文件与文件名匹配,则该数据被加载到 RAM 数据中。在以下示例中,三个子系统各自拥有独特的数据,每个数据的结构互不影响。
subsystem1.c:
``
#include "utfs.h"
typedef struct{
int settings;
}s1data_t;
s1data_t s1data;
utfs_file_t s1file;
utfs_set(&s1file,"file1",&s1data,sizeof(s1data));
utfs_register(&s1file, UTFS_NOFLAGS, UTFS_NOOPT);
``
subsystem2.c:
``
#include "utfs.h"
typedef struct{
int data;
int data2;
}s2data_t;
s2data_t s2data;
utfs_file_t s2file;
utfs_set(&s2file,"file2",&s2data,sizeof(s2data));
utfs_register(&s2file, UTFS_NOFLAGS, UTFS_NOOPT);
``
subsystem3.c:
``
#include "utfs.h"
typedef struct{
int myval;
}s3data_t;
s3data_t s3data;
utfs_file_t s3file;
utfs_set(&s3file,"file3",&s3data,sizeof(s3data));
utfs_register(&s3file, UTFS_NOFLAGS, UTFS_NOOPT);
``
utfs.h:
``
utfs_result_e utfs_init(bool verbose);
utfs_result_e utfs_register(utfs_file_t * f, utfs_flags_e flags, utfs_options_e options);
utfs_result_e utfs_set(utfs_file_t * fp, char * name, void * data, uint32_t size);
utfs_result_e utfs_load();
utfs_result_e utfs_save();
``
一旦调用 utfs_load() 函数,所有数据将从存储介质加载,并填入 RAM 数据结构中。应用程序可以根据需要修改 RAM 结构中的数据。当需要保存数据时,调用 utfs_save() 将所有数据写入存储介质。
## 数据大小的变化
数据结构经常发生变化,通常是由于业务需求变更或功能增加。如果 RAM 中的数据结构小于 UTFS 存储介质上的文件数据,则只加载当前 RAM 结构的大小,以防止 RAM 结构溢出。如果 RAM 中的数据结构大于 UTFS 存储介质上的文件数据,则只加载 UTFS 文件数据的大小。数据结构大小的变化仅影响 UTFS 加载的数据。当执行 utfs_save() 时,RAM 中数据结构的当前大小会被写入存储介质。这使得数据结构可以改变大小,并且由于文件系统的全部内容是一次性写入的,所有数据会自动调整位置。在此示例中,您可以看到保存到存储介质的数据的三个修订版,每次数据结构的大小都会改变。当调用 utfs_save() 时,所有数据在存储介质上自动对齐,而不会丢失数据。[](https://clisystems.com/img/articles/utfs-img4.png) 应用程序需要检测不同的文件大小(即修订版),并相应升级结构中的数据。请参阅**最佳实践**部分中关于签名变量使用的内容。
## 最佳实践
建议应用程序使用文件头部内置的签名变量来管理存储数据的版本。签名变量是一个 uint16_t 类型,随文件数据一起保存。使用签名变量,应用软件可以验证数据,或将数据从一个版本升级到更新版本。在以下示例中,签名值用于判断从存储介质加载的数据是哪个版本:
``
struct datav1{
char serialnumber[10];
char modelnumber[10];
};
struct datav2{
char serialnumber[16];
char modelnumber[16];
};
typedef union {
struct datav1 data1;
struct datav2 data2;
}data_u;
uint8_t buffer[sizeof(data_u)+1];
utfs_file_t sysfile;
// 注册 UTFS 文件
utfs_set(&sysfile,"system",buffer,sizeof(buffer));
utfs_register(&sysfile, UTFS_NOFLAGS, UTFS_NOOPT);
// 加载 UTFS 数据
utfs_load();
// 检查数据后的签名以确定版本
if(sysfile.signature == 0x00A1){
printf("Data is v1\n");
}else if(sysfile.signature == 0x00A2){
printf("Data is v2\n");
}else{
printf("Data unknown version, making it 0x00A2\n");
memset(buffer,0,sizeof(buffer));
sysfile.signature = 0x00A2;
}
``
签名变量有意不命名为“version”,因为数据的签名可能用于除版本管理之外的其他用途,例如数据校验和。此选项由应用程序开发者决定。
## 与现有数据存储的集成
现有的遗留固件通常是一种棘手的情况。固件在现场运行,存储介质上的数据通常不能丢失。开发的 UTFS 接口允许设置存储介质中数据的“基地址”。这使得 UTFS 系统可以定义数据起始位置,该位置可以远离现有数据存储。在该示例中,现有数据存储位于地址 0x00000000,而 UTFS 系统的基地址为 0x00001000,远在现有数据存储之外。[](https://clisystems.com/img/articles/utfs-img5.png)
## 结论
总体而言,UTFS 提供了一种简单的方法,将数据从非易失性存储加载到 RAM 或者从 RAM 保存到存储介质,同时将数据存储结构与应用程序级代码解耦。它专为与新系统或现有系统集成而设计,并提供低开销的 RAM 和闪存方法来组织数据。GitHub 上的 UTFS 代码以 MIT 许可发布,欢迎使用,并随时在 GitHub 上提交任何更改或意见。https://github.com/clisystems/utfs/
相似文章
littlefs的设计
littlefs是一个专为微控制器设计的故障安全文件系统,具有掉电恢复能力、动态磨损均衡以及有限的RAM/ROM。它提供类似POSIX的API,并用C语言编写,以实现较小的内存占用。
探索构建微型FUSE文件系统
本文将引导你使用Rust构建一个名为magicfs的最小FUSE文件系统,该系统使用metadata.json和blob文件作为后端存储,演示了名称查找、inode稳定性和内核缓存等核心文件系统概念。
F* 文件系统——直接绕过操作系统内核读取SSD的文件搜索
一款名为 ffs 的 CLI 工具,通过直接读取磁盘来搜索文件,绕过操作系统内核的 VFS 层,在处理大型、未缓存目录时相比 ripgrep 等工具具有潜在的速度优势。支持 ext4、btrfs 和 APFS 文件系统。
我在Arduino UNO(2KB内存)上写了个微型类Unix“操作系统”,带Shell和文件系统
Arc1011发布KernelUNO,为Arduino UNO打造的类Unix Shell与文件系统,仅占用2KB RAM,提供22条命令用于硬件控制与文件操作。
文件系统是AI代理的新原语
本文认为,文件系统因其悠久历史和在LLM训练数据中的广泛包含,为AI代理记忆提供了一种自然直观的原语,在探索性推理和持久化上下文方面优于传统数据库和API。