Added building blocks for dynamic wear-leveling

Initially, littlefs relied entirely on bad-block detection for
wear-leveling. Conceptually, at the end of a devices lifespan, all
blocks would be worn evenly, even if they weren't worn out at the same
time. However, this doesn't work for all devices, rather than causing
corruption during writes, wear reduces a devices "sticking power",
causing bits to flip over time. This means for many devices, true
wear-leveling (dynamic or static) is required.

Fortunately, way back at the beginning, littlefs was designed to do full
dynamic wear-leveling, only dropping it when making the retrospectively
short-sighted realization that bad-block detection is theoretically
sufficient. We can enable dynamic wear-leveling with only a few tweaks
to littlefs. These can be implemented without breaking backwards
compatibility.

1. Evict metadata-pairs after a certain number of writes. Eviction in
   this case is identical to a relocation to recover from a bad block.
   We move our data and stick the old block back into our pool of
   blocks.

   For knowing when to evict, we already have a revision count for each
   metadata-pair which gives us enough information. We add the
   configuration option block_cycles and evict when our revision count
   is a multiple of this value.

2. Now all blocks participate in COW behaviour. However we don't store
   the state of our allocator, so every boot cycle we reuse the first
   blocks on storage. This is very bad on a microcontroller, where we
   may reboot often. We need a way to spread our usage across the disk.

   To pull this off, we can simply randomize which block we start our
   allocator at. But we need a random number generator that is different
   on each boot. Fortunately we have a great source of entropy, our
   filesystem. So we seed our block allocator with a simple hash of the
   CRCs on our metadata-pairs. This can be done for free since we
   already need to scan the metadata-pairs during mount.

What we end up with is a uniform distribution of wear on storage. The
wear is not perfect, if a block is used for metadata it gets more wear,
and the randomization may not be exact. But we can never actually get
perfect wear-leveling, since we're already resigned to dynamic
wear-leveling at the file level.

With the addition of metadata logging, we end up with a really
interesting two-stage wear-leveling algorithm. At the low-level,
metadata is statically wear-leveled. At the high-level, blocks are
dynamically wear-leveled.

---

This specific commit implements the first step, eviction of metadata
pairs. Entertwining this into the already complicated compact logic was
a bit annoying, however we can combine the logic for superblock
expansion with the logic for metadata-pair eviction.
This commit is contained in:
Christopher Haster
2018-08-08 16:34:56 -05:00
parent 20b669a23d
commit e4a0d586d5
4 changed files with 356 additions and 306 deletions

17
lfs.h
View File

@@ -178,12 +178,6 @@ struct lfs_config {
// multiple of this value.
lfs_size_t prog_size;
// Size of block caches. Each cache buffers a portion of a block in RAM.
// This determines the size of the read cache, the program cache, and a
// cache per file. Larger caches can improve performance by storing more
// data. Must be a multiple of the read and program sizes.
lfs_size_t cache_size;
// Size of an erasable block. This does not impact ram consumption and
// may be larger than the physical erase size. However, this should be
// kept small as each file currently takes up an entire block.
@@ -193,6 +187,17 @@ struct lfs_config {
// Number of erasable blocks on the device.
lfs_size_t block_count;
// Number of erase cycles before we should move data to another block.
// May be zero to never move data, in which case no block-level
// wear-leveling is performed.
uint32_t block_cycles;
// Size of block caches. Each cache buffers a portion of a block in RAM.
// This determines the size of the read cache, the program cache, and a
// cache per file. Larger caches can improve performance by storing more
// data. Must be a multiple of the read and program sizes.
lfs_size_t cache_size;
// Number of blocks to lookahead during block allocation. A larger
// lookahead reduces the number of passes required to allocate a block.
// The lookahead buffer requires only 1 bit per block so it can be quite