Devices as files
A device worth reaching is a file under /dev, served by its driver (sysk 046). The first is entropy: cat /dev/rng shows fresh random bytes, every time, from sysc's generator seeded by the device (sysc 009). The driver holds the device and answers the same files protocol the kernel answers for /sys and the file server for /, so the shell needed no new command, and the kernel no new line.
Three trees, one namespace
The shell's namespace is three trees. The first name in a path says which server answers: /sys the kernel, through a call; /dev the entropy driver, over a pair of channels; anything else the file server, which keeps its tree on the disk. Pick a path to see where its requests go and what the shell says.
Every top-level directory is one server (docs/directions.md, "The file tree"), and the shell lists the other trees' names at the root: ls / shows dev/ net/ prg/ sys/ after the disk's own, its namespace's mounts (sysk 053, Files). Writing, making and removing are the disk's alone: the shell refuses them on any other tree before it asks, and the servers refuse them too.
A stream has no end
A file in the kernel's tree is a snapshot, rendered whole when it is read. A file on the disk has a length. /dev/rng has neither: every read is new bytes, as many as asked, and offsets mean nothing. The files protocol already named this kind, stream, in the formats research (sysk 026, F5), beside dir, file and ctl; sysk 046 is its first use. Its format is bytes, which promises nothing about the contents, unlike sexp.
So cat cannot read to the end, as it does a file. On a stream it reads once, as much as one line shows, and shows bytes in hex: 31 bytes, 62 digits. A program that wants entropy reads as much as it needs, up to a payload's 512 bytes a request.
One cat, five messages
Every request and reply is 576 bytes: a 64-byte header and a 512-byte payload (Files). The shell names nodes with fids, small numbers the driver keeps for it in an 8-byte table: a node and whether it is open. Select a row to see what the driver does.
| The shell asks | The driver answers |
|---|
The device lends nothing back but bytes
The entropy device is the simplest virtio device there is. It has one queue, and a request is one buffer for the device to fill (virtio 1.2 §5.4). The specification forbids a part the device would read, so the driver lends 32 bytes, all of it for the device to write. What the device wrote comes back through virtio-written, at most what was lent, and rng-gather lends again until 32 have come:
(let l (virtio-lend dev q (alloc h 32) 0 0 0)) ; one part, the device writes it (while (not (virtio-poll q)) (invariant (and (running k) (heap-ok h) (queue-busy q) (lent-for q l) (queue-lent q 32) …)) (let s (alt k wants from-device)) ; the device's interrupt, and nothing else (virtio-ack dev) (device-ack k dev)) (let b (virtio-take q l)) (let n (virtio-written q)) ; ≤ 32: the queue says so
Those bytes are a seed, not the answer (sysc 009). A read is answered by sysc's generator, sysc/rnd: ChaCha20 under a key only sysc's file sees, as OpenBSD's arc4random uses it, the key replaced after every read so a state stolen later gives back nothing already read (Random bytes). The driver gathers the seed at start, before it takes a request, and makes its Rnd with it, in 32 words from alloc-w32; when no seed came, a Rnd from rnd-none, which sysc proves no read can draw from, so every read asks rnd-ready and answers io. A read that comes first, netd's key at boot among them, waits for the seed: an error that only means "not yet" would hand every caller a choice none of them should make, so the driver waits, as Linux's getrandom(2) waits for its pool to be ready; OpenBSD's arc4random, likewise, never fails. Once a mebibyte has been served, the next read first gathers 32 fresh bytes and mixes them into the key with rnd-mix, which a weak batch cannot make worse; the device's bytes are zeroed before their buffer goes back. Every wait is on the device alone, inside the branch of the request it answers, which the deadlock check reads. A fill writes the reply's payload and is proven to leave its header as it was.
This is the measure sysk 042 set for its generic queue: a second kind of device landed with no line changed in kernel/devices.sysp. Everything that keeps the device from reaching the wrong memory, lending as a move, addresses only from the queue's own file, contents unknown until read, holds for it unchanged (Storage). The entropy is QEMU's, from the host's own generator; on real hardware it would be the device's. sysk trusts it as it trusts the disk to hold what was written.
The manifest
The device is a line in arch/rv64/devices.sysk, on QEMU's second virtio transport, and the system hands it to one task, which serves a tree. The shell's profile mounts it at /dev, and tools/wire.rkt makes the pair of streams between them, rng-ask-shell and rng-reply-shell (sysk 062, 063):
(virtio-rng (mmio 268443648 4096) (irq 2)) … (profile observer … ("/dev" rng) …) (task rng (device virtio-rng) (heap 4096) (tree (bytes 576)))
When input ends, the shell ends rng-ask-shell and reads rng-reply-shell to its end. Once every holder of its tree has ended (sysk 063; the default system has one, the shell), the driver closes the device and finishes: the deadlock check proves every run can still finish. On a machine without the device, or one that gives no seed, the driver still serves its tree, and every read answers io: the shell says device failed: /dev/rng.
The driver takes the requests of every holder of its tree, through the kernel's Tree (sysk 063, Several clients, one server), each holder with 8 fids of its own: holder w's fid f is the driver's 8w + f. tests/clients/rng.sysk has two readers hold it through one profile, (profile dev-only ("/" rng)): each opens rng on a fid of its own, 1 or 2, reads 8 bytes, and asks 50 times to read the other's fid, which is refused each time, bad-fid, as a fid it never made. A holder whose mount names a directory below the driver's root is answered no-such-name on attach: the tree has no directory but its own (sysk 064). A read that mixes waits for the device inside the request it answers, so the other holder's next request waits for it, once a mebibyte, for as long as the device takes to fill 32 bytes.
What comes next
With one device, /dev is its driver's tree. A second, such as the console at /dev/cons, asks a question best answered with both in view: the shell's namespace could make /dev a union of the drivers' trees, as Plan 9 binds each kernel device into /dev, or one small server could front them. The shell's side was made ready in sysk 048: its namespace is one value, Ns, so a tree more is a field more; and since sysk 053 the namespace is a table the manifest gives, so a union is a mount one name deeper, ("/dev/cons" cons) beside ("/dev" rng), when the second device comes.