首页 / 资讯中心 / 文章详情

RT-Thread上FatFs文件系统重入配置与多线程并发读写实战

RT-Thread上FatFs文件系统重入配置与多线程并发读写实战 ★ FEATURED ARTICLE
1. 项目背景与核心需求拆解1.1 为什么要在实时操作系统上折腾文件系统做过嵌入式开发的人都有一个共识裸机跑程序的时候数据存储怎么简单怎么来直接操作Flash扇区或者EEPROM读写就行。但一旦项目复杂度上来尤其是引入实时操作系统之后事情就变了。你可能会遇到这样的场景设备需要同时记录运行日志、保存用户配置、还要支持通过串口导出数据文件——这些需求背后都指向同一个东西文件系统。我最早接触这个需求是在一个数据采集项目上。主控跑的是RT-Thread采集模块每200毫秒往存储介质写一批数据同时还有一个通信任务需要读取配置文件。最开始我用的是自己写的简易存储管理直接按固定地址读写Flash。结果问题很快就来了配置文件一改日志区的地址就得跟着调日志写满之后要循环覆盖逻辑越写越复杂更头疼的是两个线程同时操作存储区域的时候偶尔会出现数据错乱。后来痛定思痛决定上FatFs。FatFs是ChaN老师开发的一个通用FAT文件系统模块完全用ANSI C写成跟平台无关。它的设计目标就是给小型嵌入式系统提供一个可裁剪、可移植的文件系统方案。支持FAT12、FAT16、FAT32甚至exFAT需要单独配置代码量小RAM占用可控对于资源受限的MCU来说非常友好。但问题在于FatFs本身的设计是非重入的。什么意思就是说如果你在多个线程里同时调用f_open、f_write这些接口底层的数据结构会被破坏轻则文件损坏重则整个文件系统挂掉。这在裸机时代不是问题因为裸机只有一个执行流。但在RT-Thread这种多线程环境下这就是一个必须解决的核心矛盾。1.2 重入功能到底解决什么问题先把这个概念说清楚。重入指的是一个函数在被多个执行流线程或中断同时调用时仍然能保证正确性的能力。FatFs的API大部分是不可重入的原因在于它内部维护了一个全局的文件系统对象数组FATFS结构体每个逻辑驱动器对应一个。当你调用f_mount挂载一个驱动器后后续的f_open、f_read等操作都会访问这个全局对象。如果两个线程同时操作同一个驱动器就会出现竞态条件。举个具体的例子线程A正在执行f_write刚把文件大小更新到FATFS对象里还没来得及写回目录项线程B调用f_read读取同一个文件。这时候线程B看到的文件大小是更新后的值但实际数据可能还没完全落盘读出来的内容就是错的。更严重的情况是两个线程同时操作FAT表可能导致FAT链断裂整个分区就废了。所以重入功能的核心目标就是让多个线程能够安全地并发访问文件系统。注意这里说的是“安全”不是“高性能并发”。在嵌入式场景下我们通常不需要真正的并行读写只需要保证当一个线程在操作文件系统时其他线程要么等待要么被正确阻塞不会破坏数据一致性。1.3 方案选型的几个关键考量解决重入问题业界常见的有三种思路第一种是全局互斥锁。在FatFs的每个API入口加一把大锁谁进来谁加锁出去解锁。这是最简单粗暴的方案优点是实现简单、不容易出错缺点是并发度低一个线程在读写大文件时其他线程全部阻塞。对于大多数嵌入式应用来说这个缺点其实可以接受因为文件操作通常不是高频操作。第二种是每个驱动器一把锁。RT-Thread的DFS框架设备文件系统就是这种思路它为每个挂载点维护一个互斥量。这样不同驱动器之间的操作可以并行同一个驱动器的操作仍然串行。这个方案比全局锁灵活一些适合有多存储介质的场景。第三种是真正的可重入设计比如为每个线程维护独立的文件系统上下文。这个方案实现复杂度极高RAM开销也大在MCU上基本不现实。综合来看基于RT-Thread的DFS框架利用其提供的互斥保护机制是最务实的方案。RT-Thread的DFS本身就对下层文件系统有重入保护的要求它会在调用底层文件系统接口前后进行加锁解锁。我们需要做的就是正确配置DFS的锁机制并确保FatFs的移植层与DFS框架正确对接。这里有一个关键点需要说明RT-Thread的DFS框架在调用底层文件系统的操作接口时会自动获取对应挂载点的互斥量。但这个互斥量保护的是DFS层面的操作如果FatFs内部还有自己的全局状态比如FATFS对象数组那么仅靠DFS的锁是不够的。所以我们需要确保FatFs的配置中每个逻辑驱动器对应独立的FATFS对象并且DFS的锁能够覆盖到所有对FatFs的调用路径。2. FatFs移植的核心细节与实操要点2.1 源码获取与目录结构规划FatFs的源码可以从其官方网站获取当前稳定版本是R0.14系列。下载下来之后核心文件就两个ff.c和ff.h以及可选的ffconf.h配置文件。另外还有diskio.c和diskio.h这两个是移植层需要自己实现的。在RT-Thread项目中我建议的目录结构是这样的project/ ├── applications/ │ └── main.c ├── drivers/ │ └── diskio.c # 移植层实现 ├── packages/ │ └── fatfs/ │ ├── ff.c │ ├── ff.h │ ├── ffconf.h │ ├── diskio.h │ └── SConscript └── rtconfig.h把FatFs放在packages目录下通过SConscript脚本控制编译这样在menuconfig里可以方便地开关。diskio.c放在drivers目录下因为它是跟具体硬件相关的换一个板子可能就需要改这个文件。注意ffconf.h是FatFs的配置文件里面有很多宏定义控制功能裁剪。不要直接改ff.c和ff.h所有配置都通过ffconf.h来做这样后续升级FatFs版本的时候会轻松很多。2.2 ffconf.h的关键配置项ffconf.h里有几十个配置项但真正影响重入和RTOS适配的主要是下面这几个#define FF_FS_READONLY 0 #define FF_FS_MINIMIZE 0 #define FF_USE_STRFUNC 1 #define FF_USE_FIND 1 #define FF_USE_MKFS 1 #define FF_USE_FASTSEEK 1 #define FF_USE_LABEL 0 #define FF_USE_FORWARD 0 #define FF_CODE_PAGE 936 #define FF_USE_LFN 2 #define FF_MAX_LFN 255 #define FF_LFN_UNICODE 0 #define FF_FS_RPATH 1 #define FF_VOLUMES 2 #define FF_STR_VOLUME_ID 0 #define FF_MULTI_PARTITION 0 #define FF_MIN_SS 512 #define FF_MAX_SS 4096 #define FF_FS_TINY 0 #define FF_FS_EXFAT 0 #define FF_FS_NORTC 1 #define FF_FS_LOCK 0 #define FF_FS_REENTRANT 1 #define FF_FS_TIMEOUT 1000 #define FF_SYNC_t rt_mutex_t重点说几个FF_FS_REENTRANT这个宏是重入功能的总开关。设置为1时FatFs会要求你提供一套同步机制通过ff_req_grant、ff_rel_grant等宏。设置为0时FatFs完全不考虑重入问题。我们这里必须设为1。FF_FS_TIMEOUT同步超时时间单位是系统节拍。1000表示等待1000个tick在RT-Thread默认配置下就是1秒。这个值需要根据实际场景调整如果文件操作比较频繁可以适当加大。FF_FS_LOCK这个宏控制文件锁功能。设置为0时FatFs不检查文件是否被重复打开。在多线程环境下我建议至少设置为1这样可以防止一个线程打开的文件被另一个线程意外关闭。但要注意这个锁是文件级别的不是线程级别的它只能防止重复打开不能替代互斥量。FF_VOLUMES逻辑驱动器数量。根据实际需要设置不要设太大每个驱动器都会占用一个FATFS结构体的RAM。FF_USE_LFN长文件名支持。设置为2表示使用动态内存分配长文件名缓冲区。如果你的系统RAM比较紧张可以设置为1使用静态缓冲区但那样就不支持重入了因为静态缓冲区是全局的。2.3 同步机制的实现当FF_FS_REENTRANT设置为1时FatFs会调用下面这几个宏来实现同步#define ff_req_grant(fp) (rt_mutex_take(fp, FF_FS_TIMEOUT) RT_EOK) #define ff_rel_grant(fp) rt_mutex_release(fp) #define ff_del_syncobj(fp) rt_mutex_delete(fp) #define ff_cre_syncobj(bvol, fp) \ do { \ char name[8]; \ rt_snprintf(name, sizeof(name), fat%c, 0 bvol); \ *fp rt_mutex_create(name, RT_IPC_FLAG_PRIO); \ } while(0)这几个宏定义在ffconf.h的末尾或者单独放在一个头文件里。注意ff_cre_syncobj的第二个参数是FF_SYNC_t类型的指针在RT-Thread下就是rt_mutex_t的指针。这里有一个细节FatFs在挂载驱动器的时候会调用ff_cre_syncobj创建互斥量在卸载的时候调用ff_del_syncobj删除。所以如果你反复挂载卸载同一个驱动器要注意互斥量的创建和删除是否配对。实操心得rt_mutex_create的第二个参数我推荐用RT_IPC_FLAG_PRIO而不是RT_IPC_FLAG_FIFO。因为文件操作通常有优先级继承的需求比如低优先级线程持有文件锁的时候高优先级线程来请求优先级继承可以避免优先级反转问题。2.4 diskio.c的移植要点diskio.c是FatFs与底层存储介质的桥梁需要实现以下几个函数DSTATUS disk_initialize(BYTE pdrv); DSTATUS disk_status(BYTE pdrv); DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count); DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count); DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff);这些函数的实现跟具体的存储介质相关。如果是SD卡就调用SD卡的读写接口如果是SPI Flash就调用SPI Flash的驱动。这里不展开讲具体驱动的实现重点说几个容易踩坑的地方。第一个坑是扇区大小。FatFs默认的扇区大小是512字节FF_MIN_SS但有些SPI Flash的擦除单位是4KB。如果你直接按512字节读写驱动层需要自己做缓冲。我建议在disk_ioctl里正确返回GET_SECTOR_SIZE让FatFs知道实际的扇区大小。如果FF_MIN_SS和FF_MAX_SS都设置为512那FatFs就只认512字节的扇区。第二个坑是写操作的原子性。disk_write在写入过程中如果被打断比如被高优先级中断抢占可能导致数据写坏。所以disk_write内部需要做好临界区保护。但注意这个保护跟FatFs的重入锁是两回事FatFs的锁保护的是文件系统层面的数据结构disk_write的临界区保护的是硬件操作层面。第三个坑是disk_ioctl的CTRL_SYNC命令。这个命令要求把缓存的数据全部写入介质。对于有写缓存的存储设备必须正确实现这个命令否则在断电等异常情况下可能丢数据。3. 与RT-Thread DFS框架的对接实现3.1 DFS框架的工作机制RT-Thread的DFSDevice File System是一个虚拟文件系统层它向上提供标准的POSIX文件操作接口open、read、write、close等向下对接具体的文件系统实现FatFs、LittleFS、YAFFS等。DFS的核心是struct dfs_filesystem_ops每个文件系统需要实现这个结构体里定义的操作函数。DFS的重入保护机制是这样的每个挂载点struct dfs_mount_tbl在挂载时会创建一个互斥量保存在struct dfs_filesystem的lock字段里。当应用程序调用open、read等接口时DFS会先获取这个互斥量然后调用底层文件系统的对应函数完成后释放互斥量。但这里有一个关键问题DFS的锁是在VFS层加的它保护的是从VFS到具体文件系统的调用路径。如果两个线程同时调用readDFS会串行化这两个调用。但如果一个线程调用read另一个线程直接调用FatFs的f_read绕过DFS那就没有保护了。所以移植时必须确保所有文件操作都走DFS接口不要混用。3.2 dfs_fatfs的移植实现RT-Thread已经提供了FatFs的DFS移植层在components/dfs/filesystems/elmfat目录下。这个移植层实现了dfs_filesystem_ops结构体static const struct dfs_filesystem_ops _fatfs_ops { elm, DFS_FS_FLAG_DEFAULT, _fatfs_fops, dfs_fatfs_mount, dfs_fatfs_unmount, dfs_fatfs_mkfs, dfs_fatfs_statfs, dfs_fatfs_stat, dfs_fatfs_rename, dfs_fatfs_unlink, dfs_fatfs_ioctl, ... };其中dfs_fatfs_mount会调用FatFs的f_mountdfs_fatfs_unmount调用f_mount(NULL)其他函数对应FatFs的f_open、f_read等。如果你使用的是RT-Thread官方版本这个移植层已经做好了你只需要在menuconfig里使能elmfat即可。但如果你用的是自己移植的FatFs版本或者需要对移植层做定制那就需要自己实现这些函数。我遇到过一个情况项目使用的RT-Thread版本比较老自带的elmfat移植层不支持FF_FS_REENTRANT。这时候就需要手动修改移植层在mount的时候创建互斥量在unmount的时候删除。3.3 重入配置的完整检查清单配置完成后建议按下面的清单逐项检查检查项预期值说明FF_FS_REENTRANT1重入总开关FF_FS_TIMEOUT1000超时时间根据场景调整FF_FS_LOCK≥1文件锁防止重复打开FF_VOLUMES实际需要驱动器数量FF_USE_LFN2动态缓冲区支持重入RT_USING_DFS使能DFS框架RT_USING_DFS_ELMFAT使能FatFs移植层RT_DFS_ELMFAT_REENTRANT使能移植层重入支持RT_USING_MUTEX使能互斥量支持这个表格里的每一项都需要在rtconfig.h或者menuconfig里确认。特别是最后三项如果RT_USING_MUTEX没有使能ff_cre_syncobj里的rt_mutex_create会编译失败。注意有些RT-Thread版本里DFS的重入支持是通过RT_USING_DFS_REENTRANT宏控制的而不是RT_DFS_ELMFAT_REENTRANT。具体用哪个宏要看你的RT-Thread版本。建议直接在源码里搜索REENTRANT关键字确认。4. 多线程并发读写的实操验证4.1 测试方案设计配置完成之后怎么验证重入功能真的生效了我设计了一个简单的测试方案创建三个线程一个持续写文件一个持续读文件一个持续创建和删除文件。三个线程操作同一个驱动器上的不同文件运行一段时间后检查文件系统是否损坏。测试代码的大致结构如下static rt_mutex_t test_lock; static volatile rt_bool_t test_running RT_TRUE; void write_thread_entry(void *param) { int fd; char buf[256]; rt_memset(buf, W, sizeof(buf)); while (test_running) { fd open(/sdcard/write_test.txt, O_WRONLY | O_CREAT | O_TRUNC); if (fd 0) { for (int i 0; i 100; i) { write(fd, buf, sizeof(buf)); } close(fd); } rt_thread_mdelay(10); } } void read_thread_entry(void *param) { int fd; char buf[256]; while (test_running) { fd open(/sdcard/write_test.txt, O_RDONLY); if (fd 0) { while (read(fd, buf, sizeof(buf)) 0) { /* 校验数据 */ } close(fd); } rt_thread_mdelay(5); } } void create_delete_thread_entry(void *param) { int fd; char filename[32]; while (test_running) { for (int i 0; i 10; i) { rt_snprintf(filename, sizeof(filename), /sdcard/tmp_%d.txt, i); fd open(filename, O_WRONLY | O_CREAT | O_TRUNC); if (fd 0) { write(fd, test, 4); close(fd); } unlink(filename); } rt_thread_mdelay(20); } }三个线程的优先级设置要有差异这样才能测试优先级继承是否正常工作。我一般把写线程设为最高优先级读线程中等创建删除线程最低。4.2 测试结果分析在没有启用重入功能的情况下这个测试跑不了几分钟就会出现问题。常见现象包括文件内容错乱、目录项丢失、甚至整个分区无法挂载。用调试器抓取的时候经常能看到FATFS结构体里的fsi文件系统信息字段被破坏。启用重入功能之后同样的测试可以稳定运行数小时不出错。但要注意稳定运行的前提是所有文件操作都通过DFS接口。如果某个线程直接调用了f_open那就绕过了DFS的锁仍然可能出问题。我还做过一个对比测试把FF_FS_TIMEOUT分别设置为100、1000、10000观察不同超时时间下的表现。结果发现超时时间太短100的时候高优先级线程偶尔会超时失败超时时间太长10000的时候系统响应变慢。最终我选择了1000在RT-Thread默认的1000Hz节拍下就是1秒这个值在大多数场景下比较均衡。4.3 性能影响评估加锁必然会带来性能开销。我实测的数据是单线程读写的时候启用重入功能前后性能差异在5%以内基本可以忽略。多线程并发的时候由于串行化总吞吐量会比单线程略低但这是保证数据一致性的必要代价。具体来说写一个256字节的块单线程情况下大约需要200微秒不包括存储介质本身的写入时间加锁解锁的开销大约在10微秒左右。对于大多数嵌入式应用来说这个开销完全可以接受。实操心得如果你的应用对文件操作性能要求极高可以考虑把文件操作集中到一个专用线程里其他线程通过消息队列发送请求。这样虽然增加了一次消息传递的开销但避免了锁竞争整体吞吐量可能反而更高。5. 常见问题与排查技巧实录5.1 挂载失败问题排查挂载失败是最常见的问题表现是f_mount返回FR_NOT_READY或者FR_NO_FILESYSTEM。排查思路如下首先检查disk_initialize是否返回0表示成功。如果返回STA_NOINIT说明底层存储初始化失败。这时候需要检查硬件连接、供电、时钟配置等。如果disk_initialize成功但f_mount仍然失败用disk_read读取0扇区检查是否有有效的引导记录。FAT文件系统的0扇区最后两个字节应该是0x55AA。如果不是说明存储介质上没有有效的文件系统需要先格式化。格式化可以通过f_mkfs来完成。但要注意f_mkfs需要较大的栈空间默认的线程栈可能不够。我遇到过在512字节栈的线程里调用f_mkfs导致栈溢出的情况后来把栈加大到2048才解决。5.2 文件操作返回FR_INT_ERRFR_INT_ERR表示内部错误通常是因为文件系统数据结构被破坏。在多线程环境下这个错误几乎可以肯定是重入问题导致的。排查步骤确认FF_FS_REENTRANT是否设置为1。确认ff_req_grant和ff_rel_grant宏是否正确实现。确认所有文件操作是否都走了DFS接口。确认DFS的互斥量是否正常工作可以在加锁解锁的地方加打印。还有一个容易被忽略的点如果使用了FF_USE_LFN2动态长文件名需要确保ff_memalloc和ff_memfree正确实现。这两个宏默认使用malloc和free但在RT-Thread下如果使用了内存池或者特殊的内存管理需要做相应适配。5.3 系统运行一段时间后死机这种情况通常是死锁导致的。可能的原因有一是互斥量嵌套获取。比如线程A持有DFS的锁然后在某个回调里又调用了文件操作试图再次获取同一个锁。RT-Thread的互斥量默认不支持递归获取所以会死锁。解决办法是避免在持有文件锁的情况下调用文件操作。二是优先级反转。低优先级线程持有文件锁高优先级线程等待中等优先级线程抢占CPU导致高优先级线程一直等不到锁。解决办法是使用支持优先级继承的互斥量RT_IPC_FLAG_PRIO。三是超时时间设置不当。如果FF_FS_TIMEOUT设置为0表示无限等待一旦出现异常就会永久阻塞。建议设置为一个合理的值超时后返回错误让上层有机会处理。5.4 常见问题速查表现象可能原因排查方法解决方案挂载失败存储未初始化检查disk_initialize返回值修复底层驱动挂载失败无文件系统读0扇区检查0x55AA执行f_mkfs格式化FR_INT_ERR重入冲突检查FF_FS_REENTRANT配置启用重入并检查锁实现数据错乱绕过DFS直接调用FatFs搜索代码中的f_open调用统一走DFS接口系统死锁互斥量嵌套检查回调中的文件操作避免嵌套获取系统死锁优先级反转检查互斥量创建标志使用RT_IPC_FLAG_PRIO超时失败超时时间太短观察超时发生频率适当增大FF_FS_TIMEOUT栈溢出f_mkfs栈需求大检查线程栈使用情况增大线程栈5.5 几个容易被忽略的细节第一个细节是ff_cre_syncobj的返回值。这个宏在创建互斥量失败时应该返回0但很多移植版本直接忽略了返回值。如果互斥量创建失败后续的ff_req_grant会操作一个空指针直接导致硬件异常。建议在ff_cre_syncobj里加上返回值检查。第二个细节是卸载时的资源释放。f_mount(NULL)会调用ff_del_syncobj删除互斥量。但如果此时还有其他线程在等待这个互斥量删除操作会导致未定义行为。正确的做法是先确保所有文件操作都已完成再执行卸载。第三个细节是FF_FS_LOCK和DFS锁的关系。FF_FS_LOCK是FatFs层面的文件锁它记录的是哪个文件被打开了。DFS锁是VFS层面的互斥量它保证同一时间只有一个线程在操作文件系统。两者配合使用才能既保证数据一致性又防止文件被意外关闭。第四个细节是长文件名的内存分配。FF_USE_LFN2时每次文件操作都会动态分配长文件名缓冲区。如果内存碎片化严重可能导致分配失败。在长时间运行的系统里建议使用内存池而不是malloc来管理这部分内存。6. 进阶优化与扩展思路6.1 性能优化减少锁竞争如果文件操作比较频繁锁竞争会成为瓶颈。一个有效的优化手段是减小锁的粒度。RT-Thread的DFS默认是在整个操作期间持有锁比如f_write从开始到结束都持有锁。但实际上f_write内部可以分为几个阶段参数检查、FAT表更新、数据写入、目录项更新。其中数据写入阶段是最耗时的但这个阶段其实不需要持有文件系统锁只需要保证同一文件的写入互斥即可。不过修改DFS的锁粒度需要改动框架代码风险较大。更稳妥的做法是在应用层做优化把大文件拆分成多个小文件减少单次操作的时间或者把文件操作分散到不同的驱动器上利用多驱动器的并行能力。6.2 掉电保护数据安全加固嵌入式设备经常面临意外掉电的情况。FatFs本身有一定的掉电保护能力但不够完善。我通常会在应用层做几件事一是定期调用f_sync把缓存数据刷到存储介质。f_sync比f_close轻量不会关闭文件只是把脏数据写回。二是使用双备份策略。重要的配置文件保存两份读取的时候先读主文件校验失败再读备份文件。三是在文件内容里加校验和。每次写入的时候计算CRC读取的时候验证。如果校验失败说明数据损坏可以从备份恢复。6.3 多驱动器配置如果系统有多个存储介质比如SD卡和SPI Flash可以配置多个逻辑驱动器。每个驱动器有独立的FATFS对象和独立的互斥量可以并行操作。配置方法是在ffconf.h里把FF_VOLUMES设置为2或更大然后在diskio.c里根据pdrv参数区分不同的物理驱动器。挂载的时候分别挂载到不同的路径f_mount(fs[0], 0:, 1); // SD卡 f_mount(fs[1], 1:, 1); // SPI Flash在DFS层面可以通过dfs_mount把不同的FatFs驱动器挂载到不同的目录dfs_mount(sd0, /sdcard, elm, 0, 0); dfs_mount(flash0, /flash, elm, 0, 0);这样应用程序就可以通过路径来区分不同的存储介质了。6.4 与LittleFS的对比选型FatFs不是唯一的选择。RT-Thread还支持LittleFS这是一个专为嵌入式设计的日志型文件系统具有更好的掉电保护能力和更低的RAM占用。但LittleFS的兼容性不如FatFsPC上不能直接读取。我的选型建议是如果设备需要跟PC交换数据比如通过USB导出日志用FatFs如果纯粹是设备内部存储对掉电保护要求高用LittleFS。两者在RT-Thread下都支持重入配置方式类似。6.5 调试技巧用SystemView分析锁竞争RT-Thread支持SEGGER的SystemView工具可以实时显示线程状态和互斥量操作。在调试重入问题的时候我经常用SystemView来观察哪个线程持有文件锁、持有了多长时间、哪些线程在等待、等待了多久。通过这些信息可以快速定位锁竞争的热点有针对性地优化。如果没有SystemView也可以在ff_req_grant和ff_rel_grant里加打印输出时间戳和线程ID。虽然会影响性能但调试阶段很有用。7. 实际项目中的经验总结7.1 配置阶段的关键决策回顾我经手的几个项目在FatFs重入配置上有几个决策点值得分享。第一个是FF_FS_TIMEOUT的值。我最初设置的是100觉得1秒太长了。结果在一个高频写入的场景下偶尔会出现超时失败。后来分析发现当存储介质正在进行垃圾回收或者坏块管理的时候单次写操作可能超过100毫秒。改成1000之后问题消失。所以这个值要根据最坏情况下的操作耗时来定不能拍脑袋。第二个是FF_USE_LFN的选择。如果项目不需要长文件名果断设置为0可以省下不少RAM和代码空间。如果确实需要设置为2动态分配而不是1静态缓冲区因为静态缓冲区不支持重入。但设置为2之后要确保内存管理是线程安全的。第三个是FF_FS_LOCK的值。我一般设置为FF_VOLUMES的2倍这样每个驱动器可以同时打开多个文件。如果设置为0虽然省了一点RAM但失去了文件保护在多线程环境下风险很大。7.2 测试阶段的有效方法功能测试通过之后一定要做压力测试和异常测试。压力测试就是前面说的多线程并发读写跑至少24小时。我一般会写一个脚本自动记录每次操作的返回值和耗时跑完之后分析有没有异常。异常测试包括在文件操作过程中突然断电、在写入过程中拔掉SD卡、在挂载状态下复位系统。这些异常场景最能暴露问题。我遇到过一个bug在写入过程中断电重新上电后文件系统无法挂载。后来发现是FAT表更新到一半被打断导致FAT链损坏。解决办法是在disk_write里实现掉电保护确保FAT表的更新是原子的。7.3 维护阶段的注意事项系统上线之后文件系统的维护也很重要。我建议在应用层加一个文件系统健康检查功能定期执行以下检查检查剩余空间是否充足、检查文件数量是否超过预期、检查关键文件是否存在且完整、检查文件系统是否可读写。如果发现异常及时告警或者自动修复。比如剩余空间不足的时候自动删除最旧的日志文件关键文件损坏的时候从备份恢复。还有一个经验不要频繁挂载卸载。有些开发者习惯在每次文件操作前挂载操作完卸载。这样做虽然省电但会增加文件系统损坏的风险。正确的做法是系统启动时挂载一次运行期间一直保持挂载状态只有在确定不再使用的时候才卸载。7.4 一个真实的踩坑案例最后分享一个我踩过的坑。在一个项目里我按照标准流程配置了FatFs的重入功能测试也通过了。但产品小批量试产的时候有几台设备出现了文件系统损坏的问题。排查了很久最后发现是中断里调用了文件操作。这个项目有一个定时器中断每10毫秒触发一次在中断里往文件里写一条记录。中断的优先级高于所有线程所以当线程持有文件锁的时候中断来了直接调用f_write绕过了互斥量导致数据结构被破坏。解决办法是把中断里的文件操作改成发送消息到队列由专门的线程来处理。这个案例告诉我们重入保护只对线程有效对中断无效。任何在中断里调用文件系统的行为都是危险的。实操心得如果你确实需要在中断里记录数据可以在中断里把数据写入一个环形缓冲区然后由一个低优先级线程定期把缓冲区的内容写入文件。这样既保证了实时性又避免了中断里的文件操作。7.5 关于RT-Thread版本的兼容性提醒不同版本的RT-Thread在DFS和elmfat的实现上有差异。比如RT-Thread 3.x和4.x的DFS接口就有变化4.x引入了dfs_file结构体文件操作接口从struct dfs_fd改成了struct dfs_file。移植的时候要注意版本匹配。另外RT-Thread的软件包中心里有elmfat包可以直接通过menuconfig添加。但我建议还是把FatFs源码放在项目里因为软件包中心的版本可能跟你的RT-Thread版本不兼容而且出了问题不好调试。如果你使用的是RT-Thread Nano版本那就要注意了Nano版本默认不包含DFS组件需要手动添加。而且Nano版本的内存管理比较简单可能不支持动态内存分配这时候FF_USE_LFN就只能设置为0或者1。整个移植过程下来我的体会是FatFs的重入功能本身不复杂配置项就那么几个但要把整个链路都打通需要同时对FatFs、RT-Thread的DFS框架、以及底层存储驱动都有清晰的理解。任何一个环节出问题都会表现为文件系统异常排查起来比较费劲。所以建议在项目初期就把这套机制搭好、测好不要等到产品快发布的时候再来补。
阅读完成 · 觉得有帮助?
咨询建站