175 lines
9.4 KiB
Markdown
175 lines
9.4 KiB
Markdown
# Character devices, the console, and the tty question
|
||
|
||
A design note for the **stream** half of the device world. danos has block devices
|
||
(the USB storage service behind the FAT mount) but no character devices — and three
|
||
tracks now need them at once: the terminal application, Zig self-hosting Phase 1
|
||
("wire fd 0/1/2 to a console byte stream"), and [Python on danos](python-on-danos.md)
|
||
Phase 1. This note settles what a character device *is* on danos before any of those
|
||
tracks build one.
|
||
|
||
## The Unix picture, briefly
|
||
|
||
Unix splits devices in two: **block devices** are seekable arrays of fixed-size
|
||
sectors (disks); **character devices** are unseekable byte streams (keyboards,
|
||
serial ports, terminals, `/dev/null`, entropy). A **tty** is the canonical
|
||
character device — a byte stream plus a *line discipline* (echo, line buffering,
|
||
erase handling, Ctrl-C-to-signal) that lives in the kernel. A **pty** is a pair of
|
||
character devices (master/slave) that exists so a *userspace* program — a terminal
|
||
emulator — can impersonate terminal hardware to the kernel's in-kernel line
|
||
discipline.
|
||
|
||
The identification asked for and confirmed: yes, tty and pty are character
|
||
devices in this taxonomy.
|
||
|
||
## The realization that shapes everything: danos already has the mechanism
|
||
|
||
A Unix character device is an in-kernel dispatch table: major/minor numbers route
|
||
`read()`/`write()` to a driver. danos already has exactly that dispatch — the VFS:
|
||
`fs_resolve` routes a path to a mounted backend service, and `Operation.mount`
|
||
attaches a backend *endpoint* at a prefix. What is missing is not a device model;
|
||
it is **one node kind with stream semantics**. And the protocol already reserved
|
||
it: `NodeKind.character_device = 2` sits unimplemented in
|
||
[vfs-protocol.zig](../library/protocol/vfs/vfs-protocol.zig), exactly like
|
||
`symbolic_link`.
|
||
|
||
So the design is small:
|
||
|
||
**A character device on danos is a VFS node, served by an ordinary service over
|
||
the existing VFS wire protocol, whose read/write have stream semantics.**
|
||
|
||
No device numbers, no `/dev` special casing, no new syscalls, no new protocol —
|
||
a service is reachable at a path, clients open it with `runtime.fs` like any
|
||
file, and the node kind says what it is. (Since the protocol namespace landed
|
||
in design, that path is `/protocol/console` — a protocol node, see
|
||
[os-development/protocol-namespace.md](os-development/protocol-namespace.md) —
|
||
rather than a mounted device file; the stream semantics below are unchanged.)
|
||
|
||
### Stream semantics (the actual contract change)
|
||
|
||
For a node whose kind is `character_device`:
|
||
|
||
- **`offset` is ignored** on read and write; there is no seek position. (`lseek`,
|
||
when the C layer exists, returns `ESPIPE`.)
|
||
- **Reads block** until at least one byte is available, then return what is there —
|
||
**short reads are normal**, not EOF. A zero-length read reply means the stream
|
||
is closed (hangup), not end-of-file-at-size.
|
||
- **`FileStatus.size` is 0** and means nothing; `mtime` may be 0.
|
||
- Writes may be short if the service's buffer is full; the client loops as it
|
||
already must for the 256-byte message cap.
|
||
|
||
This is a semantics note on existing operations, not a wire change — the `Request`
|
||
and `Reply` structs are untouched. The one true protocol addition is a **`control`
|
||
operation** (appended to `Operation`, values stable): a typed request the stream's
|
||
service interprets. Deliberately *not* an `ioctl` grab-bag — the control payloads
|
||
are enumerated per protocol, starting with the terminal set below.
|
||
|
||
## The first character device is a pseudo-device
|
||
|
||
The first device is deliberately **not hardware**: an in-memory **loopback** — a
|
||
byte queue served over the stream contract, where bytes written to one end are
|
||
read from the other. It is the reference implementation of the semantics above
|
||
(blocking reads, short reads, hangup on close, the `control` round-trip), it
|
||
tests deterministically with no QEMU serial scripting, and it keeps hardware off
|
||
the critical path entirely. `null` and `zero` come along nearly for free as
|
||
degenerate cases. This is a decision, not a convenience: the dead-COM1 boot bug
|
||
on real hardware already proved serial cannot be assumed present or alive, so
|
||
**nothing in this milestone writes to COM1**. (A serial-backed stream node can
|
||
exist *later* as one more optional backend for headless debugging; it is on
|
||
nobody's critical path.)
|
||
|
||
The loopback is also not throwaway — it is the seed of P5's `pipe()`, which is
|
||
the same object with two fds.
|
||
|
||
## The console service
|
||
|
||
A `console` service owns the line discipline — **in userspace**, where a
|
||
microkernel wants it, not in the kernel as Unix has it:
|
||
|
||
- **The discipline is a pure library first**: bytes and key events in, bytes
|
||
out, no I/O of its own — developed and host-tested against in-memory buffers,
|
||
then shared verbatim between the console and the future terminal application.
|
||
- **Input**: subscribes to keyboard `InputEvent` IPC (the structured events that
|
||
exist today) and cooks them into bytes. Cooked mode is the default: echo, line
|
||
buffering, backspace/erase, so a line is delivered on Enter. Raw mode delivers
|
||
bytes as they come (the REPL's line editor and any full-screen program need it).
|
||
- **Output is a pluggable sink**, and the stream contract is independent of it:
|
||
the bring-up sink is in-memory (readable back by tests, mirrored to the boot
|
||
log), and the real one is the framebuffer text renderer when the display
|
||
track's font work lands.
|
||
- **Control set** (the `control` payloads): mode raw/cooked, echo on/off, and
|
||
window-size query — the minimal termios. Ctrl-C-to-signal joins when M17
|
||
signals-over-IPC lands; until then Ctrl-C is just a byte.
|
||
- Mounts itself at `/device/console` as a `character_device` node.
|
||
|
||
**fd 0/1/2** then stop being special: spawn hands the child three open handles
|
||
(console by default; anything else if the parent chooses), and `runtime`'s fd
|
||
table maps 0/1/2 to them. `isatty` is simply "does `status` say
|
||
`character_device`" — no side channel needed.
|
||
|
||
## The pty answer: there is no pty
|
||
|
||
The pty exists in Unix *because the line discipline is in the kernel* — userspace
|
||
terminal emulators need a kernel gadget to impersonate hardware. On danos the
|
||
terminal emulator is already a userspace server, so the pair collapses:
|
||
|
||
**The graphical terminal application serves the VFS stream protocol itself and
|
||
hands its own endpoints to the children it spawns as their fd 0/1/2.**
|
||
|
||
The terminal *is* the console service for its children — same protocol, same
|
||
control set, same line discipline code (shared as a library with the boot
|
||
console). No master/slave device pair, no `/dev/pts`, no new kernel object. When
|
||
CPython arrives, the libc's `isatty`/read/write see a character device and are
|
||
none the wiser; when xonsh eventually wants job control, that lands as control
|
||
messages + M17 signals, still with no pty object.
|
||
|
||
What this costs: programs that *specifically* manipulate Unix ptys
|
||
(`os.openpty()`, `pexpect`-style tools) have no direct equivalent — the danos
|
||
answer is "spawn the child yourself with your own stream endpoints," which is the
|
||
same capability with less machinery. Accepted.
|
||
|
||
## Milestone slicing
|
||
|
||
1. **pseudo-devices** — VFS honors `character_device` semantics end to end;
|
||
`Operation.control` added; the in-memory **loopback** (plus `null`/`zero`)
|
||
as the first device. QEMU test: one client writes, another reads — open,
|
||
offsetless read/write, blocking read, short read, hangup on close, control
|
||
round-trip. No hardware anywhere.
|
||
2. **console-service** — the line-discipline library (host-tested, pure) plus
|
||
the console composing keyboard `InputEvent`s with an in-memory output sink;
|
||
mounted at `/device/console`. QEMU test injects key events and reads cooked
|
||
lines and raw bytes back through the sink.
|
||
3. **fd-inheritance** — spawn passes 0/1/2 handles; `runtime` fd table; `isatty`
|
||
via `status`; existing binaries' stdout migrates from `debug_write` to fd 1
|
||
(the logger keeps its own path).
|
||
4. **terminal-as-server** — deferred to the terminal application milestone
|
||
(Python track P3): the terminal reuses the discipline library and serves its
|
||
children directly.
|
||
|
||
Steps 1–3 are exactly the shared seam that Zig self-hosting Phase 1 and Python
|
||
Phase 1 both list; neither track repeats them.
|
||
|
||
## Decisions needing sign-off
|
||
|
||
- **No pty object; the terminal serves its children directly** (the section
|
||
above) — the load-bearing simplification.
|
||
- **`control` as an enumerated, typed operation** rather than an ioctl-style
|
||
opaque pass-through.
|
||
- **Line discipline in userspace services** (console + terminal, shared library),
|
||
never in the kernel.
|
||
|
||
## Related
|
||
|
||
- [python-on-danos.md](python-on-danos.md) — consumes this as its Phase 1.
|
||
- [zig-self-hosting.md](zig-self-hosting.md) — ditto ("stdio as fds").
|
||
- [file-system-development/vfs-protocol.md](file-system-development/vfs-protocol.md) —
|
||
the wire protocol this note extends.
|
||
- [file-system-development/file-system-hierarchy.md](file-system-development/file-system-hierarchy.md)
|
||
— the tree the console surfaces in.
|
||
- [os-development/protocol-namespace.md](os-development/protocol-namespace.md) —
|
||
supersedes this note's device-node naming: the console lands as a protocol
|
||
(`/protocol/console`, a protocol node), not a `/dev`-style device file. The
|
||
stream semantics designed here (line discipline, cooked/raw modes) carry over
|
||
unchanged.
|
||
- [device-driver-development/input.md](device-driver-development/input.md) — the
|
||
`InputEvent` stream the console cooks.
|