The TTY Subsystem: One Model for Every Terminal, Real or Virtual
Linux still uses the decades-old teletypewriter model — a line discipline sitting between a low-level driver and userspace — to unify physical serial ports, pseudo-terminals, and the system console under one
tty_struct/tty_driverobject model
Getting Started
Every terminal session — a physical serial console on an embedded board, an ssh login, a GUI terminal emulator window, even the kernel's own boot messages scrolling past — is, from the kernel's point of view, a TTY. The name is inherited from real hardware: a teletypewriter, an electromechanical device that printed characters sent to it and sent back whatever a human typed. Linux (like every Unix before it) still uses that decades-old model today as the shape for all character-oriented, line-buffered terminal I/O, even though almost nobody has plugged an actual teletype into a computer in this century. The kernel's own documentation puts it plainly: "Teletypewriter (TTY) layer takes care of all those serial devices. Including the virtual ones like pseudoterminal (PTY)."
drivers/tty/ is also one of the oldest and, by the admission of the people who maintain it, one of the trickiest corners of the kernel. An LWN retrospective on a TTY regression opened with "there are dark areas of the kernel where only the bravest hackers dare to tread... arguably, no part of the kernel is darker and scarier than the serial terminal (TTY) code." That reputation is a useful piece of context for a page like this one: the object model below is stable and well-documented, but the code that implements it has accreted for decades and rewards caution.
The problem the TTY subsystem solves
Three very different things all need to look like "a terminal" to userspace:
- Physical serial ports — a real RS-232 line behind a UART chip, still common on servers (for out-of-band management) and embedded boards.
- Pseudo-terminals (ptys) — a purely software master/slave pair with no hardware behind it at all. This is what every terminal emulator (xterm, GNOME Terminal, VS Code's integrated terminal), every
sshsession, and everyscreen/tmuxpane runs on. - The system console — the device the kernel's own text output (boot messages,
printk(), an interactive shell if you have one) is currently pointed at, which can itself be a physical serial line, a virtual console on the screen, or a pty.
Without a shared model, each of these would need its own reimplementation of things every terminal has to handle: turning a stream of raw bytes into editable lines (backspace deletes the last character), deciding whether input is buffered a line at a time or delivered byte-by-byte, echoing typed characters back to the user, generating a signal when the user presses an interrupt key, and negotiating window size. Linux's TTY layer, rooted at drivers/tty/ with its data structures in include/linux/tty.h, include/linux/tty_driver.h, and include/linux/tty_ldisc.h, factors this into three layers so that only the bottom one — moving bytes to and from an actual UART, pty peer, or screen — has to be written per device family. Everything above that is shared.
The three-layer stack
- The low-level driver. This is the layer that actually moves bytes: a UART driver for real serial hardware (
drivers/tty/serial/), the pty master/slave implementation (drivers/tty/pty.c), or the virtual-console/framebuffer console driver (drivers/tty/vt/) for the screen. Each of these registers astruct tty_driverand implements astruct tty_operations— the vtable the tty core calls into foropen(),write(),ioctl(), and so on. - The line discipline. Sitting between the driver and the character-device interface userspace sees, a line discipline (
struct tty_ldisc_ops) interprets the raw byte stream. The default, and the one almost every interactive terminal uses, is N_TTY (N_TTYis0, defined ininclude/uapi/linux/tty.h) — it implements canonical ("cooked") vs. raw mode, character echo, and signal generation from control characters, most famously turning Ctrl-C intoSIGINT. Every newly opened tty gets N_TTY automatically:tty_ldisc_init()indrivers/tty/tty_ldisc.ccallstty_ldisc_get(tty, N_TTY)unconditionally, and it's also the fallback a tty reverts to if something goes wrong switching to another one. A process can swap in a different line discipline with theTIOCSETDioctl — the kernel ships around two dozen others (N_PPP,N_HDLC,N_SLIP,N_HCIfor Bluetooth UART,N_GSM0710, and more) that repurpose a serial line as a protocol framer rather than a human-facing terminal. tty_structandtty_driver. These are the core kernel objects that tie the other two together:struct tty_driverrepresents a driver family (e.g., "the serial driver" or "the pty slave driver"), andstruct tty_structrepresents one open tty instance, holding the pointer to its current line discipline (ldisc), its driver (driver,ops), its terminal settings (termios), and the rest of its live state.
Core structures
struct tty_struct (include/linux/tty.h) is allocated per open tty and lives (reference-counted via an embedded kref) until the last reference drops. Key fields:
struct tty_struct {
struct kref kref;
int index;
struct device *dev;
struct tty_driver *driver;
struct tty_port *port;
const struct tty_operations *ops;
struct tty_ldisc *ldisc;
struct ld_semaphore ldisc_sem;
/* ... locking: atomic_write_lock, legacy_mutex, throttle_mutex,
* termios_rwsem, winsize_mutex ... */
struct ktermios termios, termios_locked;
char name[64];
unsigned long flags;
int count;
unsigned int receive_room;
struct winsize winsize;
struct {
spinlock_t lock;
bool stopped;
bool tco_stopped;
} flow;
struct {
struct pid *pgrp;
struct pid *session;
spinlock_t lock;
unsigned char pktstatus;
bool packet;
} ctrl;
/* ... hw_stopped, closing, flow_change ... */
struct tty_struct *link;
void *disc_data;
void *driver_data;
/* ... */
};
A few of these are worth calling out: name is built by tty_line_name() (e.g. ttyS3); flags is a bitmask of states like TTY_THROTTLED, TTY_IO_ERROR, TTY_OTHER_CLOSED (set on a pty whose other end has closed), and TTY_HUPPED; receive_room is how many bytes the current line discipline is willing to accept from the driver in one receive_buf() call, maintained by the line discipline itself (N_TTY updates it dynamically in n_tty_receive_buf_common() as it drains input, and the tty core zeroes it in tty_set_termios_ldisc(); a couple of other line disciplines, like n_gsm.c and n_hdlc.c, just set a fixed value at .open()); ctrl.pgrp/ctrl.session are the process group and session that make this the controlling terminal for job control (setpgrp(2), setsid(2)); and link connects a pty's two tty_structs (master and slave) to each other.
struct tty_driver (include/linux/tty_driver.h) represents a driver, not an individual open tty:
struct tty_driver {
struct kref kref;
struct cdev **cdevs;
struct module *owner;
const char *driver_name;
const char *name;
int name_base;
int major;
int minor_start;
unsigned int num;
enum tty_driver_type type;
enum tty_driver_subtype subtype;
struct ktermios init_termios;
unsigned long flags;
/* ... proc_entry, other, flip_wq ... */
struct tty_struct **ttys;
struct tty_port **ports;
struct ktermios **termios;
void *driver_state;
const struct tty_operations *ops;
/* ... */
};
name is what's used to build the /dev node ("ttyS", "pty", "pts"); type/subtype (enum tty_driver_type: TTY_DRIVER_TYPE_SYSTEM, _CONSOLE, _SERIAL, _PTY, _SCC, _SYSCONS) tell the core roughly what kind of driver this is; and ttys/ports/termios are the per-line arrays the standard tty_standard_install() path uses to look up an existing tty by minor number. A driver is set up with tty_alloc_driver(), has ops attached via tty_set_operations(), and goes live with tty_register_driver().
struct tty_operations (also tty_driver.h) is the vtable connecting the tty core to a specific driver. The kernel doc comment marks both open() and close() "Required method"; in practice only open()'s absence is enforced at runtime (the doc comment: "if this routine is not filled in, the attempted open will fail with ENODEV"). The rest are optional but most drivers implement the load-bearing ones: write()/write_room()/chars_in_buffer() for output, set_termios() to react to changed terminal settings, throttle()/unthrottle() and stop()/start() for flow control, hangup(), ioctl(), break_ctl() for sending a BREAK, and install()/lookup()/remove() for drivers (like the pty and console drivers) that don't use the standard ttys[] array lookup.
struct tty_ldisc_ops (include/linux/tty_ldisc.h) is the line discipline's vtable, and the kernel doc comment splits its hooks into two directions: hooks marked [TTY] are called from the tty core downward — open(), close(), read(), write(), ioctl(), set_termios(), poll(), hangup() — and hooks marked [DRV] are called from the low-level driver upward, feeding received data in: receive_buf() and its newer variant receive_buf2() (preferred when present, since it can report back how many bytes it consumed for automatic flow control), write_wakeup() (the driver has room for more output), dcd_change() (used by the N_PPS Pulse-Per-Second discipline), and lookahead_buf() (a fast path for characters — like software flow-control bytes — that need handling before the regular receive_buf() call gets to them). N_TTY's implementation of receive_buf()/receive_buf2() is where canonical-mode line editing, echo, and ISIG signal generation actually happen: when the local flag ISIG (c_lflag) is set and the incoming character matches INTR_CHAR(tty) (the terminal's configured interrupt character, normally Ctrl-C), drivers/tty/n_tty.c calls n_tty_receive_signal_char(tty, SIGINT, c) to raise SIGINT on the tty's foreground process group.
The console: printk, boot messages, and the tty layer
The kernel's own text output — early boot messages, printk(), everything you see on a serial console or the screen before and often after a shell starts — goes through a related but distinct mechanism: struct console (include/linux/console.h), registered console drivers, flags like CON_BOOT (an early console, replaced once the real one registers) and CON_PRINTBUFFER (replay buffered messages so nothing is lost during the handoff). This isn't the tty core itself, but the two are deliberately linked: struct console has a device callback,
whose job is to say which tty_driver backs a given console. This is exactly the mechanism /dev/console uses when it's opened: tty_lookup_driver() in drivers/tty/tty_io.c calls console_device() to find the tty_driver currently registered as the system console, then opens a tty through it like any other. The virtual-console driver that drives the text screen (drivers/tty/vt/, backing /dev/tty1–/dev/tty63 and the currently-active-VT alias /dev/tty0) is itself an ordinary struct tty_driver with type == TTY_DRIVER_TYPE_CONSOLE; a headless system with a serial console instead points the same device callback at whichever tty_driver backs that serial port. The struct console/printk-routing side of this (the NBCON non-blocking console rework, multiple simultaneous console drivers, etc.) is its own subsystem and out of scope for this page — the point here is just that "the console" is, underneath, one more consumer of the same tty_driver/tty_struct model everything else uses.
Device node naming
/dev/ttyS0,/dev/ttyS1, ... — real serial ports, one per UART line a driver enumerates (the 8250/16550-family driver underdrivers/tty/serial/8250/, for the most common case). ThettyS(4)man page puts it simply: "ttyS[0-3] are character devices for the serial terminal lines."/dev/pts/N— UNIX 98 pseudo-terminal slaves. Userspace opens/dev/ptmx, a clone device handled byptmx_open()indrivers/tty/pty.c, to allocate a fresh pty master; thedevptsfilesystem then creates the matching/dev/pts/Nslave node. Every terminal emulator, everysshsession, and everytmux/screenpane is one of these pairs. On the kernel side this is two separatestruct tty_drivers —ptm_driver(driver_name = "pty_master",name = "ptm", master side) andpts_driver(driver_name = "pty_slave",name = "pts", slave side) — cross-linked throughtty_driver.other;ptmx_open()allocates the master viatty_init_dev(ptm_driver, index). This is distinct from the legacy BSD-stylepty_driver(name = "pty"), set up separately bylegacy_pty_init()underCONFIG_LEGACY_PTYS, which backs the old/dev/ptyXX//dev/ttyXXpairs rather than/dev/ptmx./dev/tty— not a distinct device family but a fixed alias, device numberMKDEV(TTYAUX_MAJOR, 0)— majorTTYAUX_MAJOR(5), minor0— per thetty(4)man page: "a character file with major number 5 and minor number 0... a synonym for the controlling terminal of a process, if any." Opening it callstty_open_current_tty(), which looks up the calling process's controlling tty viaget_current_tty()and reopens that./dev/console— the system console alias, device numberMKDEV(TTYAUX_MAJOR, 1)(major5, minor1), resolved the same way viaconsole_device()as described above — it can point at a serial port, a virtual console, or (less commonly) a pty, depending on how the system was booted (console=kernel parameter) and what's currently registered.
Architecture: from hardware to userspace
Hardware / peer object
UART chip (8250/16550, ...) pty peer (other half VT/fbcon screen, or a
of the master/slave serial line acting as
pair) the system console
│ │ │
▼ ▼ ▼
drivers/tty/serial/* drivers/tty/pty.c drivers/tty/vt/*
struct tty_driver "ttyS" tty_driver "ptm"/"pts" console_driver
+ struct tty_operations + struct tty_operations + struct tty_operations
│ │ │
└─────────────────────────────┼────────────────────────────┘
▼
Line discipline: struct tty_ldisc_ops
N_TTY (default) — canonical/raw mode, echo,
signal generation (INTR_CHAR -> SIGINT), ...
swappable per-tty via TIOCSETD (N_PPP, N_HDLC, N_SLIP, ...)
│
▼
struct tty_struct
ldisc, termios, ops, driver, port, receive_room,
flow{stopped}, ctrl{pgrp,session}, link (pty pairing)
│
▼
character device: open()/read()/write()/ioctl()/poll()
│
┌──────────────────────────────┼──────────────────────────────┐
▼ ▼ ▼
/dev/ttyS0 /dev/pts/N /dev/tty1..ttyN
real serial hw ptmx-cloned pty; every virtual consoles
terminal emulator/SSH (VT layer)
session runs on one
Two more names are resolved at open() time to whichever tty above is
currently appropriate, rather than being separate device families:
/dev/tty -> the calling process's controlling terminal
(tty_open_current_tty(), device number MKDEV(TTYAUX_MAJOR, 0))
/dev/console -> whichever tty_driver is the registered system console
(console_device(), device number MKDEV(TTYAUX_MAJOR, 1))
The rest of this section
- Serial Core: One Abstraction Over Every UART — how a real UART chip driver plugs into
serial_core,tty_driver, andtty_port, worked through the 8250/16550 anduartlitedrivers. - Line Disciplines, termios, and Pseudo-Terminals — canonical vs. raw mode,
termios, signal generation, job control, and pseudo-terminals, mostly through N_TTY (the non-N_TTY disciplines get a short overview). - War Stories — real incidents from the TTY/serial subsystem.
Prerequisites and neighbors
Every /dev/tty*, /dev/pts/*, and /dev/ttyS* node is an ordinary character device underneath, registered through the same cdev/file_operations machinery as any other driver — the tty layer's struct tty_operations sits one level above that as a terminal-specific vtable. Serial and console drivers are ordinary Linux device-model citizens (platform devices for on-SoC UARTs, PCI devices for add-in serial cards). Reading order: this page, then Serial Core: One Abstraction Over Every UART if you're working with real UART hardware, or Line Disciplines, termios, and Pseudo-Terminals if you want the N_TTY internals.
Further reading
Kernel source
- include/linux/tty.h —
struct tty_struct, theTTY_*flag bits, and the termios accessor macros - include/linux/tty_driver.h —
struct tty_driver,struct tty_operations,tty_alloc_driver()/tty_register_driver() - include/linux/tty_ldisc.h —
struct tty_ldisc_ops,struct tty_ldisc,tty_register_ldisc()/tty_set_ldisc() - include/uapi/linux/tty.h — the
N_*line discipline numbers (N_TTY,N_PPP,N_HDLC, ...) - include/linux/console.h —
struct console,CON_BOOT/CON_PRINTBUFFER, thedevicecallback linking a console to itstty_driver - drivers/tty/tty_io.c — the tty core:
tty_open(),tty_lookup_driver(),/dev/ttyand/dev/consolehandling - drivers/tty/tty_ldisc.c — line discipline attach/detach/reinit, the N_TTY fallback logic
- drivers/tty/n_tty.c — the default N_TTY line discipline: canonical mode, echo, signal generation
- drivers/tty/pty.c — the pty master/slave drivers,
/dev/ptmx, UNIX 98 pty allocation
Man pages
tty(4)—/dev/tty, the controlling-terminal aliasttyS(4)— the serial terminal line devicespty(7)— pseudoterminal master/slave pairs, UNIX 98 vs. BSD ptys,/dev/ptmxand/dev/pts/*termios(3)— canonical vs. noncanonical mode, thec_iflag/c_oflag/c_cflag/c_lflagfields, and theISIGspecial characters
Related pages
- Serial Core: One Abstraction Over Every UART · Line Disciplines, termios, and Pseudo-Terminals · War Stories — the rest of this section
- Character and Misc Devices · Linux Device Model — the driver-core layers the TTY subsystem sits on
LWN articles
- A tempest in a tty pot — on the TTY code's reputation for complexity, and the maintenance dispute that briefly left it without a maintainer
External
- Kernel docs: TTY driver-api index — the kernel's own overview of
tty_driver,tty_port,tty_struct, line disciplines, and the console - Kernel docs: TTY Line Discipline —
tty_ldisc_ops, the N_TTY default, and the N_NULL fallback when a discipline fails - Kernel docs: TTY Driver and TTY Operations — the full
tty_driver/tty_operationsreference - Kernel docs: Console —
struct console, boot-to-runtime console handoff, and how a console maps to atty_driver