kernel (threads, demand paging, TLB shootdowns, dynamic linking, RAM direct map), syscalls, filesystem (/proc, /sys, /dev), terminals (12, VT handshake, ptys), networking (r8169 through LDLS), LDLS (module sets; Linux code may ship), devices (what stays in the kernel; hybrid.md's lasting content), boot (the loader's placement of the kernel), graphics (fbdev, GMMOS, QR 1-40), audio (ALSA over OSS), debugging, usb. New: packages.md (the apk root) and desktop.md (Xorg on the framebuffer). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
425 lines
20 KiB
Markdown
425 lines
20 KiB
Markdown
<!-- SPDX-License-Identifier: GPL-2.0-or-later -->
|
|
# Virtual terminals
|
|
|
|
Twelve of them, `/dev/tty1` through `/dev/tty12`, one per function key.
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Ctrl+Alt+F1 .. F12** | go to that terminal — on the console and under X alike |
|
|
| **Alt+F1 .. F12** | the same, on the text console only (X keeps plain Alt) |
|
|
| **Ctrl+Alt+-** / **Ctrl+Alt+=** | previous / next terminal *in use* |
|
|
| `chvt N` | the same, from a script or a serial console |
|
|
|
|
The chord is settable — `chvt -c` — for the reasons below. Each one is an independent terminal: its own screen, its own
|
|
cursor, its own line discipline, its own foreground process group and its own
|
|
session. Two shells on two terminals are two unrelated processes that happen to
|
|
share a keyboard and a screen.
|
|
|
|
A display server takes one of them with `VT_SETMODE` and does its own
|
|
switching; X switches with Ctrl+Alt+F-key, which is why that is the default
|
|
here too.
|
|
|
|
---
|
|
|
|
## What makes switching possible
|
|
|
|
A backing store, and nothing else.
|
|
|
|
Before this, the console drew straight into the framebuffer and scrolled by
|
|
moving pixels. That works exactly as long as nothing else ever wants the
|
|
screen: there was nowhere to put terminal 2 while terminal 1 was visible, and
|
|
nothing to repaint terminal 1 from when it came back.
|
|
|
|
So each terminal owns a grid of cells and the framebuffer shows whichever grid
|
|
is active. `vt_switch()` is three lines — set the active index, repaint from
|
|
the grid, return — because nothing has to be *saved* on the way out. The
|
|
outgoing terminal's grid has been current all along.
|
|
|
|
The rule the whole layer is built on:
|
|
|
|
> **A terminal does not know whether it is visible.**
|
|
|
|
Writes land in the grid, the cursor moves, the grid scrolls, all of it
|
|
unconditionally. Drawing is a separate question, asked once, at the bottom of
|
|
`kernel/dev/vt.c`, answered by comparing against `vt_active`. Every alternative
|
|
spreads "am I on screen" through the escape parser, and then a terminal
|
|
switched away from mid-sequence comes back wrong.
|
|
|
|
## A cell
|
|
|
|
Eight bytes:
|
|
|
|
| | |
|
|
|---|---|
|
|
| `ch` | the Unicode codepoint, 32 bits |
|
|
| `fg`, `bg` | palette indices, 0-15 ANSI plus two theme defaults |
|
|
| `attr` | reverse video; room for underline and the rest |
|
|
| `pad` | to eight, so the grid indexes without a multiply |
|
|
|
|
`ch` was one byte holding a CP437 glyph, not a codepoint: the console mapped
|
|
UTF-8 down to CP437 on the way in, because that is what `include/gfx/font.h`
|
|
contained. Widening it to the codepoint is what made Ą, Ę, Ś, Ω and the rest
|
|
possible, and no font change could have done it alone — a byte cannot *hold*
|
|
U+0104 whatever is drawing it. Mapping on the way in also meant the decision
|
|
was made once, at write time, and a later font could not revisit it.
|
|
|
|
The cost is real: the grid went from four bytes per cell to eight, and six
|
|
terminals at 240x75 is 864 KB of BSS rather than 432 KB. Paid once at link
|
|
time, which is the only way to pay it — there is no contiguous multi-page
|
|
allocator to ask.
|
|
|
|
The colours stay indices rather than RGB, for the same reason they always
|
|
were: a cell holding two 32-bit colours would be sixteen bytes.
|
|
|
|
## What is per-terminal
|
|
|
|
Everything that used to be a single global:
|
|
|
|
| Was | Now |
|
|
|---|---|
|
|
| `int tty_fg_pgrp = 1;` | `v->fg_pgrp`, one per terminal |
|
|
| one `struct termios tio` | `v->tio` |
|
|
| one 256-byte input ring | `v->in` |
|
|
| one escape-parser state | `v->esc_*` |
|
|
| one UTF-8 decoder | `v->u8_*` |
|
|
| one cursor, in pixels, in `gfx.c` | `v->cx`, `v->cy`, in cells |
|
|
|
|
`TIOCSCTTY` used to be `return 0` — it claimed to make a terminal the caller's
|
|
controlling terminal and did nothing, because there was one terminal and
|
|
nothing to choose between. It records `proc->ctty` now, which is what
|
|
`/dev/tty` resolves through.
|
|
|
|
## The devices
|
|
|
|
| Path | Minor | What |
|
|
|---|---|---|
|
|
| `/dev/console` | 1 | the **active** terminal, so kernel messages follow a switch |
|
|
| `/dev/tty` | 10 | the caller's **controlling** terminal, via `proc->ctty` |
|
|
| `/dev/tty0` | | the active terminal, which is what Xorg and `openvt` ask for |
|
|
| `/dev/tty1`..`tty12` | 11-22 | that terminal, whether or not it is on screen |
|
|
| `/dev/ptmx`, `/dev/pts/N` | | pseudo-terminals, below |
|
|
|
|
`/dev/tty` was an alias for `/dev/console` with the same major and minor. They
|
|
are different questions and now have different answers.
|
|
|
|
## The ioctls
|
|
|
|
Linux's numbers, because they do Linux's job.
|
|
|
|
| | |
|
|
|---|---|
|
|
| `VT_ACTIVATE` | switch. Argument is a 1-based number **by value**, not a pointer |
|
|
| `VT_WAITACTIVE` | returns immediately: `vt_switch` repaints synchronously, so there is nothing left to wait for |
|
|
| `VT_GETSTATE` | active terminal and a bitmask of those in use |
|
|
| `VT_OPENQRY` | the first terminal nobody has open |
|
|
| `VT_GETMODE`/`VT_SETMODE` | accepted and ignored — the kernel drives switching and there is no process mode to set |
|
|
| `VT_GETCHORD`/`VT_SETCHORD` | SchOS's own: which key combination switches. See below |
|
|
| `KB_GETLAYOUT`/`KB_SETLAYOUT`/`KB_LISTLAYOUT` | SchOS's own: the keyboard layout, by name |
|
|
|
|
## The keyboard
|
|
|
|
`kbd.c` turns scancodes into characters and chords. `keymap.c` holds the
|
|
layouts. Between them is one rule, and it is the rule the whole keycode space
|
|
is built around:
|
|
|
|
> **A key produces either a Unicode codepoint or a value above `0x10FFFF`.
|
|
> Nothing in between means anything.**
|
|
|
|
The chord codes always obeyed it. The navigation keys did not: `KEY_UP` was
|
|
`0xE2`, and `0xE0..0xE9` are exactly the UTF-8 lead bytes for three-byte
|
|
characters. Typing any of U+0800..U+FFFF — most punctuation, every arrow, every
|
|
maths symbol, the whole of CJK — produced a value the console read as an arrow
|
|
key and turned into `ESC [ A`. `∞`, `→`, `≤` and `'` vanished from the input
|
|
line while `ą` and `ę` came through, because those are two-byte sequences whose
|
|
lead byte is `0xC4` and nothing claimed `0xC4`. They are `KBD_*` now and sit
|
|
above every codepoint there is.
|
|
|
|
What the chord *means* is decided in `console.c`, where the terminals are. The
|
|
keyboard driver names it and stops.
|
|
|
|
### Layouts
|
|
|
|
Three, in the kernel: `us`, `pl` (Polish programmers), `de` (QWERTZ). `loadkeys`
|
|
lists and switches them; `/etc/vconsole.conf` records the choice and the
|
|
`os.schos.keymap` job applies it at boot.
|
|
|
|
**In the kernel, not in files.** Linux's `loadkeys` compiles a text keymap and
|
|
uploads it a key at a time through `KDSKBENT`, which puts the data in userspace
|
|
— and a machine whose `/usr` has not mounted then has no layout at all. Here
|
|
the kernel always has a working keyboard and userspace only names which one.
|
|
|
|
A layout is four tables of codepoints — plain, Shift, AltGr, Shift+AltGr —
|
|
indexed by scancode. Two things are deliberately *not* tables:
|
|
|
|
- **Ctrl is derived from the plain codepoint**, not looked up. The table it
|
|
replaced was indexed by scancode, which quietly assumed US: on QWERTZ the Z
|
|
and Y keys swap, so Ctrl+Z would have suspended a job only on some layouts.
|
|
- **Caps Lock is a case toggle on the result**, so `ą` from the AltGr level
|
|
becomes `Ą` — which no second table could have said, because Caps Lock
|
|
combines with AltGr rather than replacing it.
|
|
|
|
Codepoints and not bytes is the entire point: `ó` fits in Latin-1 and `ą` does
|
|
not, and half a Polish alphabet is not a Polish layout. `kbdgetc()` encodes to
|
|
UTF-8 and hands the bytes to `consoleintr()` one call at a time, because that
|
|
function's contract is bytes — the serial line delivers UTF-8 already and
|
|
nothing could tell an encoded codepoint from a byte of the same value.
|
|
|
|
**No dead keys.** On a real German layout the acute and circumflex keys wait
|
|
for the next key and combine; here they type the accent alone. It needs
|
|
per-keyboard pending state that this interface has no room for, so layouts that
|
|
depend on it — French AZERTY, Spanish — are not shipped rather than shipped
|
|
wrong.
|
|
|
|
### Locale
|
|
|
|
`/etc/locale.conf`, read by `/etc/profile` and `/etc/zsh/zshenv`, default
|
|
`LANG=C.UTF-8`.
|
|
|
|
**Only the charset half of it is real.** musl decodes and encodes UTF-8
|
|
whatever the locale says and ships no locale data, so collation stays codepoint
|
|
order and messages stay English whichever language is named. What `LANG` does
|
|
is tell the programs that ask that the terminal is UTF-8 — an unset `LANG`
|
|
reads as "ASCII only", and that is what makes a program fall back to `?`.
|
|
|
|
### The chord is a setting, and that is the point
|
|
|
|
**There is no combination that is free for everyone on every host**, and a
|
|
guest cannot find out what its host takes: a key the host swallowed is
|
|
indistinguishable from a key nobody pressed. Three parties have a claim on
|
|
every candidate.
|
|
|
|
| | |
|
|
|---|---|
|
|
| **The host takes it, and the guest never sees it** | |
|
|
| `Alt+F4`, `Alt+F2` | window manager |
|
|
| `Ctrl+Alt+F1..F12` | display manager — and it matches **loosely**, so adding Shift does not get past it |
|
|
| `Ctrl+Alt+`digit | QEMU's own console tabs |
|
|
| **Applications want it, and lose it permanently if the kernel eats it** | |
|
|
| `Alt+`anything | Meta. zsh binds Meta-digit as `digit-argument`; so does every readline editor |
|
|
| `Ctrl+`letter | control characters |
|
|
| `Ctrl+Shift+`digit | types a character — `Ctrl+Shift+2` is `^@` |
|
|
| `F1..F12` alone | help, and whatever a full-screen program wants |
|
|
| **The keyboard layout wants it** | |
|
|
| `AltGr+`letter | Polish diacritics — ą ć ę ł ń ó ś ź ż — and on some setups AltGr arrives as Ctrl+Alt |
|
|
|
|
So the kernel ships a default and lets it be moved:
|
|
|
|
```sh
|
|
chvt # report the active terminal and the current chord
|
|
chvt -c ctrl+alt+F # Ctrl+Alt+F1..F12 (the default)
|
|
chvt -c altgr+F # AltGr+F1..F12 (the default until 2026-10)
|
|
chvt -c ctrl+shift+F # Ctrl+Shift+F1..F12
|
|
chvt -c alt+1 # Alt+1..9
|
|
chvt -c off # no chord; chvt N is then the only way
|
|
```
|
|
|
|
`off` is a legitimate setting, not a failure mode. On a machine whose host
|
|
grabs everything worth binding, `chvt N` from a shell is the honest answer.
|
|
|
|
The default is **Ctrl+Alt+F1..F12**. It was AltGr+F-key for a long time —
|
|
AltGr with a **function key** produces nothing on any layout, no window manager
|
|
binds it, and no application asks for it — but X owns the keyboard while it
|
|
runs and switches only with Ctrl+Alt+F-key, so the console and the desktop
|
|
answered different keys for the same thing. One chord everywhere wins. Inside
|
|
a VM whose host grabs Ctrl+Alt+F-key, `chvt -c altgr+F` brings the old one
|
|
back.
|
|
|
|
That distinction is only expressible because Right Alt is tracked as its own
|
|
modifier bit. It used to share `ALT`, which is harmless for typing — neither is
|
|
used to look up a character — and made this impossible to say.
|
|
|
|
AltGr+letter still reaches the shell untouched, so ą is unaffected either way.
|
|
|
|
Whatever the modifiers are, they also bind **minus** and **equals** to the
|
|
previous and next terminal. Those step only through terminals that are *in
|
|
use* — the same `opened` count `VT_GETSTATE` reports — because twelve terminals
|
|
with two shells on them means four blank screens in between, and cycling past
|
|
them is worse than not having the key.
|
|
|
|
Which chords reach a guest at all was **measured, not assumed**: a temporary
|
|
probe in `kbdgetc` logged every modified keypress, and QEMU's `sendkey` fired
|
|
each candidate at it. Everything arrived, `Ctrl+Alt+F2` included — so that
|
|
conflict is the host's display manager, not QEMU, and no amount of extra
|
|
modifiers gets around a grab that matches loosely.
|
|
|
|
The modifier test is an **equality**, not a mask:
|
|
|
|
```c
|
|
if (vt_chord_mods != 0 &&
|
|
(shift & (CTL | SHIFT | ALT | RALT)) == vt_chord_mods && ...)
|
|
```
|
|
|
|
A mask fires on any superset, so a chord aimed at the host would switch
|
|
terminals behind your back. Right Alt is tracked as its own bit (`RALT`) for
|
|
the same reason; it used to share `ALT`, which is harmless for typing — neither
|
|
is used to look up a character — and wrong for chords.
|
|
|
|
`include/uapi/posix.h`'s `VTC_*` bits mirror `include/dev/kbd.h`'s modifier bits, and a
|
|
`_Static_assert` in `kbd.c` holds them together. A mismatch would be silent:
|
|
a chord set from userspace would simply never match, and the only symptom
|
|
would be a key that does nothing.
|
|
|
|
### Taking the chord back
|
|
|
|
Three layers, in increasing order of how much a program is claiming:
|
|
|
|
| | |
|
|
|---|---|
|
|
| **`VT_SETMODE(VT_PROCESS)`** | the terminal's owner handles switching itself. The kernel stops acting on the chord entirely. This is what a display server uses. |
|
|
| **`EVIOCGRAB` on `/dev/input/event0`** | an exclusive keyboard grab. Checked in `kbdgetc` *before* the chord is even recognised, so a grabbing program receives every scancode including this one. |
|
|
| nothing | the kernel switches, which is the default and right for a plain terminal |
|
|
|
|
`VT_SETMODE` used to be accepted and ignored — a program could ask for the
|
|
chord, be told yes, and still not get it. It works now.
|
|
|
|
What `VT_PROCESS` does **not** do yet is the release handshake: Linux signals
|
|
the owning process on every switch request and blocks until it answers
|
|
`VT_RELDISP`, which is how a display server releases the GPU before the screen
|
|
changes hands. Nothing here has anything to release, so the kernel simply
|
|
stops switching rather than negotiating. `struct vt_mode` already carries
|
|
`relsig`/`acqsig` for when Schcom needs it.
|
|
|
|
Input goes to whichever terminal is on screen. That is what makes a terminal a
|
|
terminal: the keyboard follows the eyes.
|
|
|
|
## Two rules that are easy to get wrong
|
|
|
|
**The grid is the only source of truth for the size.** `TIOCGWINSZ` reports
|
|
`v->cols`/`v->rows`, not a fresh `gfx_cols()`/`gfx_rows()`. When it computed
|
|
its own answer there were two, and they disagreed: the grids are sized at
|
|
`vt_init()`, which runs before the display driver is found on the PCI bus, so
|
|
they were 80x24 while `TIOCGWINSZ` said 160x50. Text stopped two thirds of the
|
|
way down the screen, `ls` formatted for a width the grid did not have and split
|
|
words across the wrap, and zsh's cursor tracking was wrong on every line --
|
|
three symptoms, one disagreement. `vt_resize()` now corrects the grids when a
|
|
scanout arrives, and the ioctl reads them.
|
|
|
|
**A glyph in the last column does not wrap.** It sets `wrap_pending`, and the
|
|
wrap happens when the *next* glyph arrives. Every real terminal behaves this
|
|
way and none of them document it clearly. Wrapping immediately makes a line
|
|
exactly as wide as the screen take two rows instead of one, and makes it
|
|
impossible to write the bottom-right cell without scrolling -- and zsh's
|
|
partial-line marker depends on the rule directly: it writes exactly
|
|
`COLUMNS - 1` spaces and expects the cursor to still be on that line. With
|
|
immediate wrap, a reverse-video `#` appeared above every prompt.
|
|
|
|
Anything that moves the cursor deliberately -- CR, LF, backspace, an escape
|
|
sequence -- clears the debt.
|
|
|
|
## Ctrl-C used to kill the shell
|
|
|
|
Worth its own note, because the cause was one character and the effect was a
|
|
machine you could not use.
|
|
|
|
When Ctrl-C arrives, `consoleread()` discards the partial line and breaks off
|
|
the read in progress. It returned **`-1`** to do so — and `-1` is `-EPERM`. So a
|
|
program that handles `SIGINT` by carrying on saw a permanent error instead of
|
|
an interrupted call, and exited:
|
|
|
|
```
|
|
zsh: error on TTY read: operation not permitted
|
|
schinitd: restarting os.schos.console-shell
|
|
```
|
|
|
|
and then the next Ctrl-C did the same thing to the replacement. A shell that
|
|
cannot survive Ctrl-C is a shell nobody can use, and the supervisor faithfully
|
|
restarting it made it a loop.
|
|
|
|
It returns **`EINTR`** now, which is what a signal-interrupted read is required
|
|
to give: the whole convention — retry, or return to the caller — is built on
|
|
receiving that and not something else.
|
|
|
|
## Editing a line you have already typed
|
|
|
|
Two bugs, both in the same half-second of using a shell, and both worth
|
|
recording because the symptom pointed away from the cause.
|
|
|
|
**Arrow keys corrupted the next two keystrokes.** They arrive from the keyboard
|
|
as single keycodes and were pushed into the input buffer exactly like that. No
|
|
terminal has ever worked that way. `consoleintr()` translates them to the
|
|
DEC/xterm sequences now -- `ESC[A`, `ESC[D`, `ESC[5~` and the rest -- which is
|
|
what every terminfo entry and every line editor already expects.
|
|
|
|
The *value* those keycodes had was a second bug wearing the same coat, and it
|
|
outlived the fix: `KEY_UP` was `0xE2`, a UTF-8 lead byte, so a character typed
|
|
from the other direction was eaten just as thoroughly. See the keycode rule
|
|
above.
|
|
|
|
**Then the end of the line vanished as you typed into it.** With the arrows
|
|
working, editing mid-line left the buffer perfectly correct and the screen
|
|
wrong, which is a maddening pair. The cause is one line in `vt_putc()`:
|
|
backspace *erased the cell it moved onto*.
|
|
|
|
That is what a line-discipline erase does. It is not what the BS control
|
|
character does — a program that wants to rub a character out sends `\b \b`,
|
|
backspace, space, backspace, and the separation matters because programs also
|
|
use plain `\b` simply to go back. zsh does, on every mid-line edit:
|
|
|
|
```
|
|
echo alpha \b\b zha \b\b
|
|
```
|
|
|
|
Move back two, rewrite the tail, move back two again to leave the cursor where
|
|
the user expects it. Against a destructive backspace that last pair deleted the
|
|
`ha` it had just drawn.
|
|
|
|
`\b` moves and nothing else now; `0x7f` is ignored, as it is on a real
|
|
terminal; and `consputc()` spells its own erase out as `\b \b`, exactly as it
|
|
already did for the serial line. `ESC[@`, `ESC[P` and `ESC[X` — insert, delete
|
|
and erase characters — were added at the same time, because `TERM=linux`
|
|
declares them and a terminal that drops them silently is the same class of bug
|
|
waiting for a different editor.
|
|
|
|
## Handing a terminal to a program
|
|
|
|
A display server takes a terminal with `VT_SETMODE(VT_PROCESS)` and `KDSETMODE(KD_GRAPHICS)`,
|
|
and from then on switching away from it is a **request**: the kernel sends the owner
|
|
`relsig` and waits for `VT_RELDISP`, so the server stops drawing and only then says
|
|
yes; switching back sends `acqsig` and the server redraws. A terminal in graphics mode
|
|
is not repainted by the kernel. If the owner dies, the terminal goes back to the kernel
|
|
whole. This is how Xorg runs on a terminal of its own, with Ctrl+Alt+F-key switching
|
|
between it and the text consoles and Ctrl+Alt+Backspace ending the session.
|
|
|
|
## Pseudo-terminals
|
|
|
|
`/dev/ptmx` makes a pair on every open and hands back the master; the slave is
|
|
`/dev/pts/N`, which the program on the other side gets as its stdin, stdout and
|
|
controlling terminal. Between them is a line discipline, the console's in miniature:
|
|
what the master writes is the slave's input (canonical editing, echo, `^C` as `SIGINT`
|
|
to the foreground group) and what the slave writes is read back by the master with NL
|
|
turned into CR NL. xterm, tmux, `script` and `sshd` all need exactly this. The pairs
|
|
share one lock; `kernel/dev/pty.c`.
|
|
|
|
## The text cursor
|
|
|
|
A steady inverted block — the cell with its colours swapped relative to what it would
|
|
show, so a cursor on reverse-video text is the plain text and every cursor on screen
|
|
is visible against its cell. `ESC[?25l` hides it, `ESC[?25h` shows it. It is drawn into
|
|
the grid's repaint and does not blink.
|
|
|
|
## What it does not do
|
|
|
|
- **No scrollback.** The grid is exactly the visible screen. Shift+PageUp does
|
|
nothing because there is nothing above the top row to show.
|
|
- **No reflow on resize.** The geometry is read once at `vt_init()`; the console's
|
|
font is sized to the screen at that point, and a resolution change from `schmode`
|
|
repaints it, but text is not re-wrapped.
|
|
- **No terminal past 12.** Linux allows 63. Twelve is one per function key.
|
|
- **Only 240x75 of any screen.** Larger framebuffers work; the grid is clamped
|
|
and the remainder stays background. Raising `VT_MAXCOLS`/`VT_MAXROWS` is the
|
|
fix, and costs BSS.
|
|
- **No UTF-8 the font lacks.** The console draws through the TrueType
|
|
rasteriser, so the limit is the font's `cmap` — Latin, Latin Extended, Greek,
|
|
Cyrillic and the common punctuation and maths symbols all render. Anything
|
|
genuinely absent still draws as `?`, which is deliberate: drawing a
|
|
*different* character would be worse.
|
|
- **No shaping, and no wide characters.** One codepoint is one cell of one
|
|
advance. Combining marks land on top of their own cell rather than the
|
|
preceding one, and CJK draws at half the width it wants. Both need a
|
|
width table and a cell that can span two columns.
|
|
- **No dead keys**, so French and Spanish layouts are absent rather than
|
|
broken.
|
|
- **No per-terminal layout.** One keyboard, one layout. Switching to tty3 does
|
|
not move anybody's fingers.
|
|
- **Only three layouts** (US, Polish programmers', German). Adding one is a table in `keymap.c` and a line in the
|
|
catalogue; the mechanism is not the limit, the typing is.
|