Linux TTY and PTY Semantics: Line Disciplines, Job Control, and Raw Mode
Trace Linux terminal input through TTY line disciplines and PTYs, then debug canonical mode, signals, controlling terminals, and termios safely.
A terminal is not simply a byte pipe connected to a display. Linux terminal I/O involves a device file, a TTY core, a line discipline, terminal attributes, sessions and process groups, and often a userspace terminal emulator. A serial port can provide a physical TTY; a pseudoterminal creates a virtual master/slave pair used by shells, SSH, terminal emulators, screen, and tmux. Many “input got stuck” bugs come from confusing one of these layers with another.
The TTY subsystem owns the interface and line discipline, but the terminal emulator owns the screen. The kernel typically does not know whether an escape sequence means “move the cursor” or “change a color.” It transports bytes and applies configured input/output processing. A program that changes terminal mode must save and restore the previous attributes, including on errors and signals, or the shell may inherit a broken input mode after the program exits.
TTY core, driver, and line discipline
The TTY core provides common terminal behavior for different drivers. A driver connects that core to an underlying device such as a UART or a PTY. A line discipline sits between the TTY driver and userspace and can transform or interpret characters. The default N_TTY discipline implements familiar terminal features such as canonical line input, echo, signal characters, and flow control. Other line disciplines may serve specialized protocols; do not assume every TTY uses the default discipline.
In canonical mode, input is generally made available to a reader line by line after a delimiter such as newline. Editing characters can alter the buffered line before the application reads it. In noncanonical mode, reads are governed by VMIN and VTIME, which define byte-count and timeout behavior; a read is not guaranteed to return the full requested buffer. ICANON, ECHO, ISIG, IXON, input transformations, and output post-processing are independent flags that jointly determine observable behavior.
Raw mode is a practical setting for full-screen applications, but “raw” does not mean the kernel has stopped handling the device. Libraries such as ncurses often choose a carefully limited mode and handle terminal capabilities themselves. Python’s tty.setraw() and libc’s cfmakeraw() change a defined subset of termios flags; inspect the exact API version and restore prior state rather than writing a homegrown flag mask from memory.
A PTY is a bidirectional terminal pair
A UNIX 98 PTY has a master endpoint, commonly created through /dev/ptmx, and a corresponding slave under /dev/pts. The slave behaves like a terminal to the child process. Bytes written by the master arrive as input on the slave side, and output written by the slave is read from the master. The kernel’s line discipline and termios rules apply to the slave, so PTYs reproduce terminal semantics rather than just forwarding an unstructured byte stream.
The terminal emulator usually owns the master, while a shell or child application owns the slave. SSH servers, terminal multiplexers, automation tools, and container consoles use the same broad model. A PTY does not supply the terminal’s visual rendering or guarantee that every hardware-specific ioctl makes sense. For example, a PTY has no physical baud clock even though the termios interface includes speed fields.
When a master closes, the slave-side process observes terminal hangup behavior. Programs must handle EOF, SIGHUP, and platform-specific read errors as part of their lifecycle. A terminal emulator crash should not leave a child that assumes its UI is still consuming output. Supervisors should tie child lifetime and PTY lifetime together deliberately.
Sessions, controlling terminals, and job control
A controlling terminal is associated with a session, and one process group is designated as the foreground process group for that terminal. The shell uses process groups to implement pipelines and job control. When a user presses the configured interrupt character, usually Control-C, the line discipline can signal the foreground process group if ISIG is enabled. The signal is not necessarily delivered to only the process that was reading a byte.
Background process groups have distinct rules. A read from the controlling terminal by a background job can trigger SIGTTIN; a write can trigger SIGTTOU when the TOSTOP behavior applies. Shells coordinate foreground ownership with tcsetpgrp() and wait for the foreground job. A daemon that accidentally retains a controlling terminal may receive unexpected signals or keep a terminal session alive. Use setsid() and explicit descriptor handling when creating a daemon or supervising a PTY child.
These relationships explain why redirecting stdin from a file does not always detach a process from its controlling terminal. File descriptors 0, 1, and 2 are just open descriptors; controlling-terminal ownership is session state. Diagnose both with ps fields for session, process group, terminal, and foreground group, plus tty and /proc/PID/fd where available.
Safely use noncanonical input
This Python program reads one byte from the caller’s terminal without waiting for Enter and restores the saved termios settings through tcsetattr(). It requires a real terminal on standard input. If the program is interrupted by a catchable exception, the finally block restores terminal state before the exception propagates. The kernel may normalize implementation-specific bits when attributes are applied, so compare the effective terminal behavior as well as the saved structure.
#!/usr/bin/env python3
import os
import sys
import termios
import tty
fd = sys.stdin.fileno()
saved = termios.tcgetattr(fd)
try:
tty.setraw(fd, termios.TCSANOW)
value = os.read(fd, 1)
finally:
termios.tcsetattr(fd, termios.TCSANOW, saved)
sys.stdout.write(f"read {len(value)} byte(s): {value.hex()}\n")
The byte may be the first byte of a multibyte key sequence, such as a cursor key. Applications should use a terminal-input parser with timeout and buffering rather than assuming one byte equals one key. If the process is killed with an uncatchable signal or the machine loses power, it cannot run cleanup; an interactive application should minimize the duration of raw mode and provide a documented recovery command such as stty sane for a damaged shell.
Inspect a live session before changing it
stty -a reports attributes for the selected terminal, and /proc can expose a process’s open descriptors and session metadata on Linux. These are read-only diagnostics. Run stty -a </dev/tty from a process that has a controlling terminal; it will fail when no such terminal is available. Do not use stty to alter a shared production console until you know which session owns it.
tty
stty -a </dev/tty
ps -o pid,ppid,sid,pgid,tpgid,stat,tty,command -p "$PID"
readlink "/proc/$PID/fd/0" "/proc/$PID/fd/1" "/proc/$PID/fd/2"
For a suspected PTY hang, check both endpoints and the process tree. A full PTY output buffer can block a writer if the master is not drained. Conversely, a reader that assumes one read() returns one logical line can mishandle canonical and noncanonical modes. Log byte counts, termios flags, process group IDs, and the time at which the master or slave closed.
Signals, flow control, and byte transformations
The terminal’s special characters are configurable through the c_cc array. VINTR, VEOF, VERASE, and VSUSP names describe roles, not fixed byte values. With ISIG enabled, configured signal characters can signal the foreground process group. With software flow control enabled, VSTOP and VSTART can pause and resume output. A binary protocol over a TTY must account for these transformations or disable the relevant behavior safely.
Input flags can map carriage return to newline, ignore parity errors, or strip bits. Output processing can translate newline and carriage-return sequences. A byte-for-byte file transfer over a serial terminal therefore needs a documented raw configuration on both endpoints, plus a framing and error-detection protocol. A local PTY test is useful for terminal semantics but does not reproduce UART framing errors, modem control, or electrical characteristics.
Production acceptance checklist
For terminal applications, test canonical and noncanonical modes, short reads, signal interruption, resize events, master closure, background process-group access, and restoration after ordinary errors. Verify the application’s behavior over both a local PTY and any physical serial devices it supports. Confirm that a terminal multiplexer or SSH hop does not change assumptions about the controlling terminal.
For a service that spawns a PTY, establish who owns the master, who owns the slave, which side drains output, how child exit is reaped, and how a closed master terminates the session. Bound output buffering and propagate cancellation. Test a child that writes faster than the supervisor reads; otherwise a production workload can stall even though both processes appear healthy.
Never diagnose terminal failures solely by changing stty until the screen looks normal. Capture the original settings first, identify whether the fault is line discipline, application state, PTY lifetime, terminal capability, or job control, and restore configuration in a finally or signal-safe cleanup path. A correct terminal program returns the user’s shell to its saved terminal mode; account for kernel-normalized status bits when comparing a later tcgetattr() result byte for byte.
Related:
- How udev and the Device Model Actually Discover and Name Hardware
- Reading /proc and /sys: The Kernel’s Window into Userspace
Sources: