#littlefs原理分析#[五]文件读写 精华
作者:蒋卫峰 李涛
前言
上一篇文章介绍了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开发技术,欢迎投稿和订阅,让我们一起携手前行共建生态。
前排!
感觉有点复杂,需要多读几次理解一下
从原理了解读写操作