docs: architecture describes what is built now

kernel (threads, demand paging, TLB shootdowns, dynamic linking, RAM direct map), syscalls, filesystem (/proc, /sys,
/dev), terminals (12, VT handshake, ptys), networking (r8169 through LDLS), LDLS (module sets; Linux code may ship),
devices (what stays in the kernel; hybrid.md's lasting content), boot (the loader's placement of the kernel),
graphics (fbdev, GMMOS, QR 1-40), audio (ALSA over OSS), debugging, usb. New: packages.md (the apk root) and
desktop.md (Xorg on the framebuffer).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
This commit is contained in:
Nexoniarz 2026-10-26 07:57:25 +01:00
commit 2556a955b3
15 changed files with 921 additions and 341 deletions

View file

@ -4,7 +4,50 @@ How hardware gets found, claimed and used, and what the rest of the kernel sees
of it.
Writing a driver is [../guides/adding-a-driver.md](../guides/adding-a-driver.md).
Moving drivers out of the kernel entirely is [../plan/hybrid.md](../plan/hybrid.md).
Drivers that live in a process rather than in the kernel are described below
(*Drivers that are not in the kernel*), and the foreign ones in [ldls.md](ldls.md).
---
## What stays in the kernel, and what may leave
"Hybrid" is used to mean everything from "a monolith with a nice device model" to
"a microkernel that gave up", so the line is worth drawing exactly.
**Stays in, permanently:** scheduling, address spaces, the trap and syscall path;
IPC and shared memory; the VFS and the **root** filesystem; IRQ demultiplexing; and
the console and PSOGM. That last one is not a size compromise but an invariant: the
crash screen must work when everything else is dead, so it cannot depend on anything
that could *be* dead. A crash screen served by a userspace process is absent in
exactly the case it exists for.
**May leave, and should:** GPU drivers, non-root filesystems, NICs after the first,
and anything written by someone else.
> **Anything needed to reach a shell stays in. Anything discovered at runtime,
> replaceable, or plausibly written by someone else goes out.**
>
> **The kernel keeps a minimal driver for everything on the path to a shell, and
> userspace owns the good one.** Break that and the compositor cannot start because
> the display driver is a process that needs a compositor.
### USB stays in the kernel, and why
It was on the list to leave, and the premise was wrong. USB is not a peripheral; it
is the bus peripherals arrive on, structurally the same kind of thing as PCI, and
nobody has proposed evicting PCI. The keyboard and the root disk that arrive over it
are on the path to a shell, so the stack that finds them is part of that path. Even
with a minimal in-kernel fallback written, the good driver and the minimal one would
be the same driver, and an eviction that leaves an identical copy behind has moved
nothing. A second, quieter difference: `/dev/pci` hands over one PCI *function*, one
owner and nothing shared, while a USB device sits behind a controller other devices
also sit behind — "the xHCI controller" includes the keyboard. What the eviction was
for, third-party drivers living outside the kernel, survives and belongs to
[ldls.md](ldls.md).
The root filesystem stays in the kernel for the same reason (SchFS cannot be
restarted, and a bug in it is fatal), and so does the first NIC, because the network
thread and lwIP are in the kernel.
---
@ -184,8 +227,47 @@ client — and that is worth a sentence here because it is the thing the device
model makes possible rather than the thing it does. A modeset is five commands
to the device. In the kernel those five commands are also a reason to be
nervous, because a wrong one takes the machine; in a process the worst case is
that `schinit` starts another one. See
[../plan/hybrid.md](../plan/hybrid.md), "A modeset, from a process".
that `schinit` starts another one. That is why the in-kernel driver never grew
one — not difficulty, exposure.
```
# schmode
1280x800 current
1280x800 maximum, framebuffer 4000 KB
# schmode 1024 768
1024x768 ok
```
The scanout really resizes, and nothing in the kernel changed for it: `schmode`
writes a line to a socket and `vgpud` sends five commands to the device. The
framebuffer is allocated **once, at the largest mode**, and every smaller mode
reuses it, because `PCIIOC_DMA` has no free — a driver's blocks come back when it
exits and not before — so a modeset that allocated would spend the arena and the
failure would look like a GPU bug rather than like counting. A larger mode is
refused, and says why. The control socket speaks lines of text and has no buffer
submission, damage or vblank, which a compositor would need and nothing has asked
for.
### A panic has to reach the screen
Handing a device away costs something that was not on the list. The kernel panics,
paints the report into the console framebuffer, and halts — and the display it gave
to a process keeps showing whatever that process last put there. On a machine with
two heads, one says the kernel is dead and the other shows a shell prompt. There is
no scheduling after a panic, so the process holding the device is never going to run
again, and the kernel let go of the device deliberately.
So the driver says in advance where it is scanning out, with `PCIIOC_PANICFB`
(`include/dev/pciuser.h`), and the kernel: (1) paints the report into that memory;
(2) asks the *kernel* driver the device was taken from to put it on screen —
`panic_present` in `struct pci_driver`; (3) which resets the device and rebuilds one
queue from **static** memory — no allocation, no locks, no sleeping, because a
corrupted heap is one of the ways a machine gets here. The registration is checked
like the promise it is: the range must lie inside a block that process got from
`PCIIOC_DMA` on that device, or it would be a way to nominate any physical address
to be overwritten at panic. It costs 20 KB of static memory in the virtio-gpu driver,
paid whether or not the machine ever panics; allocating it on demand would be
cheaper and would not work.
### Taking it back
@ -222,8 +304,9 @@ enforces it.
Coming back is a job, not a kernel feature. `etc/schinit/vgpud.job` carries
`KeepAlive = yes`, so killing the driver is the entire test: `schinitd` starts
another one, it claims the device, and the pid of whichever process drew the
frame is legible in the frame itself. See
[../plan/hybrid.md](../plan/hybrid.md), "The driver came back".
frame is legible in the frame itself: read off the scanout across four generations of
the driver it said 4, 8, 10, 12, and the DMA arena handed back the same addresses
every time.
### What this is not
@ -231,8 +314,7 @@ frame is legible in the frame itself. See
buffer can point the device at any physical address. This isolates a driver
against its own *bugs* and not against its own *intent* — a crashed driver that
restarts instead of taking the machine down is the entire benefit, and it is
worth having on those terms. [../plan/hybrid.md](../plan/hybrid.md) says so at
length.
worth having on those terms. VT-d/AMD-Vi is its own project.
**One device per process.** A driver drives one thing; if you need two, run two.
@ -240,29 +322,24 @@ length.
## Linux drivers
**Decided 2026-08-08.** Linux drivers are not shipped with SchOS and never
will be. The user may install one on their own machine, through a device
manager or equivalent, and SchOS will load it.
[ldls.md](ldls.md) is how SchOS uses them: a Linux `.ko` runs in a process and
reaches the hardware through `/dev/pci`. The licensing position, stated exactly
because it is easy to get wrong in both directions:
The licensing reasoning matters and is worth stating exactly, because it is
easy to get wrong in both directions:
- **Distribution is what triggers the GPL.** SchOS is GPL-2.0-or-later, so shipping a
GPL-2.0-only driver in an image is *lawful* — see [../licensing.md](../licensing.md) §6 —
and the owner has decided it is allowed (code and firmware blobs). The repository does
not carry one all the same: `.gitignore` refuses `*.ko`, and the modules are staged into
a *local* image from the build machine's own Linux kernel (`make ldls-stage`,
`make ldls-r8169`), where the matching BTF is.
- **Implementing a loader is not copying.** Writing support for a module format and
a driver ABI from published documentation produces our own work; the layouts come
from the kernel's BTF at load time on the user's machine, so the source holds none
of Linux's.
- **Distribution is what triggers the GPL.** SchOS is GPL-2.0-or-later, so
shipping a GPL-2.0-only driver in an image is now *possible* — see
[../licensing.md](../licensing.md) §6 — but LDLS still does not, by policy. A
user obtaining a driver themselves and loading it on their own machine is
their act, not ours, and is not a distribution by us.
- **Implementing a loader is not copying.** Writing support for a module format
and a driver ABI from published documentation produces our own work. It is a
clean-room implementation, and it stays one only if it is written from
documentation rather than transcribed from Linux source. Reading Linux to
understand *hardware* is fine; translating its code is not, whatever the
identifiers say.
So the rule for this subsystem is the same as everywhere else in the tree: the
loader is ours and GPL-2.0-or-later, what it loads is the user's business, and
no third-party module enters an image we build. `RAMDISK_EXCLUDE` and `NOTPORTS` are the
existing mechanisms for keeping that true mechanically rather than by intention.
The loader is ours and GPL-2.0-or-later; what it loads is not in the tree.
`RAMDISK_EXCLUDE` and `NOTPORTS` are the mechanisms that keep that true mechanically
rather than by intention.
---
@ -371,12 +448,13 @@ the console's minors to the console and the rest to their own functions. But
- **No hot-plug.** Devices are registered at boot and never removed; the
`in_use` flag is the hook a hot-unplug path would need, and nothing sets it
for that purpose.
- **`NDEVICE` is 32**, compile-time, like every other table in the kernel.
- **`NDEVICE` is 128**, compile-time, like every other table in the kernel.
- **In-kernel drivers are fixed at link time.** There is no module loader, and
nothing can be added to the kernel after the build. A driver *outside* the
kernel is an ordinary program and can be started whenever — but it reaches its
device through `/dev/pci` and publishes nothing to the class vtables, so the
console cannot be handed to one yet.
kernel is an ordinary program and can be started whenever — it reaches its
device through `/dev/pci`, and a NIC driver can publish itself to the network
class through `/dev/netdev` (LDLS does); a display driver publishes nothing to
`display_ops`, so the console cannot be handed to one.
- **The kernel does not rebind its own driver.** `PCIIOC_RELEASE`, or the process
exiting, leaves the interrupt line masked and the in-kernel driver unbound —
the console stays wherever the release put it. A *supervisor* can start a