Skip to content

Commit b9f24f3

Browse files
committed
ZLFS: multi-block inode map (format v2) and cached reads
Two limits fall: the 512-inode ceiling of the single-block inode map, and the uncached (B_INVAL) read path. Format v2: the checkpoint block now carries zc_imap_nblocks and, right after its header, a little-endian array of the inode-map block LBAs, so one checkpoint addresses about 500 map blocks -- roughly 256000 inodes at a 4 KB block size (ZLFS_MAXINO). The in-core map is allocated in whole blocks and grown on demand by zlfs_imap_grow; a commit writes however many map blocks the inode count needs, snapshotting zm_ninodes once so the block count, the entries written, and zc_ninodes always agree. The cleaner's durable pass re-reads the checkpoint block and walks every map block it names (the zm_imap_lba field is gone). Mount validates the map-block LBAs and rejects v1 images (version bump); newfs_zlfs writes the new layout. Reads now go through the buffer cache. This is coherent with the raw zoned writes because the log never overwrites a live LBA: contents change only when a zone is reset, and both reset sites (the cleaner and the superblock-zone ping-pong) now purge the device's cached buffers via vinvalbuf before the LBAs can be reused. Add zlfs-manyfiles.sh: creates 700 files (past the old 512 ceiling), verifies the population and sampled contents across a remount, then checks removal persists. functional_testing.md updated.
1 parent 8873212 commit b9f24f3

10 files changed

Lines changed: 330 additions & 84 deletions

File tree

README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -348,10 +348,12 @@ On-disk format (`sys/sys/zlfs.h`):
348348
because NVMe ZNS namespaces have no conventional zones; the header
349349
documents append rules, mount discovery, the block-size bootstrap, and
350350
the both-zones-full crash recovery case.
351-
- The checkpoint locates a block-sized inode map (inode number to device
352-
LBA) and the root inode. Directories are arrays of fixed 128-byte
353-
entries. Inodes carry twelve direct block pointers plus a single
354-
indirect block.
351+
- The checkpoint locates the inode map (inode number to device LBA) and
352+
the root inode. The map spans multiple blocks -- the checkpoint block
353+
carries the array of map-block LBAs after its header, addressing about
354+
256000 inodes at a 4 KB block size (format v2). Directories are
355+
arrays of fixed 128-byte entries. Inodes carry twelve direct block
356+
pointers plus a single indirect block.
355357

356358
Userland:
357359

@@ -415,15 +417,15 @@ Current limitations (documented in the code; each is a natural next step):
415417
live and superseded blocks are not compacted (a copying cleaner is
416418
future work), so space in mixed zones is reclaimed only once
417419
everything in them is superseded.
418-
- ZLFS blocks are read uncached (`B_INVAL`): raw zoned writes and zone
419-
resets bypass the buffer cache, so caching would serve stale data once
420-
a zone is recycled. Proper buffer-cache integration is future work.
420+
- Reads go through the buffer cache. This is coherent with the raw
421+
zoned writes because the log never overwrites a live LBA; the cache
422+
is purged whenever a zone reset makes cached blocks stale.
421423
- The commit path is not yet safe against concurrent vnode operations.
422424

423425
Remaining sequence toward a general-purpose filesystem:
424426

425-
1. Double/triple indirect blocks and block-level buffer-cache integration
426-
(so files need not be buffered whole and reads cache safely).
427+
1. Double/triple indirect blocks and per-block dirty tracking (so files
428+
need not be buffered whole in memory).
427429
2. A copying cleaner (compact mixed live/dead zones).
428430
3. Concurrency-safe commit.
429431

functional_testing.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ size 4096; superblock (SB) zones 0-1, 126 data zones.
3636

