1、littlefs主要用在微控制器和flash上,是一种嵌入式文件系统。主要有3个特点:

1)、掉电恢复

在写入时即使复位或者掉电也可以恢复到上一个正确的状态。

2)、擦写均衡

有效延长flash的使用寿命

3)、有限的RAM/ROM

节省ROM和RAM空间

2. 2、已有的文件系统

1)非掉电恢复,基于block的文件系统,常见的有FAT和EXT2。这两个文件系统在写入文件时是原地更新的,不具备非掉电恢复的特性。

2) 日志式的文件系统,比如JFFS,YAFFS等,具备掉电恢复的特性。但是这几个系统消耗了太多的RAM,且性能较低。

3) EXT4和COW类型的btrfs具有良好的恢复性和读写性能,但是需要的资源过多,不适合小型的嵌入式系统。

littlefs综合了日志式文件系统和COW文件系统的优点。从sub-block的角度来看,littlefs是基于日志的文件系统,提供了metadata的原子更新;从super-block的角度,littlefs是基于block的COW树。

Littlefs:重要的结构

1.文件类型

// File types

enum lfs_type {

    // file types

    LFS_TYPE_REG            = 0x001,

    LFS_TYPE_DIR            = 0x002,

 

    // internally used types

    LFS_TYPE_SPLICE         = 0x400,

    LFS_TYPE_NAME           = 0x000,

    LFS_TYPE_STRUCT         = 0x200,

    LFS_TYPE_USERATTR       = 0x300,

    LFS_TYPE_FROM           = 0x100,

    LFS_TYPE_TAIL           = 0x600,

    LFS_TYPE_GLOBALS        = 0x700,

    LFS_TYPE_CRC            = 0x500,

 

    // internally used type specializations

    LFS_TYPE_CREATE         = 0x401,

    LFS_TYPE_DELETE         = 0x4ff,

    LFS_TYPE_SUPERBLOCK     = 0x0ff,

    LFS_TYPE_DIRSTRUCT      = 0x200,

    LFS_TYPE_CTZSTRUCT      = 0x202,

    LFS_TYPE_INLINESTRUCT   = 0x201,

    LFS_TYPE_SOFTTAIL       = 0x600,

    LFS_TYPE_HARDTAIL       = 0x601,

    LFS_TYPE_MOVESTATE      = 0x7ff,

 

    // internal chip sources

    LFS_FROM_NOOP           = 0x000,

    LFS_FROM_MOVE           = 0x101,

    LFS_FROM_USERATTRS      = 0x102,

};

2.文件打开时的标志

 

// File open flags

enum lfs_open_flags {

    // open flags

    LFS_O_RDONLY = 1,         // Open a file as read only 只读

    LFS_O_WRONLY = 2,         // Open a file as write only 只写

    LFS_O_RDWR   = 3,         // Open a file as read and write 读写

LFS_O_CREAT  = 0x0100,    // Create a file if it does not exist 如果指定文件不存在,则创建这个文件

    LFS_O_EXCL   = 0x0200,    // Fail if a file already exists 如果创建的文件已存在,返回-1.并修改errno的值

    LFS_O_TRUNC  = 0x0400,    // Truncate the existing file to zero size如果文件存在,并且以只写/读写方式打开,则清空文件全部内容(即将其长度截短为0)

    LFS_O_APPEND = 0x0800,    // Move to end of file on every write每次写操作都写入文件的末尾

 

    // internally used flags

    LFS_F_DIRTY   = 0x010000, // File does not match storage 

    LFS_F_WRITING = 0x020000, // File has been written since last flush

    LFS_F_READING = 0x040000, // File has been read since last flush

    LFS_F_ERRED   = 0x080000, // An error occured during write

    LFS_F_INLINE  = 0x100000, // Currently inlined in directory entry

    LFS_F_OPENED  = 0x200000, // File has been opened

};

3.文件seek时的标志

