xipfs: compaction command, test suite and NXFLAT execute-in-place demo#3665
xipfs: compaction command, test suite and NXFLAT execute-in-place demo#3665casaroli wants to merge 3 commits into
Conversation
|
The failing The The fixes are tracked separately:
The two fixes were validated together with the CI-equivalent |
| break; | ||
|
|
||
| case 't': | ||
| max_ms = (uint32_t)atoi(optarg); |
| memset(&arg, 0, sizeof(arg)); | ||
| arg.max_ms = max_ms; | ||
|
|
||
| fd = open(mount, O_RDONLY | O_DIRECTORY); |
| arg.result.extents_moved == 1 ? "" : "s", | ||
| (unsigned long)arg.result.blocks_reclaimed, | ||
| arg.result.blocks_reclaimed == 1 ? "" : "s", | ||
| (unsigned long)arg.result.blocks_pinned); |
| (unsigned long)arg.result.blocks_pinned); | ||
|
|
||
| printf(" largest free run now %lu bytes\n", | ||
| (unsigned long)arg.result.largest_free_run); |
|
|
||
| printf("%s: %lu blocks of %lu bytes (%lu KB)\n", | ||
| mount, (unsigned long)s->nblocks, (unsigned long)s->blocksize, | ||
| (unsigned long)(s->nblocks * (uint64_t)s->blocksize / 1024)); |
| #define DF_FILES 55 | ||
| #define DF_MAPCOLS 50 | ||
|
|
||
| static char g_map[512]; |
There was a problem hiding this comment.
move to macro/data section
| if (fd >= 0) | ||
| { | ||
| n = read(fd, got, len); | ||
| ok = (n == (ssize_t)len) && (memcmp(got, want, len) == 0); |
|
|
||
| fill_pattern(buf, len, seed); | ||
|
|
||
| fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); |
There was a problem hiding this comment.
add O_CLOEXEC for ALL open
|
|
||
| for (i = 0; i < len; i++) | ||
| { | ||
| buf[i] = (uint8_t)(seed + (i * 7)); |
| if (fd < 0) | ||
| { | ||
| free(buf); | ||
| return -1; |
There was a problem hiding this comment.
all -1 to the correct error code
xipfs has a defragmentation ioctl and no way to reach it from a shell. The only caller was a demo in examples/nxflatxip, which built its own block map to show what compaction had done. xipfs [-n] [-t <ms>] [<mountpoint>] -n surveys and reports without moving anything: where each file sits, a map of the volume, and how much of the free space a single allocation can reach. That last number is the one a caller facing -ENOSPC actually wants; at 0% the largest possible file already fits however scattered the map looks. The compaction is asked for through a descriptor for the mountpoint directory, so no file inside the volume is open while it runs and a single pass can reach every extent. The walk descends into the directories xipfs synthesises from names, and reports each file by its path relative to the mount, so a volume that uses them is described in full rather than down to its first level. Block totals come from statfs rather than XIPFSIOC_EXTENTINFO, because that one does name a file and an empty volume has none, yet its geometry is still worth reporting. Assisted-by: Claude Code:claude-opus-4-8 Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Twelve sections, selected by name on the command line because the power loss sweeps run for minutes while everything else runs in seconds. The routine part covers the VFS paths, the write-once rules (reopen for write, append, seek during write, truncate of a written file are all refused) and both mmap variants: that a plain mapping lands inside the media window with no heap growth, that MAP_XIP_STRICT fails with ENXIO rather than copying, and that N mappings of one file produce N pins on one extent. The rest is aimed at the two properties that are easy to get wrong and quiet when they are: Pin release. A pin taken by one task and a task that dies with a mapping still live both have to end with the extent movable again -- the second without the task ever calling munmap, since a module that faults never will. Both are checked by asking the defragmenter to move the extent afterwards. Power-loss atomicity. With CONFIG_FS_XIPFS_FAULT_INJECT the suite fails the Nth flash operation, remounts, and checks the volume is consistent and every file that had been committed is byte-for-byte intact. It sweeps N across create, unlink and defragmentation, and repeats the sweep with the failing write torn -- a partial program rather than a clean refusal -- which is what a real power loss mid-program leaves behind. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
The point of a writable execute-in-place file system is that a module can arrive after the firmware was built and still run without being copied into RAM. This demonstrates exactly that, end to end: it writes an NXFLAT module into xipfs the way a download would, declaring the size up front so the extent is exact, checks the file system can hand out a real flash pointer for it, and runs two instances concurrently. Both instances report the address of their own text and of their stack. The text address is the same in both and is inside the flash window -- it is the address the file occupies on the media -- while the stacks differ. While they run, the extent carries one pin per instance, which is what stops the defragmenter relocating code that is executing. Two subcommands cover the file system rather than the module. bench times a bulk read out of the mapped flash before and after a write, which on the RP2350 is the cost of whichever path the driver used to restore XIP. defrag fills the volume, deletes every other file until an allocation genuinely cannot be satisfied, compacts, retries the same allocation, then verifies every relocated file byte for byte and again after a remount, printing a block map at each step. The module has neither static data nor string constants, and reports through a callback into the firmware instead of formatting its own output. That is not stylistic: the ldnxflat in circulation resolves the GOT entries for .bss objects into the import table, and drops the addend when it resolves the PC-relative references to .rodata, so a module using either quietly computes wrong addresses. The comment in module/xipmod.c records both. The callback also exercises the other direction of the interface, firmware code entered with the module's data base still live in r10. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
casaroli
left a comment
There was a problem hiding this comment.
@xiaoxiang781216 i addressed most of the concerns, most casts are gone with the use of <inttypes.h>, some of them are necessary (signed/unsigned comparison, larger-width arithmethic), and some of them are explicit truncation, I think we should keep them to avoid modern compiler warnings.
| width = XIPFS_NAME_MAX; | ||
| for (f = 0; f < s->nfiles; f++) | ||
| { | ||
| int len = (int)strlen(s->files[f].name); |
There was a problem hiding this comment.
i think we should keep this to make the conversion explicit
| frag = 0; | ||
| if (s->freeblocks > 0) | ||
| { | ||
| frag = (uint32_t)(((uint64_t)(s->freeblocks - s->largestrun) * 100) / |
There was a problem hiding this comment.
I think these are necessary because we do the calculation with uint64_t then truncate to 32 bit explicitly
| } | ||
|
|
||
| nread = read(fd, g_buffer, len + 1); | ||
| if (nread == (ssize_t)len && memcmp(g_buffer, expect, len) == 0) |
There was a problem hiding this comment.
I think this is necessary
|
|
||
| for (i = 0; i < len; i++) | ||
| { | ||
| buf[i] = (uint8_t)(seed + (i * 7)); |
There was a problem hiding this comment.
I think it is better to keep the truncation explicit here
| fill_pattern(g_buffer, len, seed); | ||
|
|
||
| nwritten = write(fd, g_buffer, len); | ||
| if (nwritten != (ssize_t)len) |
There was a problem hiding this comment.
this is necessary to avoid warning
| { | ||
| snprintf(seed[i], sizeof(seed[i]), "%d", i + 1); | ||
|
|
||
| args[0] = (FAR char *)"xipmod"; |
There was a problem hiding this comment.
we need to remove the const here from the string literal.
Summary
The applications that go with XIPFS, the writable execute-in-place file
system added in the companion PR:
Companion PR: apache/nuttx#19536
This one depends on that one and cannot be built without it: every Kconfig
option here depends on
CONFIG_FS_XIPFS.Three additions:
system/xipfs— a command that compacts a volume and reports how itserase blocks are laid out. Because a XIPFS file is a single contiguous
extent, an allocation can fail for lack of a contiguous run while plenty of
free space remains, and compaction is never automatic: the file system
returns
-ENOSPCand the caller decides. This command is that decision madeby hand.
-nsurveys without moving anything and prints the per-file table,a block map, and the share of free space a single allocation cannot reach —
which is the number a caller facing
-ENOSPCactually wants. The pass isasked for through a descriptor for the mountpoint directory, so no file is
held open while it runs and one pass reaches every extent.
testing/fs/xipfs— the test suite, 14 sections selectable by namebecause the power-loss sweeps run for minutes while everything else runs in
seconds. Beyond the routine VFS paths it aims at the properties that are
easy to get wrong and quiet when they are: that a pin taken by a task that
then dies without unmapping is still released, and that failing the Nth
flash operation and remounting always yields a consistent volume with every
committed file intact. The sweeps cover create, unlink, mkdir/rmdir and
defragmentation, each with the failing operation left torn as well as
cleanly refused.
examples/nxflatxip— the end-to-end demonstration: write an NXFLATmodule into the volume at run time the way a download would, confirm the
file system hands out a real flash pointer for it, and run two instances
concurrently. Both report the same text address, inside the flash window,
while their stacks differ. Two subcommands cover the file system rather than
the module:
benchtimes a bulk read out of mapped flash before and after awrite, and
defragfragments the volume until an allocation genuinelycannot be satisfied, compacts, retries, then verifies every relocated file
byte-for-byte and again after a remount.
A note on the demo module
The module is built in-tree exactly the way
examples/nxflatbuilds its testprograms, so it needs the same two host tools,
mknxflatandldnxflat.It deliberately has no static data and no string constants, and reports
through a callback into the firmware instead of formatting its own output.
That is not stylistic. The
ldnxflatin circulation resolves the GOT entriesfor
.bssobjects into the import table — so the module overwrites theaddresses of the functions it imports and branches into nothing on its next
call out — and drops the addend already in the instruction when resolving the
PC-relative references to
.rodata, so every string pointer lands in themiddle of the code. Both were diagnosed on hardware; the comment at the top of
module/xipmod.crecords them. The callback also exercises the otherdirection of the interface: firmware code entered with the module's data base
still live in its PIC register.
Impact
CONFIG_FS_XIPFS, so nothing changes for existing configurations.examples/nxflatxipadditionally depends onNXFLATandLIBC_EXECFUNCS,and its build needs
mknxflat/ldnxflatand a boardMake.defsthat namesthem — the same requirement
examples/nxflatalready has.testing/fs/xipfswrites and erases its volume continuously, the power-losssections most of all. The Kconfig help and the documentation both say to
point it at a volume whose contents do not matter.
Testing
Host: macOS 15 (arm64),
arm-none-eabi-gcc 14.2.rel1.Targets:
sim:xipfs— XIPFS onrammtd.pimoroni-pico-2-plus:xipfs— XIPFS on real QSPI NOR via/dev/rpflash.Every hardware run was bracketed by a
uname -acheck of the git revision andboard name, before the first command and after the last, so the logs cannot be
from a different build.
Full suite on real flash
Same suite on
sim:xipfs:==== 90 passed, 0 failed ====.nxflatxipon hardwarenxflatxip defragon hardwarenxflatxip benchon hardwarexipfscommand on hardwaretools/checkpatch.sh -c -u -m -gpasses on every commit in the branch.