关键词:
作者:蒋卫峰 李涛
前言
上一篇文章介绍了littlefs中的目录操作,这一篇文章则将介绍littlefs中的文件读写操作。
本文会根据文件的存储类型进行介绍,即inline文件和outline文件,其读写过程也有差别。另外还会介绍inline文件到outline文件的转换,以及littlefs底层的读写API。
1. inline文件读写
因为inline文件数据存储于其父目录的元数据中,inline文件的读写实际上通过commit机制实现。读是通过遍历tag,写则是通过commit一个INLINESTRUCT类型的tag。
对于inline文件的数据读取,实际上就是从其父目录的元数据中进行读取,其过程已在commit机制中描述。
对于inline文件的写入,即commit一个INLINESTRUCT类型的tag,大致过程如下:
2. inline文件转outline文件
当文件大小超过1/8 block_size、或超过文件cache大小时,inline文件会转为outline文件,该转换过程在文件写入过程中触发。inline文件转为outline文件之后就不会再转回inline文件,即使对文件进行truncate操作。
转换过程步骤如下:
-
为文件重分配块,将inline数据写入块中
-
commit一个新的CTZSTRUCT类型的tag
commit过程如下图:
其中,CTZSTRUCT类型的tag中包含了新分配的文件跳表头节点的块指针。当读取文件,遍历tag时,检测到CTZSTRUCT,就会从其中文件跳表头节点的块指针读取文件数据。具体跳表中读写文件的过程在下小节中说明。
3. outline文件读写
回顾outline文件的存储结构,其数据是用一个跳表进行存储的:
outline文件的读写通过跳表的机制完成,commit时只需要commit带有更新后的跳表头的CTZSTRUCT tag。下面进行具体说明。
3.1 outline文件读操作
读取数据的步骤如下:
-
调用lfs_ctz_find找到目标数据所在的块
-
调用lfs_bd_read进行读取,该函数在后文进行分析
其中,lfs_ctz_find函数从头节点开始,通过块头处储存的跳表节点块指针进行遍历、寻找目标块位置。
跳表中块指针按固定规律分布:对block n,如果n可以被2^x整除,那么该block就含有一个指向block n-2^x的块指针。以block 4为例:
-
4可以被2^0整除,则block 4含有4-2^0即block 3的块指针
-
4可以被2^1整除,则block 4含有4-2^1即block 2的块指针
-
4可以被2^2整除,则block 4含有4-2^2即block 0的块指针
由此规律,又因为块的大小是固定的,那么只要知道文件的偏移位置,就可以获取该偏移位置所在block在跳表中的序号、该块上有几个块指针等信息。lfs_ctz_find函数就是根据此规律进行查找:
-
获取跳表中块序号:根据文件偏移和块大小计算,相关函数为lfs_ctz_index
-
获取块头部块指针数量:用ctz指令,ctz(块序号)
3.2 outline文件写操作
outline文件写入数据时又分为两种情况,其写入步骤也不同:
-
如果写入数据后不超过当前块,则调用lfs_bd_prog进行写入。该步骤相对简单。
-
如果写入数据后超过当前块:
-
调用lfs_ctz_find找到写入位置所在的块
-
调用lfs_ctz_extend在写入位置插入新的头节点
-
最后当调用lfs_file_sync或lfs_file_close时进行commit,实际将更新后的CTZSTRUCT tag写入元数据
-
当数据写入后超过当前块时,会涉及到跳表的更新,下面着重对这种情况进行说明。
3.2.1 lfs_ctz_extend
lfs_ctz_extend函数的作用是在文件写入的位置插入新的头节点。其步骤如下:
- 分配一个新块作为新的头节点,并调用lfs_bd_prog将原头节点块中的数据复制到新块中。下图中,调用lfs_bd_prog传入的pcache参数为file->cache,lfs_bd_prog会先将数据写入到file->cache中,等到需要进行flush操作时才将数据实际写回block。
- 将新的头节点与左边的后继结点链接,右边的旧的前继节点被舍弃(但块中内容不会被立即擦除):
注:如果文件写入位置位于文件末尾,则图示中ctz block即为旧头节点。调用lfs_file_seek函数可改变文件写入位置。
commit后会写入新的CTZSTRUCT tag,其过程如下:
3.2.2 COW策略
outline文件写入数据时是COW(copy-on-write)策略,lfs_ctz_extend函数插入新的头节点时并不会将旧头节点与后继节点的链接断掉。只有当最后将新的CTZSTRUCT tag写入其父目录的元数据中后,新的CTZSTRUCT tag中所包含的outline文件跳表头节点才更新成功。
因此,如果发生掉电等异常情况导致outline文件的写入操作未能完成时,其原有的数据也不会被丢弃。
如下图,outline文件插入新的节点时不会去破坏原有的块的数据。只有commit完成后,才会将新的头节点写入父目录的元数据中,将原来的头节点覆盖。
4. block device读写
littlefs中block device相关的读写操作是其他各种上层读写操作的基础,前文中提到的文件读写等操作均由block device相关的读写操作完成。block device相关读写操作是直接对具体的块进行操作。文件读写、元数据commit过程中都是通过调用了block device相关的读写操作完成的。主要的相关函数为:
-
lfs_bd_read:从源块或cache中读取数据
-
lfs_bd_prog:写入数据到目标块或cache
-
lfs_bd_flush:把cache中数据写入到块中。文件写入后,只有当进行文件flush、sync或关闭操作时,才会调用lfs_bd_flush将数据实际写入块中,并将所有的更改进行commit。
以上函数利用cache或直接从块中进行读写。
当直接从块中进行读写时,是调用了用户配置中提供的相关读写函数:
// Configuration provided during initialization of the littlefs
struct lfs_config
...
// Read a region in a block. Negative error codes are propogated
// to the user.
int (*read)(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, void *buffer, lfs_size_t size);
// Program a region in a block. The block must have previously
// been erased. Negative error codes are propogated to the user.
// May return LFS_ERR_CORRUPT if the block should be considered bad.
int (*prog)(const struct lfs_config *c, lfs_block_t block,
lfs_off_t off, const void *buffer, lfs_size_t size);
// Erase a block. A block must be erased before being programmed.
// The state of an erased block is undefined. Negative error codes
// are propogated to the user.
// May return LFS_ERR_CORRUPT if the block should be considered bad.
int (*erase)(const struct lfs_config *c, lfs_block_t block);
// Sync the state of the underlying block device. Negative error codes
// are propogated to the user.
int (*sync)(const struct lfs_config *c);
...
;
4.1 cache
block device读写函数均接受两个cache,即rcache和pcache作为参数,用作读缓存和写缓存。具体作用见后面分析。
littlefs中cache共有以下几种:
-
全局rcache,lfs->rcache。用作rcache参数。
-
全局pcache,lfs->pcache。读写元数据时用作pcache参数。
-
文件的cache,file->cache。当对文件进行读写操作时用作pcache参数。
4.2 block device读操作
lfs_bd_read将源块中数据读到目标buffer中。读取过程中,根据数据是否在缓存中,分为以下几种情况:
- 在pcache或rcache中:直接从cache中复制
- 不在pcache和rcache中,且所需读取大小小于一次能加载到cache中数据的大小:将源块中数据加载到rcache,以便后面从rcache中读
- 不在pcache和rcache中,且所需读取大小不小于一次能加载到cache中数据的大小:直接从源块中读
相关函数:
lfs_bd_read(lfs_t *lfs,
| const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint,
| lfs_block_t block, lfs_off_t off,
| void *buffer, lfs_size_t size)
| // 1. 检查是否已读完,未读完则继续步骤,否则结束
|-> while (size > 0) ...
|
| // 2. 如果pcache中有缓存对应数据,则从pcache中读
|-> if (pcache && block == pcache->block &&
| off < pcache->off + pcache->size)
| if (off >= pcache->off)
| // is already in pcache?
| diff = lfs_min(diff, pcache->size - (off-pcache->off));
| memcpy(data, &pcache->buffer[off-pcache->off], diff);
|
| data += diff;
| off += diff;
| size -= diff;
| continue;
|
| // pcache takes priority
| diff = lfs_min(diff, pcache->off-off);
|
|
| // 3. 如果rcache中有缓存对应数据,则从rcache中读
|-> if (block == rcache->block &&
| off < rcache->off + rcache->size)
| if (off >= rcache->off)
| // is already in rcache?
| diff = lfs_min(diff, rcache->size - (off-rcache->off));
| memcpy(data, &rcache->buffer[off-rcache->off], diff);
|
| data += diff;
| off += diff;
| size -= diff;
| continue;
|
| // rcache takes priority
| diff = lfs_min(diff, rcache->off-off);
|
|
| // 4. 如果未命中cache且size大于等于read_size,
| // 则读取内容大小超过cache一次加载的大小,此时从块中读
|-> if (size >= hint && off % lfs->cfg->read_size == 0 &&
| size >= lfs->cfg->read_size)
| // bypass cache?
| diff = lfs_aligndown(diff, lfs->cfg->read_size);
| lfs->cfg->read(lfs->cfg, block, off, data, diff);
|
| data += diff;
| off += diff;
| size -= diff;
| continue;
|
|
| // 5. 如果未命中cache且size小于read_size,则将块数据加载到rcache
|-> rcache->block = block;
| rcache->off = lfs_aligndown(off, lfs->cfg->read_size);
| rcache->size = lfs_min(
| lfs_min(
| lfs_alignup(off + hint, lfs->cfg->read_size),
| lfs->cfg->block_size)
| - rcache->off,
| lfs->cfg->cache_size);
| int err = lfs->cfg->read(lfs->cfg, rcache->block,
| rcache->off, rcache->buffer, rcache->size);
4.3 block device写操作
lfs_bd_prog的作用是将源数据写入到目标块中。但实际上没有立即将数据写入的目标块,而是先将数据复制到pcache中,等到flush操作时才将pcache中的数据写到块中:
相关函数:
lfs_bd_prog(lfs_t *lfs,
| lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate,
| lfs_block_t block, lfs_off_t off,
| const void *buffer, lfs_size_t size)
| // 1. 检查是否已写完,未写完则继续步骤,否则结束
|-> while (size > 0) ...
|
| // 2. 如果pcache已准备好,则将数据复制到pcache中
|-> if (block == pcache->block &&
| off >= pcache->off &&
| off < pcache->off + lfs->cfg->cache_size)
| // already fits in pcache?
| lfs_size_t diff = lfs_min(size,
| lfs->cfg->cache_size - (off-pcache->off));
| memcpy(&pcache->buffer[off-pcache->off], data, diff);
|
| data += diff;
| off += diff;
| size -= diff;
|
| // 2.1 如果pcache已满,则进行flush
|-> if (pcache->size == lfs->cfg->cache_size)
| // eagerly flush out pcache if we fill up
| lfs_bd_flush(lfs, pcache, rcache, validate);
| continue;
|
|
| // 3. 如果pcache未准备好,则准备pcache
|-> pcache->block = block;
| pcache->off = lfs_aligndown(off, lfs->cfg->prog_size);
| pcache->size = 0;
总结
本文介绍了littlefs中的文件读写机制,到这里littlefs大部分的操作就都已经做了分析了。下一篇文章将会介绍littlefs中的磨损均衡相关策略。
更多原创内容请关注:深开鸿技术团队
入门到精通、技巧到案例,系统化分享OpenHarmony开发技术,欢迎投稿和订阅,让我们一起携手前行共建生态。
本文作者:深开鸿Kaihong
https://ost.51cto.com/#bkwz
鸿蒙轻内核源码分析:文件系统littlefs(代码片段)
...为云社区《#鸿蒙轻内核M核源码分析系列二一02文件系统LittleFS》,作者:zhushy。LittleFS是一个小型的Flash文件系统,它结合日志结构(log-structured)文件系统和COW(copy-on-write)文件系统的思想,以日... 查看详情
esp8266littlefs文件系统(代码片段)
ESP8266LittleFS文件系统LittleFS文件系统,在未来的某个版本中将取代SPIFFS文件系统。目前SPIFFS系统已经停滞维护更新了。推荐使用LittleFS。LittleFS正在积极开发中,它支持目录,并且对于大多数操作来说速度更快。SPIFFS使用的方法与... 查看详情
esp8266littlefs文件系统基于vscodeandplatformio使用(代码片段)
ESP8266LittleFS文件系统基于VSCodeandPlatformIO使用学习如何上传文件到ESP8266NodeMCU板文件系统(LittleFS)使用VSCode与PlatformIOIDE扩展(快速和简单)。使用ESP8266的文件系统可以保存HTML、CSS和JavaScript文件来构建web服务器,而不必在ArduinoIDE中编... 查看详情
esp8266esp8266与littlefs文件系统(代码片段)
目的:在ESP8266使用LittleFS文件系统,而不是弃用的SPIFFS文件系统。环境:vscode+platformio+ESP8266的NodeMCU开发板文章目录1、更改默认的文件系统2、新建data文件夹3、上传文件4、测试5、总结1、更改默认的文件系统在p... 查看详情
java虚拟机原理class字节码二进制文件分析五(方法计数器|方法表|访问标志|方法名称索引|方法返回值类型|方法属性数量|方法属性表)(代码片段)
...法返回类型4、方法属性数量前言上一篇博客【Java虚拟机原理】Class字节码二进制文件分析四(字段表数据结构|字段表详细分析|访问标志|字段名称|字段描述符|属性项目)分析了字段表的一些数据;当前的字节码文件中只有111个字段... 查看详情
esp8266esp8266与littlefs文件系统(代码片段)
目的:在ESP8266使用LittleFS文件系统,而不是弃用的SPIFFS文件系统。环境:vscode+platformio+ESP8266的NodeMCU开发板文章目录1、更改默认的文件系统2、新建data文件夹3、上传文件4、测试5、总结1、更改默认的文件系统在p... 查看详情
mysql数据库--主从复制读写分离(代码片段)
文章目录一、主从复制1、原理2、二进制日志的保存方式3、主从复制策略4、搭建MySQL主从复制二、读写分离1、原理2、读写分离3、读写分离的好处4、实现方式4.1、Amoeba简介五、搭建MySQL读写分离1、实验准备2、amoeba(192.168.35.30)安... 查看详情
golang读写锁实现与核心原理分析(代码片段)
读写锁的特点 读写锁区别与互斥锁的主要区别就是读锁之间是共享的,多个goroutine可以同时加读锁,但是写锁与写锁、写锁与读锁之间则是互斥的写锁饥饿问题 因为读锁是共享的,所以如果当前已经有读锁... 查看详情
[五]类加载机制双亲委派机制底层代码实现原理源码分析java类加载双亲委派机制是如何实现的
Launcher启动类 本文是双亲委派机制的源码分析部分,类加载机制中的双亲委派模型对于jvm的稳定运行是非常重要的不过源码其实比较简单,接下来简单介绍一下 我们先从启动类说起 有一个Launcher类 sun.misc.Launcher; &nb... 查看详情
mysql读写分离(代码片段)
MySQL读写分离一.原理二.为什么要做读写分离三.实现方式四.搭建MySQL读写分离五.测试读写分离一.原理读写分离就是只在主服务器上写,只在从服务器上读。基本的原理是让主数据库处理事务性查询,而从数据库处理select... 查看详情
[redis]主从复制读写分离哨兵集群的原理分析和配置关注点(代码片段)
前言:要理解Redis大规模集群的由来,就必然跨不过理解哨兵和主从复制的由来以及存在的缺陷,不管谁学习这几个知识点都逃不过这个顺序… 一、主从复制+读写分离现在的公司项目如果有Redis那么一定是集群的&... 查看详情
golangsync.map原理以及性能分析(代码片段)
sync.Map原理以及性能分析支持并发的mapsync.Map数据结构LoadStoredeleteRangesync.Map总结sync.Map,读写锁的适用场景参考文献golang支持map关键字,golang的map的读写是编译成runtime的函数调用。但是默认的map是非线程安全的。go1.9版本... 查看详情
编译原理——算符优先分析文法(附源代码)
算符优先分析文法一、写在前面 算符优先分析文法是一种工具,在编译的过程中,隶属于语法分析环节,却又与中间代码的生成息息相关,编译可以分为五个阶段:词法分析、语法分析、语义分析(中间代... 查看详情
嵌入式文件系统fatfs和littlefs对比
...应该知道FatFS这个文件系统,今天就给大家讲讲FatFS和LittleFS的内容,以及他们之间的一些差 查看详情
esp8266闪存文件系统(spiffs)基本操作指令(包含部分littlefs)以及实例
ESP8266闪存文件系统基本操作(包含部分LittleFS)SPIFFS文件系统操作官方说明文档:https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#file-system-object-spiffs-littlefs-sd-sdfsSPIFFS弃用警告SPIFFS目前已被弃用,并可能在内核的未来版本中被删... 查看详情
markdown交易迁移-分析片(代码片段)
我的渲染技术进阶之旅google开源的基于物理的实时渲染引擎filament源码分析:在android中如何使用matc命令自动将输入材质定义.mat文件转换为输出材质包.filamat文件?(代码片
文章目录一、需求描述二、先打开.mat文件查看一下三、matc工具3.1什么是matc工具呢?3.2matc工具在哪里?3.3matc的底层实现源代码3.4命令行使用matc3.4.1执行matc.exe--help命令查看matc命令的用法3.4.2执行matc命令将.mat文件转换为.filamat文... 查看详情
android逆向整体加固脱壳(dex优化流程分析|dvmdexfileopenpartial|dexfileparse|脱壳点|获取dex文件在内存中的首地址)(代码片(代码片段)
文章目录前言一、DexPrepare.cpp中rewriteDex()方法分析二、DvmDex.cpp中dvmDexFileOpenPartial()方法分析(脱壳点)三、DexFile.cpp中dexFileParse()方法分析(脱壳点)前言上一篇博客【Android逆向】整体加固脱壳(DEX优化流程分析|DexPrepare.cpp中dvmContinueOpti... 查看详情