// File seek flags

enum lfs_whence_flags {

    LFS_SEEK_SET = 0,   // Seek relative to an absolute position 0代表从文件开头开始

    LFS_SEEK_CUR = 1,   // Seek relative to the current file position 1代表从当前位置开始

    LFS_SEEK_END = 2,   // Seek relative to the end of the file 2代表从文件末尾

};

4.lfs的配置参数

// Configuration provided during initialization of the littlefs

struct lfs_config {

    // Opaque user provided context that can be used to pass

    // information to the block device operations

    /* 这个参数主要是传递给block驱动代码 */

    void *context;

 

    /* 从设备读数据 */

    int (*read)(const struct lfs_config *c, lfs_block_t block,

            lfs_off_t off, void *buffer, lfs_size_t size);

 

    /* 向设备写入数据,block设备在写入前必须已经erase了 */

    int (*prog)(const struct lfs_config *c, lfs_block_t block,

            lfs_off_t off, const void *buffer, lfs_size_t size);

 

    /* 擦除block */

    int (*erase)(const struct lfs_config *c, lfs_block_t block);

 

    /* sync块设备的状态 */

    int (*sync)(const struct lfs_config *c);

 

    /* 最小的读取单元大小 */

    lfs_size_t read_size;

 

    /* 最小的写入数据单元大小,也是数据metadata pair中tag的对齐尺寸 */

    lfs_size_t prog_size;

 

    /* 最小的擦除单元大小。可以比flash的实际block尺寸大。但是对于ctz类型的文件,block size是最小的分配单元。同时block size必须是

    read size和program size的倍数,block size会存储在superblock中 */

    lfs_size_t block_size;

 

    /* 属于文件系统的block数量,block count会存储在superblock中 */

    lfs_size_t block_count;

 

    /* 文件系统进行垃圾回收时的block的擦除次数,推荐取值100-1000.值越大垃圾回收的次数越少,性能越好 */

    int32_t block_cycles;

 

    /* littlefs需要一个read cache,一个program cache,每个文件也需要一个cache。cache越大性能越好,会减少会flash的访问次数,cache必须是block的read size和program size的倍数,同时是block size的因数 */

    lfs_size_t cache_size;

 

    /* lookahead buffer的尺寸。lookahead buffer主要是block alloctor在分配块的时候用到。lookahead size必须是8的倍数,

    因为它是采用bitmap的形式存储的 */

    lfs_size_t lookahead_size;

 

    /* cache size大小的read buffer,可以静态分配也可以动态分配 */

    void *read_buffer;

 

    /* cache size大小的program buffer,可以静态分配也可以动态分配 */

    void *prog_buffer;

 

    /* lookahead_size大小的lookahead buffer,且是32-bit对齐的,即可以静态分配也可以动态分配 */

    void *lookahead_buffer;

 

    /* 文件名的最大长度,这个值会存储在superblock中 */

    lfs_size_t name_max;

 

    /* 文件的最大长度,存储在superblock中 */

    lfs_size_t file_max;

 

    /* 用户属性的最大长度 */

    lfs_size_t attr_max;

};

5.文件信息

// File info structure

struct lfs_info {

    // Type of the file, either LFS_TYPE_REG or LFS_TYPE_DIR 普通文件或者目录

    uint8_t type;

 

    // Size of the file, only valid for REG files. Limited to 32-bits. 对于普通文件才有意义

    lfs_size_t size;

 

    // Name of the file stored as a null-terminated string. Limited to

    // LFS_NAME_MAX+1, which can be changed by redefining LFS_NAME_MAX to

    // reduce RAM. LFS_NAME_MAX is stored in superblock and must be

    // respected by other littlefs drivers.

    /* 字符串形式的文件名 */

    char name[LFS_NAME_MAX+1];

};

6.用户属性

struct lfs_attr {

    /* 属性类型 */

    uint8_t type;

 

