4. 文件系统基础概念:文件、目录、路径、挂载点、文件描述符、文件系统类型
大家好,我是你们的嵌入式系统讲师。今天我们来聊聊文件系统的基础概念。说实话,很多初学者一听到「文件系统」就觉得头大,觉得这是Linux内核工程师才需要关心的事。但我在Zephyr项目里踩过不少坑之后,发现——嗯,这些基础概念搞不清楚,后面写存储代码真的会出大问题。
咱们一个一个来拆解。我会结合我在Zephyr上的实际经验来讲。
4.1 文件(File)
文件是什么?说白了,就是一段有名字的数据。在Zephyr里,文件可以存在Flash、SD卡、甚至RAM里。我个人习惯把文件理解成「带标签的盒子」——盒子里装的是字节流,标签就是文件名。
在Zephyr中,文件操作的基本API长这样:
/* 打开一个文件 */
struct fs_file_t file;
fs_open(&file, "/spiflash/config.bin");
/* 写入数据 */
uint8_t data[] = {0x01, 0x02, 0x03};
fs_write(&file, data, sizeof(data));
/* 关闭文件 */
fs_close(&file);
这里要注意:Zephyr的文件API和POSIX很像,但又不完全一样。比如fs_file_t这个结构体,它内部其实维护了一个文件位置指针。我在项目中遇到过一个问题——连续读写时忘记重置位置指针,结果数据全写歪了。嗯,这个坑我后面会细说。
核心要点:文件是字节流的容器。在嵌入式环境里,文件的大小通常受限于底层存储介质。比如SPI Flash的一个扇区是4KB,那你一个文件最好别跨太多扇区,否则读写性能会很难看。
4.2 目录(Directory)
目录,也叫文件夹。它的作用就是给文件分门别类。你想想看,如果所有文件都堆在根目录下,那找起来得多痛苦?
在Zephyr里,目录操作也很直接:
struct fs_dir_t dir;
fs_dir_open(&dir, "/spiflash");
struct fs_dirent entry;
while (fs_dir_read(&dir, &entry) == 0) {
if (entry.type == FS_DIR_ENTRY_FILE) {
printk("文件: %s\n", entry.name);
} else if (entry.type == FS_DIR_ENTRY_DIR) {
printk("目录: %s\n", entry.name);
}
}
fs_dir_close(&dir);
我曾经在一个项目里,把日志文件全部放在根目录下。结果跑了三个月,根目录下堆了上千个文件。每次遍历目录都卡得要死。后来我改成按日期建子目录,问题就解决了。所以——目录结构的设计,真的不是小事。
4.3 路径(Path)
路径就是文件或目录在文件系统中的「地址」。Zephyr支持两种路径:
- 绝对路径:从根目录
/开始写,比如/spiflash/config.bin - 相对路径:相对于当前工作目录,比如
config.bin
我个人建议在嵌入式环境里尽量用绝对路径。为什么?因为Zephyr的当前工作目录概念比较弱,不同文件系统实现的行为可能不一样。用绝对路径最保险,不会出现「咦,我明明在/data目录下,怎么文件写到根目录去了」这种诡异问题。
小技巧:路径字符串的长度要预留够。我见过有人用char path[32],结果路径一长就溢出。建议至少用CONFIG_FS_MAX_PATH_LEN这个宏定义的长度,或者直接给128字节。
4.4 挂载点(Mount Point)
挂载点这个概念,很多做单片机开发的同学一开始不太理解。说白了,挂载点就是把一个存储设备「接入」到文件系统树上的那个位置。
在Zephyr里,挂载操作是这样的:
static struct fs_mount_t mp = {
.type = FS_LITTLEFS,
.fs_data = &littlefs_fs,
.storage_dev = (void *)&flash_area,
.mnt_point = "/lfs",
};
fs_mount(&mp);
你看,.mnt_point = "/lfs"就是挂载点。之后所有对/lfs目录下的操作,都会映射到那块SPI Flash上。
我记得有一次调试,怎么都挂载不上。后来发现是storage_dev传错了指针。嗯,这个参数一定要和你底层Flash的分区信息对上。Zephyr的Flash分区通常用fixed-partitions设备树来定义,挂载的时候要指定正确的分区ID。
注意:挂载点路径不能和其他挂载点重叠。比如你已经在/spiflash挂了一个文件系统,就不能再在/spiflash/data挂另一个。Zephyr会直接报错返回。
4.5 文件描述符(File Descriptor)
文件描述符,简称fd。它是操作系统给每个打开的文件分配的一个整数编号。在Zephyr里,文件描述符是通过fs_open返回的struct fs_file_t结构体来管理的,而不是一个简单的int。
但底层原理是一样的:每个打开的文件都占用一个描述符槽位。Zephyr默认的槽位数量由CONFIG_FS_FILE_MAX_OPEN控制,默认值通常很小,比如4或8。
我曾经在一个项目里同时打开了6个文件,结果第7个打开失败。查了半天才发现是描述符用完了。所以——
经验之谈:养成用完就关的好习惯。每次fs_open之后,记得配对fs_close。如果实在要同时打开很多文件,记得在prj.conf里调大CONFIG_FS_FILE_MAX_OPEN。
4.6 文件系统类型(File System Type)
Zephyr支持多种文件系统类型。我整理了一个表格,方便大家对比:
| 文件系统类型 | 适用场景 | 特点 | 我常用的场景 |
|---|---|---|---|
| LittleFS | SPI Flash、NOR Flash | 掉电安全、磨损均衡、小内存占用 | 配置存储、日志记录 |
| FAT FS | SD卡、USB存储 | 兼容性好、PC可直接读取 | 数据导出、固件升级包 |
| NVS (Non-Volatile Storage) | 小数据量存储 | 键值对方式、极简实现 | 设备校准参数、WiFi密码 |
| FCB (Flash Circular Buffer) | 日志类数据 | 循环写入、固定大小 | 传感器数据流 |
选型的时候怎么选?我个人习惯是:
- 如果存配置文件,用LittleFS。掉电不丢数据,这是硬道理。
- 如果要把数据拿到PC上看,用FAT。插上读卡器就能读。
- 如果只是存几个int型的参数,用NVS。简单粗暴,代码量最少。
你想想看,要是用FAT存配置文件,突然掉电了,文件可能就损坏了。而LittleFS有掉电保护机制,这就是它最大的价值。
补充一点:Zephyr的LittleFS默认配置是CONFIG_FILE_SYSTEM_LITTLEFS。打开之后,记得还要配一下CONFIG_FS_LITTLEFS_FC_HEAP_SIZE,这个值太小会导致文件操作失败。我一般设成4096起步。
小结
好了,今天的基础概念就讲到这里。文件、目录、路径、挂载点、文件描述符、文件系统类型——这六个概念是后续所有文件系统操作的地基。地基打不牢,后面盖楼肯定歪。
下一章我们会深入讲Zephyr的虚拟文件系统层(VFS),看看这些概念是怎么在代码层面串联起来的。到时候我会带大家手写一个简单的文件读写示例,把今天讲的东西都用上。
有什么问题,欢迎在课后交流。我是你们的讲师,咱们下节课见。