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

系统 OpenHarmony
本文介绍了littlefs中的文件读写机制,到这里littlefs大部分的操作就都已经做了分析了。

​想了解更多关于开源的内容,请访问:​

​51CTO 开源基础软件社区​

​https://ost.51cto.com​

前言

上一篇文章介绍了littlefs中的目录操作,这一篇文章则将介绍littlefs中的文件读写操作。

本文会根据文件的存储类型进行介绍,即inline文件和outline文件,其读写过程也有差别。另外还会介绍inline文件到outline文件的转换,以及littlefs底层的读写API。

1、inline文件读写

因为inline文件数据存储于其父目录的元数据中,inline文件的读写实际上通过commit机制实现。读是通过遍历tag,写则是通过commit一个INLINESTRUCT类型的tag。

对于inline文件的数据读取,实际上就是从其父目录的元数据中进行读取,其过程已在commit机制中描述。

对于inline文件的写入,即commit一个INLINESTRUCT类型的tag,大致过程如下:

littlefs原理分析#[五]文件读写-开源基础软件社区

2、inline文件转outline文件

当文件大小超过1/8 block_size、或超过文件cache大小时,inline文件会转为outline文件,该转换过程在文件写入过程中触发。inline文件转为outline文件之后就不会再转回inline文件,即使对文件进行truncate操作。

转换过程步骤如下:

  1. 为文件重分配块,将inline数据写入块中。
  2. commit一个新的CTZSTRUCT类型的tag。

commit过程如下图:

littlefs原理分析#[五]文件读写-开源基础软件社区

其中,CTZSTRUCT类型的tag中包含了新分配的文件跳表头节点的块指针。当读取文件,遍历tag时,检测到CTZSTRUCT,就会从其中文件跳表头节点的块指针读取文件数据。具体跳表中读写文件的过程在下小节中说明。

3、outline文件读写

回顾outline文件的存储结构,其数据是用一个跳表进行存储的:

littlefs原理分析#[五]文件读写-开源基础软件社区

outline文件的读写通过跳表的机制完成,commit时只需要commit带有更新后的跳表头的CTZSTRUCT tag。下面进行具体说明。

(1)outline文件读操作

读取数据的步骤如下:

  1. 调用lfs_ctz_find找到目标数据所在的块。
  2. 调用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(块序号)

(2)outline文件写操作

outline文件写入数据时又分为两种情况,其写入步骤也不同:

  • 如果写入数据后不超过当前块,则调用lfs_bd_prog进行写入。该步骤相对简单。
  • 如果写入数据后超过当前块:
  1. 调用lfs_ctz_find找到写入位置所在的块。
  2. 调用lfs_ctz_extend在写入位置插入新的头节点。
  3. 最后当调用lfs_file_sync或lfs_file_close时进行commit,实际将更新后的CTZSTRUCT tag写入元数据。

当数据写入后超过当前块时,会涉及到跳表的更新,下面着重对这种情况进行说明。

lfs_ctz_extend

lfs_ctz_extend函数的作用是在文件写入的位置插入新的头节点。其步骤如下:

  1. 分配一个新块作为新的头节点,并调用lfs_bd_prog将原头节点块中的数据复制到新块中。下图中,调用lfs_bd_prog传入的pcache参数为file->cache,lfs_bd_prog会先将数据写入到file->cache中,等到需要进行flush操作时才将数据实际写回block。

littlefs原理分析#[五]文件读写-开源基础软件社区

  1. 将新的头节点与左边的后继结点链接,右边的旧的前继节点被舍弃(但块中内容不会被立即擦除):

littlefs原理分析#[五]文件读写-开源基础软件社区

注:如果文件写入位置位于文件末尾,则图示中ctz block即为旧头节点。调用lfs_file_seek函数可改变文件写入位置。

commit后会写入新的CTZSTRUCT tag,其过程如下:

littlefs原理分析#[五]文件读写-开源基础软件社区

COW策略

outline文件写入数据时是COW(copy-on-write)策略,lfs_ctz_extend函数插入新的头节点时并不会将旧头节点与后继节点的链接断掉。只有当最后将新的CTZSTRUCT tag写入其父目录的元数据中后,新的CTZSTRUCT tag中所包含的outline文件跳表头节点才更新成功。

因此,如果发生掉电等异常情况导致outline文件的写入操作未能完成时,其原有的数据也不会被丢弃。

如下图,outline文件插入新的节点时不会去破坏原有的块的数据。只有commit完成后,才会将新的头节点写入父目录的元数据中,将原来的头节点覆盖。

littlefs原理分析#[五]文件读写-开源基础软件社区

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);
...
};

(1)cache

block device读写函数均接受两个cache,即rcache和pcache作为参数,用作读缓存和写缓存。具体作用见后面分析。

littlefs中cache共有以下几种:

  • 全局rcache,lfs->rcache。用作rcache参数。
  • 全局pcache,lfs->pcache。读写元数据时用作pcache参数。
  • 文件的cache,file->cache。当对文件进行读写操作时用作pcache参数。

(2)block device读操作

lfs_bd_read将源块中数据读到目标buffer中。读取过程中,根据数据是否在缓存中,分为以下几种情况:

  1. 在pcache或rcache中:直接从cache中复制。

littlefs原理分析#[五]文件读写-开源基础软件社区

  1. 不在pcache和rcache中,且所需读取大小小于一次能加载到cache中数据的大小:将源块中数据加载到rcache,以便后面从rcache中读。

littlefs原理分析#[五]文件读写-开源基础软件社区

  1. 不在pcache和rcache中,且所需读取大小不小于一次能加载到cache中数据的大小:直接从源块中读。

littlefs原理分析#[五]文件读写-开源基础软件社区

相关函数:

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);

(3)block device写操作

lfs_bd_prog的作用是将源数据写入到目标块中。但实际上没有立即将数据写入的目标块,而是先将数据复制到pcache中,等到flush操作时才将pcache中的数据写到块中:

littlefs原理分析#[五]文件读写-开源基础软件社区

相关函数:

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中的磨损均衡相关策略。

​想了解更多关于开源的内容,请访问:​

​51CTO 开源基础软件社区​

​https://ost.51cto.com​​。

责任编辑:jianghua 来源: 51CTO开源基础软件社区
相关推荐

2022-11-07 15:27:07

LittlefsFetch操作

2022-11-02 15:56:45

littlefscommit机制

2022-11-22 15:21:55

littlefs磨损均衡

2022-10-27 16:07:24

littlefs存储结构

2022-11-09 08:52:57

littlefs目录操作

2009-12-18 09:27:23

Ruby读写excel

2009-11-02 10:53:34

VB.NET INI文

2013-06-09 11:31:13

Android开发XML读写XML解析

2010-03-10 15:54:33

Python读写文件

2021-02-26 20:55:56

JavaNIO随机

2021-08-05 10:00:02

Python编程语言

2009-10-14 10:25:52

VB.NET读写文本文

2013-05-27 10:58:10

propertiesJava语言

2021-04-02 06:35:49

Bash读写文件Linux

2023-01-04 13:43:24

读写锁AQS共享模式

2009-07-06 17:47:44

2010-07-16 09:06:57

Perl文件

2010-11-19 11:32:25

Oracle存储过程

2010-11-19 11:22:25

oracle对系统文件

2021-04-04 08:00:39

C++编程语言软件开发
点赞
收藏

51CTO技术栈公众号