Ckfetches implements what might be your first idea on how to check
checksums in a filesystem: Check each block/mdir on first access
(fetch) to make sure the data is sound.
Unfortunately, there are two problems with this approach, both which
come from the fact that blocks are big and can't fit in RAM:
1. We still have a checksum-read hole.
We can't keep a whole block around in RAM, so reads after a fetch may
need to reread from disk, at which point new bit-errors may slip in
undetected.
This is especially problematic for traversing our rbyds, which
involves a lot of small reads in a block.
2. Ckfetches may have a surprisingly negative performance impact.
Consider the case of reading a large file with a bunch of small
reads. Because we don't cache blocks, each read may need a btree
lookup, and a full block fetch. On paper this can quickly end up
O(b^2), which is not great.
Though this is helped by the file buffer. It will be interesting to
benchmark and see if this theoretical O(b^2) translates to poor
performance in practice.
Note ckreads has this same performance issue.
Still, despite these problems, ckfetches may be useful for cases where
you just want an extra layer of safety, or don't care about the tiny
chance an error is introduced between a fetch an subsequent read.
---
Like ckprogs/ckreads, ckfetches is an opt-in feature, and requires both
1. defining LFS_CKFETCHES, and 2. passing LFS_M_CKFETCHES during mount.
This is a bit of a quick implementation to get testing in place, so the
code cost is probably higher than strictly necessary. If we can refactor
the code internally to avoid all the duplicate lfsr_rbyd_fetchck/
lfsr_bptr_ck calls, we can probably bring this down a bit:
code stack
before: 36428 2680
yes-ckfetches: 36848 (+1.2%) 2680 (+0.0%)
no-ckfetches: 36428 (+0.0%) 2680 (+0.0%)
Oh, and also added lfs_emubd_flipbit to allow tests to manually flip
bits themselves. LFS_EMUBD_BADBLOCK_PROGFLIP is quick to find the above
mentioned checksum-read hole.
This could be done manually with read+erase+prog, but no reason to make
it harder than it needs to be.
So just like ckreads, ckprogs is now opt-in, requiring both 1. defining
LFS_CKPROGS at compile-time, and 2. passing the LFS_M_CKPROGS flag
during lfsr_mount.
_Unlike_ ckreads, ckprogs is actually a very lightweight feature. So the
difference between compiling with/without ckprogs is really quite small:
code stack
before: 36480 2680
yes-ckprogs: 36480 (+0.0%) 2680 (+0.0%)
no-ckprogs: 36428 (-0.1%) 2680 (+0.0%)
It's almost not worth putting behind an ifdef if not for consistency
with ckreads.
Mainly to make space for more shared open/mount flags that are future
planned.
The nice thing about our flags is they don't live on-disk, so we can
always change them whenever we need to.
It gets a bit messy, but this is what the current layout looks like:
8 8 8 8
.----++----++----++----.
.----..-..-..----------.
o_flags: |type||f||t|| o |
|----||-|:-:'--.-.-----'
|----||-|:-:---:-:-----.
d_flags: |type||f|: : : : |
|----||-|:-:---:-:-----'
|----||-|:-'--..-..----.
t_flags: |type||f|| t ||f||tstt|
'----''-'|----|'-''----'
.--------|----|:-:-----.
gc_flags: | | t |: : |
'--------|----|:-:-----'
.----..-.|----|:-:.----.
f_flags: | f ||m|| t |: :| f |
'----'|-||----|:-:'----'
.----.|-||----||-|.----.
m_flags: | i ||m|| t ||o|| m |
|----||-|'----'|-||----|
|----||-|------|-||----|
i_flags: | i ||m| |o|| m |
'----''-'------'-''----'
The main downside of this layout is that some of the traversal flags,
LFS_T_DIRTY/MUTATED, now risk ambiguity with open/mount flags. But I
don't think this is really avoidable as traversals are already using
almost the entire 32-bit encoding space...
No code changes:
code stack
before: 36480 2680
after: 36480 (+0.0%) 2680 (+0.0%)
These fall out quite naturally when you consider that we call
lfsr_mountinited internally to check that our format was successful.
That being said... they don't really do anything right now since we only
write a single mdir:
- LFS_F_COMPACT - The only gc operation that _might_ actually do
something is LFS_F_COMPACT, but only if our fs config exceeds >1/2 the
block size. But I'm not sure littlefs will even be able to write file
metadata if this happens...
- LFS_F_CKMETA - We already check the only mdir by calling
lfsr_mountinited, which implicitly fetches the mrootanchor.
- LFS_F_CKDATA - We uh, don't have any data immediately after
lfsr_format. But I guess it doesn't hurt to keep this around for
consistency, it at least implies CKMETA.
Hopefully these flags will be more interesting if/when we start adding
auxiliary trees to the filesystem, otherwise they may be worth reverting
in the future...
Until then, they at least provide some consistency, and I guess a way to
triply check that format was successful.
---
This could probably be better deduplicated, but calling lfsr_fs_gc from
both lfsr_mount and lfsr_format provides a bit better code organization:
code stack
before: 36448 2680
after: 36480 (+0.1%) 2680 (+0.0%)
This is mainly to solve the weird check-hole where passing CKPROGS/
CKREADS as mount flags has no effect on lfsr_format (I mean, it'd be a
bit silly if it did somehow):
LFS_F_RDWR 0 // Format the filesystem as read and write
LFS_F_CKPROGS 0x00000010 // Check progs by reading back progged data
LFS_F_CKREADS 0x00000020 // Check reads via parity bits/checksums
This makes lfsr_format a more cumbersome interface, but I don't know if
this is necessarily a bad thing. There's always risk of data loss when
calling lfsr_format, so maybe it should be a pain to call.
At the very least, format flags may be useful in the future for
enabling/disabling format-time things such as the planned block-map,
parity-tree, etc. Though it's unclear if such significant settings
should be format flags or somehow encoded as fields in our config
struct.
---
The LFS_F_* format flags of course ended up conflicting with our
internal LFS_F_* flags, so I renamed most of the internal flags to match
the closest flag set they participate in:
- LFS_F_TYPE -> LFS_O_TYPE
- LFS_F_UNFLUSH -> LFS_O_UNFLUSH
- LFS_F_UNSYNC -> LFS_O_UNSYNC
- LFS_F_ORPHAN -> LFS_O_ORPHAN
- LFS_F_ZOMBIE -> LFS_O_ZOMBIE
- LFS_F_ORPHANS -> LFS_I_ORPHANS
- LFS_F_UNCOMPACTED -> LFS_I_UNCOMPACTED
- LFS_F_TSTATE -> LFS_T_TSTATE
- LFS_F_BTYPE -> LFS_T_BTYPE
- LFS_F_DIRTY -> LFS_T_DIRTY
- LFS_F_MUTATED -> LFS_T_MUTATED
This may make it a bit less clear which flags are a part of the public
API, vs intended only for internal use, but at the very least our asserts
in format/mount/open/etc should catch most of these mistakes.
---
Code cost ended up being pretty minimal. Actually negative. This is the
second time we're _adding_ a feature that somehow saves code, though the
reality for this one is we're really just pushing constants up into the
user's stack frame. Still, it's a good indication the cost of format
flags is small:
code stack
before: 36452 2680
after: 36448 (-0.0%) 2680 (+0.0%)
This was only noticed when forcing btrees for other unrelated tests
(INLINED_SIZE=0, CRYSTAL_THRESH=-1), where even simple file writes would
end up with some unaligned fragments the size of our file buffer.
It was hard to notice without forcing btrees, since our crystallization
algorithm has a tendency to fix alignment issues.
The problem was that we weren't bypassing the file buffer correctly when
buffer.size == 0. We relied on the LFS_F_UNFLUSH flag to know if we
could do a bypassing write, but inlined files set the LFS_F_UNFLUSH flag
even for empty files. This led to blocked bypassing writes, attempts
to merge with empty buffers, and unaligned fragments.
To avoid this, lfsr_file_write now checks for buffer.size == 0
explicitly. There may be a better solution, but for now this gets the
job done.
---
To make sure we don't end up with unaligned fragments again in the
future, I've extend the fwrite litmus tests to check for well-aligned
fragments in addition to blocks:
- test_fwrite_simple_litmus_fragments
- test_fwrite_incr_litmus_fragments
These fixes end up adding a bit of code, as checking for both the
unflushed flag and buffer.size == 0 has a cost:
code stack
before: 36424 2680
after: 36452 (+0.1%) 2680 (+0.0%)
But hey, file aren't stuck with unaligned fragments anymore.
I'm not entirely sure what I was thinking when I thought we couldn't use
read hints in lfsr_bd_cmp. It's true read hints will just be clobbered
when ckprogs are enabled, but if ckprogs aren't enabled, and, perhaps
more rarely, rcache_size > pcache_size, we should still be able to
benefit from read hints in lfsr_bd_cmp.
This has a bigger effect on ckreads, where we likely need to read
trailing data to validate checksums/parity bits and can benefit from
earlier reads keeping more data in the rcache.
Curiously this actually saves a bit a code, not sure why that is:
code stack
before: 36428 2680
after: 36424 (-0.0%) 2680 (+0.0%)
One of these was missed during the crc -> cksum rename, so it wouldn't
have even linked correctly. Rather than fixing it I'm just going to drop
these functions.
At some point they were useful for debugging, but with emubd's disk
mirroring and dbgblock.py, it's both easier and more reliable to
find checksums with external scripts.
This moves all ckread-related logic behind the new opt-in compile-time
LFS_CKREADS flag. So in order to use ckreads you need to 1. define
LFS_CKREADS at compile time, and 2. pass LFS_M_CKREADS during
lfsr_mount.
This was always the plan since, even if ckreads worked perfectly, it
adds a significant amount of baggage (stack mostly) to track the
ck context of all reads.
---
This is the first non-trivial opt-in define in littlefs, so more test
framework features!
test.py and build.py now support the optional ifdef attribute, which
makes it easy to indicate a test suite/case should not be compiled when
a feature is missing.
Also interesting to note is the addition of LFS_IFDEF_CKREADS, which
solves several issues (and general ugliness) related to #ifdefs in
expression. For example:
// does not compile :( (can't embed ifdefs in macros)
LFS_ASSERT(flags == (
LFS_M_CKPROGS
#ifdef LFS_CKREADS
| LFS_M_CKREADS
#endif
))
// does compile :)
LFS_ASSERT(flags == (
LFS_M_CKPROGS
| LFS_IFDEF_CKREADS(LFS_M_CKREADS, 0)));
---
This brings us way back down to our pre-ckread levels of code/stack:
code stack
before-ckreads: 36352 2672
ckreads: 38060 (+4.7%) 3056 (+14.4%)
after-ckreads: 36428 (+0.2%) 2680 (+0.3%)
Unfortunately, we do end up with a bit more code cost than where we
started. Mainly due to code moving around to support the ckread
infrastructure:
code stack
lfsr_bd_readtag: +52 (+23.2%) +8 (+10.0%)
lfsr_rbyd_fetch: +36 (+5.0%) +8 (+6.2%, cold)
lfs_toleb128: -12 (-25.0%) -4 (-20.0%, cold)
total: +76 (+0.2%) +8 (+0.3%)
But oh well. Note that some of these changes are good even without
ckreads, such as only parsing the last ecksum tag.
Added some code, though we don't _really_ care:
code stack
before: 37872 3048
after: 38060 (+0.5%) 3056 (+0.3%)
Also interesting to note the difference in testing time, this highlights
_some_ of the performance cost of ckreads:
with ckreads: 1135.92s
without ckreads: 821.24s
Metastability is a rather nasty error condition where successive reads
to a memory location may return different values, either due to bus
issues or a failed prog. It's a tricky error condition to detect, and
one that ckreads was, in theory, supposed to help with.
To help test metastability (and other single-bit errors), emubd gained
several new features:
- LFS_EMUBD_BADBLOCK_PROGFLIP - Prog flips a bit
- LFS_EMUBD_BADBLOCK_READFLIP - Read flips a bit sometimes
- LFS_EMUBD_POWERLOSS_METASTABLE - Reads may flip a bit
These only affect a single bit in a given block, but by randomizing
which bit during every erase (and exhaustive bit testing in test_ck) we
should still see some fairly interesting bit-error patterns over time.
It's a bit difficult to test with more than a single bit error because
you can quickly find checksum/parity collisions when fuzz testing. But
there may be other interesting error patterns to look at in the future?
Also the erase_cycles implementation got a bit of a rework since it was
lopsided previously (progs/reads would always error before erases). And
since I was messing with emubd's internals I added lfs_emubd_markbad/
markgood and a few other convenience functions that seem useful:
- lfs_emubd_seed - Manually set the prng, needed in test_ck actually
- lfs_emubd_markbad - Mark block as bad, same as wear=-1
- lfs_emubd_markgood - Mark block as good, same as wear=0
- lfs_emubd_badbit - Get which big failed
- lfs_emubd_setbadbit - Set which bit will fail
- lfs_emubd_randomizebadbit - Randomize bad bit on erase
- lfs_emubd_markbadbit - Mark bit as bad, same as setbadbit+markbad
---
The intention of this new metastability emulation was to extend test_ck
to test ckreads/ckprogs. This went... interestingly.
The good news, the new emulation and tests worked quite well. They were
able to quite quickly show that ckreads is fundamentally not able to
detect all single-bit errors in our current design.
The problem boils down to the fact that the location of our parity bits
depends on the tag's leb128-encoded size. If a bit flip changes this
size field, we end up with a new parity bit, which 50/50 may or may not
detect the error.
For example, one bit flip:
40 0c 00 12 80 0d ff ff
'----.----' ^--------------------.
'- altble 0xc w0 -18 parity=1
40 0c 80 12 80 0d ff ff
'-------.-------' ^----------------------.
'- altble 0xc w2304 -1664 parity=1
This doesn't make ckreads _completely_ useless, just mostly useless. We
can still use it to check parity bits, but without a systematic proof.
But there's enough problems with ckreads: performance, RAM, code, etc,
that I think it may just be an interesting proof-of-concept and not
something users should actually use. Checking reads in the bd-layer
solves all of these problems...
---
At the very least ckprogs gets better testing, thanks to new tests in
test_ck and the addition of LFS_EMUBD_BADBLOCK_PROGFLIP in
test_badblocks.
The extra testing also found a ckprog/ckread hole in that we don't
ckprog/ckread during lfsr_format! I fixed this by making lfsr_format
always use ckprogs/ckreads if available, but maybe lfsr_format should
take its own set of flags?
Funnily enough this had no impact on code size since it probably just
changed the constant in a constant pool:
code stack
before: 37872 3048
after: 37872 (+0.0%) 3048 (+0.0%)
We should always know the worst-case leb128 size when calling
lfs_toleb128, so it's really a developer-error if lfs_toleb128 results
in a buffer overflow.
This is different from lfs_fromleb128, since we use lfs_fromleb128 to
parse on-disk leb128s, which may be corrupted, incomplete, malformed,
etc.
Changing this to an assert (unreachable really) saves a bit of code
since the compile can eliminate the LFS_ERR_CORRUPT that was only
reachable via developer-error:
code stack
before: 37884 3048
after: 37872 (-0.0%) 3048 (+0.0%)
These pieces of logic were common across the lfsr_bd_readck/cmpck/cpyck/
readtag functions and made sense to break out into their own functions.
It was just a bit tricky to figure out what the internal API should look
like.
This saves a bit of code at the cost of some stack. But it also makes
the code cleaner so this tradeoff is worth it to me:
code stack
before: 38100 3032
after: 37884 (-0.6%) 3048 (+0.5%)
With the adoption of the odd-parity-zero rbyd perturb scheme, it's now
possible to validate individual tag's parity with neighboring valid
bits. This sparked an idea that I previously thought was intractable.
If we:
1. Validate all metadata reads by checking their on-disk parity bits.
2. Validate all data reads by checking their in-metadata checksums.
We end up with a closed system where all reads are checked by at least
a parity bit.
Being able to check all reads is a very valuable filesystem feature, but
difficult for littlefs:
- We need to keep relevant data in RAM while validating checksums.
We can't just validate checksums and then perform a second read as
that creates a hole where new bit-errors may be introduced.
- This is solved in other filesystems by loading and checking whole
blocks in RAM. We just can't do that here.
- Without parity, we would need to check the rbyd's checksum on every
tag read. This would lead to a crazy O(n^2 log n) rbyd compaction
runtime.
Which is why I original thought ckreads was just intractable.
Now, this isn't all sunshine and rainbows. ckreads, as implemented here,
has some deeply concerning flaws:
- A parity bit is, mathematically, the minimum possible error-detection
possible. Is validating reads with only a parity bit sufficient for
real world applications?
- Validating data checksums on every read may have severe performance
implications. We need to read up to the entire block, which can lead
to O(n^2) behavior when performing a lot of small reads in a file.
- In order to validate checksums/parity-bits, we need to know where the
checksums/parity-bits actually are for each piece of data.
Our lfsr_data_t struct provides a surprisingly nice abstraction for
this, but oof is it expensive.
For the added code/stack cost alone, we probably want to eventually make
this an opt-in compile-time feature.
---
Implementation notes:
- This found an actual compiler bug! Turns out increasing lfsr_data_t
from 3-words to 5-words confuses GCC:
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=101854
- Mid-commit, we may have not actually written the last tag's parity
yet, which is a bit of a problem because we may read the last tag when
building the next trunk!
Fixing this required a whole separate tailck mechanism, which just
tracks in-progress commit's parity bits.
This doesn't help the code/stack cost situation...
- lfsr_bd_read/cmp/cpy all need to be extended to support calculating a
checksum on the side, which is a bit of a mess.
- bptr's cksize/cksum is redundant now, which is going to make
conditional compilation a mess.
- The extra parity byte we need to read makes hint calculation a pain.
Code cost wise... yeah, it's significant. Turns out almost doubling
lfsr_data_t has a significant impact on stack usage. Add in all the
extra code to track checksums/parity-bits and validate checksums/
parity-bits and you got yourself a pretty heavy feature:
code stack
before: 36352 2672
after: 38100 (+4.8%) 3032 (+13.5%)
I've been scratching my head over our rbyd perturb scheme. It's gotten
rather clunky with needing to xor valid bits and whatnot.
But it's tricky with needing erased-state to be included in parity bits,
while at the same time excluded from our canonical checksum. If only
there was some way to flip the checksums parity without changing its
value...
Enter the crc32c odd-parity zero: 0xfca42daf!
This bends the definition of zero a bit, but it is one of two numbers in
our crc32c-ring with a very interesting property:
crc32c(m) == crc32c(m xor 0xfca42daf) xor 0xfca42daf // odd-p zero
crc32c(m) == crc32c(m xor 0x00000000) xor 0x00000000 // even-p zero
Recall that crc32c's polynomial, 0x11edc6f41, is composed of two
polynomials: 0x3, the parity polynomial, and 0xf5b4253f, a maximally
sized irreducible polynomial. Because our polynomial breaks down into
two smaller polynomials, our crc32c space turns out to not be a field,
but rather a ring containing two smaller sub-fields. Because these
sub-fields are defined by their polynomials, one is the 31-bit crc
defined by the polynomial 0xf5b4253f, while the other is the current
parity.
We can move in the parity sub-field without changing our position in the
31-bit crc sub-field by xoring with a number that is one in the parity
sub-field, but zero in the 31-bit crc sub-field.
This number happens to be 0xf5b4253f (0xfca42daf bit-reversed)!
(crcs being bit-reversed will never not be annoying)
So long story short, xoring any crc32c with 0xfca42daf will change its
parity but not its value.
---
An that's basically our new perturb scheme. If we need to perturb, xor
with 0xfca42daf to change the parity, and after calculating/validating
the checksum, xor with 0xfca42daf to get our canonical checksum.
Isn't that neat!
There was one small hiccup: At first I assumed you could continue
including the valid bits in the checksum, which would have been nice for
bulk checksumming. But this doesn't work because while valid bits cancel
out so the parity doesn't change, changing valid bits _does_ change the
underlying 31-bit crc, poisoning our checksum and making everything a
mess.
So we still need to mask out valid bits, which is a bit annoying.
But then I stumbled on the funny realization that by masking our valid
bits, we accidentally end up with a fully functional parity scheme.
Because valid bits _don't_ include the previous valid bit, we can figure
out the parity for not only the entire commit, but also each individual
tag:
80 03 00 08 6c 69 74 74 6c 65 66 73 80
^'----------------.---------------' ^
| | |
v + parity = v'
Or more simply:
80 03 00 08 6c 69 74 74 6c 65 66 73 80
'----------------.----------------' ^
| |
parity = v'
Double neat!
Some other notes:
- By keeping the commit checksum perturbed, but not the canonical
checksum, the perturb state is self-validating. We no longer need to
explicitly check the previous-perturb-bit (q) to avoid the perturb
hole we ran into previously.
I'm still keeping the previous-perturb-bit (q) around, since it's
useful for debugging. We still need to know the perturb state
internally at all times in order to xor out the canonical checksum
correctly anyways.
- Thanks to all of our perturb iterations, we now know how to remove the
valid bits from the checksum easily:
cksum ^= 0x00000080 & (tag >> 8)
This makes the whole omitting-valid-bits thing less of a pain point.
- It wasn't actually worth it to perturb the checksum when building
commits, vs manually flipping each valid bit, as this would have made
our internal appendattr API really weird.
At least the perturbed checksum made fetch a bit simpler.
Not sure exactly how to draw this with our perturb scheme diagrams,
maybe something like this?
.---+---+---+---. \ \ \ \
|v| tag | | | | |
+---+---+---+---+ | | | |
| commit | | | | |
| | +-. | | |
+---+---+---+---+ / | | | |
|v|qp-------------->p>p-->p .
+---+---+---+---+ | . . .
| cksum | | . . .
+---+---+---+---+ | . . .
| padding | | . . .
| | | . . .
+---+---+---+---+ | | | |
|v------------------' | | |
+---+---+---+---+ | | |
| commit | +-. | +- rbyd
| | | | | | cksum
+---+---+---+---+ / | +-. /
|v----------------------' | |
+-------+---+---+ / |
| cksum ----------------'
+---+---+---+---+
| padding |
| |
+---+---+---+---+
| erased |
| |
. .
. .
---
Code changes were minimal, saving a tiny bit of code:
code stack
before: 36368 2664
after: 36352 (-0.0%) 2672 (+0.3%)
There was a stack bump in lfsr_bd_readtag, but as far as I can tell it's
just compiler noise? I poked around a bit but couldn't figure out why it
changed...
So simply calculating the ecksum over the whole prog size, instead of
manually CRCing the leading byte that we need to separately read to
check if we need to perturb.
Yes, this risks two reads instead of the one we need, but it's simpler,
less error prone, and less code. Our caching layer should prevent double
reads like this, so we might as well rely on it.
Saves some code:
code stack
before: 36396 2664
after: 36368 (-0.1%) 2664 (+0.0%)
I think this describes the goal of the non-perturbed rbyd checksums
decently. At the very least it's less wrong that "data checksum", and
calling it the "metadata checksum" would just be confusing. (Would our
commit checksum be the "metametadata checksum" then?)
The naive implementation is simpler, less code, and more likely to be
correct, each of these are more valuable than speed in our debug
scripts.
We're in Python anyways (no offense Python!).
Plus I think it's good to show that the underlying logic of CRCs aren't
really that complex, at least until we throw optimizations into the mix.
It's expected for our crystal boundary calculation to underflow, but
when checking for holes we were using the wrong signed/unsigned
comparison, so lfsr_file_carve thought there was a hole when there
wasn't:
-crs pos pos -crs
.-------| <-- this lookup ------| .--
'---. | +crs --. | +crs '--
. |---|---. ended up |---|---. .
. v v v looking --> v v v .
. .---. like this .---. .
. |dat| |dat| .
. '---' '---' .
. 0 . n 0 . n .
'---.---' '---.---'
no hole clearly a hole
This led to unoptimal block compaction and weird block alignment for
even relatively simple files.
The crystallization threshold is only a heuristic so this didn't exactly
break anything, but it was causing block-aligned files to waste a bit of
of space which wasn't great.
---
To hopefully protect against this in the future, I've added a couple
*_litmus tests to check that at least some simple block-aligned files
end up with the correct number of branches/blocks. This should at least
give us some confidence our crystallization algorithm is working as
intended.
We don't have all that many tests (any?) over the exact topology of
files, mainly because of how many heuristics are involved. Maybe we
should look into adding a couple more.
No code changes:
code stack
before: 36396 2664
after: 36396 (+0.0%) 2664 (+0.0%)
It was a bit tricky to figure out what this should look like.
Traditionally, filesystems tend to fallback to readonly if they detect
unsupported wcompat (ro_compat) flags or similar config mismatch.
We could do something similar in littlefs, but since we default to
asserting on writes to readonly objects for smaller code size, this
would be really weird and hard to use from a users perspective...
Instead, lfsr_mount returns LFS_ERR_NOTSUP on encountering wcompat-
mismatch in RDWR mode, but _not_ RDONLY mode. This allows the common
rdonly-fallback pattern to be implemented on the user's side of things,
similar to the common format-fallback pattern:
int err = lfsr_mount(&lfs, LFS_M_RDWR, &cfg);
if (err && err != LFS_ERR_NOTSUP) {
return err;
}
if (err == LFS_ERR_NOTSUP) {
err = lfsr_mount(&lfs, LFS_M_RDONLY, &cfg);
if (err) {
return err;
}
}
Note that lfsr_mount may still return LFS_ERR_NOTSUP if it encounters
rcompat-flags, even with RDONLY. Detecting this state will likely need
two lfsr_mount calls with the current API, but I don't think that will
be a big deal.
The main benefit of this scheme is that it is quite cheap thanks to
pushing the fallback logic on the user:
code stack
before: 36356 2664
after: 36396 (+0.1%) 2664 (+0.0%)
One missing puzzle piece here is how do you upgrade the filesystem? But I
think the lesson from the on-disk v2.0 -> v2.1 version bump is that this
should really be an explicit function (lfsr_fs_upgrade?). If explicit
and stand-alone, like lfsr_format, we shouldn't need a weird pseudo-
rdonly mode at all.
These simply imply LFS_O_FLUSH/SYNC on all open writable files.
LFS_M_SYNC is equivalent to MS_SYNCHRONOUS in Linux/etc, while
LFS_M_FLUSH is just provided for consistency.
As pure conveniences, these may seem a bit out of scope for littlefs,
except they are _very_ cheap:
code stack
before: 36356 2664
after: 36356 (+0.0%) 2664 (+0.0%)
Ok, they're not _completely_ free! It just turns out they cost 8 bytes,
and a bit of simplification around flag checking in lfsr_mount saved
8 bytes:
code stack
before: 36356 2664
m_flush/sync: 36364 (+0.0%) 2664 (+0.0%)
mount-no-mask: 36356 (+0.0%) 2664 (+0.0%)
These flags just call lfsr_file_ckmeta/ckdata under the hood, but make
it very easy to check metadata/data when opening a file. As an extra
plus they implicitly close the file on failure, so might make cleanup
easier.
Of course, everything has a cost:
code stack
before: 36368 2664
after: 36424 (+0.2%) 2664 (+0.0%)
These also ruin my previous "you don't pay for what you don't call"
assertion, since runtime flags unfortunately always pull in code.
We should add a compile-time switch for these evntually.
These are basically the same as lfsr_fs_ckmeta/ckdata but limited to a
single file. They may be useful when you need to validate a file but
don't want to bother validating the entire filesystem:
// Check a file for metadata errors
int lfsr_file_ckmeta(lfs_t *lfs, lfsr_file_t *file);
// Check a file for metadata + data errors
int lfsr_file_ckdata(lfs_t *lfs, lfsr_file_t *file);
I've also added test_ck to test these and added some more
lfsr_fs_ckmeta/ckdata tests there. These currently just test simple
full-block clobbering, but we should eventually test more interesting
error patterns.
Unfortunately lfsr_file_ckmeta/ckdata can't reuse the internal
lfsr_mtree_traverse in quite the same way lfsr_fs_ckmeta/ckdata can, so
they're actually a bit more expensive. Though keep in mind with
link-time gc you won't pay the cost unless you call these functions:
code stack
before: 36024 2696
after: 36368 (+1.0%) 2664 (-1.2%)
Oh, and the multiple calls to lfsr_btree/bshrub_traverse apparently
uninlined it out of lfsr_mtree_traverse, saving the stack cost in the
stack hot-path... Yay?
It's really not that much code (36 bytes, and only if you call
lfsr_fs_gc), and implicit state is better the explicit state (less
things that can fall out of sync).
I'm keeping the fancy F/GC flag masking in lfsr_fs_gc though.
Code changes:
code stack
before: 35988 2696
after: 36024 (+0.1%) 2696 (+0.0%)
This is equivalent to the user-facing LFS_I_CANLOOKAHEAD flag, but
explicitly set in lfs_alloc/lfs_alloc_markfree, rather than being
implied.
Usually, I prefer implicit state, as this means less things that can
fall out-of-sync if there is a filesystem bug, but for
LFS_F_CANLOOKAHEAD explicit state might be warranted.
The main benefit is we can take advantage of the matching F/GC bit
patterns to simplify lfsr_fs_gc's progress checks.
This ends up saving a bit of code:
code stack
before: 36048 2696
after: 35988 (-0.2%) 2696 (+0.0%)
If we add CKMETA/CKDATA and eventually REPAIRMETA/REPAIRDATA to the file
open flags, we'll end up with 17 flags total (13 user-facing,
4 internal), which is a bit (heh) too much for a 16-bit flags field!
There are a few ways to solve this, dropping features for one, instead
I've decided to expand the fields flag to 32-bits. Fortunately this was
already the field size for all user-facing fields.
To avoid a RAM increase, I've also shoved the opened-file types and
traversal tstates into the same field.
We have various flags in quite a few places now, here's how
everything fits together:
8 8 8 8
.----++----++----++----.
.----..---..--..-------.
o_flags: |type|| f ||t || o |
|----||---|:--:'-------'
|----||---|:--:--------.
d_flags: |type|| f |: : |
|----||---|:--:--------'
|----||---|:--'--..----.
t_flags: |type|| f || t ||tstt|
'----''---'|-----|'----'
.----------|-----|-----.
gc_flags: | | t | |
'----------|-----|-----'
.-----.---.|-----|.----.
m_flags: | | m || t || m |
'-----|---|'-----'|----|
.----.|---|-------|----|
i_flags: | i || m | | m |
'----''---'-------'----'
Unfortunately, using the full 32-bit flag space highlights that C99's
enum types are kind of garbage...
In C99 enums are strictly signed ints, which means attempting to use
them for 32-bit bit fields overflows. There is no way around this so
I've switched our flag definitions to #defines.
I've kept types as enums for now but I'm keeping my eye on them...
---
The tradeoff of merging the type/btype/tstate/flags fields is that it
takes more code to extract/encode the various subfields. Since these
fields our heavily used in our codebase, this really adds up:
code stack
before: 35888 2696
after: 36048 (+0.4%) 2696 (+0.0%)
At least in theory the type fields can be optimized to a byte load, but
not btype/tstate. Also accessing bits in higher positions may be adding
cost.
After thinking about this for a while, btree node compaction is
subtlety different from mdir compaction, less valuable, and adds more
risk:
- Unlike mdirs, btree node compaction will always allocate a new
block, leading to a higher chance of alloc failure.
- Btree node compaction also always requires additional writes to
propagate btree changes, whereas mdir compaction is usually
self-contained unless it triggers a relocation. If btree nodes are
mostly full this risks being counter-productive.
- Btree node compaction requires a full tree traversal, whereas mdir
compaction requires only traversing the mtree. Though you can always
force mtree-only traversal manually with LFS_GC_MTREEONLY.
- Btrees/bshrubs are also more likely to be "cold storage", that is it
probably won't be uncommon to create long-lived read-only btrees as a
part of files. Compacting these btrees can actually be counter-
productive as it can encourage splitting.
- Btrees/bshrubs are also more likely to be one use, and discarded as a
file is truncated and rewritten. Compacting btree nodes in this case
is a waste of erase cycles.
And since btree node compaction also introduces a lot of complexity/risk
of bugs, I'm going to drop this for now and limit LFS_GC_COMPACT to only
compacting mdirs. At least this tested implementation will live in the
history and can always be reintroduced in the future if it becomes a
wanted feature.
---
As is usually the case, doing less work ends up with less code:
code stack
before: 36292 2704
after: 35888 (-1.1%) 2696 (-0.3%)
Note this still keeps the rbyd-specific commit logic necessary for
committing to specific btree nodes, even though btree node compaction
was the only current use case. This should eventually be useful for
metadata repair. Hopefully const-propagation can minimize the cost, but
realistically this means we're probably leaving some code savings on the
table.
These should never need to mutate the filesystem, so calling
lfsr_mtree_gc doesn't really make sense. This mainly matters for
link-time gc in case we never need lfsr_mtree_gc (readonly mode?).
Unfortunately this adds a code cost because the optional pointers to
lfsr_mtree_traverse can no longer be const-propagated:
code stack
before: 36284 2704
after: 36292 (+0.0%) 2704 (+0.0%)
Nothing consequential.
One interesting question is if we should swap our dirty bits during the
call to lfsr_mtree_traverse. At the moment I think limiting this to just
our lfsr_mtree_gc logic will create the least surprise in the future.
Code changes minimal:
code stack
before: 36288 2704
after: 36284 (-0.0%) 2704 (+0.0%)
It was a bit weird to have this in lfsr_mtree_traverse, which doesn't
change any filesystem state otherwise.
At the very least we should call lfs_alloc_markinuse and
lfs_alloc_markfree in the same function, and rerouting
lfsr_mtree_traverse eot to handle this would have added code cost
anyways.
The main cost is stack:
code stack
before: 36256 2680
after: 36288 (+0.1%) 2704 (+0.9%)
Unfortunately this reveals one of the bigger issues with our optional
return parameters: if a function with optional return parameters needs
the structs to perform work, in this case lfsr_mtree_traverse needs
lfsr_bptr_t in case ckmeta/ckdata is requested, it requires an
additional stack allocation.
In theory, these stack allocations could be elided if the return structs
are provided, but you can't really express this in standard C.
Combine this with the fact that lfs_alloc is sensitive to stack changes,
and lives at the bottom at every hot-path, and the end result is more
stack usage.
Note that the additional stack cost, 24 bytes, is exactly equal to one
tag + one bptr, 4 bytes + 20 bytes.
Two main reasons:
1. If we mount without ckprogs, we do actually have a pretty decent hole
here where data can be written with errors and go unchecked during
lfsr_fs_gc.
2. If we're traversing a btree that gets mutated mid-traversal, we're
kicked entirely off the btree. This means we could miss large ranges
of btree nodes/data blocks that may not have themselves been mutated.
Not great.
Worst case, it doesn't hurt to check things again if the filesystem
changes. If this is too much of a bottleneck, you should probably be
running gc in incremental mode anyways, which always starts a new
traversal on ckmeta/ckdata.
Checking for dirty/mutated doesn't really add that much code:
code stack
before: 36240 2680
after: 36256 (+0.0%) 2680 (+0.0%)
There is really no reason to continue lookahead traversals if our
filesystem has been mutated. Clearing the flag and restarting in this
case is more likely to make progress.
Note that it's worth continuing for all of the other current gc flags:
- LFS_GC_MKCONSISTENT - Except maybe for mkconsistent. We can't actually
make progress, since we can't prove the filesystem is free of orphans,
but it's beneficial to keep traversing and clearing orphans in case of
other traversal flags that mutation would force a second traversal
anyways.
Continuing mkconsistent traversals also spreads out orphan cleanup a
bit better, instead of just repeatedly cleaning up the first couple
mdirs when under heavy contention.
But to be honest, the chance of mutation that still leaves the
filesystem with orphans is just so low that it's not worth doing
anything. mkconsistent only needs to traverse the mtree anyways...
- LFS_GC_COMPACT - Like mkconsistent, compacting traversals are worth
continuing for better mtree coverage under heavy contention.
We will need a second pass to prove we compacted everything anyways,
so might as well try to get as much mutation done as possible in the
current traversal.
- LFS_GC_CKMETA/CKDATA - Continuing ckmeta/ckdata traversals provides
better mtree coverage under heavy contention.
This is much more important for CKMETA/CKDATA than the others, because
_eventually_ checking every block for errors is more valuable than
proving anything.
This adds some code, but the use of flags here is quite valuable for
expressing complex constraints like this cheaply:
code stack
before: 36228 2680
after: 36240 (+0.0%) 2680 (+0.0%)
Now we consider if it's still possible for the current traversal to make
progress. If it can, we continue with the relevant masked flags,
otherwise we restart. This should prevent us from traversing the
filesystem for no reason.
I also reverted the ckedmeta/ckeddata flags, these ended up just adding
code cost. We're not in the stack hot-path anyways...
Code changes:
code stack
before: 36244 2680
after: 36228 (-0.0%) 2680 (+0.0%)
Also moved ckmeta/ckdata progress into lfs->flags. We have the bits
available so we might as well use them instead of allocating bools on
the stack...
Whether or not to consider ckmeta/ckdata successful when the filesystem
has been mutated is a bit nuanced.
Initially, I thought we trigger a re-traversal, since we may have
introduced new blocks that haven't been checked. But think about it,
where did those blocks come from?
Any new blocks introduced by filesystem mutation will have just been
written. And if a write introduces corruption you probably have bigger
problems...
... Actually as I write this I realized mounting without ckprogs makes
this even more nuanced, but since ckmeta/ckdata is more intended for
data-at-rest error detection I'm going to keep the change for now.
If you want to catch write errors, you really should enable ckprogs.
This is only a problem for lfsr_fs_gc, and the use cases for
ckmeta/ckdata in lfsr_fs_gc will probably catch any write errors on the
next cycle anyways...
Code changes:
code stack
before: 36208 2680
after: 36244 (+0.1%) 2680 (+0.0%)
It makes the the are-any-files-still-open assert a bit uglier, but we
really don't need to bother calling lfsr_omdir_close here. We're done
with this struct anyways...
Not doing something saves a bit of code:
code stack
before: 36240 2680
after: 36208 (-0.1%) 2680 (+0.0%)
Thinking about this more, we probably don't want to entangle
lfsr_fs_mkconsistent/ckmeta/etc and lfsr_fs_gc:
- lfsr_fs_ckmeta/ckdata are readonly and don't need to clobber
traversals. The system can make more progress if these use separate
states.
- We already need a bit of code to force traversals to restart for
lfsr_fs_ckmeta/ckdata, so these already aren't simple wrappers.
- lfsr_fs_mkconsistent should also probably not invalidate gc traversals
when the filesystem is already consistent. It is called by... checks
notes... every function that writes to disk.
This could be fixed in lfsr_fs_mkconsistent, but it'd be pretty close
to just calling lfsr_mtree_gc...
- We don't really benefit from reusing the gc traversal state.
lfsr_fs_mkconsistent/ckmeta/etc aren't on the stack hot-path, so the
stack usage is more-or-less free (though I realize this depends on
what functions are called in a given system).
- Calling lfsr_fs_gc can actually be a detriment for code size when
considering link-time-gc (not related to fs-gc), since it will drag in
the function when we don't need the traversal-invalidation features.
- Calling lfsr_fs_gc vs lfsr_mtree_gc shouldn't really be a significant
code size difference. We should probably look into lfsr_mtree_gc,
which is called from many places, instead of tangling everything
together...
So this commit reverts gc-restarts and brings back gc masking on flag
change.
At the very least, moving all the code around led to a bit of code
savings:
code stack
before gc-restart: 36316 2680
gc-restart: 36068 (-0.7%) 2680 (+0.0%)
after gc-restart: 36240 (-0.2%) 2680 (+0.0%)
This simplifies lfsr_fs_gc a bit, and allows lfsr_fs_mkconsistent/
ckmeta/etc to call lfsr_fs_gc directly (it would be a bit strange for
these function to finish up unrelated gc traversals).
Unfortunately, this does risk gc getting stuck constantly restarting if
there is contention between two lfsr_fs_gc calls with different flags,
but you could argue this would be a system design mistake...
The deduplication of traversal state leads to some pretty nice code
savings:
code stack
before: 36316 2680
after: 36068 (-0.7%) 2680 (+0.0%)
So instead of configuring gc_steps at mount time (or eventually compile
time), lfsr_fs_gc now takes a steps parameter that controls how much gc
work to attempt:
int lfsr_fs_gc(lfs_t *lfs, lfs_soff_t steps, uint32_t flags);
This API was needed internally to better deduplicate on-mount gc, and I
figured it might also be useful for users to be able to easily change
gc_steps per lfsr_fs_gc call.
I realize this could also be accomplished with the theoretical
lfsr_fs_gccfg, but it's a bit easier to not need a struct every call.
Most likely, depending on project/system, users will always call
lfsr_fs_gc with either 1 (minimal work) or -1 (maximal work), or, worst
case, can define a system-wide GC_STEPS somewhere.
---
Deduplicating on-mount gc work better saved some code, though it's worth
noting this could have been done internally and not exposed to users:
code stack
before: 36476 2680 (+0.0%)
after: 36316 (-0.4%) 2680 (+0.0%)
Matching lfsr_mdir_commit, it's probably safer if lfs_alloc_ckpoint
calls are always explicit.
This is doubly true for traversals since we absolutely must not call
lfs_alloc_ckpoint in lfsr_mtree_traverse, or else lfs_alloc will break
in a really comical fashion.
It's also a bit silly how little an impact on code size this had:
before: 36472 2680
after: 36476 (-0.0%) 2680 (+0.0%)
This is mainly to avoid confusion around if lfs_alloc_ckpoint needs to
be called before this function.
I guess it's not problematic to call lfs_alloc_ckpoint unnecessarily...
but unlike every other lfsr_mdir_* function we call lfs_alloc_ckpoint
internally for every orphan we fix. Otherwise we could end up with
ENOSPC too early.
lfsr_fs_fixorphans_ is really only internal glue between
lfsr_fs_fixorphans and lfsr_mtree_gc anyways...
I thought it was a bit funny we test various disk sizes in test_grow,
but no where else! test_grow actually found several bugs when reworking
the lookahead buffer related to small disks, so I figured we should have
some more intentional tests... And behold! A bug!
The issue is that we implicitly call lfs_alloc_ckpoint in
lfsr_mdir_commit. Originally the thinking was that this would be fine
since any in-flight blocks should be committed to a tracked btree/bshrub
first, but lfsr_bshrub_commit goes _through_ lfsr_mdir_commit. Bit of a
problem.
So if we call lfsr_bshrub_commit to add a recently allocated block, it
may end up calling lfsr_mdir_commit, erronously ckpointing the
allocator, and then clobbering the new block if the mdir needs to be
relocated, split, etc.
---
The fix here is to just move lfs_alloc_ckpoint out of lfsr_mdir_commit.
This adds a bit of noise, but it's probably a good thing for alloc
ckpoints to be explicit.
At least lfs_alloc_ckpoint is cheap:
code stack
before: 36412 2680
after: 36472 (+0.2%) 2680 (+0.0%)
The main reason for this change is to allow keeping track of existing
known-free blocks while trying to find more free blocks. This makes it
so failed filesystem traversals don't result in negative progress, which
is nice.
This was difficult in the previous lookahead scheme, since we we'd need
to shift the lookahead buffer to keep off=0 rooted at the first bit.
Shifting bytes is relatively easily with memmove, but it gets tricky
when shifting bits:
lookahead before: ???? ???? ???? ??00 1101 0101 00?? ????
^ ^
off off+size
shift: 0011 0101 0100 ???? ???? ???? ???? ????
^ ^
off off+size
traverse: 0011 0101 0100 0000 0000 0000 1100 0000
^ ^
off off+size
Instead, we now just let the lookahead buffer wrap around. No shifting
required:
lookahead before: ???? ???? ???? ??00 1101 0101 00?? ????
^ ^
off off+size
traverse: 0000 0000 1100 0000 1101 0101 0000 0000
^
off
^
off+size
This gets a bit confusing with the lookahead window also wrapping around
disk, but the math works out with enough modulos (if modulos are too
expensive, we should eventually be able to optimize these into simple
bit masks via compile-time config).
In the future, if we move away from the const config struct, it would
also be nice to try to reducing the number of modulos by storing the
lookahead buffer size in bits instead of bytes...
Note that if the lookahead buffer is larger than disk, the lookahead
window will sort of travel around the underlying buffer. This isn't
inherently a problem, but it did cause some bugs.
To avoid similar bit-related problems with zeroing, lfs_alloc_inc now
also zeros bits as we allocate/skip them, so bits should always be zero
when we start a lookahead traversal. Though note we still need to
manually memset the buffer when discarding lookahead state in init/grow.
---
The end result is surprisingly a net savings in terms of code size. I
guess mainly due to dropping all the lfs_alloc_shift calls:
code stack
before: 36472 2680
after: 36412 (-0.2%) 2680 (+0.0%)
These functions provide an easy API for checking all metadata/data
checksums in the filesystem:
// Check the filesystem for metadata errors
int lfsr_fs_ckmeta(lfs_t *lfs);
// Check the filesystem for metadata + data errors
int lfsr_fs_ckdata(lfs_t *lfs);
These are more-or-less the same as calling lfsr_fs_gc with
LFS_GC_CKMETA/CKDATA, but don't involve the gc/traversal-invalidation
machinery, and may be a bit easier for users to pick up.
---
Unfortunately, for simple wrappers, we're again hit with a somewhat
surprising code cost:
code stack
before: 36288 2680
after: 36472 (+0.5%) 2680 (+0.0%)
But I think we can again blame the high overhead of LFS_TRAVERSAL/
lfsr_mtree_gc. We should look into reducing/deduplicating this logic...