Part 7: File systems
NoovexOS 64 has no block cache layer under a generic inode cache the way Linux does. Every file the system knows about lives in one fixed in-memory table, FS[], and every directory in a second table, DT[]. The on-disk formats (NVX4, or NTFS/FAT when installed that way) are back ends that fill those tables at mount, load file contents on demand, and get the changes written back. The rest of the kernel, the shell, the desktop apps and the Linux system-call layer all work on FS[]/DT[] through a small set of helpers.
Every file here is its own compilation unit with a header under include/nvx/fs/. Before v8.96, fs/vfs/unixfs.c included fs/nvx/nvx4.c and fs/ntfs/ntfsk.c included fs/fat/vfat.c; they are now separate units too.
shell / desktop apps / core/linux.c (Linux syscalls)
|
+-------------------------+-----------------------------+
| vfs.c: vfs_open/read/write/lseek/close, procfs, /dev |
| unixfs.c: lookup, links, symlinks, permissions, stat |
| dirtree.c: DT[] tree, path resolution, rename/unlink |
+-------------------------+-----------------------------+
|
ramfs.c: FS[1024] in-memory files (heap buffers)
|
+-------------------------+-----------------------------+
| NVX4 (nvx4.c) | rootfs: NTFS / FAT32 / FAT16 |
| partition 0x7F or | (ntfsk.c + rfs_ops, |
| scratch disk @LBA 2048 | ntfs-3g bridge, vfat.c) |
+-------------------------+-----------------------------+
gdisk_* (AHCI/NVMe) or ata_* (legacy IDE)
7.1 The in-memory file table (fs/vfs/ramfs.c)
Constants
| Symbol | Value | Meaning |
|---|---|---|
FS_N |
1024 | Number of file slots (FS[FS_N]). The slot index is also the file's "inode" for every back end. |
FS_CAP |
0x40000000 (1 GiB) |
Largest file. fs_reserve refuses need > FS_CAP, and writers clamp to FS_CAP-1. |
FS_OLDCAP |
2097152 (2 MiB) | The old fixed per-file size. Now only used as the read buffer size when converting NVX3 disks. |
ramfs.c also defines the Unix mode bits taken from V6 ino.h (S_IFMT, S_IFDIR 0040000, S_IFREG 0100000, S_IFCHR 0020000, S_ISUID, S_ISGID, S_IRUSR...). S_IFLNK 0120000 is defined in unixfs.c.
FsFile
typedef struct{ char name[64]; char* data; int len; int cap; int used; int folder; int dir;
unsigned int mode; int uid, gid; u32 nlink, mtime, ctime, atime;
u8 lazy, clean, orphan, pad; int dlo, dhi; u64 tick; } FsFile;
static FsFile FS[FS_N];
| Field | Meaning |
|---|---|
name[64] |
Leaf name (up to 63 characters plus NUL). An empty name is used for orphans. |
data, cap |
Heap buffer from kmalloc and its size. data may be NULL while len > 0, for a file that has not been loaded yet or has been evicted. The buffer always keeps one spare byte so data[len] can be 0. |
len |
Logical file size in bytes. |
used |
Slot is in use. |
folder |
Legacy category index used by the file explorer (apps/system/file_explorer.c filters on FS[k].folder). Not part of the directory tree. |
dir |
Index of the parent directory in DT[]. -2 marks an orphan (see 7.3). |
mode, uid, gid, nlink, mtime, ctime, atime |
Unix metadata. Times are seconds since the epoch from ux_now() (= k_realtime_ms()/1000). |
lazy |
The contents are on disk but not in memory. The next fs_ensure loads them. |
clean |
The memory copy equals the on-disk copy. |
orphan |
Unlinked but still open (see fs_slot_freed). |
dlo, dhi |
The dirty byte range [dlo, dhi) when clean == 0. 0x7FFFFFFF as dhi means "to the end". |
tick |
Value of the global g_fs_tick at the last fs_ensure, used as an LRU clock for eviction. |
There are two separate "dirty" concepts:
FS[k].clean/dlo/dhi: the memory copy differs from disk, and which bytes do. The back ends use this to decide what to write.g_fs_dirty[k](au8[FS_N]): the file was written through a descriptor since it was opened.vf_unrefuses it on the last close to schedule background writeback (7.1.4).
Buffer management
fs_reserve(k, need)makes surecap > need. It grows the buffer by 1.5x (starting at 64 bytes), at least toneed+1, at most toFS_CAP+1, rounded up to 64 bytes. If that allocation fails it retries with an exact size. The old contents are copied anddata[len] = 0.fs_alloc(k, n)wrapskmalloc. On failure it callsfs_evict(n+4096, k)and tries once more.fs_ensure(k)is the "make it resident" call. It bumpsFS[k].tick = ++g_fs_tick. If the file islazyit callsfs_backend_load(k)and marks the fileclean. If there is still no buffer it reserveslenbytes and zero-fills them.fsd(k)returnsFS[k].dataafterfs_ensure, or the static 64 KiBg_fs_zerobuffer if loading fails, so callers never get a NULL pointer.fs_dirty(k, lo, hi)widens the dirty range, or starts a new one if the file was clean, and clearsclean.fs_wr(k, need)ensures and reserves, then marks the whole file dirty.fs_wr_range(k, lo, hi)marks only[lo, hi)dirty.vfs_writeuses the range form, so a small write into a large file only dirties a few blocks.fs_set(k, src, n)replaces the contents. If the old buffer is much larger than needed (cap > n + cap/2 + 64K) it is freed first.src == NULLmeans zero-fill. The whole file is marked dirty andlazyis cleared.fs_truncate_to(k, n): whenn == 0, a buffer larger than 64 KiB is freed outright, without loading the file. Otherwise the file is loaded, grown or shrunk, the gap is zero-filled, and the range frommin(n, old len)to the end is marked dirty.fs_free(k)releases the buffer and resetslazy,clean,dlo,dhiandorphan. It does not clearused.
Lazy loading and eviction
At mount, the back ends fill only metadata and set lazy = len > 0 and clean = 1 (n4_mount, nk_load_tree). fs_backend_load(k) (in nvx4.c) allocates len bytes and reads the file with nk_root_read when g_rootfs is set, or otherwise with n4_read_file.
static long fs_evict(long want,int keep){
long got=0;
for(int pass=0;pass<2&&got<want;pass++)
for(int k=0;k<FS_N&&got<want;k++){
if(k==keep||!FS[k].used||!FS[k].data||FS[k].lazy||!FS[k].clean)continue;
if(FS[k].tick+(pass==0?64u:8u)>g_fs_tick)continue;
if(fs_is_open(k)||!fs_reloadable(k))continue;
got+=FS[k].cap; kfree(FS[k].data); FS[k].data=0; FS[k].cap=0; FS[k].lazy=1; }
return got;
}
A file can be evicted only if all of these hold:
- it is resident and clean;
- it is not the file being grown;
- it has not been used in the last 64
fs_ensurecalls (8 on the second, more aggressive pass); - it is not open in any
g_vfentry; fs_reloadablesays it can be read back.
fs_reloadable returns false for orphans, when there is no disk, and for anything under /tmp. On NVX4 it also requires that the extents already cover len and that no indirect block is pending (!g_n4indd[k]).
Background writeback
static volatile int g_fs_wb_pending; static u64 g_fs_wb_due;
static void fs_wb_mark(int k){ (void)k; if(!g_fs_wb_pending){ g_fs_wb_pending=1; g_fs_wb_due=g_ticks+100; } }
When the last reference to a file vfile goes away (vf_unref in core/proc/jobs.c) and g_fs_dirty[k] is set, the flag is cleared and fs_wb_mark is called. Then fs_orphan_close(k) runs. The kernel main loop (core/init/main.c) checks if(g_fs_wb_pending&&g_ticks>=g_fs_wb_due)nvx3_sync_all();, so a burst of closes becomes one commit about 100 ticks (one second at 100 Hz) later. nvx3_sync_all and nvx3_flush_meta both clear g_fs_wb_pending.
Synchronous paths bypass the timer:
fs_sync(k)tonvx3_sync(k)ton4_sync_file+n4_commit. This is used byk_fsync,k_truncate_idx,k_rename,ux_mkfile,ux_symlink, package extraction andfs_save_bininsys/native/syscall.c.nvx3_flush_meta()commits metadata after link, unlink, chmod, chown and utime.
File descriptors (declared in ramfs.c)
#define VMAXOPEN 1024
#define FDMAX 256
struct vfile { int used, fs_idx, off, flags, dev, ref; const vfs_fs_ops* vfs; char vpath[64]; };
static struct vfile g_vf[VMAXOPEN];
There is one global table of open files, g_vf. Per-process descriptor tables come from fdt(), and fdce() holds the close-on-exec flags. Both are defined with the process code. A descriptor slot holds -1 when free and -2 for the kernel's own stdin/stdout/stderr. vfile.dev selects the kind of object:
dev |
Object |
|---|---|
| 0 | regular file FS[fs_idx] |
1 /dev/null, 2 /dev/zero, 3 /dev/random and /dev/urandom, 4 /dev/full, 5 /dev/tty and /dev/console |
character devices (dev_id) |
| 6 / 7 | pipe read end / write end (jc_pipe_read / jc_pipe_write) |
9 /dev/fb0, 10 /dev/input/mouse and /dev/input/mice, 12 /dev/dsp and /dev/audio, 13 /dev/kmsg |
devices handled by ux_dev_read/ux_dev_write, snd_dev_write and klog |
| 11 | open directory (fs_idx = DT index). dt_busy checks it. |
| 100 | a file served by a registered vfs_fs_ops (procfs, usbfs). The path is kept in vpath. |
fs_find_name(nm) is a legacy flat lookup by leaf name only.
7.2 The directory tree (fs/vfs/dirtree.c)
#define DN_MAX 512
typedef struct { char name[64]; int parent; int used; unsigned int mode; int uid,gid; u32 mtime, ctime, atime; } DNode;
static DNode DT[DN_MAX];
DT[0] is always the root, with name "/" and parent = -1. A file belongs to directory FS[k].dir. A directory belongs to DT[i].parent. Nothing on disk is kept as directory entries: listing a directory means scanning DT[], FS[] and the hard-link table for matching parents (k_readdir_at, then k_readdir_extra).
| Function | Behaviour |
|---|---|
dt_init() |
Runs once (g_dtree_init). It clears the tree and creates the root. If an old NVX2/NVX3 tree was loaded (g_nvx_have_tree) it calls dt_load_from_disk(). Otherwise it creates /home /bin /etc /downloads /tmp /dev and sets the cwd to /home. |
dt_load_done() |
Called by n4_mount after it fills DT[] itself. It repairs the root, makes sure /dev exists and sets g_dcwd to /home (or /). |
dt_mkdir_at(parent, name) |
Returns an existing child of that name, or allocates a slot with mode 0040755 owned by g_uid:g_gid. It updates the parent's mtime and ctime. Names must be 1..63 characters. |
dt_child(parent, name) |
Linear search for a child directory. |
dt_resolve_dir(p) |
Walks an absolute or relative (from g_dcwd) path one component at a time, handling . and ... A component longer than 63 characters fails. Returns a DT index or -1. |
dt_path_n(node, out, cap) |
Builds an absolute path by climbing parents, at most 16 levels. dt_path(n, o) is a macro in sys/native/syscall.c that passes sizeof(o) as the capacity. |
dt_is_tmp(d) |
Climbs up to 32 levels. Returns true if the ancestor directly under / is named tmp. |
dt_save() |
Persists the tree: nk_root_dt_save() on NTFS/FAT, otherwise nvx3_flush_meta(). |
dt_dir_ok, dt_dir_empty, k_is_ancestor |
Validity, emptiness and ancestry tests. A directory is not empty while it holds files, subdirectories or hard links (ux_links_in). |
dt_busy(d) (core/proc/jobs.c) |
True if d is the current cwd, the cwd of any live process (read from g_cpu[].dcwd for processes running on another core), or open as a directory descriptor (dev == 11). |
k_split
k_split(path, &dir, base) divides a path into its parent directory index and its leaf name. It refuses leaves longer than 63 characters and prefixes longer than 127, rather than truncating them. A bare name resolves against g_dcwd. k_find_file uses it, and as a fallback also matches a file whose dir no longer exists (!dt_dir_ok) by its full string. This is the only place a "name anywhere" match is still allowed.
Operations
k_unlink(p):k_find_file, thenux_can_unlink(dir, k), thenfs_drop_name(k).k_rmdir(p): refuses the root, directories withoutperm_dir, ancestors of the cwd, busy directories, and non-empty ones (subdirectories, files or links). It clearsused, touches the parent and callsdt_save().k_rename(from, to):ux_rename_linkis tried first, in casefromis a hard-link name (it returns -2 when it is not).- For a file: the caller needs
ux_can_unlinkon the source andperm_diron the destination. Iftois an existing directory the file moves into it under its own name. A same-named subdirectory blocks the rename. An existing file or link with the target name is dropped, which needsux_can_unlinkon it. The file is then renamed and moved, both parents are touched, andfs_syncandnvx3_flush_metaare called. - For a directory: refuses
/and moves into its own subtree (k_is_ancestor). It may replace an existing empty directory that is not busy and is not the cwd. A file with the target name blocks it. On success it updatesparentand the times, then callsdt_save().
k_truncate(p, len)/k_ftruncate(fd, len)tok_truncate_idx: needs write permission andlen <= FS_CAP-1. It callsfs_truncate_to,ux_touch, thenfs_syncwhen a disk is present.k_fsync(fd):fs_syncon a regular file. It succeeds trivially for devices and the standard streams.
/tmp is memory-only
dt_is_tmp is checked in every persistence path:
n4_sync_filefrees any blocks a/tmpfile still holds and writes nothing.n4_serializeskips/tmpfiles and links that point into or out of/tmp.nvx3_syncandnk_root_sync_filereturn early.fs_reloadablerefuses eviction, because the data could not be read back.
ux_boot_layout additionally clears every file and link in /tmp at boot and sets its mode to 041777.
Time
k_realtime_ms() reads the RTC once (read_clock), converts it with k_days_from_civil, and from then on adds g_ticks*10 ms.
7.3 Unix semantics (fs/vfs/unixfs.c)
Lookup and symlinks
ux_lookup_nofollow(p, &isdir) first tries dt_resolve_dir and returns a DT index with isdir = 1 if that succeeds. Otherwise it splits the path and searches FS[], then the link table g_lk[], in the parent directory. Last, it tries the orphaned-directory fallback.
ux_lookup(p, &isdir, follow) adds symlink resolution. A symlink is an ordinary FS slot with mode S_IFLNK|0777 whose data is the target path.
- At most 8 hops are followed.
- Paths and targets are limited to 191 characters.
- A relative target is joined to
dt_path(FS[k].dir).
Related calls:
ux_symlink(target, path)creates the slot. The caller needsperm_diron the parent and the name must not be taken (ux_name_takenchecks files, links and directories). It stores the target withfs_setand syncs it.ux_readlinkreturns the raw target.
Hard links: g_lk[NLINKS]
#define NLINKS 256
struct ulink { int used, dir, ino; char name[52]; };
static struct ulink g_lk[NLINKS];
A file's primary name lives in FS[k]. Each extra name is a g_lk entry pointing at slot ino. ux_link_count(k) = 1 + the number of links, and it is stored in FS[k].nlink.
| Call | Behaviour |
|---|---|
ux_link(from, to) |
Source must be a file (never followed). Non-root callers who are not the owner need read and write permission on it. The target name must be free and at most 51 characters, and the caller needs perm_dir. |
ux_unlink(p) |
If p names a link, removes only that entry (after ux_can_unlink). If it is a primary name, calls fs_drop_name(k). |
fs_drop_name(k) |
If any link points at k, the first one is promoted: its name and directory become the file's primary name and the link entry is freed. Otherwise the slot is released through fs_slot_freed. |
ux_rename_link(from, to) |
Moves or renames a link entry. It needs write permission on the target file. It drops an existing file or link of the target name, and refuses if a directory of that name exists or if the target is the same inode. |
Orphaned open files
static void fs_slot_freed(int k){
...
for(int i=0;i<NLINKS;i++) if(g_lk[i].used&&g_lk[i].ino==k)g_lk[i].used=0;
g_fs_dirty[k]=0;
if(fs_is_open(k)&&fs_ensure(k)==0){ FS[k].used=1; FS[k].orphan=1; FS[k].clean=0; FS[k].name[0]=0;
FS[k].dir=-2; FS[k].folder=-1; FS[k].nlink=0; return; }
for(int i=0;i<VMAXOPEN;i++) if(g_vf[i].used&&g_vf[i].dev==0&&g_vf[i].fs_idx==k){ g_vf[i].dev=1; g_vf[i].fs_idx=-1; }
FS[k].used=0; fs_free(k);
}
If the file is still open, it is made resident and kept as an orphan: no name, dir = -2, nlink = 0. Its descriptors keep working. The orphan is skipped by n4_serialize, by nvx3_sync, and by the commit loop. Because it is no longer in the metadata, its blocks become free at the next commit.
fs_orphan_close(k), called from vf_unref, frees the slot once no descriptor refers to it. If the file could not be made resident, any descriptors are redirected to /dev/null (dev = 1) instead, so a reused slot can never be reached through a stale descriptor.
Permissions
static int ux_perm_ok(int k,int want){ /* want: 4=r 2=w 1=x */
if(k<0||k>=FS_N||!FS[k].used)return 0;
if(g_uid==0)return 1;
unsigned m=FS[k].mode;
if(FS[k].uid==g_uid) return (m & (want<<6)) ? 1 : 0;
return (m & want) ? 1 : 0;
}
- The model has only two classes: owner and other. The group bits are not consulted.
perm_fileis an alias forux_perm_ok. perm_dir(d)grants permission to create or modify entries in a directory if any of these is true: the caller is root, owns the directory, the directory is world-writable (0002, default mode0040755if unset), orux_is_admin(g_uid).ux_can_unlink(d, k): root always may. Otherwise the caller must owndordmust be world-writable. The exception is an admin deleting their own file in someone else's directory (FS[k].uid==g_uid && ux_is_admin). If the sticky bit01000is set ond, a non-root caller must own either the file or the directory. Whendis invalid the check falls back toperm_file(k, 2).ux_is_admin(uid): true for uid 0 and uid 1000, and for any user whose name (from/etc/passwdviaux_uid_name) appears in the comma-separated member list (fourth field) of theadmin:line of/etc/group.ux_chmodandux_chownrequire root or ownership. A non-root caller cannot give a file away.ux_chmodkeeps theS_IFMTbits and fills inS_IFDIR/S_IFREGif they are missing. Directory changes calldt_save(), file changes callnvx3_flush_meta().
ux_stat2 and inode numbers
ux_stat2(path, u32 o[10], follow) fills o[] = {mode, uid, gid, nlink, size, ino, mtime, atime, ctime, o[9]} (o[9] is set to 1 only for directories). Inode numbers come from disjoint ranges:
| Object | o[5] (inode) |
Other values |
|---|---|---|
Regular file / symlink in slot k |
k + 1 |
nlink from FS[k] (at least 1) |
Directory DT[k] |
10000 + k |
size 4096; nlink = 2 + number of child directories; o[9] = 1 |
/dev device |
20000 + dev_id |
mode 0020666, nlink 1 |
/mnt/usb file fi |
21000 + fi |
mode 0100644, size from ufs[fi].size |
Built-in program (jc_find_prog) whose directory is /bin, /sbin, /usr/bin or /usr/sbin (relative names are made absolute with the current directory first) |
22000 |
mode 0100755, size = image length. Before v10.07 any bare name matched, so stat("vim") succeeded in every directory and nvxbox mkdir vim said "File exists" |
k_readdir_at reports type 4 for directories, 10 for symlinks and 8 for files. k_readdir_extra then appends:
- hard links;
- the built-in
JS_BINprograms in/bin; initin/sbin;- USB files in
/mnt/usb; - the fixed device names in
/dev(type 2).
Account files
ux_boot_layout(), run by ux_core_start, builds the default tree and seeds /etc if the files are absent (ux_mkfile returns an existing slot unchanged):
/etc /usr/bin /var/log /sbin /root /home /bin /dev /mnt/usb /tmp (041777)
/sys/{kernel,devices,class,power} /proc
/etc/passwd root:x:0:0:root:/root:/bin/nsh
user:x:1000:1000:user:/home:/bin/nsh
/etc/group root:x:0:
admin:x:10:user
users:x:1000:user
/etc/hostname noovex (copied into g_hostname)
/etc/rc # /etc/rc - run by init (PID 1) at boot ...
/etc/passwdhas seven colon-separated fields.ux_pw_fieldreads them,ux_login_useruses fields 0 (name), 2 (uid), 3 (gid) and 5 (home), and setsg_uid,g_gid,g_dcwd,HOMEandUSER.ux_ensure_account(name)renames uid 1000 to the setup account name in both/etc/passwd(field 0) and/etc/groupmember lists, usingux_rewrite_names./etc/shadow(mode0100600) is written byux_save_localpwas one line,user:<hash>.<hash>is the 32-bit FNV-1a of the password in lowercase hex, or*when there is no password.ux_load_localpwsetsg_has_pwif the second field is not empty or*.
/proc, /sys and synthetic /dev text
proc_gen(p, out, cap) returns generated text for each path below. ux_read_file consults it before the file table, which is how the shell reads these files.
| Path | Content |
|---|---|
/proc/cpuinfo |
Fixed text |
/proc/version |
Fixed text |
/proc/hostname |
g_hostname |
/proc/uptime |
g_ticks/100 seconds |
/proc/meminfo |
Total from g_ram_mb; free and available = 3/4 of total |
/proc/filesystems |
Fixed list |
/proc/loadavg |
Process count field = used FS slots |
/proc/mounts |
nvxfs /, proc, sysfs, plus vfat /mnt/usb when mounted |
/proc/self, /proc/self/status |
Name nsh, uid, gid |
/dev/null |
Empty |
/dev/zero |
Up to 4096 zero bytes |
/dev/random, /dev/urandom |
64 hex characters from an xorshift seeded by g_ticks |
/dev/tty, /dev/console |
console |
/sys/kernel/ostype |
NoovexOS |
/sys/kernel/osrelease |
8.96 |
/sys/kernel/hostname |
g_hostname |
/sys/kernel/arch |
x86_64 |
/sys/devices/system/cpu/online |
0 or 0-N (from g_cores_online when SMP is on) |
/sys/class/net/eth0/operstate |
down |
/sys/power/state |
mem disk |
ux_statfs reports a RAM-based view: f_type 0x4e5658, 4 KiB blocks, g_ram_mb*256 blocks, FS_N inodes, f_namelen 255. The file explorer and Settings use n4_usage for real disk numbers.
7.4 The VFS layer (fs/vfs/vfs.c)
struct vfs_fs_ops {
const char* name;
int (*match)(const char* path);
int (*read)(const char* path,char* buf,int off,int len);
int (*size)(const char* path);
};
#define VFS_MAXFS 8
vfs_register appends to g_vfs[], and vfs_lookup returns the first back end whose match accepts the path. vfs_init() registers PROCFS. ux_mount registers USBFS the first time /mnt/usb is mounted. Registered back ends are read-only.
procfs (procfs_build) is the descriptor-level /proc. It matches /proc and /proc/... and serves:
meminfouptime(in ticks)version(NoovexOS 64 v8.95 ...)cpuinfo(one block per onlineg_cpu[], plus the scheduling mode)fb(width, height, 32, pitch)mounts(registered back-end names plusrootfs)- an empty leaf, which lists the entries above.
The output is built into the 4 KiB g_procbuf on every read. This set differs from proc_gen, which serves the shell's ux_read_file path.
vfs_open(nm, flags)
- If a registered back end matches, allocate a
vfilewithdev = 100and copy the path. - Otherwise, if
dev_id(nm)is non-zero, open a device.g_fb0_openandg_mouse_opencount users of/dev/fb0and the mouse. - Otherwise resolve with
ux_lookup(..., follow=1). Directories are refused here; directory descriptors are opened elsewhere. O_CREAT|O_EXCL(0x40|0x80) fails if the file exists.- If the file is missing and
O_CREATis set,k_splitthe path, requireperm_dir(parent), take the first free slot, and set mode0100000 | (0666 & ~g_umask)with ownerg_uid. - For an existing file: reading (access mode not 1) needs
perm_file(k, 4). Writing orO_TRUNCneedsperm_file(k, 2). O_TRUNCon a non-empty file tofs_truncate_to(k, 0). Create and truncate both setg_fs_dirty.- The starting offset is
lenforO_APPEND, otherwise 0. Descriptors 0-2 are not handed out when no process is current (g_jcur < 0).O_CLOEXEC(0x80000) is recorded infdce().
Flag values: O_RDONLY 0, O_WRONLY 1, O_RDWR 2, O_CREAT 0x40, O_EXCL 0x80, O_TRUNC 0x200, O_APPEND 0x400, O_CLOEXEC 0x80000. The Linux layer (sys/linux/linux.c) also decodes O_DIRECTORY (0x10000).
vfs_read / vfs_write
- Reads from a regular file clamp to
len - off, callfs_ensure, and copy the bytes out. - For writes:
O_APPENDmoves the offset to the end first.- The write is clamped to
FS_CAP-1. fs_wr_range(k, min(off, len), off+n)marks only the touched range dirty.- A gap past the old end is zero-filled.
lengrows,data[len] = 0,ux_touch(k, 1), andg_fs_dirty[k] = 1.
- Writes never commit to disk; that happens on close, on
fsyncorsync, or at the next commit.
| Device | Read | Write |
|---|---|---|
| null (1) | EOF | accepted and discarded |
| zero (2) | zeros | accepted and discarded |
| random (3) | bytes from the LCG __dev_rng*1664525+1013904223 |
accepted and discarded |
| full (4) | zeros | -1 (device full) |
| tty (5) | jc_tty_read while a process runs, else 0 |
jc_tty_out while a process runs, else the g_r3out buffer |
| fb0 (9) | raw SCENE pixels, W*H*4 bytes |
writes pixels and sets g_fb0_dirty. vfs_lseek supports SEEK_END = the frame size. |
| mouse (10) | 3-byte PS/2-style packets from the g_mq ring that ux_mouse_push fills. Returns JS_BLOCK when empty. |
- |
| dsp (12) | - | snd_dev_write |
| kmsg (13) | - | split into printable lines to klog |
vfs_lseek clamps negative results to 0 and refuses offsets above FS_CAP-1. vfs_close drops the descriptor and calls vf_unref, which runs pipe cleanup, writeback scheduling and orphan release when the reference count reaches zero. Closing descriptors 0-2 is refused when no process is current (g_jcur < 0). vfs_dup shares the vfile and increments ref. vfs_fstat returns the file length.
7.5 Locating the data area (fs/nvx/nvxfs.c)
disk_init() (called from kmain after fs_init) calls nvx_locate(), which picks the disk interface and the base LBA g_nvx_base. The legacy ATA driver is used when ata_detect finds a drive. Otherwise the generic disk is used: gdisk_* for AHCI or NVMe, with g_nvx_generic = 1 and partition LBAs scaled by dk_ratio() on 4 KiB-sector drives.
read sector 0 (MBR)
+- valid 55AA and an entry with type 0x7F (NVX_PTYPE) and size >= 2048
| -> base = its start, secs = its size "NoovexOS data partition" (2)
+- any other non-empty partition entry "Disk belongs to another system - not touched" (0)
+- sector 0 non-zero and no 55AA "Unrecognised disk - not touched" (0)
+- otherwise: scratch disk, base = 2048, secs = total-2048
read LBA 2048:
first u32 = NVX2 / NVX3 / NVX4 magic "Scratch disk" (2)
all zero "Blank scratch disk" (1)
anything else "Unrecognised data at LBA 2048 - not touched" (0)
When nvx_locate returns 0, disk_init tries nk_root_try() (NTFS/FAT root, 7.8). Otherwise it reads the first sector at base and dispatches on its magic:
| First u32 at base | Format | Action |
|---|---|---|
0x3458564E ("NVX4") |
NVX4 | n4_mount(g_nvx_secs). On failure the disk is left alone (disk_ok = 0) and the note is g_n4_err. |
0x3358564E ("NVX3") |
NVX3 (v8.13-v8.90) | nvx3_try_load(), then dt_init(), then convert with n4_format. |
0x4E565849 (NVX_MAGIC) |
NVX2 (before v8.13) | nvx_mount_old(), read each file, then convert. |
zero, and the location said "blank" (how == 1) |
- | dt_init() then n4_format ("Formatted as NVX4"). |
| other | - | "Unrecognised data - not touched". |
The legacy readers nvx_rd/nvx_rdn return -1 whenever g_rootfs is set, so the old NVXFS code cannot touch an NTFS/FAT root.
7.6 The NVX4 on-disk format (fs/nvx/nvx4.c)
Geometry
| Constant | Value |
|---|---|
N4_MAGIC |
0x3458564E, bytes 4E 56 58 34 = "NVX4" |
N4_MMAGIC |
0x544D344E, "N4MT", the metadata header magic |
N4_BS / N4_SPB |
4096-byte blocks = 8 sectors |
N4_INL |
17 extents in the inode |
N4_INDN |
512 extents in one indirect block (512 x 8 bytes = 4096) |
N4_MAXEXT |
529 extents per file |
N4_MAXBLK |
16 Mi blocks = 64 GiB cap (n4_layout clamps nblocks) |
N4_METAB |
ceil(sizeof(n4meta)/4096) = ceil(344128/4096) = 85 blocks |
Block numbers are relative to g_nvx_base: lba = g_nvx_base + blk*8 (n4_rd/n4_wr). With the fixed table sizes (FS_N 1024, DN_MAX 512, NLINKS 256) the layout is:
block 0 superblock (first 512 bytes; rest of block unused)
blocks 1..3 unused
blocks 4..88 metadata copy A (meta_a = 4, 85 blocks)
blocks 89..173 metadata copy B (meta_b = 4+N4_METAB)
blocks 174.. data (data0 = 4+2*N4_METAB) ... nblocks-1
n4_layout refuses a partition smaller than data0 + 64 blocks ("partition too small").
Superblock n4super (512 bytes, at LBA g_nvx_base)
| Offset | Type | Field | Value |
|---|---|---|---|
| 0 | u32 | magic |
N4_MAGIC |
| 4 | u32 | version |
4 |
| 8 | u32 | bsize |
4096 |
| 12 | u32 | nblocks |
partition blocks (at most 16 Mi) |
| 16 | u32 | metab |
N4_METAB (85) |
| 20 | u32 | meta_a |
4 |
| 24 | u32 | meta_b |
89 |
| 28 | u32 | data0 |
174 |
| 32 | u64 | created |
seconds since epoch at format |
| 40 | u32 | inodes |
FS_N (1024) |
| 44 | u32 | dirs |
DN_MAX (512) |
| 48 | u32 | links |
NLINKS (256) |
| 52 | u32[115] | pad |
0 |
Metadata image n4meta (344128 bytes, one per copy)
typedef struct { n4hdr h; n4ino ino[FS_N]; n4dir dir[DN_MAX]; n4lnk lk[NLINKS]; } n4meta;
| Offset in image | Contents | Size |
|---|---|---|
| 0 | n4hdr |
64 |
| 64 | n4ino ino[1024] |
262144 |
| 262208 | n4dir dir[512] |
65536 |
| 327744 | n4lnk lk[256] |
16384 |
| 344128 | end; zero padding to 85 x 4096 = 348160 |
Array index = FS/DT/g_lk slot index, so slot numbers (and therefore inode numbers) stay the same across reboots. Every struct size is checked with _Static_assert.
n4hdr (64 bytes)
| Off | Type | Field |
|---|---|---|
| 0 | u32 | magic = N4_MMAGIC |
| 4 | u32 | version = 4 |
| 8 | u64 | gen, generation, incremented on every commit |
| 16 | u32 | crc, CRC-32 (poly 0xEDB88320) of the whole n4meta, computed with this field set to 0 |
| 20 | u32 | size = sizeof(n4meta) |
| 24 | u32 | ninode = 1024 |
| 28 | u32 | ndir = 512 |
| 32 | u32 | nlink = 256 |
| 36 | u32 | bsize = 4096 |
| 40 | u32[6] | pad |
n4ino (256 bytes)
| Off | Type | Field |
|---|---|---|
| 0 | u32 | used |
| 4 | u32 | dir (parent DT index) |
| 8 | u32 | mode (default written: 0100644) |
| 12 / 16 | u32 | uid / gid |
| 20 | u32 | nlink |
| 24 / 28 / 32 | u32 | mtime / ctime / atime |
| 36 | u32 | folder |
| 40 | u64 | len, clamped on write to the allocated blocks x 4096 |
| 48 | u32 | nex, number of extents in use |
| 52 | u32 | ind, indirect block number (0 when nex <= 17) |
| 56 | n4ext[17] |
{u32 start, count} inline extents |
| 192 | char[64] | name |
n4dir (128 bytes): name[64] @0, int parent @64, used @68, mode @72 (default 0040755), uid @76, gid @80, mtime @84, ctime @88, atime @92, pad[8] @96.
n4lnk (64 bytes): used @0, dir @4, ino @8, name[52] @12.
Indirect block: 4096 bytes holding n4ext[512], the extents numbered 17..528 of one file. It is rewritten to a newly allocated block whenever the extent list changes (g_n4indd[k]), and the old block goes on the pending-free list.
In-memory state
| Variable | Role |
|---|---|
g_n4_bm |
Free-block bitmap, one bit per block, never stored on disk |
g_n4_free, g_n4_hint |
Free count and the next-fit allocation cursor |
g_n4x[k][17], g_n4nx[k] |
Inline extents and extent count per file |
g_n4xx[k] |
Heap array of 512 indirect extents (allocated on demand) |
g_n4ind[k], g_n4indd[k] |
Current indirect block and its "needs rewrite" flag |
g_n4_img, g_n4_shadow |
Metadata image being built, and the last committed image |
g_n4_stale[2] |
Copy A/B must be rewritten in full |
g_n4pf, g_n4pfn |
Pending frees (n4ext list) |
g_n4_gen |
Generation of the last committed image |
Allocation and extents
n4_alloc(want, hint, &got)takes the run starting athintif that block is free. Otherwise it callsn4_find_free, which scans from the cursor to the end and then wraps todata0, skipping full bytes (0xFF) 8 blocks at a time. It then extends the run while blocks are free, up towant. It may return fewer blocks than asked for.n4_resize(k, nb)grows a file by appending runs. The hint is the block after the last extent, so files stay contiguous when possible.n4_pushmerges a run into the last extent when they are adjacent. If the disk is full or the file has more than 529 extents, the new blocks are released immediately (n4_trim(k, cur, 1)), withg_n4_err = "disk full".- Shrinking (
n4_trim(k, keep, 0)) moves the cut blocks onto the pending-free list instead of freeing them. n4_map(k, fb, &run)turns a file block into a disk block plus the length of the contiguous run.
Writing a file: n4_sync_file(k)
- Skip if
g_n4is off, the slot is unused, or the file is an orphan. Files under/tmpdrop all their blocks. nb = ceil(len/4096). Alazyfile (never loaded) can only shrink; nothing is read back.- Otherwise, unless the file is clean with the right block count and no indirect change pending:
n4_resize(k, nb);- the blocks to write are
[dlo/4096, ceil(dhi/4096)), widened to include every newly added block (cur < nb); n4_write_rangewrites runs of up to 256 blocks with multi-sector commands (gdisk_writen/ata_writen). The partial last block is padded with zeros throughg_n4blk.- Mark the file
clean,dlo = dhi = 0.
- If
g_n4indd[k]is set: allocate a new block for the indirect extents, write it, and put the old one on the pending list. If the file no longer needs an indirect block, the old one is queued andind = 0.
The commit protocol: n4_commit()
n4_commit()
|
+- 1. for every used, non-orphan file: n4_sync_file(k)
| data blocks + new indirect blocks -> only blocks that are
| FREE in the last committed state (old blocks are still "used")
+- n4_barrier() (gdisk_flush; no-op on legacy ATA)
|
+- 2. n4_serialize() -> g_n4_img: header gen = g_n4_gen+1, CRC-32
|
+- 3. n4_write_meta(0)
| copy A: write only 4 KiB blocks that differ from g_n4_shadow
| (runs of <=64 blocks), or all if stale[0]
| n4_barrier() stale[0]=0
| copy B: same comparison against the shadow, or all if stale[1]
| n4_barrier() stale[1]=0
| (any write error -> stale[c]=1, "metadata write failed", abort)
|
+- 4. shadow := img ; g_n4_gen++
|
+- 5. release pending frees: n4_mark(start,count,0) for each
Power-cut reasoning, from the code:
- Until step 3 finishes copy A, both on-disk copies still describe the old state. None of the blocks they reference were overwritten, because freed blocks are only released in step 5, after both copies hold the new generation.
- If the cut happens while copy A is partly written, A's CRC fails and B (old generation) is used.
- If it happens after A and during B, A is valid with the higher generation and wins.
n4_mount then marks the losing copy stale, so the next commit rewrites it in full.
The metadata write is a delta: n4_write_meta compares each 4 KiB block of the new image with g_n4_shadow (64-bit word compare). The header block always changes because of gen and crc.
Callers of the commit: nvx3_sync (after one file), nvx3_flush_meta and nvx3_sync_all (background writeback, sync).
Mounting: n4_mount(secs)
- Read the superblock at
g_nvx_base. Reject it ("unsupported NVX4 layout") unless magic, version,bsize,metab,inodes,dirsandlinksall match the compiled constants. Reject it ("does not fit the partition") ifnblocks*8 > secs,nblocks > N4_MAXBLK,meta_a/meta_b/data0differ from the fixed layout, ornblocks < data0+64. n4_alloc_state(): allocate the image, the shadow and the bitmap, zero the bitmap, and mark blocks0..data0-1used.- Read both copies (85 blocks each) and validate each with
n4_valid_meta: header fields plus the CRC withcrczeroed during the computation.- Neither valid to "both metadata copies are damaged", mount fails.
- Only B valid, or B newer (
gb > ga) to use B and setstale[0] = 1. - Otherwise use A, and set
stale[1]unless B is valid with the same generation.
- Clear all
FSslots and fill them from the inodes. For each file:nex <= 529is checked;- the indirect block (if any) is checked to lie in
[data0, nblocks)and is read; - every extent and the indirect block are claimed in the bitmap with
n4_claim, which fails on any overlap or out-of-range run. On failure the claims made for that file are rolled back and the file is emptied (len = 0), so it is counted as damaged rather than failing the mount.
lenis clamped to the allocated blocks x 4096 and toFS_CAP-1, andlazy = len > 0,clean = 1. No file data is read at mount.- Load
DT[]. A directory whose parent is invalid, unused or itself is re-parented to/. Loadg_lk[], dropping links to unused slots. Files with an invaliddirgo to/. g_n4 = 1,dt_load_done(). If any files were damaged,g_n4_err = "some damaged files were emptied"and aklogline is written.
Because the bitmap is rebuilt from the extents on every mount, blocks leaked by a crash (written but never committed) come back automatically.
Formatting: n4_format(secs)
if(n4_layout(secs))return -1; n4_alloc_state(); ... n4_apply_res(1);
g_n4=1; g_n4_gen=0;
for each used, non-orphan FS[k]: fs_ensure(k); mark fully dirty; n4_sync_file(k);
n4_barrier(); n4_serialize(); g_n4_stale[0]=g_n4_stale[1]=1;
n4_write_meta(1); /* both copies, in full, gen = 1 */
shadow = img; g_n4_gen=1;
n4_write_super(k_realtime_ms()/1000); /* sector 0 of the partition - LAST */
n4_apply_res(0); g_n4_nres=0; g_n4pfn=0;
Whatever is in FS[] at that point is written out: the default files from fs_init, or the files just read from an old disk. n4_write_super writes one sector and then flushes. n4_usage reports (nblocks - data0)*4096 total bytes and g_n4_free*4096 free bytes.
7.7 Old formats and conversion
NVX2 (NVX_MAGIC 0x4E565849, nvx_mount_old)
typedef struct { char name[64]; u32 start,len,folder,dir,mode,uid,gid; } nvx_ent; /* 92 B */
typedef struct { char name[32]; int parent, used; unsigned int mode; } nvx_dnode; /* 44 B */
typedef struct { u32 magic,count; nvx_ent e[NVX_MAX]; u32 dcount; nvx_dnode d[NVX_DN_MAX]; } nvxfs_t;
The directory lives at NVX_DIR_LBA = g_nvx_base. It holds 24 files (NVX_MAX), each with an absolute starting LBA in start, and up to 64 directory nodes with 31-character names. disk_init reads each file (through a 2 MiB static buffer) into a free slot. Entries starting before base+8 are skipped. Each file's sector range is recorded with n4_reserve_secs.
NVX3 (0x3358564E, nvx3_try_load in unixfs.c)
typedef struct { u32 used, dir, mode, uid, gid, nlink, len, mtime, ctime, atime, folder; char name[64]; } nvx3_ino;
typedef struct { u32 used, dir, ino; char name[52]; } nvx3_link;
typedef struct { u32 magic, version, ninode, nlinks, ndn; nvx3_ino ino[24]; nvx3_link lk[64]; nvx_dnode d[64]; } nvx3_t;
Each of the 24 inodes has a fixed 2 MiB slot at nvx3_slot(k) = NVX_DATA0 + (k+1)*4096 sectors (NVX_DATA0 = base+8). The loader:
- requires
ninode == 24andnlinks == 64; - loads each file into
FS[k]with the same index; - copies links into
g_lk; - copies the directory nodes into
g_nvx.dsodt_inittodt_load_from_diskcan rebuildDT[]; - reserves every data range.
Crash-safe conversion
static u32 g_n4_res[64][2]; static int g_n4_nres;
static void n4_reserve_secs(u32 sec,u32 n){ ... g_n4_res[i] = {sec/8, blocks covering [sec, sec+n)} ... }
static void n4_apply_res(int set){ /* clamp each range to [data0, nblocks) and mark used/free */ }
n4_format marks the reserved ranges as used before writing anything, so new file data can only go into blocks the old format did not use. The NVX4 metadata copies occupy blocks 4..173 (sectors 32..1391 of the partition). The NVX3 header (about 9.5 KB at sectors 0-18) and NVX3 data (from sector 4104) lie outside that range. The new superblock is written into sector 0 last, replacing the old magic.
If power fails at any earlier point, sector 0 still carries the NVX2/NVX3 magic and the next boot converts again. If the format fails, disk_init reports "Old disk could not be converted - left unchanged" and leaves disk_ok = 0. Reservations are clamped to data0 and above, so they protect old data only in the data area.
7.8 NTFS and FAT root file systems (fs/ntfs/ntfsk.c)
When no NoovexFS location is found, the system can run from an NTFS, FAT32 or FAT16 volume. Every back end is reached through one table:
typedef struct { const char* name;
int (*mount)(nvxblk_rd,nvxblk_wr,unsigned long long base,unsigned long long nsect,int ro);
void (*umount)(void);
int (*exists)(const char*); int (*mkdir)(const char*);
long (*write)(const char*,const char*,long); long (*read)(const char*,char*,long);
int (*del)(const char*); int (*ls)(const char*,nvxntfs_lscb,void*);
int (*format)(nvxblk_rd,nvxblk_wr,unsigned long long,unsigned long long,const char*); } rfs_ops;
static const rfs_ops g_rfs_ntfs = {"NTFS", nvxntfs_mount, ..., nvxntfs_format};
static const rfs_ops g_rfs_fat32 = {"FAT32", vf_mount, ..., vf_fmt32};
static const rfs_ops g_rfs_fat16 = {"FAT16", vf_mount, ..., vf_fmt16};
Sector I/O.
nk_rd/nk_wrprovide 512-byte sector callbacks overgdisk_read/gdisk_write. On 4 KiB-sector drives (dk_ratio() > 1) they read-modify-write the containing 4 KiB sector and callbc_invalidate.Finding volumes.
nk_scanprobes, in order:- the whole disk;
- the four MBR entries;
- when an entry has type
0xEE, up to 128 GPT entries (header "EFI " at LBA 1, entry LBA at offset 72, count at 80, entry size at 84, first and last LBA at entry offsets 32 and 40).
nk_probeneeds the55AAsignature, then recognises"NTFS "at byte 3 (returns 1), or a FAT BPB: 512 bytes/sector, a power-of-two cluster size, FAT count non-zero, reserved sectors non-zero (returns 2).nk_find, used by thentfscommand, looks only for NTFS: MBR types0x07/0x17/0x27, or the GPT Microsoft basic-data GUID.Taking over a root (
nk_root_try). Each candidate is mounted read-only (type 1 with the NTFS ops, type 2 with the FAT ops;vf_mountdecides FAT16 or FAT32 from the cluster count). It then reads/.noovexos.- If the marker is missing, the volume is unmounted and logged as "left untouched", so a Windows disk is never written.
- If it is present, the volume is remounted read-write, and the system sets
g_rfs = o, runsnk_load_tree()andnk_meta_apply(), and setsg_rootfs = 1.
Lazy tree load (
nk_load_tree). A breadth-first walk withg_rfs->lscreatesDTnodes withdt_mkdir_atand fillsFSslots with name, size,lazy = 1andclean = 1. Mode defaults to0100644and owner to root.- Names beginning with
$,.noovexos,.noovexmetaandSystem Volume Informationare skipped. - Files larger than
FS_CAP-1are skipped. - Contents are read on demand by
nk_root_read, which fills any short read with zeros.
- Names beginning with
Permissions file
/.noovexmeta. Each line is<D|F> <mode octal> <uid> <gid> <absolute path>, for every directory except/and every non-orphan file, in a 128 KiB buffer.nk_meta_writerewrites the whole file after every save, flush and tree save.nk_meta_applyapplies it at mount.
Saving:
nk_root_sync_file(k): when the file is clean and its path is in the known-files listg_nk_kf, only the meta file is rewritten. Otherwise the parent directories are created (nk_mkdirs), the whole file is written withg_rfs->write, and it is marked clean.nk_root_flush(): deletes on the volume every known path that no longer matches a liveFSslot (this is how deletes and renames reach the disk), then rewrites the meta file.nk_root_dt_save(): creates the directories, then removes known directories that no longer resolve, longest path first (up to 8 per call).nvx3_sync,nvx3_flush_meta,nvx3_sync_allanddt_saveall route here whileg_rootfsis set.
Installing (
nk_install, used bycore/init/installer.c): format with the chosen ops (volume labelNOOVEXOS), mount, write/.noovexos="NoovexOS\n", create every directory, copy every file (reporting progress), write the meta file, then unmount. The installer sets the data partition's MBR type to0x07(NTFS),0x0C(FAT32),0x0E(FAT16) or0x7F(NoovexFS).
7.9 FAT drivers
fs/fat/vfat.c, read/write FAT16/FAT32 on sector callbacks
- State. The state is
g_vfat:- geometry:
spc,rsvd,nfats,fatsz,root_lba,data_lba,clusters,rootclus,fsinfo,cbytes; - the whole FAT in memory (
fat), up to 16 MiB, otherwise mount returns -3; - one dirty flag per FAT sector (
fdirty); - a one-cluster buffer;
- a one-sector directory cache (
g_vfat_sec/g_vfat_secno).
- geometry:
- Mount. Mount requires 512-byte sectors, a power-of-two
spcand a volume no bigger than the partition. The type is FAT16 when there are fewer than 65525 clusters. Fewer than 4085 clusters is refused (-2). The cluster count is clamped to what the FAT can index. The free count is computed by scanning the FAT. On FAT32 the FSInfo (0x41615252) next-free hint is used. - Flush.
vf_flushwrites each dirty FAT sector to every FAT copy, and on FAT32 updates FSInfo's free count (offset 488) and next-free (492). - Directories.
vf_nextwalks entries and assembles long file names. Each LFN entry holds 13 UTF-16 characters at offsets{1,3,5,7,9,14,16,18,20,22,24,28,30}. An LFN is accepted only when its checksum matchesvf_cksumof the 8.3 name. Otherwise the short name is used, honouring the NT lowercase flags in byte 12 (0x08base,0x10extension).vf_walkresolves paths case-insensitively. - Creating entries.
vf_mkentstores a name as a pure 8.3 entry whenvf_make83accepts it. Otherwise it writes LFN entries plus a~Nalias fromvf_alias.vf_slotfinds free directory slots and grows a subdirectory by a zeroed cluster when needed (the FAT16 root cannot grow). - Writes.
vf_writealways writes a new cluster chain (vf_writechain), then points the entry at it and frees the old chain.vf_mkdirwrites.and...vf_deleterefuses non-empty directories. - Format (
vf_format):- cluster sizes from the Microsoft tables;
- FAT16 needs at least 8400 sectors and at most 4194304 (2 GiB); FAT32 needs at least 66600 sectors;
- 32 reserved sectors on FAT32, 1 on FAT16; 2 FATs; 512 root entries on FAT16;
- the FAT size is iterated until the clusters fit;
- on FAT32 a backup boot sector goes in sector 6 and FSInfo in sectors 1 and 7;
- a volume-label entry is written in the root.
fs/fat/fat_ata.c, boot-media reader
A minimal read-only FAT16/FAT32 reader on the legacy ATA drive (ata_read). It handles an MBR partition (types 0x01,0x04,0x06,0x0B,0x0C,0x0E) or a superfloppy whose sector 0 starts with a jump. It supports only the root directory and 8.3 names (fat_name83):
fat_listdir(cb)backs the shell'sfatls;fat_read_file(name, out, cap)backsfatcat, theshfallback andpkgloading.
USB mass storage at /mnt/usb
The USB FAT driver lives in drivers/usb/xhci.c. ufat_mount reads the first MBR entry or a superfloppy and caches up to 32 root-directory entries in ufs[32] (name[16], size, clus). ufat_read follows the cluster chain.
On the file-system side (unixfs.c):
ux_mount("...", "/mnt/usb")is root only (syscall 168 and Linuxmount, number 165). It initialises the device if needed (msd_autoinit,ufat_mount), creates/mnt/usb, and registers the read-onlyUSBFSback end once.ux_usb_idxmatches/mnt/usb/<name>case-insensitively.usbfs_loadcaches one whole file (up to 1 MiB) ing_usbbuf.ux_umountclearsg_usb_mounted.- Files appear in
readdirthroughk_readdir_extra, and instatwith inode21000+fi.
7.10 Packages (fs/pkg/pkg.c)
pkg_install(file, n, extract) (shell command in shell/commands.c) works from an 8 MiB raw buffer (g_pkg_raw) and an 8 MiB inflate buffer (g_pkg_inf).
pkg_open recognises three kinds of package:
.deb(!<arch>header, kind 1):ar_findwalks the 60-bytearheaders (size is the decimal at bytes 48..57, bodies are 2-byte aligned).- From
control.tar*(gunzipped when it starts1F 8B),./controlsuppliesPackage,Version,ArchitectureandDescription. data.tar*becomes the payload.
- From
- Alpine
.apk(gzip, kind 2): the whole file is gunzipped..PKGINFOsuppliespkgname,pkgver,archandpkgdesc. - Android/ZIP
.apk(PK\3\4, kind 3): recognised, but not extracted.
xz and zstd payloads are passed through unchanged and are not unpacked.
tar_walk handles ustar headers: name at 0 (plus the 155-byte prefix at 345 for ustar), octal mode at 100, octal size at 124, type at 156. It visits regular files (0, NUL, 7) and directories (5).
With extract:
/pkgand then/pkg/<name>are created (unnamedwhen there is no name).pkg_extract_cbstrips./, creates every intermediate directory with mode0040755, and stores each file in a freeFSslot. The mode is0100000 | (mode & 0777)(0644when zero) and the owner isg_uid.- Each file is synced, then
dt_save()is called.
Without extract, only the file count and total size (g_pkg_nfiles, g_pkg_total) are computed.
