7

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

 1 year ago
source link: https://ost.51cto.com/posts/19251
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

上一篇文章介绍了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。下面进行具体说明。

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。
littlefs原理分析#[五]文件读写-开源基础软件社区
  1. 将新的头节点与左边的后继结点链接,右边的旧的前继节点被舍弃(但块中内容不会被立即擦除):
littlefs原理分析#[五]文件读写-开源基础软件社区

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

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

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

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

    ...
};

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中复制

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

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

更多原创内容请关注:深开鸿技术团队

入门到精通、技巧到案例,系统化分享OpenHarmony开发技术,欢迎投稿和订阅,让我们一起携手前行共建生态。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK