OxideBSD

OxideBSD terminals: design specification

Status: accepted design, partly implemented (see §10). Target release: v0.3.0.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119. Interfaces are documented in tty(4), termios(4), console(4) and uart(4); this document records the design. Where the BSDs agree it follows them; where they differ, the majority. Pseudo-terminals build on it and are specified in PTY.md.

1. Scope

The kernel’s terminal layer: terminal devices, the line discipline, controlling terminals and job control, and the first two terminals, the console and a serial line. Today’s terminal state is one set of console-wide globals (sys/console/stdin.rs); this design replaces it.

2. Terminals

2.1. A terminal is a kernel object with its own input queue, termios, window size, session and foreground process group, and an output driver. Every terminal device is one terminal; the state MUST NOT be shared between them.

2.2. The first terminals:

Device Terminal Input Output
/dev/ttyv0 the framebuffer console PS/2 and USB keyboards the ANSI engine and framebuffer (sys/console/vga.rs)
/dev/tty01 the second serial port (COM2, I/O 0x2F8, IRQ 3) UART receive interrupts UART transmit

2.3. /dev/console is the system console, a device of its own in every BSD: output written to it, and kernel messages, go to the terminal that is currently the console (ttyv0), or to the terminal that took it over with TIOCCONS; input read from it comes from ttyv0.

2.3.1. TIOCCONS on a terminal descriptor redirects console output to that terminal, as in all three BSDs; only root may take the console from another terminal, and it reverts when that terminal closes.

2.3.2. The kernel keeps its messages in a message buffer, as every BSD does, in addition to printing them on the console. /dev/klog reads it, for syslogd and dmesg; dmesg moves to sysctl kern.msgbuf once sysctl(3) exists.

2.4. /dev/tty opens the calling process’s controlling terminal, or fails with ENXIO if it has none.

2.5. COM1 is not a terminal. It carries the kernel’s messages. With the boot flag -D (dual console, as in FreeBSD) it also carries a copy of everything written to ttyv0, which is how tests read their results; -h (serial console) sends ttyv0’s output to COM1 only. Without either flag the console is the screen alone: each byte to COM1 costs a port write, an exit to the hypervisor under a virtual machine.

Rationale. NetBSD and OpenBSD name the serial ports tty00, tty01…; FreeBSD’s ttyu0 is the exception. Keeping COM1 (tty00) as the log and test channel, and putting the login line on COM2, avoids moving every test. There is no /dev/tty00 node while COM1 serves this purpose.

2.6. Device numbers (st_rdev): ttyv<n> is major 4, minor n; /dev/tty is (5, 0); /dev/console is (5, 1); tty0<n> (serial port n) is major 6, minor n. oxfs seeds the nodes; opening one opens the kernel terminal, not an oxfs file.

2.7. More virtual terminals (ttyv1… switched with Alt+Fn) MAY be added later; the design does not assume one.

3. Line discipline

3.1. Every terminal applies POSIX general terminal interface semantics (XBD chapter 11) itself, between its device and its readers and writers.

3.2. Input processing (c_iflag): ICRNL, INLCR, IGNCR, ISTRIP, IXON and IXOFF (VSTOP/VSTART flow control), IXANY, IMAXBEL.

3.3. Canonical mode (ICANON): input is held until a line ends (NL, VEOL, VEOL2, or VEOF). VERASE, VWERASE, VKILL, VREPRINT and VLNEXT edit the pending line. VEOF at the start of a line makes read return 0. A read returns at most one line.

3.4. Non-canonical mode: VMIN and VTIME MUST behave as POSIX specifies for all four combinations.

3.5. Echo (c_lflag): ECHO, ECHOE (erase visually), ECHOK, ECHOKE, ECHONL, ECHOCTL (^X for control characters), ECHOPRT. Echo goes to the terminal’s own output.

3.6. Signals (ISIG): the characters in c_cc[VINTR], c_cc[VQUIT] and c_cc[VSUSP] send SIGINT, SIGQUIT and SIGTSTP to the terminal’s foreground process group and flush pending input unless NOFLSH. The characters come from c_cc, not fixed values.

3.7. Output processing (c_oflag): OPOST, ONLCR, OCRNL, ONOCR, ONLRET, OXTABS (tab expansion). Output is bytes: the UTF-8 check on console writes is removed.

3.8. The default termios MUST be 4.4BSD’s TTYDEF_* values (<sys/ttydefaults.h>), which all three BSDs share: ICRNL|IXON|IXANY|IMAXBEL|BRKINT, OPOST|ONLCR, CREAD|CS8|HUPCL, ICANON|ISIG|IEXTEN|ECHO|ECHOE|ECHOKE|ECHOCTL, the standard control characters, 9600 baud for serial lines.

Rationale. A line discipline is required for pseudo-terminals, and gives programs that don’t edit their own input (cat, read, passwd) the behavior every Unix has, including end-of-file from ^D.

