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:
parent
655c7d8d01
commit
2556a955b3
15 changed files with 921 additions and 341 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue