The filesystem every other system can read: a USB stick, an SD card, the EFI system partition. It is here first for the hardware report, which has to leave a machine on something that can be read on another one. mount -t vfat /dev/usb0p1 /mnt (msdos and fat work too; -o ro) Written from Microsoft's specification, and matched to Linux vfat where the specification leaves a choice: 8.3 names with the NT lowercase bits, long-name entries otherwise, Linux's alias rule (REPORTS, then BIGNUM~1), case-insensitive lookup, UTC times, the boot-sector dirty bit, every FAT copy and FSInfo kept up to date. It uses its own sector cache over the new blkdev_xfer, since a partition need not be a whole number of 4 KB blocks, and every operation that changes something reaches the disk before it returns. Inode numbers come from the entry position, or the first cluster for a directory, plus a small table for entries that moved. Around it: - /dev lists every disk and partition the block layer found (major 8, minor = block device), and raw reads and writes on them go by sector. - The ESP and MBR FAT partitions are registered, typed so the root probe passes over them. MBR entries with a bad status byte are skipped, so a superfloppy's boot code is not read as a partition table. - statfs, sync, fsync, umount (EBUSY while in use), shutdown, reboot and the power button all know about FAT. - create undoes and reports a failed ialloc or dirlink (ENOSPC, EINVAL, EPERM) instead of panicking; fs_ops gains refuse_types and no_hardlinks. - filewrite returns a short write instead of panicking on it. - utimensat is implemented. As a stub returning 0 it made toybox touch believe the file existed, so `touch newfile` created nothing on any filesystem. Tested in QEMU against mkfs.vfat images (FAT12 floppy, FAT16 on MBR, FAT32 superfloppy and GPT ESP) by writing, renaming, deleting, filling to ENOSPC and truncating there, then checking with fsck.fat -n and mtools on the host: all clean, contents match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
335 lines
11 KiB
C
335 lines
11 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
//
|
|
// devtmpfs: /dev, generated rather than stored.
|
|
//
|
|
// It used to be ~20 mknod() calls in cmd/schinit, which was wrong in two ways
|
|
// that are worth separating.
|
|
//
|
|
// *The cost.* Every node was a real inode on the root filesystem, and SchFS
|
|
// commits each mutation as its own transaction group -- a copy-on-write rewrite
|
|
// of the path to the root, then an uberblock behind a cache flush. Measured:
|
|
// 2600 ms of a first boot spent writing device nodes to a disk, before schinit
|
|
// printed its first line. On later boots the nodes already existed and the same
|
|
// code took 0 ms, so the cost was invisible unless you built a fresh image --
|
|
// which is exactly what anyone working on the system does every time.
|
|
//
|
|
// *The duplication, which matters more.* Major and minor numbers are the
|
|
// kernel's: they are what devsw[] in kernel/dev/cdev.c dispatches on. schinit
|
|
// held a second copy of that mapping and nothing checked the two agreed. A new
|
|
// character device meant editing a userspace program, and getting a minor wrong
|
|
// there produced a node that opened successfully and did nothing.
|
|
//
|
|
// So the table below sits next to the thing it describes, and /dev is a
|
|
// mount rather than a directory somebody populates. `mount("dev", "/dev",
|
|
// "devtmpfs", 0, 0)` is the whole of what init does now.
|
|
//
|
|
// The namespace is all this provides. Reads and writes never reach here --
|
|
// fileread/filewrite see T_DEV and dispatch through devsw themselves -- which
|
|
// is why there is no read op below that does anything.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "param.h"
|
|
#include "stat.h"
|
|
#include "spinlock.h"
|
|
#include "sleeplock.h"
|
|
#include "proc.h"
|
|
#include "fs.h"
|
|
#include "file.h"
|
|
#include "errno.h"
|
|
#include "blkdev.h"
|
|
|
|
// Inode numbering. Two directories and a flat table of nodes, so the number is
|
|
// either a directory id or an index into devnodes[] offset past them.
|
|
#define DEVFS_ROOT 1ULL
|
|
#define DEVFS_INPUT 2ULL /* the /dev/input directory */
|
|
#define DEVFS_FILE0 16ULL /* devnodes[0] is inode 16 */
|
|
// Disks and partitions the block layer found: /dev/sata0, /dev/sata0p1,
|
|
// /dev/usb0p1, named as it named them. Major 8 like hda, minor the block
|
|
// device number, and inode DEVFS_BLK0 plus that number. Not in devnodes[],
|
|
// because they come and go: a stick plugged in appears, pulled out vanishes.
|
|
#define DEVFS_BLK0 4096ULL
|
|
|
|
// Which directory a node lives in. Only two exist and a third would want a real
|
|
// tree rather than a flag, which is the point at which this file should grow
|
|
// one instead of adding DEV_DIR_THIRD.
|
|
#define D_ROOT 0
|
|
#define D_INPUT 1
|
|
|
|
struct devnode {
|
|
const char *name;
|
|
short major;
|
|
short minor;
|
|
short dir;
|
|
};
|
|
|
|
// The devices this kernel implements, in the order they appear in a listing.
|
|
//
|
|
// Minor numbers under major 1 follow Linux's mem-major layout where it costs
|
|
// nothing (null=2, zero=5, full=7, random=8, urandom=9), which is worth keeping
|
|
// because it is one less arbitrary mapping for anyone reading both.
|
|
//
|
|
// The majors are devsw[] indices and nothing else; see kernel/dev/cdev.c.
|
|
static const struct devnode devnodes[] = {
|
|
{ "console", 1, 1, D_ROOT },
|
|
{ "null", 1, 2, D_ROOT },
|
|
{ "keyboard",1, 3, D_ROOT },
|
|
{ "zero", 1, 5, D_ROOT },
|
|
{ "full", 1, 7, D_ROOT },
|
|
{ "random", 1, 8, D_ROOT },
|
|
{ "urandom", 1, 9, D_ROOT },
|
|
/* /dev/tty is the caller's *controlling* terminal, which is a different
|
|
thing from the console and used to be the same thing only because there
|
|
was one terminal. Minor 10 resolves through proc->ctty; see
|
|
vt_for_minor() in kernel/core/console.c. */
|
|
{ "tty", 1, 10, D_ROOT },
|
|
{ "tty1", 1, 11, D_ROOT },
|
|
{ "tty2", 1, 12, D_ROOT },
|
|
{ "tty3", 1, 13, D_ROOT },
|
|
{ "tty4", 1, 14, D_ROOT },
|
|
{ "tty5", 1, 15, D_ROOT },
|
|
{ "tty6", 1, 16, D_ROOT },
|
|
{ "stdin", 1, 1, D_ROOT },
|
|
{ "stdout", 1, 1, D_ROOT },
|
|
{ "stderr", 1, 1, D_ROOT },
|
|
{ "fb0", 2, 0, D_ROOT },
|
|
{ "pci", 9, 0, D_ROOT },
|
|
{ "dbg", 10, 0, D_ROOT },
|
|
// The same device, answering only the views that read. Minor 1 rather than
|
|
// a flag, because the minor is the one piece of per-open state a character
|
|
// device already sees -- so which one you opened *is* the permission, and
|
|
// nothing has to be remembered between the write and the read. See
|
|
// include/dbg.h and cmd/schdbgd.
|
|
{ "dbgro", 10, 1, D_ROOT },
|
|
{ "netdev", 11, 0, D_ROOT },
|
|
// Audio, spelled the way UNIX spells it. See include/audio.h.
|
|
{ "dsp", 12, 0, D_ROOT },
|
|
{ "mouse", 3, 1, D_ROOT },
|
|
|
|
// Raw disks. Created whether or not a drive is present: opening one with no
|
|
// disk behind it fails with ENXIO, which is a better answer than a missing
|
|
// path -- and with no node at all, `echo x > /dev/hdb` silently creates an
|
|
// ordinary file of that name and appears to work.
|
|
{ "hda", 8, 0, D_ROOT },
|
|
{ "hdb", 8, 1, D_ROOT },
|
|
|
|
// Raw input events, Linux evdev layout. The minor is the device index, so
|
|
// event0 is the keyboard and event1 the mouse.
|
|
{ "event0", 7, 0, D_INPUT },
|
|
{ "event1", 7, 1, D_INPUT },
|
|
};
|
|
|
|
#define NDEVNODES ((int)(sizeof(devnodes) / sizeof(devnodes[0])))
|
|
|
|
static int
|
|
node_of(uint64 inum)
|
|
{
|
|
if (inum < DEVFS_FILE0)
|
|
return -1;
|
|
uint64 i = inum - DEVFS_FILE0;
|
|
return i < (uint64)NDEVNODES ? (int)i : -1;
|
|
}
|
|
|
|
static void
|
|
devfs_iload(struct inode *ip)
|
|
{
|
|
int i = node_of(ip->inum);
|
|
|
|
ip->major = ip->minor = 0;
|
|
ip->size = 0;
|
|
|
|
if (ip->inum == DEVFS_ROOT || ip->inum == DEVFS_INPUT) {
|
|
ip->type = T_DIR;
|
|
ip->nlink = 2;
|
|
} else if (i >= 0) {
|
|
ip->type = T_DEV;
|
|
ip->nlink = 1;
|
|
ip->major = devnodes[i].major;
|
|
ip->minor = devnodes[i].minor;
|
|
} else if (ip->inum >= DEVFS_BLK0 &&
|
|
blkdev_present((uint)(ip->inum - DEVFS_BLK0))) {
|
|
ip->type = T_DEV;
|
|
ip->nlink = 1;
|
|
ip->major = 8;
|
|
ip->minor = (short)(ip->inum - DEVFS_BLK0);
|
|
} else {
|
|
ip->type = 0; /* no such node */
|
|
ip->nlink = 0;
|
|
}
|
|
ip->valid = 1;
|
|
}
|
|
|
|
static void devfs_istore(struct inode *ip) { (void)ip; }
|
|
static void devfs_ifree(struct inode *ip) { (void)ip; }
|
|
static void devfs_truncate(struct inode *ip, uint64 n) { (void)ip; (void)n; }
|
|
|
|
// Never reached for a device node: fileread and filewrite see T_DEV and go
|
|
// through devsw. Present because fs_ops has the slots, and answering -EIO is
|
|
// better than a null pointer if something ever does arrive here.
|
|
static int devfs_read(struct inode *ip, int u, uint64 d, uint64 o, uint n)
|
|
{ (void)ip; (void)u; (void)d; (void)o; (void)n; return -1; }
|
|
static int devfs_write(struct inode *ip, int u, uint64 s, uint64 o, uint n)
|
|
{ (void)ip; (void)u; (void)s; (void)o; (void)n; return -1; }
|
|
|
|
static int
|
|
streq(const char *a, const char *b)
|
|
{
|
|
while (*a && *a == *b) { a++; b++; }
|
|
return *a == *b;
|
|
}
|
|
|
|
static struct inode *
|
|
devfs_dirlookup(struct inode *dp, char *name, uint *poff)
|
|
{
|
|
if (poff)
|
|
*poff = 0;
|
|
|
|
if (streq(name, ".") || (streq(name, "..") && dp->inum == DEVFS_ROOT))
|
|
return iget(DEVFS_DEV, dp->inum);
|
|
if (streq(name, ".."))
|
|
return iget(DEVFS_DEV, DEVFS_ROOT); /* out of /dev/input */
|
|
|
|
if (dp->inum == DEVFS_ROOT && streq(name, "input"))
|
|
return iget(DEVFS_DEV, DEVFS_INPUT);
|
|
|
|
short want = dp->inum == DEVFS_INPUT ? D_INPUT : D_ROOT;
|
|
for (int i = 0; i < NDEVNODES; i++)
|
|
if (devnodes[i].dir == want && streq(name, devnodes[i].name))
|
|
return iget(DEVFS_DEV, DEVFS_FILE0 + (uint64)i);
|
|
|
|
if (want == D_ROOT)
|
|
for (int k = 0, b; (b = blkdev_nth(k)) >= 0; k++)
|
|
if (streq(name, blkdev_name((uint)b)))
|
|
return iget(DEVFS_DEV, DEVFS_BLK0 + (uint64)b);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// A read-only filesystem, and these say so rather than pretending. mknod on
|
|
// /dev returning EROFS is the correct answer: the node either exists because
|
|
// the kernel has the device, or it does not exist because it has not.
|
|
static int devfs_dirlink(struct inode *dp, char *name, uint64 inum)
|
|
{ (void)dp; (void)name; (void)inum; return -EROFS; }
|
|
static int devfs_dirunlink(struct inode *dp, char *name, uint off)
|
|
{ (void)dp; (void)name; (void)off; return -EROFS; }
|
|
static int devfs_dirempty(struct inode *dp) { (void)dp; return 0; }
|
|
static struct inode *devfs_ialloc(uint dev, short type)
|
|
{ (void)dev; (void)type; return 0; }
|
|
|
|
struct linux_dirent64 {
|
|
uint64 d_ino;
|
|
int64 d_off;
|
|
uint16 d_reclen;
|
|
uchar d_type;
|
|
char d_name[];
|
|
};
|
|
|
|
static int
|
|
emit(char *out, int cap, int *used, uint64 ino, uchar type, const char *name)
|
|
{
|
|
int nl = (int)strlen(name);
|
|
int rec = (int)((sizeof(struct linux_dirent64) + nl + 1 + 7) & ~7);
|
|
|
|
if (*used + rec > cap)
|
|
return -1; /* caller stops and resumes next call */
|
|
|
|
struct linux_dirent64 *d = (struct linux_dirent64 *)(out + *used);
|
|
d->d_ino = ino;
|
|
d->d_off = 0;
|
|
d->d_reclen = (uint16)rec;
|
|
d->d_type = type;
|
|
memmove(d->d_name, name, nl);
|
|
memset(d->d_name + nl, 0, rec - (int)sizeof(*d) - nl);
|
|
*used += rec;
|
|
return 0;
|
|
}
|
|
|
|
// `off` is an ordinal over the entries of this directory: 0 is ".", 1 is "..",
|
|
// and the rest follow. An opaque cookie the way SchFS uses one buys nothing
|
|
// here, because the table is fixed and small enough to index directly.
|
|
static int
|
|
devfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
|
|
{
|
|
int used = 0;
|
|
uint64 n = *poff;
|
|
|
|
if (dp->inum != DEVFS_ROOT && dp->inum != DEVFS_INPUT)
|
|
return -1;
|
|
|
|
if (n == 0) {
|
|
if (emit(out, cap, &used, dp->inum, 4 /* DT_DIR */, ".") < 0) return -1;
|
|
n++;
|
|
}
|
|
if (n == 1) {
|
|
uint64 up = dp->inum == DEVFS_ROOT ? DEVFS_ROOT : DEVFS_ROOT;
|
|
if (emit(out, cap, &used, up, 4, "..") < 0) goto out;
|
|
n++;
|
|
}
|
|
|
|
// /dev/input is a directory entry of the root, and it is emitted between the
|
|
// dots and the nodes so that its ordinal is stable.
|
|
if (dp->inum == DEVFS_ROOT && n == 2) {
|
|
if (emit(out, cap, &used, DEVFS_INPUT, 4, "input") < 0) goto out;
|
|
n++;
|
|
}
|
|
|
|
short want = dp->inum == DEVFS_INPUT ? D_INPUT : D_ROOT;
|
|
uint64 base = dp->inum == DEVFS_ROOT ? 3 : 2;
|
|
|
|
for (int i = 0; i < NDEVNODES; i++) {
|
|
if (devnodes[i].dir != want)
|
|
continue;
|
|
if (n > base) { base++; continue; } /* already emitted */
|
|
if (emit(out, cap, &used, DEVFS_FILE0 + (uint64)i, 2 /* DT_CHR */,
|
|
devnodes[i].name) < 0)
|
|
goto out;
|
|
n++;
|
|
base++;
|
|
}
|
|
|
|
if (want == D_ROOT)
|
|
for (int k = 0, b; (b = blkdev_nth(k)) >= 0; k++) {
|
|
if (n > base) { base++; continue; }
|
|
if (emit(out, cap, &used, DEVFS_BLK0 + (uint64)b, 6 /* DT_BLK */,
|
|
blkdev_name((uint)b)) < 0)
|
|
goto out;
|
|
n++;
|
|
base++;
|
|
}
|
|
|
|
out:
|
|
*poff = n;
|
|
return used;
|
|
}
|
|
|
|
const struct fs_ops devfs_ops = {
|
|
.name = "devtmpfs",
|
|
.iload = devfs_iload,
|
|
.istore = devfs_istore,
|
|
.ifree = devfs_ifree,
|
|
.read = devfs_read,
|
|
.write = devfs_write,
|
|
.truncate = devfs_truncate,
|
|
.dirlookup = devfs_dirlookup,
|
|
.dirlink = devfs_dirlink,
|
|
.dirunlink = devfs_dirunlink,
|
|
.dirempty = devfs_dirempty,
|
|
.ialloc = devfs_ialloc,
|
|
.getdents = devfs_getdents,
|
|
};
|
|
|
|
// Graft it over /dev. Called by init, which has made the directory.
|
|
void
|
|
devfs_mount(void)
|
|
{
|
|
struct inode *dp = namei("/dev");
|
|
|
|
if (dp == 0) {
|
|
cprintf("devfs: /dev does not exist; not mounted\n");
|
|
return;
|
|
}
|
|
if (mount_add(dp->dev, dp->inum, DEVFS_DEV, DEVFS_ROOT, "/dev", "dev",
|
|
"devtmpfs") < 0)
|
|
cprintf("devfs: /dev already has something mounted on it\n");
|
|
iput(dp);
|
|
}
|