    /* 存储属性的buffer */

    void *buffer;

 

    /* 属性的长度,最大值为LFS_ATTR_MAX */

    lfs_size_t size;

};

 

 

7.文件open时的配置

struct lfs_file_config {

    /* cache size长度的buffer,可以静态分配也可以动态分配 */

    void *buffer;

 

    /* 用户属性,读文件时,attr存储从flash上读取的文件用户属性,写入文件时,attr存放用户指定的文件属性并会写入到flash中 */

    struct lfs_attr *attrs;

 

    /* 用户属性的长度 */

    lfs_size_t attr_count;

};

8.lfs_cache结构

typedef struct lfs_cache {

    lfs_block_t block;    // cache中的数据属于的block

    lfs_off_t off;        // cache中的数据在block上的偏移地址

    lfs_size_t size;      // cache的大小

    uint8_t *buffer;      // cache数据的存放地址

} lfs_cache_t;

9.lfs_mdir结构,代表metadata pair,dir本身所在的block

typedef struct lfs_mdir {

    lfs_block_t pair[2];    // dir的metadata pair所在的block

    uint32_t rev;           // metadata pair的revision

    lfs_off_t off;          // tag的偏移地址

    uint32_t etag;         

    uint16_t count;

    bool erased;

    bool split;             // metadata pair是否是链表

    lfs_block_t tail[2];    // 用于metadata pair链表

} lfs_mdir_t;

 

10.lfs目录结构

// littlefs directory type

typedef struct lfs_dir {

    struct lfs_dir *next;    // 指向子目录

    uint16_t id;

    uint8_t type;            // LFS_TYPE_DIR

    lfs_mdir_t m;            // 代表dir的metadata pair

    lfs_off_t pos;           // 在目录中的当前位置,主要用在seek,tell和rewind操作中

    lfs_block_t head[2];

} lfs_dir_t;

 

11.lfs文件类型

// littlefs file type

typedef struct lfs_file {

    struct lfs_file *next;

    uint16_t id;     // metadata tag中的id,在文件open时获取

    uint8_t type;    // LFS_TYPE_REG 或者 LFS_TYPE_DIR

    lfs_mdir_t m;    // 文件所在的目录的metadata pair

 

    struct lfs_ctz {

        lfs_block_t head;

        lfs_size_t size;

    } ctz;    // 指向大文件的CTZ skip-list。对于小文件则直接inline了,无需CTZ skip-list

 

    uint32_t flags;      // lfs_open_flags中的值

    lfs_off_t pos;       // 文件访问时的偏移

    lfs_block_t block;   // file当前的block

    lfs_off_t off;       // 在block内的offset

    lfs_cache_t cache;    // 文件访问时的cache

 

    const struct lfs_file_config *cfg;    // 文件open时的配置参数,包含一个buffer以及用户属性

} lfs_file_t;

 

12、lfs superblock结构

typedef struct lfs_superblock {

    uint32_t version;        // 文件系统的版本号

    lfs_size_t block_size;   // 文件系统的block size,和flash的block size不一定相同

    lfs_size_t block_count;  // 文件系统包含的block数量,每个block的大小等于上面的block size

    lfs_size_t name_max;     // 文件名的最大长度

    lfs_size_t file_max;     // 文件的最大长度

    lfs_size_t attr_max;     // 用户属性的最大长度

} lfs_superblock_t;

13.lfs文件系统类型结构

// The littlefs filesystem type

