当前位置: 首页 > 科技观察

Littlefs原理分析——文件读写(五)

时间:2023-03-19 20:06:54 科技观察

了解更多开源请访问:开源基础软件社区https://ost.51cto.com前言一篇介绍littlefs中的目录操作,本文会介绍littlefs中的文件读写操作。本文将根据文件的存储类型进行介绍,即内联文件和大纲文件,读写过程也不同。另外还会介绍inline文件到outline文件的转换,以及littlefs的底层读写API。1.内联文件的读写由于内联文件的数据保存在其父目录的元数据中,所以内联文件的读写实际上是通过commit机制实现的。读取是通过遍历标签,写入是通过提交一个INLINESTRUCT类型的标签。内联文件的数据读取实际上是从其父目录的元数据中读取,这个过程在commit机制中已经描述过了。对于写内联文件,即提交一个INLINESTRUCT类型的标签,一般过程如下:2.将内联文件转换为外联文件。当文件大小超过1/8block_size或超过文件缓存大小时,内联文件将被转换为轮廓文件。这个转换过程是在文件写入过程中触发的。内联文件转换为大纲文件后,即使文件被截断,也不会再转换回内联文件。转换过程步骤如下:为文件重新分配块,将内联数据写入块。提交一个类型为CTZSTRUCT的新标记。提交过程如下图所示:其中,CTZSTRUCT类型标签包含了新分配的文件跳表头节点的块指针。读取文件遍历标签时,如果检测到CTZSTRUCT,则从文件跳转表头节点的块指针读取文件数据。具体跳转表中文件的读写过程在下一节中介绍。3.大纲文件读写回顾一下大纲文件的存储结构,它的数据是用跳表存储的:大纲文件的读写是通过跳表的机制完成的,只有commit和updated提交CTZSTRUCT标记时需要跳转表头。下面将给出详细描述。(1)大纲文件读取操作读取数据的步骤如下:调用lfs_ctz_find查找目标数据所在块。调用lfs_bd_read进行读取,这个函数后面会分析。其中,lfs_ctz_find函数从头节点开始,遍历存储在块头的跳表节点块指针,找到目标块位置。跳转表中的块指针按照固定的规则分布:对于块n,如果n能被2^x整除,那么该块包含一个指向块n-2^x的块指针。以块4为例:4能被2^0整除,则块4包含4-2^0,即块3的块指针。4能被2^1整除,则块4包含4-2^1是block2的block指针。4能被2^2整除,那么block4包含4-2^2,是block0的block指针。根据这个规则,又因为block的大小是fixed,只要知道文件的偏移位置,就可以得到偏移位置所在块在跳转表中的序号,块上有多少个块指针等信息.lfs_ctz_find函数是按照这个规则查找:获取跳转表中的块号:根据文件偏移量和块大小计算,相关函数是lfs_ctz_index获取块头块指针个数:使用ctz命令,ctz(blocknumber)(2)outline文件写入操作outline文件写入数据有两种情况,写入步骤也不同:如果写入的数据没有超过当前block,则调用lfs_bd_prog写入。这一步比较简单。如果数据写入超出当前块:调用lfs_ctz_find查找写入位置所在的块。调用lfs_ctz_extend在写入位置插入一个新的头节点。最后调用lfs_file_sync或lfs_file_close时commit,实际上是将更新后的CTZSTRUCT标签写入到元数据中。当数据写入超出当前块时,会涉及跳表的更新。下面将重点介绍这种情况。lfs_ctz_extendlfs_ctz_extend的作用是在写入文件的位置插入一个新的头节点。步骤如下:分配一个新的块作为新的头节点,调用lfs_bd_prog将原头节点块中的数据复制到新块中。下图中,调用lfs_bd_prog传入的pcache参数为file->cache,lfs_bd_prog会先将数据写入file->cache,等到需要flush操作时,才真正将数据写回block。将新的头节点与左边的后继节点链接起来,右边旧的前驱节点被丢弃(但不会立即擦除块中的内容):注意:如果文件写入位置在文件的末尾文件中,ctz块是旧的头节点。调用lfs_file_seek函数改变文件写入位置。commit后会写入一个新的CTZSTRUCT标签,过程如下:COW策略大纲文件在写数据时是COW(copy-on-write)策略,lfs_ctz_extend函数不会插入旧的headnodewiththesuccessor到节点的链接已断开。只有当新的CTZSTRUCT标签最后被写入到其父目录的元数据中时,新的CTZSTRUCT标签所包含的大纲文件的头节点才能更新成功。因此,如果由于断电等异常情况导致无法完成大纲文件的写入操作,则不会丢弃原始数据。如下图所示,当大纲文件被插入到一个新的节点中时,原区块的数据不会被破坏。只有commit完成后,新的head节点才会写入父目录的metadata中,原来的head节点会被覆盖。4、块设备读写littlefs中块设备相关的读写操作是其他上层读写操作的基础。上面提到的文件读写操作都是由块设备相关的读写操作完成的。块设备相关的读写操作直接在特定的块上进行。读写文件和提交元数据的过程是通过调用与块设备相关的读写操作来完成的。主要相关函数有:lfs_bd_read:从源块或缓存中读取数据。lfs_bd_prog:将数据写入目标块或缓存。lfs_bd_flush:将缓存中的数据写入块中。文件写入后,lfs_bd_flush将被调用以将数据实际写入块并仅在文件被刷新、同步或关闭时提交所有更改。以上函数利用缓存或直接从块中读写。当直接从块中进行读取写入时,是调用了用户配置中提供的相关读取写入数://littlefsstructlfs_config初始化期间提供的配置{...//读取块中的区域。//向用户传播负错误代码。int(*read)(conststructlfs_config*c,lfs_block_tblock,lfs_off_toff,void*buffer,lfs_size_tsize);//在一个块中编程一个区域。该块之前必须//已被擦除。将负错误代码传播给用户。//如果块应该被认为是坏的,可能会返回LFS_ERR_CORRUPT。int(*prog)(conststructlfs_config*c,lfs_block_tblock,lfs_off_toff,constvoid*buffer,lfs_size_tsize);//擦除一个块。在编程之前必须擦除块。//擦除块的状态未定义。负错误代码//传播给用户。//如果块应该被认为是坏的,可能会返回LFS_ERR_CORRUPT。int(*erase)(conststructlfs_config*c,lfs_block_t堵塞);//同步底层块设备的状态。负错误代码//传播给用户。int(*sync)(conststructlfs_config*c);...};(1)cacheblockdevicereadandwrite这两个函数都接受两个缓存,即rcache和pcache作为参数,分别作为读缓存和写缓存。看后面的分析。littlefs中有以下几种缓存类型:globalrcache,lfs->rcache。用作rcache参数。全局pcache,lfs->pcache。在读取和写入元数据时用作pcache参数。文件缓存,文件->缓存。在读取和写入文件时用作pcache参数。(2)块设备读操作lfs_bd_read将源块中的数据读入目标缓冲区。在读取过程中,根据数据是否在缓存中,分为以下几种情况:Inpcacheorrcache:直接从缓存中复制。pcache和rcache都没有,而且要求的读取大小小于一次可以加载到缓存中的数据大小:将源块中的数据加载到rcache中,以便以后从rcache中读取。不在pcache和rcache中,要求读取大小不小于一次可以加载到缓存中的数据大小:直接从源块读取。相关函数:lfs_bd_read(lfs_t*lfs,|constlfs_cache_t*pcache,lfs_cache_t*rcache,lfs_size_thint,|lfs_block_tblock,lfs_off_toff,|void*buffer,lfs_size_tsize)|如果完成,继续步骤,否则结束|->while(size>0)...||//2.如果pcache中有缓存对应的数据,则从pcache中读取|->if(pcache&&block==pcache->block&&|offoff+pcache->size){|如果(关闭>=pcache->关闭){|//已经在pcache中了吗?|diff=lfs_min(diff,pcache->size-(off-pcache->off));|memcpy(data,&pcache->buffer[off-pcache->off],diff);||数据+=差异;|关闭+=差异;|大小-=差异;|继续;|}|//pcache优先|diff=lfs_min(diff,pcache->off-off);|}||//3.如果rcache中有缓存对应的数据,则从rcache中读取|->if(block==rcache->block&&|offoff+rcache->size){|如果(关闭>=rcache->关闭){|//已经在rcache中了?|diff=lfs_min(diff,rcache->size-(off-rcache->off));|memcpy(data,&rcache->buffer[off-rcache->off],diff);||数据+=差异;|关闭+=差异;|大小-=差异;|继续;|}|//rcache优先|diff=lfs_min(diff,rcache->off-off);|}||//4.如果没有命中缓存且size大于等于read_size,|//读取内容的大小一旦加载就超过了缓存的大小,然后从块中读取|->if(size>=hint&&off%lfs->cfg->read_size==0&&|size>=lfs->cfg->read_size){|//绕过缓存?|diff=lfs_aligndown(diff,lfs->cfg->read_size);|lfs->cfg->read(lfs->cfg,block,off,data,diff);||数据+=差异;|关闭+=差异;|大小-=差异;|继续;|}||//5.如果cachemiss且size小于read_size,加载块数据到rcache|->rcache->block=block;|缓存->off=lfs_aligndown(关闭,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);|interr=lfs->cfg->read(lfs->cfg,rcache->block,|rcache->off,rcache->buffer,rcache->size);(3)块设备写操作lfs_bd_prog的作用是将源数据写入目标块但实际上并没有立即写入数据的目标block,而是先将数据复制到pcache中,直到flush操作才将pcache中的数据写入block:相关函数:lfs_bd_prog(lfs_t*lfs,|lfs_cache_t*pcache,lfs_cache_t*rcache,boolvalidate,|lfs_block_t块,lfs_off_t关闭,|constvoid*buffer,lfs_size_t大小)|//1.检查是否已经写入,如果没有,继续步骤,否则结束|->while(size>0)...||//2.如果pcache就绪,将数据复制到pcache|->if(block==pcache->block&&|off>=pcache->off&&|offoff+lfs->cfg->cache_size){|//已经适合pcache了吗?|lfs_size_tdiff=lfs_min(size,|lfs->cfg->cache_size-(off-pcache->off));|memcpy(&pcache->buffer[off-pcache->off],data,diff);||数据+=差异;|关闭+=差异;|大小-=差异;||//2.1如果pcache已满,则刷新|->if(pcache->size==lfs->cfg->cache_size){|//如果我们填满了就急切地清除pcache|lfs_bd_flush(lfs,pcache,rcache,vali日期);|继续;|}||//3.如果pcache没有准备好,准备pcache|->pcache->block=block;|pcache->off=lfs_aligndown(off,lfs->cfg->prog_size);|.了解更多开源知识,请访问:开源基础软件社区https://ost.51cto.com。