Files
A task that holds a Sys can read the kernel's state as files: its tasks, its channels, the time (sysk 028). Every tree of files in sysk will speak one small protocol, Plan 9's 9P cut down to ten operations. The kernel serves its own tree, as Plan 9's kernel serves /proc; the disk's server, a task, speaks the same bytes over a pair of streams (Files on the disk, sysk 029). In the shell, ls /sys/task and cat /sys/time.
The kernel's tree
Directories are green, files blue. Each node is a number, its qid path, which says which task or channel it is about. Every file is one s-expression of at most 512 bytes, rendered when it is read, in one atomic block: a snapshot, never torn. Pick a node.
The names come from the manifest. A task is named by its function, numbered (relay-2) when several share one, and a job slot is slot-N; a channel by its name. tools/wire.rkt generates task-name and channel-name, and who sends and receives on each channel. A pool channel's ends change with each job, so the file says jobs. A task's ns is its namespace, where it finds each tree, each mount with the task that serves it (sysk 053; below); empty for a task the manifest gives none.
A request's path
The person types cat /sys/time. Step through it.
Messages
A request and its reply are each 576 bytes: a 64-byte header and a 512-byte payload, integers little-endian. Fixed sizes keep every message one channel message later, and every bound a constant the proofs use. Reserved bytes must be zero, so later versions can use them: sysk 033 took bytes 32 to 43 of a read for reads that wait.
| Op | Name | What it does | The kernel's tree |
|---|---|---|---|
| 1 | attach | newfid, which must be free, names the root of a tree: the server's first, or the one the payload names (sysk 057) | yes: its own, or prg |
| 2 | walk | newfid (fid itself, or a free one) names what the path in the payload reaches from fid; an open fid does not walk | yes |
| 3 | open | fid, not yet open, is open for what mode asks | reading only |
| 4 | read | up to count bytes from offset, zeros after them; a directory reads as 64-byte entries (a file's length in its entry is 0: stat renders it) | yes |
| 5 | write | count bytes of the payload at offset of an open file | not permitted |
| 6 | clunk | fid names nothing again | yes |
| 7 | stat | the node's qid, type, format, rights, length | yes |
| 8, 9 | create, remove | in directory fid, a node of the type mode says, named by the payload, that newfid names; fid's node gone | not permitted |
| 10 | flush | forget a request still in flight | nothing is ever in flight |
| 11 | lend | what fid names, with the rights mode names (no more than fid's), is the root of the service newfid names for the next job that binds it (sysk 031) | not permitted |
A path is relative: names separated by /, each 1 to 32 bytes, at most 16, never . or ... The whole path is checked before any name is looked up, so whether it is a path never depends on what the tree holds. Without .. a walk can only go down, so a server lent a subtree can never be walked out of. The status says how a request went: 0 ok, 1 no such name, 2 not permitted, 3 bad fid, 4 bad path, 5 no space, 6 io, 7 busy, 9 bad request, 15 exists, 16 not empty; the payload says it again, as (error no-such-name). Every server checks the header the same way, with one function both share, fs-begin: reserved bytes, the op, the count and the offset.
Reads that wait
A client that wants to know when a file changes does not ask again and again. It asks once: a read whose mode is mode-wait, with since, the version of the file it last saw, and wait, how many milliseconds it will wait (sysk 033). The server answers as soon as the file's version is not since, which is at once if it already is not, or when the wait runs out. Either way the answer is a read's answer, the bytes at the offset and the file's version, so the client tells the two apart by the version alone: the same as since means nothing changed. There is no new status, and no event to miss: a client that was slow sees the latest state, not a queue of what happened (research-formats.md S4).
The file server holds a job's waiting read, one at a time, and goes on serving the shell; after each of the shell's requests, and when its clock's alarm comes, it looks at the read again. It holds no task: the job waits for its reply as it always does, and the server, which never waits on a job but through alt, has one more source, its clock. The shell's own reads, and the kernel's tree, answer at once: the person is waiting on the shell, and the kernel answers every request in one atomic step. A version moves when a file's change is stored, at the clunk that ends its write session (Files on the disk); a directory's does not move yet. follow is built on it: follow /log & prints the file, then each line added, until five seconds pass with no change, or as long as its argument says: follow /log --quiet 30000 & (sysk 036; The shell).
Fids
A client names nodes by fids, small numbers the server keeps for it, as file descriptors are. The kernel keeps 8 for each task, in a table of (* ntasks fid-max) words: each entry is its node times 2, plus 1 once open, and 0 for a free fid. sys-serve passes the current task, so a task can only ever name its own fids: the table row is chosen by who asks, not by anything in the request.
fids, task by task: (node × 2) + open shell fid 0: 2 × 4 + 1 ; /time, open fid 1..7: 0 prober-a fid 0: 2 × 1 ; the root fid 1: 2 × 4 + 1 fid 2: 2 × 2 + 1 prober-b fid 0: 2 × 1 fid 1: 0 ; its own: names nothing
Whatever the request
files-serve takes the request's bytes as they come, from any task holding a Sys. Its contract is the whole guarantee: whatever the 576 bytes say, it ends with the kernel's invariant holding, the same task current, and a 576-byte reply. Reading a fid's entry, walking a path, rendering a file: each is bounded by constants, and each step is proven. The test system tests/files sends what no client should, and reads the statuses back:
| Request | Status |
|---|---|
| a reserved byte that is not zero (24 to 63); a count over 512; an offset of 263; op 0; op 200, on any fid | 9 bad request |
| write, create, remove; open for writing | 2 not permitted |
| a free fid; fid 8; fid 232 − 1; newfid 8; attach or walk to a fid in use; open an open fid; read before open; walk from an open fid | 3 bad fid |
task/../time, ./task, task//x, a 33-byte name, nope/../x, 17 names | 4 bad path |
nope; time/x | 1 no such name |
| a directory read at an offset off an entry | 9 bad request |
| a read past a file's end; a directory read with room for less than one entry | 0 ok, no bytes |
| a read from offset 10; stat | 0 ok, zeros after the bytes; a length |
| flush | 0 ok |
Its peer, prober-b, holds a Sys too. While prober-a has fid 1 open, prober-b's fid 1 names nothing, before and after its own attach; prober-a's still reads afterwards. A task that makes itself a Sys is rejected (N003): only kernel/files.sysp makes one, given a Wire, for the tasks the manifest hands (sys).
The namespace
Which tree a path is in, a task learns from the manifest (sysk 053). A (namespace TASK ("PATH" SERVER [TREE]) …) clause lists its mounts: each PATH, / or /NAME, is the tree of the task that receives on SERVER, a stream the task sends its requests on, or the kernel's, sys. A path is under the longest mount that starts it, a whole name at a time: /system is on the disk, /sys/time the kernel's. The rest of the path goes to that tree as its own path, from its root. The task is handed the table as text, (namespace), each server named by the alt source of room on its requests; the kernel shows it, with the servers' names, in /sys/task/NAME/ns. A server that serves jobs too is followed by its service's pool channels, (/net 7 (jobs 5 6)): a job lent something in its tree binds them (sysk 058, Files for jobs). Pick a path.
(task shell … (sys) (namespace) (tx fs-ask) … (tx dev-ask) … (tx net-ask) …)
(namespace shell ("/" fs-ask) ("/dev" dev-ask) ("/net" net-ask) ("/prg" sys prg)
("/sys" sys))
A path that does not start at / starts at the shell's dot, the directory cd chose (sysk 056), as a Plan 9 process's state is its namespace and its dot. The shell makes every path whole before it looks for its mount: from / or dot, a name at a time, . none, .. back one (and / at /), as Plan 9's cleanname. So no server ever sees . or .., and a walk still only goes down; dot is a shorter way to name, not a way to reach more. ls alone lists dot, pwd says it, and completion offers names in it.
The shell keeps the table in its Ns, beside the ends it asks each tree through, and every path it is given goes through tree-of and rest-of, which read it. ls / lists the disk's root, then each mount at the root: dev/ net/ prg/ sys/, from the table, and completing /n offers /net/ the same way. Only the disk's tree makes files. tools/wire.rkt refuses a namespace with nothing at /, a path mounted twice, a path deeper than one name, a server the task does not send on, or sys for a task without (sys), or a tree it does not serve (The manifest).
The programs: /prg
The kernel serves a second tree, prg (sysk 057): a file for each program of the catalogue, named by it. A server may serve several trees, as a Plan 9 server does, and an attach names the one it wants in its payload; the shell's namespace says which, ("/prg" sys prg). A server with one tree refuses a name. Each file is two sections, the first two of a program file as docs/directions.md designs it: (spec ...), what the job check judges the program by, its channels and its arguments, generated from the same contract the check is; and (doc ...), its doc comment from the source, a string for each sentence.
sysk> ls /prg producer filter consumer greeter talker wc follow ping sysk> cat /prg/wc (spec wc (channels (ask tx stream 576 service) (answer rx stream 576 service) (out tx 1 64)) (arguments (lines flag))) (doc "A catalogue program (sysk 031): the bytes and lines of the file it was lent, on one line of its output, ...")
A channel is (NAME tx|rx COUNT|stream BYTES [service]), service marking a service's client end, bound to the service of whatever the job is lent (sysk 058); an argument (NAME flag) or (NAME number LO HI DEFAULT); and (clock) last, for a program that keeps time, as ping does. The text never changes while the system runs, so the kernel reads it where it lies, at any offset: a program's file may be longer than one read, as follow's is, unlike a file the kernel renders. Today the programs are compiled into the image and /prg describes them; when programs are kept on the disk, in a store of entries named by their hash, the same mount names the file server's tree instead, ("/prg" fs-ask prg), and the files gain a third section, the code.
In the shell
In the shell's namespace (above), /sys and below is the kernel's tree, /dev and below the entropy driver's (sysk 046, Devices as files), /net and below netd's (sysk 052, The network), and everything else the disk's, whose root is /. (ls PATH) lists a directory, a directory's name with a / after it, and (ls) lists dot (sysk 056); (cat PATH) prints a file, wrapped to the screen's 63 bytes between fields, so (ended no) stays whole, and inside a string between words (sysk 057). Through sugar, without the parentheses:
sysk> ls /sys/task uart-driver/ terminal/ sugar/ shell/ slot-0/ slot-1/ slot-2/ slot-3/ sysk> cat /sys/task/shell/status (task (name shell) (id 3) (state live) (alarm none)) sysk> cat /sys/chan/screen (channel (name screen) (bytes 64) (depth 2) (queued 0) (ended no) (from terminal) (to uart-driver)) sysk> cat /sys/time (time (ns 37882900) (ticks 37)) sysk> cat /sys/task/../time not a path: /sys/task/../time
The client side is lib/fs.sysp. A request is bytes, whatever carries it, so the library builds them: one function per operation (fs-attach-req, fs-walk-req, fs-open-req, fs-read-req, fs-write-req, fs-create-req, and fs-fid-req for remove, stat and clunk). fs-ask-sys asks the kernel's tree through the gate; a server over streams is asked by the client that holds the streams, as the shell asks the disk's. tests/files/prober.sysp builds its requests the same way. A function both modes reach, and that calls a gate, is compiled twice by sysp (sysp 024): once for tasks, once for the kernel.
The disk's file server writes, creates and removes, with copy-on-write blocks: Files on the disk. A job can be lent part of the disk's tree, read through the same operations: Files for jobs (sysk 031). Not yet: files that wait for events (sysk 032).