typedef struct lfs {

    lfs_cache_t rcache;             // read cache

    lfs_cache_t pcache;             // program cache

    lfs_block_t root[2];            // 根目录所在的block

    struct lfs_mlist {

        struct lfs_mlist *next;      // 指向下一个节点

        uint16_t id;                 // metadata pair的id

        uint8_t type;                // metadata pair的类型

        lfs_mdir_t m;               // metadata pair

    } *mlist;                       // metadata pair list

    uint32_t seed;                  // block alloctor的随机数生成的seed

 

    struct lfs_gstate {

        uint32_t tag;

        lfs_block_t pair[2];

    } gstate, gpending, gdelta;      // 用于目录操作sync的global state,

 

    struct lfs_free {

        lfs_block_t off;             // 记录lookahead buffer中起始block的偏移

        lfs_block_t size;            // lookahead buffer中block的数量,注意lookahead采用的是bitmap的形式,因此size=8*lookahead_size

        lfs_block_t i;               // lookahead buffer内部的偏移地址

        lfs_block_t ack;             // 剩余block的数量,初始值为block count,如果该值为0,表示已经没有free block了

        uint32_t *buffer;            // buffer的长度为lookahead size

    } free;                          // lookahead buffer,用于分配free block

 

    const struct lfs_config *cfg;    // 文件系统的配置参数

    lfs_size_t name_max;             // 文件名的最大长度,和superblock中的name_max值相同

    lfs_size_t file_max;             // 文件的最大长度,和superblock中的file_max值相同

    lfs_size_t attr_max;             // 用户属性的最大长度,和superblock中的attr_max值相同

} lfs_t;

 

wear leveling 耗损平均技术

由于闪存的可擦写次数是有限的,当某些数据被频繁修改时容易导致对应的块很快被耗尽使用寿命,从而导致整块盘无法使用,所以需要有一种技术来将这些块的擦写均摊一下,延长使用寿命。

首先看几个相关的基本概念:

因为闪存不能覆盖写,如果要修改已有的数据需要将原有的数据擦除再写入新的数据。

被频繁修改的数据很烫,叫做热数据

而写入以后就很少修改的数据无人问津就像打入了冷宫一样,叫做冷数据。

写入的最小单位叫做page,大小为 512 – 4,096 bytes

擦除(erase)的最小单位是block,包含多个page(一般为128个)

一次对磁盘完整的写入或擦除叫做一个PE cycle(Program/Erase Cycle),PE cycle表示了盘的寿命,是一个有限的值,比如3000. 注意,一个PE cycle是对整块盘的擦写来写来计算的,不是一个block的擦写。

已擦写次数较少的block,还很年轻,生命力强,所以叫做Young block。相对的 Old block就是已擦写次数较多的block,剩下的次数不多了。

 

NAND FLASH LAYOUT, 注意page和block就行了

闪存又贵还擦写次数那么有限的,这还怎么玩?于是有了Wear Leveling这样的技术通过磨损均衡来延长闪存的寿命。

 

无模糊均衡和有磨损均衡的对比

在没有wear leveling的情况下,某些block很可能会被频繁的反复擦写,最终报废,降低了闪存的寿命。Wear Leveling技术就是将擦写操作均摊到各个block,以防止某些block被提前耗尽使用寿命。

Wear Leveling技术按算法分为动态和静态,按作为域分为本地和全局:

Dynamic Wear Leveling 动态磨损均衡

当需要覆盖写的时候,新的数据写到free的page上,而旧的数据被标记为invalid,等待垃圾回收擦除。

 

动态磨损均衡示意图

从上图中可以看出2nd WRITE失去改写LBA#6的数据,被写到了新分配的page并不是直接在原page上做修改。3rd WRITE也是同理,到Nth WRITE,数据已经被改写了N次,但是垃圾回收还没有发生,所以有很多的Invalid page。

 

对比上图垃圾回收的左右两个图,可以看到垃圾回收把Invalid的page都擦除了,而且数据LBA#6也被搬移到了新的block。这是因为就像开头说的,闪存擦除的最小单位是block,所以当block中有用户数据的时候是需要迁移的。

2. Static Wear Leveling 静态磨损均衡

Static Wear Leveling 会把所有block包括没被写入和包含冷数据的block都纳入到磨损均衡中。如果冷数据是在擦写次数少的young block中,会把数据迁移到擦写次数较多的old block中。这样young block就可以放到free block池中接收新数据的写入。

 