3737
| Feature | Commit | Verified so far | Missing evidence |
3838
|---|---|---|---|
39+
| Multi-block inode map (format v2: checkpoint carries the map-block LBA array; ~256k inodes at 4k; in-core map grows on demand) | pending push | adversarial round in progress | `zlfs-manyfiles.sh sd1c` (700 files > old 512 ceiling, remount, count + sampled contents); needs kernel rebuild AND `newfs_zlfs` reinstall (v2), plus reformat — v1 images are rejected |
40+
| Read caching restored (no more blanket `B_INVAL`; cache purged via `vinvalbuf` after any zone reset) | pending push | adversarial round in progress | churn v2 rerun must still PASS (the reset-purge path is exercised by the cleaner run at ~i120 and the remount) |
3941
| SB-zone recycling (reset stale SB zone on ping-pong) | `422cb631771` | 2 adversarial rounds (caught FWRITE and wp-tracking bugs, then pass) | never exercised on VM: one SB zone holds 16384 superblocks (131072 LBAs / 8 per SB), so the first ping-pong needs ~16k commits; churn v2 produces ~300 — a dedicated many-commit test or a smaller-zone QEMU profile is needed |
4042

4143
## 3. Under analysis / known gaps
@@ -46,7 +48,8 @@ size 4096; superblock (SB) zones 0-1, 126 data zones.
4648
| Corrupt-metadata hardening | a self-referential indirect entry (entry LBA == `zi_ib[0]`) would sleep forever in `getblk` (B_BUSY held by caller) | unreachable with well-formed metadata (allocator hands out distinct LBAs) | add bounds/identity checks on indirect entries when fsck-style validation is designed |
4749
| `statfs` free-space approximation | `f_bfree` counts EMPTY-condition zones only; mixed zones count as used; live_bytes is a zero/nonzero flag, inflated by duplicate marking | cosmetic accounting | proper per-zone byte accounting with the copying cleaner |
4850
| `zst_live_bytes` semantics | only tested against 0 in the reset loop; not a true byte count | none today; trap for future code | rename or fix when the copying cleaner needs real counts |
49-
| Inode map is a single block | hard cap of 512 inode numbers (510 usable files/dirs alive at once); numbers are now reused after removal, but the live-count ceiling stands | churning workloads fine; wide trees will hit ENOSPC at 510 live inodes | multi-block inode map (format change) |
51+
| Inode ceiling (was: single-block map, 512) | format v2 raises the cap to `ZLFS_CKPT_NIMAP * epb` (~256000 at a 4 KB block: ~500 map-block LBAs fit in the checkpoint block) | wide trees fine now; the ~256k cap is structural until the map gets its own indirection | none planned; revisit only if a workload needs more |
52+
| `zlfs_imap_grow` swaps `zm_imap` under `zm_lock` only | readers (`read_dinode`, commit, cleaner pass 2, remove/rmdir/rename) index the map without `zm_lock`; safe today because all accesses are single-expression under the kernel lock and grow has no sleep between copy and install — a latent use-after-free the moment these paths run MP-unlocked | none under the current single-threaded assumption | take `zm_lock` (or `zm_wlock`) in map readers as part of the concurrency-safe-commit phase |
5053

