SchOS/tools/mkfs.c
Nexoniarz a1c965da1e include: one folder per subsystem, and includes that name it
Sixty-five headers sat side by side in include/, so `#include "irq.h"`
said nothing about where irq.h was or what it belonged to, and errno.h,
stat.h and fcntl.h at the top shadowed the system's for any host tool
built with -I./include. They are in folders now -- arch/ core/ fs/
dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and
every include names the folder, as Linux's do: "core/proc.h".

Done mechanically and checked the hard way: the kernel built from this
tree disassembles identically to the one built from the tree before
it. The full image boots, and userland is the same set of programs.
Paths in docs and comments follow; docs/guides/layout.md has a table
of what is where, and tools/fix-includes.py rewrites old-style
includes from the folders in the tree, for code written before this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 15:05:12 +02:00

1252 lines
40 KiB
C

// SCHFS mkfs -- disk image builder.
//
// Aligned with fs.c improvements:
// - Writes atime/mtime/ctime timestamps into every inode.
// - Computes and writes CRC32c checksum into every inode and the superblock,
// so fsinit() and ilock() pass their validation checks on first boot.
// - OVERFLOW_DIR_START / OVERFLOW_DIR_END replace all magic "113 * BSIZE"
// literals; both loops in iappend_init_dir are unified into one helper.
// - alloc_bmap gains a T_EXTENT path that writes proper struct fsv1_extent
// entries (inline extents only; mkfs files are small).
// - ialloc writes a proper struct dinode including the new fields so the
// on-disk image is byte-compatible with what the kernel reads.
// - updatesb writes a CRC32c checksum of the superblock.
// - check_disk_space prints a helpful message before dying.
// - Dead code (cal_block_index_for_dinode, test_xpr) removed.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>
#include <time.h>
#include <stdint.h>
// Windows opens files in *text* mode by default, which turns every 0x0A
// written into 0x0D 0x0A. A filesystem image written that way is corrupt in a
// way that is invisible until the kernel tries to mount it, so every open()
// below is explicitly binary. O_BINARY does not exist on Unix, where there is
// no translation to disable.
#ifndef O_BINARY
#define O_BINARY 0
#endif
// bzero/bcopy are pre-POSIX.1-2008 BSD spellings. glibc still declares them,
// mingw-w64 does not, and this file has seventeen calls. They are defined here
// rather than rewritten at each call site on purpose: bcopy takes (src, dst)
// and memmove takes (dst, src), so a mechanical rewrite has seventeen chances
// to silently reverse a copy -- and the result would be a filesystem image
// that builds without complaint and is quietly wrong.
#define bzero(p, n) memset((p), 0, (n))
#define bcopy(src, dst, n) memmove((dst), (src), (n))
#define stat xv6_stat
#define sigset_t xv6_sigset_t
#define dirent schfs_dirent
#include "../include/core/types.h"
#include "../include/core/param.h"
#include "../include/fs/fs.h"
#include "../include/uapi/stat.h"
#undef dirent
#undef stat
#include <dirent.h>
// After the #undef above, so this gets the host's struct stat and not xv6's.
#include <sys/stat.h>
// struct dirent's d_type is a BSD/glibc extension; mingw-w64 has no such
// field. The loops below already handle DT_UNKNOWN by probing with opendir(),
// which is the portable answer and was written for filesystems that decline to
// report a type -- so on Windows every entry simply takes that path.
//
// This block must stay *below* <dirent.h>, not up with the other compatibility
// shims. glibc spells these as enumerators with matching macros, so asking
// #ifndef before the header is read always says "missing" -- and then the
// header's own `enum { DT_UNKNOWN = 0, ... }` expands to `enum { 0 = 0, ... }`
// and the build dies on a syntax error a long way from the cause. The header
// cannot move up either: it has to follow the `#undef dirent` above, or it
// would define the xv6 struct dirent instead of the host's.
#ifndef DT_UNKNOWN
#define DT_UNKNOWN 0
#define DT_DIR 4
#define DT_REG 8
#endif
#ifdef _WIN32
#define DIRENT_TYPE(dp) DT_UNKNOWN
#else
#define DIRENT_TYPE(dp) ((dp)->d_type)
#endif
#ifndef static_assert
#define static_assert(a, b) do { switch (0) case 0: case (a): ; } while (0)
#endif
#ifndef RW
#define RW 3
#endif
#ifndef HASHRANGE
#define HASHRANGE 16384
#endif
// Overflow bucket bounds -- must match fs.c definitions.
#ifndef OVERFLOW_DIR_START
#define OVERFLOW_DIR_START (113 * BSIZE)
#define OVERFLOW_DIR_END (115 * BSIZE)
#endif
// ---------------------------------------------------------------------------
// CRC32c (Castagnoli) -- must stay byte-for-byte identical to the fs.c copy.
// ---------------------------------------------------------------------------
static uint32_t crc32c_table[256];
static void
crc32c_init(void)
{
uint32_t i, j, crc;
for (i = 0; i < 256; i++) {
crc = i;
for (j = 0; j < 8; j++)
crc = (crc >> 1) ^ (crc & 1 ? 0x82F63B78U : 0);
crc32c_table[i] = crc;
}
}
static uint32_t
crc32c(uint32_t crc, const void *buf, uint32_t len)
{
const uint8_t *p = (const uint8_t *)buf;
crc = ~crc;
while (len--)
crc = (crc >> 8) ^ crc32c_table[(crc ^ *p++) & 0xFF];
return ~crc;
}
static uint32_t
inode_checksum(struct dinode *dip)
{
uint32_t saved = dip->checksum;
dip->checksum = 0;
uint32_t ck = crc32c(0, dip, sizeof(*dip));
dip->checksum = saved;
return ck;
}
// ---------------------------------------------------------------------------
// File system geometry
// ---------------------------------------------------------------------------
#define NINODES 12000
// Image size in blocks. Defaults to FSSIZE (the main root image) but is
// overridable with --size, so a small ramdisk root can be built by the same
// tool without recompiling param.h. Anything derived from it is recomputed in
// main() after argument parsing.
int fssize = FSSIZE;
// Inode count, likewise overridable (--ninodes). It is not a free choice on a
// small image: the inode table is preallocated, so 12000 inodes costs 1501
// blocks whether or not anything uses them. On the 150000-block root image that
// is 1% and irrelevant; on the 6000-block ramdisk root it was a quarter of the
// entire image, spent to describe about 260 files.
int ninodes = NINODES;
int nibitmap;
int nbitmap;
int ninodeblocks;
int nlog = LOGSIZE;
int nmeta;
int nblocks;
// Paths to leave out of the image, relative to the sysroot root and without a
// leading slash (e.g. "usr/share/misc"). Matches a whole path component prefix,
// so naming a directory drops its entire subtree.
//
// This exists because the ramdisk root is *embedded in the kernel binary*, which
// makes what goes into it a licensing question and not only a size one: anything
// staged in sysroot/ when that image is built becomes part of a distributable
// artifact. An exclude list enforces that mechanically, rather than relying on
// someone having remembered to unstage something first.
#define MAXEXCLUDE 32
const char *excludes[MAXEXCLUDE];
int nexcludes;
// strlen(sysroot_path) + 1, so a host path can be turned back into the
// sysroot-relative one the exclude patterns are written against.
size_t sysroot_prefix;
// Is this sysroot-relative path excluded? Matches exactly, or as a directory
// prefix so that "usr/share/misc" also drops "usr/share/misc/big.dat".
static int
is_excluded(const char *relpath)
{
for (int i = 0; i < nexcludes; i++) {
size_t n = strlen(excludes[i]);
if (strncmp(relpath, excludes[i], n) == 0 &&
(relpath[n] == '\0' || relpath[n] == '/'))
return 1;
}
return 0;
}
// Shared by both tree walkers: decide whether to descend into a host path, and
// say so when the answer is no.
//
// Checked before the entry is dispatched rather than inside sysroot_add_dir(),
// so an excluded directory is never opened and never gets an inode. An empty
// directory in the image would still cost one and would wrongly imply the
// content was supposed to be there.
static int
sysroot_skip(const char *child)
{
const char *rel = child + sysroot_prefix;
if (!is_excluded(rel))
return 0;
fprintf(stderr, "mkfs: excluding %s\n", rel);
return 1;
}
int fsfd;
struct superblock sb;
char zeroes[BSIZE];
uint freeinode = 1;
uint freeblock;
// ---------------------------------------------------------------------------
// Forward declarations
// ---------------------------------------------------------------------------
void balloc(int);
void image_init(void);
void image_flush(void);
void wsect(uint, void *);
void winode(uint, struct dinode *);
void rinode(uint, struct dinode *);
void rsect(uint, void *);
uint ialloc(ushort type);
void iappend(uint inum, void *p, int n);
void iappend_init_dir(uint inum, struct schfs_dirent *de);
void setibmap(int);
void updatesb(void);
void die(const char *);
void check_disk_space(void);
static uint alloc_bmap(struct dinode *dp, uint fbn);
static void sysroot_add_dir(uint parent_ino, const char *host_path, const char *name);
static void sysroot_add_file(uint parent_ino, const char *host_path, const char *name);
static void sysroot_populate(uint rootino, const char *sysroot_path);
// ---------------------------------------------------------------------------
// Byte-order helpers (little-endian)
// ---------------------------------------------------------------------------
ushort
xshort(ushort x)
{
ushort y;
uchar *a = (uchar *)&y;
a[0] = x;
a[1] = x >> 8;
return y;
}
uint
xint(uint x)
{
uint y;
uchar *a = (uchar *)&y;
a[0] = x;
a[1] = x >> 8;
a[2] = x >> 16;
a[3] = x >> 24;
return y;
}
// ---------------------------------------------------------------------------
// Content-based hard-link dedup
//
// sysroot/bin holds one full, independent copy of the toybox binary per applet
// name (57 of them, ~822 KB each) because this tool understood neither symlinks
// nor hard links -- about 44 MB of identical bytes in every image. Rather than
// teach it symlinks, dedup by *content*: identical files share a single inode
// and the extra directory entries become real hard links (nlink > 1), which the
// on-disk format already supports.
//
// Keyed on (size, CRC32c of contents); a candidate is re-read and compared
// byte-for-byte before its inode is shared, so a checksum collision cannot
// silently merge two different files.
// ---------------------------------------------------------------------------
struct dedup_ent {
dev_t hdev; // host device+inode: when these match,
ino_t hino; // it is literally the same file
off_t size;
uint32_t crc;
uint inum;
char path[512];
};
static struct dedup_ent dedup_tab[512];
static int dedup_n;
// A file the host already told us is the same file. sysroot/bin holds 236
// names for one toybox binary, and ports/toybox/Makefile makes them real hard
// links -- so this answers for all but the first of them without opening
// anything.
//
// This matters more than it looks. Without it, every one of those 236 names
// was read once to checksum it and then *both* files were re-read a byte at a
// time through getc() to confirm the match: about half a gigabyte of I/O and
// 380 million getc calls, which took 45 seconds of every build across WSL's
// filesystem boundary. The content path below is still there, and still the
// only thing that can dedup two files that merely happen to be identical.
static uint
dedup_lookup_ino(dev_t hdev, ino_t hino)
{
if (hino == 0)
return 0; // host gave us nothing usable
for (int i = 0; i < dedup_n; i++)
if (dedup_tab[i].hdev == hdev && dedup_tab[i].hino == hino)
return dedup_tab[i].inum;
return 0;
}
// Returns an existing inum holding identical content, or 0 for "not seen".
static uint
dedup_lookup(const char *host_path, off_t size, uint32_t crc)
{
for (int i = 0; i < dedup_n; i++) {
if (dedup_tab[i].size != size || dedup_tab[i].crc != crc)
continue;
FILE *a = fopen(dedup_tab[i].path, "rb");
FILE *b = fopen(host_path, "rb");
if (!a || !b) { if (a) fclose(a); if (b) fclose(b); continue; }
// Block-at-a-time, not getc-at-a-time: same answer, ~1000x fewer
// trips through stdio for the multi-megabyte files this actually sees.
char abuf[16384], bbuf[16384];
int same = 1;
for (;;) {
size_t na = fread(abuf, 1, sizeof(abuf), a);
size_t nb = fread(bbuf, 1, sizeof(bbuf), b);
if (na != nb || memcmp(abuf, bbuf, na) != 0) { same = 0; break; }
if (na == 0) break;
}
fclose(a); fclose(b);
if (same)
return dedup_tab[i].inum;
}
return 0;
}
static void
dedup_remember(const char *host_path, dev_t hdev, ino_t hino,
off_t size, uint32_t crc, uint inum)
{
if (dedup_n >= (int)(sizeof(dedup_tab) / sizeof(dedup_tab[0])))
return; // table full: stop deduping, stay correct
struct dedup_ent *e = &dedup_tab[dedup_n++];
e->hdev = hdev;
e->hino = hino;
e->size = size;
e->crc = crc;
e->inum = inum;
snprintf(e->path, sizeof(e->path), "%s", host_path);
}
// Bump nlink on an inode that is already written out, for a dedup hit.
//
// winode() writes the struct verbatim -- it does NOT recompute the checksum,
// so every caller must refresh it after touching any field. Missing this here
// shipped images whose deduped inodes carried a stale CRC32c, and the kernel
// panicked with "ilock: inode checksum mismatch" the first time one was opened.
static void
inode_addlink(uint inum)
{
struct dinode din;
rinode(inum, &din);
din.nlink = xshort(xshort(din.nlink) + 1);
din.checksum = inode_checksum(&din);
winode(inum, &din);
}
// ---------------------------------------------------------------------------
// Sysroot tree walker — mirrors sysroot/ into the FS image
// ---------------------------------------------------------------------------
static void
sysroot_add_file(uint parent_ino, const char *host_path, const char *name)
{
int fd = open(host_path, O_RDONLY | O_BINARY);
if (fd < 0) { perror(host_path); exit(1); }
// Ask the host whether this is a file we have already taken in. A hard
// link is the same inode by definition, so this is exact -- and it costs
// one fstat instead of reading the file twice.
struct stat st;
dev_t hdev = 0;
ino_t hino = 0;
uint ino = 0;
if (fstat(fd, &st) == 0 && st.st_nlink > 1) {
hdev = st.st_dev;
hino = st.st_ino;
ino = dedup_lookup_ino(hdev, hino);
}
if (ino) {
close(fd);
inode_addlink(ino);
} else {
// Checksum the contents, so files that are identical without being
// linked can still share one inode.
char buf[BSIZE];
int n;
off_t fsz = 0;
uint32_t crc = 0;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
crc = crc32c(crc, buf, (uint32_t)n);
fsz += n;
}
ino = dedup_lookup(host_path, fsz, crc);
if (ino) {
// Identical content is already in the image: emit another
// directory entry for that same inode -- a real hard link -- and
// write no data.
close(fd);
inode_addlink(ino);
} else {
ino = ialloc(T_FILE);
if (lseek(fd, 0, SEEK_SET) != 0) die(host_path);
while ((n = read(fd, buf, sizeof(buf))) > 0)
iappend(ino, buf, n);
close(fd);
dedup_remember(host_path, hdev, hino, fsz, crc, ino);
}
}
struct schfs_dirent de;
bzero(&de, sizeof(de));
de.inum = xshort(ino);
strncpy(de.name, name, DIRSIZ);
iappend_init_dir(parent_ino, &de);
}
static void
sysroot_add_dir(uint parent_ino, const char *host_path, const char *name)
{
DIR *d = opendir(host_path);
if (!d) { perror(host_path); exit(1); }
uint dir_ino = ialloc(T_DIR);
struct schfs_dirent de;
bzero(&de, sizeof(de));
de.inum = xshort(dir_ino);
strncpy(de.name, name, DIRSIZ);
iappend_init_dir(parent_ino, &de);
bzero(&de, sizeof(de));
de.inum = xshort(dir_ino);
strcpy(de.name, ".");
iappend(dir_ino, &de, sizeof(de));
bzero(&de, sizeof(de));
de.inum = xshort(parent_ino);
strcpy(de.name, "..");
iappend(dir_ino, &de, sizeof(de));
struct dirent *dp;
while ((dp = readdir(d)) != NULL) {
if (dp->d_name[0] == '.')
continue;
char child[4096];
snprintf(child, sizeof(child), "%s/%s", host_path, dp->d_name);
if (sysroot_skip(child))
continue;
unsigned char dtype = DIRENT_TYPE(dp);
if (dtype == DT_UNKNOWN) {
DIR *probe = opendir(child);
if (probe) { closedir(probe); dtype = DT_DIR; }
else dtype = DT_REG;
}
if (dtype == DT_DIR)
sysroot_add_dir(dir_ino, child, dp->d_name);
else if (dtype == DT_REG)
sysroot_add_file(dir_ino, child, dp->d_name);
}
closedir(d);
}
// Merge the contents of sysroot_path/ directly into the root inode.
static void
sysroot_populate(uint rootino, const char *sysroot_path)
{
DIR *d = opendir(sysroot_path);
if (!d) { perror(sysroot_path); exit(1); }
sysroot_prefix = strlen(sysroot_path) + 1;
struct dirent *dp;
while ((dp = readdir(d)) != NULL) {
if (dp->d_name[0] == '.')
continue;
char child[4096];
snprintf(child, sizeof(child), "%s/%s", sysroot_path, dp->d_name);
if (sysroot_skip(child))
continue;
unsigned char dtype = DIRENT_TYPE(dp);
if (dtype == DT_UNKNOWN) {
DIR *probe = opendir(child);
if (probe) { closedir(probe); dtype = DT_DIR; }
else dtype = DT_REG;
}
if (dtype == DT_DIR)
sysroot_add_dir(rootino, child, dp->d_name);
else if (dtype == DT_REG)
sysroot_add_file(rootino, child, dp->d_name);
}
closedir(d);
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
int
main(int argc, char *argv[])
{
uint rootino;
struct schfs_dirent de;
static_assert(sizeof(int) == 4, "Integers must be 4 bytes!");
if (argc < 2) {
fprintf(stderr,
"Usage: mkfs fs.img [options] --sysroot <path>\n"
" mkfs fs.img [options] file...\n"
"\n"
" --size <blocks> image size (default FSSIZE = %d)\n"
" --ninodes <n> inode table size (default %d)\n"
" --exclude <path> omit a sysroot-relative path and its subtree;\n"
" may be given more than once\n",
FSSIZE, NINODES);
exit(1);
}
// Options let one tool build both the main root image and the much smaller
// ramdisk root without recompiling param.h.
//
// Parsed by compacting argv in place, because the two modes below still
// identify themselves positionally: argv[2] is either "--sysroot" or the
// first file of the legacy flat list. The previous version of this loop
// handled exactly one option and then `break`, which silently ignored a
// second one -- fine while --size was the only flag, a trap now.
{
int out = 2;
for (int i = 2; i < argc; i++) {
int has_arg = (i + 1 < argc);
if (has_arg && strcmp(argv[i], "--size") == 0) {
fssize = atoi(argv[++i]);
if (fssize < 64) {
fprintf(stderr, "mkfs: --size %d is too small\n", fssize);
exit(1);
}
} else if (has_arg && strcmp(argv[i], "--ninodes") == 0) {
ninodes = atoi(argv[++i]);
if (ninodes < 16) {
fprintf(stderr, "mkfs: --ninodes %d is too small\n", ninodes);
exit(1);
}
} else if (has_arg && strcmp(argv[i], "--exclude") == 0) {
if (nexcludes >= MAXEXCLUDE) {
fprintf(stderr, "mkfs: more than %d --exclude paths\n", MAXEXCLUDE);
exit(1);
}
// Tolerate a leading slash, since "/usr/share/misc" is the
// natural way to write the path even though the patterns are
// matched relative to the sysroot.
const char *p = argv[++i];
excludes[nexcludes++] = (*p == '/') ? p + 1 : p;
} else {
argv[out++] = argv[i];
}
}
argc = out;
}
// Geometry must be computed now, not at static-initialiser time: it depends
// on --size and --ninodes.
nbitmap = fssize / (BSIZE * 8) + 1;
nibitmap = ninodes / (BSIZE * 8) + 1;
ninodeblocks = ninodes / IPB + 1;
assert((BSIZE % sizeof(struct dinode)) == 0);
assert((BSIZE % sizeof(struct schfs_dirent)) == 0);
crc32c_init();
fsfd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC | O_BINARY, 0666);
if (fsfd < 0)
die(argv[1]);
nmeta = 2 + nlog + ninodeblocks + nbitmap + nibitmap;
nblocks = fssize - nmeta;
sb.magic = FSMAGIC;
sb.size = xint(fssize);
sb.nblocks = xint(nblocks);
sb.ninodes = xint(ninodes);
sb.nlog = xint(nlog);
sb.logstart = xint(2);
sb.ibmapstart = xint(2 + nlog);
sb.inodestart = xint(2 + nlog + nibitmap);
sb.bmapstart = xint(2 + nlog + nibitmap + ninodeblocks);
printf("nmeta %d blocks %d total %d\n", nmeta, nblocks, fssize);
freeblock = nmeta;
// calloc gives us the zeroed image; there is nothing to write out yet.
image_init();
{
char buf[BSIZE];
memset(buf, 0, sizeof(buf));
memmove(buf, &sb, sizeof(sb));
wsect(1, buf);
}
// Root directory.
rootino = ialloc(T_DIR);
assert(rootino == ROOTINO);
bzero(&de, sizeof(de));
de.inum = xshort(rootino);
strcpy(de.name, ".");
iappend(rootino, &de, sizeof(de));
bzero(&de, sizeof(de));
de.inum = xshort(rootino);
strcpy(de.name, "..");
iappend(rootino, &de, sizeof(de));
if (argc >= 4 && strcmp(argv[2], "--sysroot") == 0) {
// New mode: mirror a sysroot directory tree into the image.
sysroot_populate(rootino, argv[3]);
} else {
// Legacy mode: flat file list → everything goes into /bin.
uint binino = ialloc(T_DIR);
bzero(&de, sizeof(de));
de.inum = xshort(binino);
strcpy(de.name, "bin");
iappend_init_dir(rootino, &de);
bzero(&de, sizeof(de));
de.inum = xshort(binino);
strcpy(de.name, ".");
iappend(binino, &de, sizeof(de));
bzero(&de, sizeof(de));
de.inum = xshort(rootino);
strcpy(de.name, "..");
iappend(binino, &de, sizeof(de));
char buf[BSIZE];
for (int i = 2; i < argc; i++) {
char *shortname = argv[i];
if (strncmp(shortname, "build/user/", 11) == 0)
shortname += 11;
else if (strncmp(shortname, "user/", 5) == 0)
shortname += 5;
char *last_slash = strrchr(shortname, '/');
if (last_slash)
shortname = last_slash + 1;
int fd = open(argv[i], O_RDONLY | O_BINARY);
if (fd < 0)
die(argv[i]);
if (shortname[0] == '_')
shortname++;
uint inum = ialloc(T_FILE);
bzero(&de, sizeof(de));
de.inum = xshort(inum);
strncpy(de.name, shortname, DIRSIZ);
if (strcmp(shortname, "README") == 0)
iappend_init_dir(rootino, &de);
else
iappend_init_dir(binino, &de);
int cc;
while ((cc = read(fd, buf, sizeof(buf))) > 0)
iappend(inum, buf, cc);
close(fd);
}
}
// Finalize: bitmap, inode bitmap, superblock (with checksum).
balloc(freeblock);
setibmap(freeinode);
updatesb();
// Nothing has touched the file until now.
image_flush();
exit(0);
}
// ---------------------------------------------------------------------------
// Disk I/O
// ---------------------------------------------------------------------------
void
check_disk_space(void)
{
if (freeblock >= (uint)fssize) {
// Naming every lever, because the old message said only "increase
// FSSIZE in param.h" -- and the image that actually overflowed was the
// ramdisk root, whose size comes from RAMDISK_BLOCKS in the Makefile.
// FSSIZE was 150000 and entirely innocent, which cost a while to work
// out. A wrong pointer in an error message is worse than none.
fprintf(stderr,
"mkfs: image full -- used %u of %u blocks"
" (%d metadata: %d log, %d inode table, %d bitmaps).\n",
freeblock, fssize, nmeta, nlog, ninodeblocks, nbitmap + nibitmap);
fprintf(stderr,
" Which knob depends on the image:\n"
" the main root image -> FSSIZE in include/core/param.h\n"
" the ramdisk root -> RAMDISK_BLOCKS in the Makefile\n"
" Before growing it, check whether the content belongs here at all:\n"
" --exclude <path> drops a subtree, and --ninodes shrinks the\n"
" %d-block preallocated inode table (currently %d inodes for %u used).\n",
ninodeblocks, ninodes, freeinode);
exit(1);
}
}
// The image is assembled in memory and written out once, in image_flush().
//
// It used to be built directly on the file: every wsect() was an lseek plus a
// 512-byte write, and building a 150000-block image meant over 300,000
// syscall pairs -- including 150,000 of them just to zero it. That is
// tolerable on a local disk and ruinous across a filesystem boundary; on
// WSL's /mnt/d it made `make` take a hundred seconds *when nothing had
// changed*. Holding the whole image (77 MB at the default size) costs less
// than that by every measure worth caring about.
static unsigned char *image;
void
image_init(void)
{
image = calloc((size_t)fssize, BSIZE);
if (!image) {
fprintf(stderr, "mkfs: out of memory for a %d-block image\n", fssize);
exit(1);
}
}
void
image_flush(void)
{
size_t total = (size_t)fssize * BSIZE;
size_t tail = total;
size_t done = 0;
// Everything above the last allocated block is zero, and a file that ends
// early reads as zeros past its end -- so write only the part that has
// anything in it and set the length with ftruncate. At the default size
// that is under 2 MB written instead of 77, which is the difference
// between a filesystem image being cheap to rebuild and being the slowest
// step in the build.
//
// The last non-zero block is *found* rather than assumed to be `freeblock`
// -- scanning memory costs nothing next to the write, and it does not
// depend on my reading of where the allocator stops being able to place
// things.
while (tail > 0 && image[tail - 1] == 0)
tail--;
tail = ((tail + BSIZE - 1) / BSIZE) * BSIZE; // round up to a whole block
while (done < tail) {
ssize_t n = write(fsfd, image + done, tail - done);
if (n <= 0)
die("write");
done += (size_t)n;
}
if (ftruncate(fsfd, (off_t)total) < 0)
die("ftruncate");
}
void
wsect(uint sec, void *buf)
{
if (sec >= (uint)fssize) {
fprintf(stderr, "mkfs: wsect %u out of range\n", sec);
exit(1);
}
memmove(image + (size_t)sec * BSIZE, buf, BSIZE);
}
void
rsect(uint sec, void *buf)
{
if (sec >= (uint)fssize) {
fprintf(stderr, "mkfs: rsect %u out of range\n", sec);
exit(1);
}
memmove(buf, image + (size_t)sec * BSIZE, BSIZE);
}
void
winode(uint inum, struct dinode *ip)
{
char buf[BSIZE];
uint bn = IBLOCK(inum, sb);
rsect(bn, buf);
struct dinode *dip = ((struct dinode *)buf) + (inum % IPB);
*dip = *ip;
wsect(bn, buf);
}
void
rinode(uint inum, struct dinode *ip)
{
char buf[BSIZE];
uint bn = IBLOCK(inum, sb);
rsect(bn, buf);
struct dinode *dip = ((struct dinode *)buf) + (inum % IPB);
*ip = *dip;
}
// ---------------------------------------------------------------------------
// Inode allocation
// ---------------------------------------------------------------------------
// Current wall-clock time as a 32-bit Unix timestamp.
static uint32_t
mkfs_now(void)
{
return (uint32_t)time(NULL);
}
uint
ialloc(ushort type)
{
uint inum = freeinode++;
struct dinode din;
bzero(&din, sizeof(din));
din.type = xshort(type);
din.nlink = xshort(1);
din.size = xint(0);
din.rwmode = RW;
din.supermode = 0;
din.showmode = 1;
// Timestamps: all three set to "now" at image creation time.
uint32_t ts = mkfs_now();
din.atime = ts;
din.mtime = ts;
din.ctime = ts;
// Checksum must be computed AFTER all fields are populated.
din.checksum = inode_checksum(&din);
winode(inum, &din);
return inum;
}
// ---------------------------------------------------------------------------
// Block allocation bitmap
// ---------------------------------------------------------------------------
void
balloc(int used)
{
uchar buf[BSIZE];
int bmap_block = 0;
printf("balloc: first %d blocks have been allocated\n", used);
bzero(buf, BSIZE);
for (int i = 0; i < used; i++) {
buf[(i % (BSIZE * 8)) / 8] |= (1 << (i % 8));
if ((i + 1) % (BSIZE * 8) == 0 || i == used - 1) {
wsect(xint(sb.bmapstart) + bmap_block, buf);
bzero(buf, BSIZE);
bmap_block++;
}
}
}
// ---------------------------------------------------------------------------
// alloc_bmap: map logical file block `fbn` to a physical disk block,
// allocating one if needed. Supports classic (direct/indirect) inodes and
// T_EXTENT inodes (inline extent array only -- sufficient for mkfs).
// ---------------------------------------------------------------------------
#define min(a, b) ((a) < (b) ? (a) : (b))
static uint
alloc_bmap_extent(struct dinode *dp, uint fbn)
{
struct fsv1_extent *ext = (struct fsv1_extent *)dp->addrs;
// Search existing inline extents.
int n = 0;
for (n = 0; n < (int)MAX_INLINE_EXTENTS; n++) {
if (ext[n].len == 0)
break;
if (fbn >= ext[n].start_block &&
fbn < ext[n].start_block + ext[n].len)
return ext[n].start_blkaddr + (fbn - ext[n].start_block);
}
// Allocate a new physical block.
check_disk_space();
uint addr = freeblock++;
// Try to extend the previous extent if the blocks are contiguous.
if (n > 0) {
struct fsv1_extent *last = &ext[n - 1];
if (fbn == last->start_block + last->len &&
addr == last->start_blkaddr + last->len) {
last->len++;
return addr;
}
} else {
// Very first block.
ext[0].start_block = 0;
ext[0].start_blkaddr = addr;
ext[0].len = 1;
return addr;
}
// Need a new extent entry.
if (n >= (int)MAX_INLINE_EXTENTS) {
fprintf(stderr,
"alloc_bmap_extent: too many extents for inline array "
"(MAX_INLINE_EXTENTS=%d). "
"Add indirect extent block support if files are this large.\n",
(int)MAX_INLINE_EXTENTS);
exit(1);
}
ext[n].start_block = fbn;
ext[n].start_blkaddr = addr;
ext[n].len = 1;
return addr;
}
static uint
alloc_bmap(struct dinode *dp, uint fbn)
{
// Dispatch to extent path for T_EXTENT inodes.
if (xshort(dp->type) == T_EXTENT)
return alloc_bmap_extent(dp, fbn);
// Classic direct / singly / doubly / triply indirect.
uint indirect[NINDIRECT];
if (fbn < NDIRECT) {
if (xint(dp->addrs[fbn]) == 0) {
check_disk_space();
dp->addrs[fbn] = xint(freeblock++);
}
return xint(dp->addrs[fbn]);
}
fbn -= NDIRECT;
// Singly indirect.
if (fbn < NINDIRECT) {
if (xint(dp->addrs[NDIRECT]) == 0) {
check_disk_space();
dp->addrs[NDIRECT] = xint(freeblock++);
}
rsect(xint(dp->addrs[NDIRECT]), (char *)indirect);
if (indirect[fbn] == 0) {
check_disk_space();
indirect[fbn] = xint(freeblock++);
wsect(xint(dp->addrs[NDIRECT]), (char *)indirect);
}
return xint(indirect[fbn]);
}
fbn -= NINDIRECT;
// Doubly indirect.
if (fbn < NINDIRECT * NINDIRECT) {
if (xint(dp->addrs[NDIRECT + 1]) == 0) {
check_disk_space();
dp->addrs[NDIRECT + 1] = xint(freeblock++);
}
rsect(xint(dp->addrs[NDIRECT + 1]), (char *)indirect);
uint idx1 = fbn / NINDIRECT;
if (indirect[idx1] == 0) {
check_disk_space();
indirect[idx1] = xint(freeblock++);
wsect(xint(dp->addrs[NDIRECT + 1]), (char *)indirect);
}
uint lvl2 = xint(indirect[idx1]);
uint ind2[NINDIRECT];
rsect(lvl2, (char *)ind2);
uint idx2 = fbn % NINDIRECT;
if (ind2[idx2] == 0) {
check_disk_space();
ind2[idx2] = xint(freeblock++);
wsect(lvl2, (char *)ind2);
}
return xint(ind2[idx2]);
}
fbn -= NINDIRECT * NINDIRECT;
// Triply indirect.
if (fbn < NINDIRECT * NINDIRECT * NINDIRECT) {
if (xint(dp->addrs[NDIRECT + 2]) == 0) {
check_disk_space();
dp->addrs[NDIRECT + 2] = xint(freeblock++);
}
rsect(xint(dp->addrs[NDIRECT + 2]), (char *)indirect);
uint idx1 = fbn / (NINDIRECT * NINDIRECT);
if (indirect[idx1] == 0) {
check_disk_space();
indirect[idx1] = xint(freeblock++);
wsect(xint(dp->addrs[NDIRECT + 2]), (char *)indirect);
}
uint lvl2 = xint(indirect[idx1]);
uint ind2[NINDIRECT];
rsect(lvl2, (char *)ind2);
uint idx2 = (fbn / NINDIRECT) % NINDIRECT;
if (ind2[idx2] == 0) {
check_disk_space();
ind2[idx2] = xint(freeblock++);
wsect(lvl2, (char *)ind2);
}
uint lvl3 = xint(ind2[idx2]);
uint ind3[NINDIRECT];
rsect(lvl3, (char *)ind3);
uint idx3 = fbn % NINDIRECT;
if (ind3[idx3] == 0) {
check_disk_space();
ind3[idx3] = xint(freeblock++);
wsect(lvl3, (char *)ind3);
}
return xint(ind3[idx3]);
}
die("alloc_bmap: fbn out of range");
return 0;
}
// ---------------------------------------------------------------------------
// iappend: append `n` bytes from `xp` to inode `inum`.
// ---------------------------------------------------------------------------
void
iappend(uint inum, void *xp, int n)
{
char *p = (char *)xp;
uint fbn, off, n1;
struct dinode din;
char buf[BSIZE];
uint x;
rinode(inum, &din);
off = xint(din.size);
while (n > 0) {
fbn = off / BSIZE;
assert(fbn < MAXFILE);
x = alloc_bmap(&din, fbn);
n1 = min((uint)n, (fbn + 1) * BSIZE - off);
rsect(x, buf);
bcopy(p, buf + off - (fbn * BSIZE), n1);
wsect(x, buf);
n -= n1;
off += n1;
p += n1;
}
din.size = xint(off);
din.mtime = mkfs_now();
din.ctime = mkfs_now();
// Recompute checksum after modifying size/mtime/ctime/addrs.
din.checksum = inode_checksum(&din);
winode(inum, &din);
}
// ---------------------------------------------------------------------------
// Directory HTree helpers
// ---------------------------------------------------------------------------
static unsigned int
BKDRHash(char *str)
{
unsigned int seed = 131;
unsigned int hash = 0;
while (*str)
hash = hash * seed + (*str++);
return (hash % HASHRANGE) + 16;
}
// Ensure that all blocks from 0 up to the block containing `target_off` exist.
// Returns the physical block address of the block containing `target_off`.
static uint
ensure_blocks_up_to(struct dinode *dp, uint target_off)
{
uint target_block = target_off / BSIZE;
uint baddr = 0;
for (uint b = 0; b <= target_block; b++)
baddr = alloc_bmap(dp, b);
uint required = (target_block + 1) * BSIZE;
if (required > xint(dp->size))
dp->size = xint(required);
return baddr;
}
// Search one bucket range [range_start, range_end) for a free dirent slot.
// Returns 1 and writes the entry if found; 0 if the bucket is full.
static int
try_insert_in_range(struct dinode *dp, struct schfs_dirent *de,
uint range_start, uint range_end)
{
char buf[BSIZE];
for (uint off = range_start; off < range_end; off += sizeof(struct schfs_dirent)) {
uint baddr = ensure_blocks_up_to(dp, off);
rsect(baddr, buf);
struct schfs_dirent *slot = (struct schfs_dirent *)(buf + (off % BSIZE));
if (slot->inum == 0) {
bcopy(de, buf + (off % BSIZE), sizeof(struct schfs_dirent));
wsect(baddr, buf);
assert(off % 16 == 0);
return 1;
}
}
return 0;
}
// Insert `de` into the HTree directory `inum`.
// Primary bucket determined by BKDR hash; overflow region used if full.
void
iappend_init_dir(uint inum, struct schfs_dirent *de)
{
struct dinode dir_inode;
rinode(inum, &dir_inode);
unsigned int hash = BKDRHash(de->name);
uint primary_start = (uint)(hash * DIRENT_BUCKET_BYTES);
uint primary_end = (uint)((hash * 16 + 16) * sizeof(struct schfs_dirent));
// Try primary bucket.
if (try_insert_in_range(&dir_inode, de, primary_start, primary_end)) {
dir_inode.mtime = mkfs_now();
dir_inode.ctime = mkfs_now();
dir_inode.checksum = inode_checksum(&dir_inode);
winode(inum, &dir_inode);
return;
}
// Primary bucket full -- fall back to overflow region.
printf("iappend_init_dir: '%s' overflows primary bucket (hash=%u), "
"using overflow region [%u, %u)\n",
de->name, hash, OVERFLOW_DIR_START, OVERFLOW_DIR_END);
if (try_insert_in_range(&dir_inode, de,
OVERFLOW_DIR_START, OVERFLOW_DIR_END)) {
dir_inode.mtime = mkfs_now();
dir_inode.ctime = mkfs_now();
dir_inode.checksum = inode_checksum(&dir_inode);
winode(inum, &dir_inode);
return;
}
fprintf(stderr,
"mkfs: directory full -- '%s' could not be inserted "
"(both primary bucket and overflow region are full).\n"
"Increase OVERFLOW_DIR_END or HASHRANGE.\n",
de->name);
exit(1);
}
// ---------------------------------------------------------------------------
// Inode bitmap
// ---------------------------------------------------------------------------
void
setibmap(int used)
{
uchar buf[BSIZE];
int bmap_block = 0;
printf("setibmap: first %d inodes have been allocated\n", used);
bzero(buf, BSIZE);
for (int i = 0; i < used; i++) {
buf[(i % (BSIZE * 8)) / 8] |= (1 << (i % 8));
if ((i + 1) % (BSIZE * 8) == 0 || i == used - 1) {
wsect(xint(sb.ibmapstart) + bmap_block, buf);
bzero(buf, BSIZE);
bmap_block++;
}
}
}
// ---------------------------------------------------------------------------
// Superblock finalisation (called last)
// ---------------------------------------------------------------------------
void
updatesb(void)
{
uchar buf[BSIZE];
sb.freeinodes = xint(ninodes - freeinode);
sb.freeblocks = xint(fssize - freeblock);
// Compute superblock checksum (same algorithm as fs.c::updatesb).
sb.checksum = 0;
sb.checksum = crc32c(0, &sb, sizeof(sb));
memset(buf, 0, sizeof(buf));
memmove(buf, &sb, sizeof(sb));
wsect(1, buf);
printf("updatesb: %u free blocks, %u free inodes, checksum=0x%08X\n",
xint(sb.freeblocks), xint(sb.freeinodes), sb.checksum);
}
// ---------------------------------------------------------------------------
// Error helper
// ---------------------------------------------------------------------------
void
die(const char *s)
{
perror(s);
exit(1);
}