SchOS/kernel/fs/devfs.c
Nexoniarz 24f79194ab hybrid: /dev/pci -- a driver can live outside the kernel now
docs/plan/hybrid.md mechanisms 2 and 4: MMIO and DMA to a process.

A process can enumerate the bus, claim a device exclusively, map its BARs and
get memory the device can be pointed at. Proven end to end against an e1000e
-- the exact device class we cannot drive and would want a foreign driver for:

    device 7: 8086:10d3 (network)
      claimed
      BAR0 at 0x81060000, 128 KB
      regs[0..3] = 0x00140261 0x00000000 0x00080283 0x00000000
      MMIO reaches the device
      DMA 8 KB at virtual 0x40001000, physical 0x3f8eb000

Those are the e1000e's CTRL and STATUS with full-duplex and link-up set, which
is how you tell a real mapping from one reading 0xFFFFFFFF off an unmapped
address. Claiming a device the kernel drives is refused -- two writers of one
register set looks like hardware that has started lying -- and everything a
process held is released when it exits, because a driver that crashed must not
keep its hardware away from its replacement.

mmap's offset is the BAR's physical address, checked against the BARs of
devices *this process has claimed*. Without that check the device is /dev/mem
with extra steps and a process could map the kernel's own memory. Mappings are
write-through: a cached write to a doorbell arrives whenever the cache feels
like it, which from a driver's side is indistinguishable from hardware that
ignores you.

The interface is about PCI devices, not about any driver model, and its first
users will be SchOS's own drivers when virtio-gpu is evicted. That ordering is
the design rather than an accident: an interface whose first consumers are
native drivers is a general mechanism something foreign can use later, instead
of a hole shaped like something foreign.

Also: PROT_* and MAP_* moved to include/posix.h. They were defined only in
sys_mem.c, so cmd/fbinfo carried a private copy and this wanted a third --
two copies of a number the kernel compares against, where a mismatch shows up
as a mapping with the wrong permissions rather than an error.

No IOMMU, so this isolates a driver against its own bugs and not against its
own DMA. The plan says so at length and it is still true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-13 15:34:28 +02:00

298 lines
9.7 KiB
C

// SPDX-License-Identifier: Apache-2.0
//
// 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"
// 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 */
// 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 },
{ "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 {
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);
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++;
}
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);
}