5154
## 4. Later (roadmap order)
5255

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
# ZLFS multi-block inode map test.
5+
#
6+
# Creates more files than a single-block inode map can hold (512
7+
# entries at a 4096-byte block), so the checkpoint must reference
8+
# multiple map blocks (format v2), and verifies the whole population
9+
# survives a remount.
10+
#
11+
# WARNING: this reformats the given disk with newfs_zlfs.
12+
13+
usage()
14+
{
15+
echo "usage: $0 disk [mount-point [count]]" >&2
16+
echo "example: $0 sd1c /mnt/zlfs 700" >&2
17+
echo "warning: this reformats the disk with newfs_zlfs" >&2
18+
exit 1
19+
}
20+
21+
die()
22+
{
23+
echo "$0: $*" >&2
24+
exit 1
25+
}
26+
27+
section()
28+
{
29+
echo
30+
echo "== $* =="
31+
}
32+
33+
disk=${1-}
34+
mnt=${2-/mnt/zlfs}
35+
count=${3-700}
36+
37+
[ -n "$disk" ] && [ $# -le 3 ] || usage
38+
[ "$(id -u)" -eq 0 ] || die "must run as root"
39+
40+
dev=/dev/$disk
41+
42+
section "newfs + mount"
43+
umount "$mnt" 2>/dev/null || true
44+
newfs_zlfs "$disk"
45+
mkdir -p "$mnt"
46+
mount_zlfs "$dev" "$mnt"
47+
48+
section "create $count files (over the old 512-inode ceiling)"
49+
mkdir "$mnt/many"
50+
i=0
51+
while [ "$i" -lt "$count" ]; do
52+
echo "payload-$i" > "$mnt/many/f$i" ||
53+
die "create failed at file $i"
54+
i=$((i + 1))
55+
done
56+
sync
57+
n=$(ls "$mnt/many" | wc -l | tr -d ' ')
58+
[ "$n" -eq "$count" ] || die "expected $count files, found $n"
59+
echo " ok: $count files created and committed"
60+
61+
section "population survives remount?"
62+
umount "$mnt"
63+
mount_zlfs "$dev" "$mnt"
64+
n=$(ls "$mnt/many" | wc -l | tr -d ' ')
65+
[ "$n" -eq "$count" ] || die "after remount: expected $count, found $n"
66+
[ "$(cat "$mnt/many/f0")" = "payload-0" ] || die "f0 corrupted"
67+
mid=$((count / 2))
68+
[ "$(cat "$mnt/many/f$mid")" = "payload-$mid" ] || die "f$mid corrupted"
69+
last=$((count - 1))
70+
[ "$(cat "$mnt/many/f$last")" = "payload-$last" ] || die "f$last corrupted"
71+
echo " ok: $count files and sampled contents intact after remount"
72+
73+
section "cleanup: remove all, rmdir, remount"
74+
rm "$mnt"/many/f*
75+
rmdir "$mnt/many"
76+
sync
77+
umount "$mnt"
78+
mount_zlfs "$dev" "$mnt"
79+
[ ! -d "$mnt/many" ] || die "directory resurrected after remount"
80+
echo " ok: removal persisted"
81+
82+
section "PASS"

sbin/newfs_zlfs/newfs_zlfs.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,17 +453,20 @@ write_filesystem(int fd, const char *path, const struct zone_geometry *g,
453453
sizeof(imap_ent));
454454
write_block(fd, path, lba[ZLFS_MKFS_IMAP], secsize, blk, block_size);
455455

456-
/* Block 6: checkpoint. */
456+
/* Block 6: checkpoint (one-block inode map; LBA follows header). */
457457
memset(blk, 0, block_size);
458458
memset(&zc, 0, sizeof(zc));
459459
zc.zc_magic = htole32(ZLFS_MAGIC);
460460
zc.zc_version = htole32(ZLFS_VERSION);
461461
zc.zc_generation = htole64(0);
462462
zc.zc_root_ino = htole64(ZLFS_ROOT_INO);
463-
zc.zc_imap_lba = htole64(lba[ZLFS_MKFS_IMAP]);
463+
zc.zc_imap_nblocks = htole64(1);
464464
zc.zc_ninodes = htole64(ZLFS_FIRST_INO + 1);
465465
memcpy(zc.zc_uuid, uuid, sizeof(zc.zc_uuid));
466466
memcpy(blk, &zc, sizeof(zc));
467+
imap_ent = htole64(lba[ZLFS_MKFS_IMAP]);
468+
memcpy(blk + sizeof(struct zlfs_checkpoint), &imap_ent,
469+
sizeof(imap_ent));
467470
crc = crc32c(blk, block_size);
468471
((struct zlfs_checkpoint *)blk)->zc_checksum = htole64(crc);
469472
write_block(fd, path, lba[ZLFS_MKFS_CKPT], secsize, blk, block_size);

sys/sys/zlfs.h

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
#include <sys/types.h>
5252

5353
#define ZLFS_MAGIC 0x54BDCC01 /* "ZLFS" NEW */
54-
#define ZLFS_VERSION 1
54+
#define ZLFS_VERSION 2 /* 2: multi-block inode map */
5555

5656
/* Zones reserved at the start of the device for the superblock log. */
5757
#define ZLFS_SB_ZONES 2
@@ -181,22 +181,31 @@ struct zlfs_dirent {
181181
/*
182182
* Checkpoint: the filesystem's mount entry point. zs_checkpoint_lba
183183
* in the superblock points at the newest checkpoint block. The
184-
* checkpoint locates the inode map -- a block-sized little-endian
185-
* array of u_int64_t device LBAs indexed by inode number, where
186-
* imap[ino] == 0 means the inode does not exist -- and names the root
187-
* inode. zc_checksum is CRC32C over the whole zs_block_size-sized
188-
* block with zc_checksum taken as zero.
184+
* checkpoint locates the inode map -- a little-endian array of
185+
* u_int64_t device LBAs indexed by inode number, where imap[ino] == 0
186+
* means the inode does not exist -- and names the root inode.
187+
*
188+
* The map spans zc_imap_nblocks block-sized extents; their device LBAs
189+
* are a little-endian u_int64_t array that starts immediately after
190+
* this header inside the checkpoint block, so a checkpoint addresses
191+
* up to ZLFS_CKPT_NIMAP(bsize) map blocks (about 500 at a 4096-byte
192+
* block, or ~256000 inodes). zc_checksum is CRC32C over the whole
193+
* zs_block_size-sized block with zc_checksum taken as zero.
189194
*/
190195
struct zlfs_checkpoint {
191196
u_int32_t zc_magic; /* ZLFS_MAGIC */
192197
u_int32_t zc_version; /* ZLFS_VERSION */
193198
u_int64_t zc_generation; /* matches the superblock generation */
194199
u_int64_t zc_root_ino;
195-
u_int64_t zc_imap_lba; /* device LBA of the inode-map block */
200+
u_int64_t zc_imap_nblocks; /* inode-map blocks; LBAs follow */
196201
u_int64_t zc_ninodes; /* number of inode-map entries */
197202
u_int8_t zc_uuid[16]; /* must match the superblock UUID */
198203
u_int64_t zc_reserved[4];
199204
u_int64_t zc_checksum;
200205
};
201206

207+
/* Inode-map block LBAs storable in one checkpoint block. */
208+
#define ZLFS_CKPT_NIMAP(bsize) \
209+
(((bsize) - sizeof(struct zlfs_checkpoint)) / sizeof(u_int64_t))
210+
202211
#endif /* _SYS_ZLFS_H_ */

sys/zlfs/zlfs_alloc.c

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -226,36 +226,67 @@ zlfs_clean(struct zlfs_mount *zmp)
226226
struct zlfs_zone_state *zst;
227227
struct zlfs_node *znp;
228228
struct buf *bp;
229-
u_int64_t *dimap;
229+
const struct zlfs_checkpoint *dc;
230+
u_int64_t *dimap, *lbas;
230231
u_int32_t bsize = zmp->zm_super.zs_block_size;
231-
u_int64_t i, ino, lba, maxino = bsize / sizeof(u_int64_t);
232-
int error;
232+
u_int64_t i, j, ino, lba, nblocks, epb = bsize / sizeof(u_int64_t);
233+
int error, nreset = 0;
233234

234235
for (i = 0; i < zmp->zm_nzones; i++)
235236
zmp->zm_zones[i].zst_live_bytes = 0;
236237

237-
/* Metadata roots of the durable checkpoint. */
238+
/* Metadata root: the durable checkpoint block. */
238239
zlfs_gc_mark(zmp, zmp->zm_super.zs_checkpoint_lba);
239-
zlfs_gc_mark(zmp, zmp->zm_imap_lba);
240240

241-
/* Pass 1: the durable live set, from the on-disk inode map. */
242-
if (zmp->zm_imap_lba != 0) {
243-
if (zlfs_bread_block(zmp, zmp->zm_imap_lba, &bp) != 0)
241+
/*
242+
* Pass 1: the durable live set. Re-read the checkpoint block to
243+
* find the inode-map blocks it references, mark them, and walk
244+
* every inode they name.
245+
*/
246+
if (zmp->zm_super.zs_checkpoint_lba != 0) {
247+
if (zlfs_bread_block(zmp, zmp->zm_super.zs_checkpoint_lba,
248+
&bp) != 0)
244249
return;
245-
dimap = malloc(bsize, M_TEMP, M_WAITOK);
246-
memcpy(dimap, bp->b_data, bsize);
250+
dc = (const struct zlfs_checkpoint *)bp->b_data;
251+
nblocks = letoh64(dc->zc_imap_nblocks);
252+
if (nblocks == 0 || nblocks > ZLFS_CKPT_NIMAP(bsize)) {
253+
brelse(bp);
254+
return; /* unusable map; reclaim nothing */
255+
}
256+
lbas = mallocarray(nblocks, sizeof(u_int64_t), M_TEMP,
257+
M_WAITOK);
258+
for (j = 0; j < nblocks; j++)
259+
lbas[j] = letoh64(((const u_int64_t *)
260+
((const u_int8_t *)bp->b_data +
261+
sizeof(struct zlfs_checkpoint)))[j]);
247262
brelse(bp);
248-
for (ino = 0; ino < maxino; ino++) {
249-
lba = letoh64(dimap[ino]);
250-
if (lba == 0)
263+
264+
dimap = malloc(bsize, M_TEMP, M_WAITOK);
265+
for (j = 0; j < nblocks; j++) {
266+
if (lbas[j] == 0)
251267
continue;
252-
if (zlfs_gc_mark_inode(zmp, lba) != 0) {
253-
free(dimap, M_TEMP, bsize);
254-
return;
268+
zlfs_gc_mark(zmp, lbas[j]);
269+
if (zlfs_bread_block(zmp, lbas[j], &bp) != 0)
270+
goto pass1_fail;
271+
memcpy(dimap, bp->b_data, bsize);
272+
brelse(bp);
273+
for (ino = 0; ino < epb; ino++) {
274+
lba = letoh64(dimap[ino]);
275+
if (lba == 0)
276+
continue;
277+
if (zlfs_gc_mark_inode(zmp, lba) != 0)
278+
goto pass1_fail;
255279
}
256280
}
257281
free(dimap, M_TEMP, bsize);
282+
free(lbas, M_TEMP, nblocks * sizeof(u_int64_t));
283+
goto pass1_done;
284+
pass1_fail:
285+
free(dimap, M_TEMP, bsize);
286+
free(lbas, M_TEMP, nblocks * sizeof(u_int64_t));
287+
return;
258288
}
289+
pass1_done:
259290

260291
/* Pass 2: the in-core live set, through zm_imap. */
261292
for (ino = 0; ino < zmp->zm_ninodes; ino++) {
@@ -297,7 +328,16 @@ zlfs_clean(struct zlfs_mount *zmp)
297328
continue; /* best effort */
298329
zst->zst_wp_lba = zst->zst_start_lba;
299330
zst->zst_cond = DK_ZONE_COND_EMPTY;
331+
nreset++;
300332
}
333+
334+
/*
335+
* Cached buffers for the reset zones would serve stale contents
336+
* once their LBAs are reused; drop the device's cache before the
337+
* allocator can hand them out again.
338+
*/
339+
if (nreset > 0)
340+
zlfs_cache_purge_dev(zmp);
301341
}
302342

303343
/*
@@ -378,7 +418,8 @@ zlfs_log_init(struct zlfs_mount *zmp, const struct dk_zone *sbz)
378418
* an empty full-block map with only the reserved inodes.
379419
*/
380420
if (zmp->zm_imap == NULL) {
381-
zmp->zm_imap = malloc(zmp->zm_super.zs_block_size, M_ZLFS,
421+
zmp->zm_imap_alloc = zmp->zm_super.zs_block_size;
422+
zmp->zm_imap = malloc(zmp->zm_imap_alloc, M_ZLFS,
382423
M_WAITOK | M_ZERO);
383424
zmp->zm_ninodes = ZLFS_FIRST_INO;
384425
}

0 commit comments

Comments
 (0)