作者:蒋卫峰 李涛

前言

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

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

1. inline文件读写

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

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

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

littlefsfilewrite inline file.drawio.png

2. inline文件转outline文件

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

转换过程步骤如下:

  1. 为文件重分配块,将inline数据写入块中

  2. commit一个新的CTZSTRUCT类型的tag

commit过程如下图:

littlefsfilefile outline.drawio.png

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

3. outline文件读写

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

littlefsfileoutline file.drawio.png

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

3.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(块序号)

3.2 outline文件写操作

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

  • 如果写入数据后不超过当前块,则调用lfs_bd_prog进行写入。该步骤相对简单。

  • 如果写入数据后超过当前块:

    1. 调用lfs_ctz_find找到写入位置所在的块

    2. 调用lfs_ctz_extend在写入位置插入新的头节点

    3. 最后当调用lfs_file_sync或lfs_file_close时进行commit,实际将更新后的CTZSTRUCT tag写入元数据

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

3.2.1 lfs_ctz_extend

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

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

littlefsfilectz extend 1.drawio.png

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

littlefsfilectz extend 2.drawio.png

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

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

littlefsfilewrite outline file.drawio.png

3.2.2 COW策略

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

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

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

littlefsfilectz extend COW.drawio.png

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中。读取过程中,根据数据是否在缓存中,分为以下几种情况:

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

littlefsfilebd prog.drawio.png

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

littlefsfilebd read load cache.drawio.png

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

littlefsfilebd read block.drawio.png

相关函数:

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中的数据写到块中:

littlefsfilebd prog.drawio.png

相关函数:

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

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

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

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