Compare commits

..

2 Commits

Author SHA1 Message Date
Christopher Haster
b8dcf10974 Changed lfs_dir_alloc to maximize block cycles for new metadata pairs
Previously we only bumped the revision count if an eviction would occur
immediately (and possibly corrupt littlefs). This works, but does risk
an unoptimal superblock size if an almost-exhausted superblock was
allocated during lfs_format.

As pointed out by tim-nordell-nimbelink, we can align the revision count
to maximize the number of block cycles without breaking the existing
requirements of increasing revision counts.

As an added benefit, littlefs's wear-leveling should behave more
consistently after this change.
2020-11-28 22:46:11 -06:00
Christopher Haster
d04c1392c0 Fixed allocation-eviction issue when erase state is multiple of block_cycles+1
This rather interesting corner-case arises in lfs_dir_alloc anytime the
uninitialized revision count happens to be a multiple of block_cycles+1.

For example, the source of the bug found by tim-nordell-nimbelink:

rev = 2742492087
block_cycles = 100

2742492087 % (100+1) = 0

The reason for this weird block_cycles+1 case is due to a fix for a
previous bug in fe957de. To avoid aliasing, which would cause metadata
pairs to wear unevenly, block_cycles incremented to the next odd number.

Normally, littlefs tweaks the revision count of blocks during
lfs_dir_alloc in order to make sure evictions can't happen on the first
compact. Otherwise, higher-level logic such as lfs_format would break.

However, this wasn't updated with the aliasing fix in fe957de, so
lfs_dir_alloc was only rounding the revision count to the nearest even
number.

The current fix is to change the logic in lfs_dir_alloc to explicitly
check for the eviction condition and increment if eviction would occur.

Found by tim-nordell-nimbelink
2020-11-22 00:40:58 -06:00
2 changed files with 20 additions and 29 deletions

47
lfs.c
View File

@@ -452,16 +452,14 @@ static int lfs_alloc_lookahead(void *p, lfs_block_t block) {
return 0;
}
// indicate allocated blocks have been committed into the filesystem, this
// is to prevent blocks from being garbage collected in the middle of a
// commit operation
static void lfs_alloc_ack(lfs_t *lfs) {
lfs->free.ack = lfs->cfg->block_count;
}
// drop the lookahead buffer, this is done during mounting and failed
// traversals in order to avoid invalid lookahead state
static void lfs_alloc_drop(lfs_t *lfs) {
// Invalidate the lookahead buffer. This is done during mounting and
// failed traversals
static void lfs_alloc_reset(lfs_t *lfs) {
lfs->free.off = lfs->seed % lfs->cfg->block_size;
lfs->free.size = 0;
lfs->free.i = 0;
lfs_alloc_ack(lfs);
@@ -507,7 +505,7 @@ static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) {
memset(lfs->free.buffer, 0, lfs->cfg->lookahead_size);
int err = lfs_fs_traverseraw(lfs, lfs_alloc_lookahead, lfs, true);
if (err) {
lfs_alloc_drop(lfs);
lfs_alloc_reset(lfs);
return err;
}
}
@@ -872,10 +870,8 @@ static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs,
ptag ^= (lfs_tag_t)(lfs_tag_chunk(tag) & 1U) << 31;
// toss our crc into the filesystem seed for
// pseudorandom numbers, note we use another crc here
// as a collection function because it is sufficiently
// random and convenient
lfs->seed = lfs_crc(lfs->seed, &crc, sizeof(crc));
// pseudorandom numbers
lfs->seed ^= crc;
// update with what's found so far
besttag = tempbesttag;
@@ -1265,13 +1261,12 @@ static int lfs_dir_commitattr(lfs_t *lfs, struct lfs_commit *commit,
}
static int lfs_dir_commitcrc(lfs_t *lfs, struct lfs_commit *commit) {
const lfs_off_t off1 = commit->off;
const uint32_t crc1 = commit->crc;
// align to program units
const lfs_off_t end = lfs_alignup(commit->off + 2*sizeof(uint32_t),
const lfs_off_t end = lfs_alignup(off1 + 2*sizeof(uint32_t),
lfs->cfg->prog_size);
lfs_off_t off1 = 0;
uint32_t crc1 = 0;
// create crc tags to fill up remainder of commit, note that
// padding is not crced, which lets fetches skip padding but
// makes committing a bit more complicated
@@ -1307,12 +1302,6 @@ static int lfs_dir_commitcrc(lfs_t *lfs, struct lfs_commit *commit) {
return err;
}
// keep track of non-padding checksum to verify
if (off1 == 0) {
off1 = commit->off + sizeof(uint32_t);
crc1 = commit->crc;
}
commit->off += sizeof(tag)+lfs_tag_size(tag);
commit->ptag = tag ^ ((lfs_tag_t)reset << 31);
commit->crc = 0xffffffff; // reset crc for next "commit"
@@ -1326,7 +1315,7 @@ static int lfs_dir_commitcrc(lfs_t *lfs, struct lfs_commit *commit) {
// successful commit, check checksums to make sure
lfs_off_t off = commit->begin;
lfs_off_t noff = off1;
lfs_off_t noff = off1 + sizeof(uint32_t);
while (off < end) {
uint32_t crc = 0xffffffff;
for (lfs_off_t i = off; i < noff+sizeof(uint32_t); i++) {
@@ -1386,8 +1375,12 @@ static int lfs_dir_alloc(lfs_t *lfs, lfs_mdir_t *dir) {
return err;
}
// make sure we don't immediately evict
dir->rev += dir->rev & 1;
// to make sure we don't immediately evict, align the new revision count
// to our block_cycles modulus, see lfs_dir_compact for why our modulus
// is tweaked this way
if (lfs->cfg->block_cycles > 0) {
dir->rev = lfs_alignup(dir->rev, ((lfs->cfg->block_cycles+1)|1));
}
// set defaults
dir->off = sizeof(dir->rev);
@@ -3808,10 +3801,8 @@ int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) {
lfs->gstate.tag += !lfs_tag_isvalid(lfs->gstate.tag);
lfs->gdisk = lfs->gstate;
// setup free lookahead, to distribute allocations uniformly across
// boots, we start the allocator at a random location
lfs->free.off = lfs->seed % lfs->cfg->block_count;
lfs_alloc_drop(lfs);
// setup free lookahead
lfs_alloc_reset(lfs);
LFS_TRACE("lfs_mount -> %d", 0);
return 0;

View File

@@ -106,7 +106,7 @@ def main(args):
struct.unpack('<HH', superblock[1].data[0:4].ljust(4, b'\xff'))))
print("%-47s%s" % ("littlefs v%s.%s" % version,
"data (truncated, if it fits)"
if not any([args.no_truncate, args.log, args.all]) else ""))
if not any([args.no_truncate, args.tags, args.log, args.all]) else ""))
# print gstate
print("gstate 0x%s" % ''.join('%02x' % c for c in gstate))