4. Reading and writing

4.1. read blocks until data is available as §3 defines. It MUST return EAGAIN for a non-blocking descriptor, and EINTR when a signal with a handler arrives while it waits.

4.2. write blocks while output is stopped (VSTOP) and on a full serial transmit queue, with the same EAGAIN/EINTR rules.

4.3. poll and select report a terminal readable when a read would not block; in canonical mode that means a complete line or end-of-file is pending.

5. Controlling terminals and job control

5.1. A session leader acquires a controlling terminal only with TIOCSCTTY, as in all three BSDs; opening a terminal never does it implicitly, and O_NOCTTY is accepted and has no effect. TIOCSCTTY fails with EPERM if the terminal is another session’s; no BSD offers a way to take it. TIOCNOTTY from a session leader fails with EINVAL, as in NetBSD and OpenBSD; the leader gives up the terminal by exiting (§5.4).

5.2. TIOCSPGRP and tcsetpgrp MUST accept only a process group in the terminal’s session.

5.3. A background process that reads its controlling terminal gets SIGTTIN; one that writes it, with TOSTOP set, gets SIGTTOU; one that changes its settings gets SIGTTOU, unless the signal is ignored or blocked, as POSIX specifies. The defaults of SIGTTIN/SIGTTOU become Stop.

5.4. When a session leader that has a controlling terminal exits, the terminal is hung up: its foreground process group gets SIGHUP and SIGCONT, the terminal stops being the session’s, and later reads by that session’s processes return 0 and writes fail with EIO.

5.5. TIOCSWINSZ stores the window size and sends SIGWINCH to the foreground process group.

5.6. ioctl requests: TCGETS, TCSETS, TCSETSW (drains output first), TCSETSF (also discards input), TIOCGWINSZ, TIOCSWINSZ, TIOCSCTTY, TIOCNOTTY, TIOCGPGRP, TIOCSPGRP, TIOCGSID, FIONREAD, TCFLSH, TCXONC, TCSBRK, TIOCOUTQ, FIONBIO. Each is valid only on a terminal descriptor; on anything else they fail with ENOTTY, so isatty is true only for terminals.

6. Descriptors

6.1. A terminal descriptor is a read-write file description that names its terminal. The bootstrap descriptors 0, 1 and 2 of the first process are one read-write description of ttyv0.

6.2. fstat on a terminal descriptor MUST report the same st_dev, st_ino and st_rdev as stat on its device node, so that ttyname(3) can match them.

6.3. /proc/self MUST name the calling process, and readlink("/proc/<pid>/fd/<n>") MUST return the path of a terminal descriptor’s device (and of any descriptor with a path), which is how musl’s ttyname(3) works.

6.4. /proc/<pid>/stat MUST report the real session, controlling terminal (tty_nr) and terminal foreground process group (tpgid).

7. The console’s keyboard and screen

7.1. The keyboard is input to ttyv0. Its special keys keep today’s Linux-console sequences.

7.2. A process that maps /dev/fb0 owns the screen and the keyboard, as today; while it does, ttyv0 neither draws nor receives keys.

