The boundary · L4, L1 and L0

Input

A key pressed at the far end of the serial line becomes an interrupt, a signal to the one task that holds the UART, a byte on a stream, and finally a whole line handed to the shell (sysk 018). Nothing on sysk's side edits the line: it arrives edited. Input ends with 0x04, and then every task finishes. A host that speaks syst (sysk 044) sends its lines as (line "…") and its end as (eof) instead; the terminal unquotes them, and the rest of the way is the same.

A typed line, step by step

The transcript from the last make docs typed greeter at the shell's prompt, then ended input. Select a step, or play it. The diagram lights up where the line is; the readouts show the kernel's view of the driver and what the terminal holds.

    Driver signalled
    Claim open
    Driver holds
    keys queued
    Terminal's line
    Ended

    The interrupt path

    The manifest binds the device to its driver, (task uart-driver (device uart) …), and arch/rv64/devices.sysk gives the device its PLIC source: . At boot the kernel enables exactly the sources some task holds (plic-setup sets one bit per source in the enable word, a w32). When the UART has a byte, the PLIC raises the machine's external interrupt and trap.S calls on-device, which claims the source, looks up its owner and marks that task signalled. The kernel does not complete the claim, so the source stays quiet: the open claim is the mask. The driver completes it with device-ack (named uart-ack before sysk 023), which takes the driver's Device, so no other task can unmask a device it does not hold. The disk's driver waits on its device the same way (Storage).

    A signal is a flag, not a count, and it stays set until alt returns the device: an interrupt that comes while the driver is busy is not lost, and one that comes twice is one signal. The driver reads one byte per signal; if more are waiting, the UART's line is still raised and the PLIC signals again as soon as the claim completes. When every task waits, the kernel idles in wfi with interrupts off and handles whatever woke it, the timer or a device, in place.

    Waiting on several sources: alt

    A task with more than one thing to wait for calls (alt k wants start). Its sources are numbered as the manifest lists them: each channel it receives on, then each stream it sends on (ready when its ring has room), then its device, then its clock (ready when its alarm is due: Time). wants holds a byte per source, 1 for one it wants now (bit in lib/choice.sysp makes it from a condition). alt returns the first source at or after start, going round, that is wanted and ready, and the contract says it is one the task wanted. Only the task receives on its channels and only it sends on its streams, so a source that is ready stays ready until the task acts: the recv-more or send-more that follows never waits.

    Fairness is the caller's: a task that starts the next scan just after the source it served, (set start (after s n)), serves every ready source in turn. A test boots a task that does and one that does not, and checks that the second starves a source. The sources in this system:

    TaskSourceReady when

    The driver never blocks in a send. A byte it has read is held while keys is full, and meanwhile it waits for room, not for the UART, so it always keeps draining screen. The terminal does the same with a finished line: it stops reading keys until lines has room. Neither can wait on the other in a cycle: this is exactly what the rule reads off their code, wait by wait.

    Streams, and how it ends

    Nobody knows how many lines a person will type, so keys, lines, commands, answers and screen are streams: channels with no message count, whose one sender closes them when it is done (see Channels). recv-more gives the next message, or the end once the channel is closed and empty. sysp checks that no task closes a receiving end before it has seen the end. The end of input starts a chain that reaches every task:

    1. The terminal reads 0x04 at the start of a line and closes lines.
    2. sugar receives the end of lines and closes commands; the shell receives that end, closes its jobs (every slot finishes) and closes answers.
    3. The terminal prints what is left of answers, to its end. Then it closes screen.
    4. The driver receives the end of screen, closes keys and finishes.
    5. The terminal reads keys to its end, discarding what comes after the end of input, and finishes.

    Input that never ends keeps the system waiting, idle in wfi; a test checks it does not finish until 0x04 arrives.

    Where the line is edited

    sysk does not echo and does not edit. The line belongs to whatever shows it, at the person's end, as in the floor of postterm's line form (postterm's contracts/line-form.md): plain lines both ways, 0x04 for the end of input, 0x03 for Ctrl-C. make run runs a relay, tools/attach.rkt, a syst host since sysk 044, which edits the line itself at a terminal since sysk 050, so that Tab can ask the shell what could come next (syst); with --plain it leaves the person's terminal in its canonical mode, which edits and echoes, and sends each finished line, Ctrl-D on an empty line as 0x04 and Ctrl-C as 0x03, over QEMU's serial socket. make attach shows sysk in a postterm window instead (pt-attach --device), with its line input docked at the bottom. make run-plain leaves the serial line on stdin and stdout, for pipes.

    The terminal task's rules, so that any of those ends works, a raw one too:

    ByteWhat the terminal does
    CR, LF, CR LFeach ends a line; an empty line is a line
    0x04at the start of a line, the end of input; elsewhere ignored
    0x03, ESC, other controlsignored (Ctrl-C means nothing yet: nothing in a closed world has work to cancel)
    0x80 and upkept, so UTF-8 passes through
    the 64th bytea line longer than a message goes over in pieces

    Measured through the relay, a typed line comes back in 1 to 8 ms.

    Known limits

    • Overrun on hardware. A real 16550 holds 16 bytes, about 1.4 ms of a pasted line at 115,200 baud, and the driver may wait longer for its turn. QEMU never overruns, so no test sees it. The fix is a scheduling rule, "a task that becomes signalled runs next"; it is not built.
    • One link, one conversation. The serial line carries the console and nothing else.
    • No regions yet. Every task's output is a line in one stream. postterm's line form has an upgrade, typed s-expression messages with a region per task, designed but built only when sysk needs it (a shell).