静态磨损均衡示意图

通俗的说:

动态的算法就是当写入新数据的时候,会自动往比较新的Block中去写,老的闪存就放在一旁歇歇;而静态的算法就更加先进,就算没有数据写入,SSD监测到某些闪存Block比较老,会自动进行数据分配,让比较老的闪存Block承担不需要写数据的储存任务。同时让较新的闪存Block腾出空间,平日的数据读写就在比较新的Block中进行——如此一来,各个Block的寿命损耗,就都差不多了。

动态和静态对比:

 

动态和静态模糊均衡对比表格

 

Littlefs :wear leveling

wear leveling的功能是通过block alloctor实现的。littlefs通过两个方法实现磨损均衡:

1) 检测和恢复bad blocks

2)通过dynamic blocks均衡磨损

检测和恢复bad blocks和block alloctor的关系并不紧密,主要依靠文件系统本身来检测坏块并标记。在littlefs中,检测坏块主要是通过写后、回读并比较的方式实现的,如果数据不一致则说明是一个坏块。一旦检测到了坏块就要进行恢复。

如果是写入错误,因为数据在RAM中还有备份,因此需要block allctor分配一个新的block,再次写入。新块替换坏块是通过CObW(copy-on-bounded-writes)数据结构实现的。CObW的一个属性就是任何一个block可以在COW的过程中替换。bounded-writes通常是由写入次数计数器触发的,检测到坏块后同样可以触发一个COW操作。

如果是读错误,则要复杂一些。因为在读取时,RAM中没有数据的备份,因此需要一个方法能恢复原来的数据。ECC就是方法之一。但是,littlefs本身并不提供ECC。littlefs采取的方法是在所有的block尽量分配磨损,同时希望在所有的block达到使用寿命前不会有某个block坏掉。

wear leveling有两种策略:

1)动态wear leveling,通过动态block来分配磨损,仅考虑未使用的block;

2)静态wear leveling,同时在动态block和静态block上分配磨损,需要考虑所有的block,包括已有存储了数据的block。通常可以认为静态block就是已经写入了数据的block。

综合考虑代码大小和复杂度,littlefs采取动态wear leveling。这是一种尽最大努力的解决方法。这种方法中,磨损不是非常好的分配,只是在空闲块中分配,但是也大大的延长了flash的使用寿命。

在littlefs中,使用了基于统计信息的wear leveling算法。即不是追踪和记录每个block的磨损情况,而是依赖所有block的磨损的均匀分布uniform distribution来近似动态wear leveling算法。block alloctor实现了均匀分布uniform distribution,主要包括两部分:

1)第一次上电创建文件系统时,block alloctor对所有的block线性分配

2)文件系统创建后,再次上电时,不能再从头开始线性分配了,而是在mount文件系统时block alloctor从一个随机偏移的block开始后续分配,只要这个随机值是均匀的,那么block的分配也会是近似均匀的。

 

从上图中可以看出,随着时间的延长,各个block的磨损逐渐趋于一致。

 

在实际的使用过程中,每次上电后的随机数的会根据flash上已有的数据来计算和产生。主要是对metadata pair的checksum进行xor计算得到。利用metadata pair的一个原因也是因为在mount的时候会获取metadata pair的数据。

在实际的使用过程中,每次上电后的随机数的会根据flash上已有的数据来计算和产生。主要是对metadata pair的checksum进行xor计算得到。利用metadata pair的一个原因也是因为在mount的时候会获取metadata pair的数据。

上述的随机数生成器并不完美,它只有在文件系统发生了更改之后才会产生一个不同的数。但这真是文件系统的block alloctor和磨损均衡所需要的。

 

总得来说坏块检测和磨损均衡都是尽最大努力的方法,来避免flash block的快速磨损。


主动一点,世界会更大!