7.3. The cursor-position reply (ESC[6n) goes into ttyv0’s input.

8. Serial line

8.1. The UART driver MUST use receive interrupts (IRQ 3) and a transmit queue; input is never polled.

8.2. c_cflag speed, character size, parity and stop bits MUST program the UART. HUPCL drops DTR on last close; CLOCAL ignores carrier.

8.3. Under QEMU, COM2 is attached with a second -serial option (for example a host pty), so a host terminal emulator or a test can use tty01.

9. Verification

9.1. On-target tests through tty01, driven from the host: canonical line editing and ^D, VMIN/VTIME, echo flags, ^C and ^Z from c_cc, SIGTTIN/SIGTTOU, hang-up on session leader exit, SIGWINCH, ttyname, and poll.

9.2. The existing interactive tests (sendkey into ttyv0) MUST keep passing.

10. Implementation status

As of OxideBSD fde98f6. The work is split into five slices.

10.1. Done: the terminal core (slice 1)

Item Section Where
Per-terminal state; ttyv0 driving the console, its output copied to COM1 (before output processing) with -D 2.1, 2.2, 2.5 sys/tty/mod.rs, sys/tty/console.rs
Canonical mode, VMIN/VTIME, echo flags, input and output processing, flow control 3 sys/tty/mod.rs
Signal characters from c_cc; 4.4BSD default termios 3.6, 3.8 sys/tty/mod.rs
Blocking reads and writes, EAGAIN, EINTR; poll/select readiness 4 sys/tty/mod.rs, sys/net/mod.rs
System call restart (ERESTART, SA_RESTART) 4.1 sys/syscall/mod.rs, sys/process/signals.rs
TIOCSCTTY/TIOCNOTTY rules, TIOCSPGRP limited to the session 5.1, 5.2 sys/tty/mod.rs
SIGTTIN/SIGTTOU, defaulting to Stop 5.3 sys/tty/mod.rs, sys/process/mod.rs
Hang-up when a session leader exits 5.4 sys/process/lifecycle.rs
SIGWINCH on a window-size change 5.5 sys/tty/mod.rs
Terminal ioctls, ENOTTY elsewhere 5.6 sys/syscall/ffi.rs
fds 0-2 of the first process are one read-write description of ttyv0 6.1 sys/fs/fd.rs
The screen’s owner (/dev/fb0) takes the keyboard, except the signal characters 7.2 sys/tty/console.rs
Cursor-position replies go to ttyv0’s input 7.3 sys/console/vga.rs

Verified: session_syscall_smoke (the BSD controlling-terminal rules), plus basic_boot, poll, ppoll, sh, sig, fd, keyevent, init_respawn and rc passing on the new layer. A live boot driven through QEMU’s sendkey confirmed line editing, ^D end-of-file, ^C (status 130), and ^Z with jobs and kill %1.

The same slice fixed a scheduler re-entrancy bug: an interrupt that landed in the scheduler’s idle loop called schedule() again, which halted the system with a process marked Running. Interrupt handlers now reschedule only when they interrupted user code.

10.2. Done: devices and descriptors (slice 2)

Item Section Where
Nodes /dev/ttyv0 (4, 0, root:tty 0600), /dev/tty (5, 0, 0666), /dev/console (5, 1, 0600); group tty (4) 2.6 sys/modules/oxfs
Opening a terminal node opens the kernel terminal (oxidebsd_tty_open), honoring the access mode and O_NONBLOCK; no such terminal is ENXIO 2.6 sys/tty/mod.rs
/dev/tty opens the caller’s controlling terminal, or fails with ENXIO 2.4 sys/tty/mod.rs
fstat on a terminal descriptor reports its node’s st_dev, st_ino, st_rdev; pipes and sockets report S_IFIFO/S_IFSOCK 6.2 sys/modules/oxfs (stat_real_fd)
/proc/self; /proc/<pid>/fd/<n> are symlinks: a terminal’s node, a file’s path ((deleted) once unlinked, following renames), pipe:[N], socket:[N], anon_inode:[mqueue] 6.3 sys/modules/oxfs, sys/fs/fd.rs (FdKind)
/proc/<pid>/stat reports the session, tty_nr and tpgid 6.4 sys/process/procfs.rs
/etc/ttys runs getty on ttyv0; console stays, off, for its secure flag — etc/ttys

Verified: tty_syscall_smoke (regress/tty-smoke/main.c: the nodes, musl’s ttyname(3), /dev/tty with and without a controlling terminal, the /proc links and fields).

10.3. To do

Slice 3: remaining job control. 1. Blocked readers woken by a caught signal (EINTR/restart) for every blocking path, not only terminals: signal_foreground_group’s SetPending path doesn’t wake a terminal reader today. 2. Orphaned process groups: SIGTTIN gives EIO instead of stopping (POSIX).

Slice 4: the serial line (§8). 1. A 16550 driver for COM2 (0x2F8, IRQ 3), with receive interrupts and a transmit queue, registered as terminal tty01 (6, 1), with node /dev/tty01. 2. c_cflag programs speed, character size, parity and stop bits; HUPCL and CLOCAL. 3. scripts/qemu_common.sh attaches COM2 to a host pty, on request. 4. /etc/ttys: tty01 as onifexists.

Slice 5: the console device and the message buffer (§2.3). 1. /dev/console as its own device: output goes to the console terminal, or to the terminal that took it with TIOCCONS; input comes from ttyv0. 2. A kernel message buffer holding every kernel message, readable through /dev/klog.

Tests (§9). 1. On-target tests through tty01, driven from a host pty: line editing, VMIN/VTIME, echo flags, signal characters, job control, hang-up, ttyname, poll. 2. A sendkey-driven console test, since no current test types into the console, and the idle-loop bug in §10.1 needs a keyboard interrupt that arrives while nothing is runnable.

Known limitations. 1. /dev/console opens ttyv0 until slice 5 makes it a device of its own. 2. F_GETFL doesn’t report a descriptor’s access mode (a general fcntl gap, not terminals’). 3. open("/proc/<pid>/fd/<n>") doesn’t reopen the descriptor; only readlink and stat follow the link. 4. Output is written synchronously, so TCSETSW does not wait for anything, and TIOCOUTQ reports 0. 5. IUCLC/OLCUC (upper-case terminals) are not implemented. 6. O_NOCTTY is accepted and ignored, which is correct because opening a terminal never acquires it.

11. Open questions

  1. The message buffer’s size, and whether it survives a warm reboot as FreeBSD’s does.

Source: TTY.md