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),看看这些概念是怎么在代码层面串联起来的。到时候我会带大家手写一个简单的文件读写示例,把今天讲的东西都用上。

有什么问题,欢迎在课后交流。我是你们的讲师,咱们下节课见。