// lesson: tty-device
TTYs, PTYs, and the Line Discipline
Type tty in your shell:
$ tty
/dev/pts/3
That path is a terminal device β a file that isn't storage but a conversation. To understand what your terminal emulator actually is, you need the (surprisingly physical) history of that file.
From teletypes to /dev/tty
"TTY" is short for teletypewriter: an electromechanical typewriter
from the early 1900s that sent each keystroke down a wire and printed
whatever came back. When Unix was born at Bell Labs in 1969, teletypes
were how you talked to it β the Model 33 ASR printed 10 characters per
second onto paper. Unix modeled each one as a device file: /dev/tty1,
/dev/tty2, β¦ Write bytes to the file, they print on that desk's
paper; read from it, you get what that person typed.
Physical teletypes died, but the model was too useful to kill.
Video terminals (the DEC VT100, 1978 β remember that name) replaced
paper with a CRT and added something new: they interpreted special
byte sequences to move a cursor around the screen instead of only
appending at the bottom. Then terminals stopped being hardware at all
and became programs β and the model still didn't change. Your
/dev/pts/3 behaves, as far as any program can tell, like a serial
cable with a 1978 DEC terminal on the other end.
The line discipline: the kernel's helpful middleman
Here's the crucial, non-obvious part. When you run cat and type at
it, your keystrokes do not go straight to cat. Between the
device and the process, the kernel runs a layer called the line
discipline, and by default it is doing a surprising amount of work:
- Line buffering. Bytes you type accumulate in a kernel buffer.
read()incatdoesn't return until you press Enter. That's why it's called canonical (line-at-a-time) mode. - Line editing. Backspace works in the kernel. So do Ctrl+W
(erase word) and Ctrl+U (erase line).
catnever sees the typo or the backspace β the kernel edits the buffer before delivering it. - Echo. The kernel copies each keystroke back to the display. Programs don't print your typing; the kernel does.
- Signals. Ctrl+C doesn't send a byte to
catat all β the line discipline swallows it and sendsSIGINTto the foreground process group. Ctrl+Z sendsSIGTSTP. These are keyboard shortcuts implemented in the kernel. - Translation. Enter arrives as carriage return (
\r, 0x0D) from the terminal, and the kernel hands your program a newline (\n, 0x0A). On output,\nbecomes\r\nso the cursor both drops a line and returns to column 0 β two distinct motions, another teletype fossil. - Flow control. Ctrl+S freezes output, Ctrl+Q resumes it β useful when your terminal printed at 10 chars/sec, mostly a trap today.
This kernel service is why most programs never think about terminals: they read lines from fd 0 and write lines to fd 1, and the line discipline makes it pleasant. A full-screen program β vim, less, htop, your terminal-to-be β needs the opposite: every keystroke immediately, no echo, no editing, no surprise translations. Turning all of this off is called raw mode, and it's the next lesson.
You can inspect the line discipline's current configuration with
stty -a, which prints the exact flags you'll soon be flipping with
tcsetattr():
$ stty -a
speed 38400 baud; rows 40; columns 132;
intr = ^C; susp = ^Z; erase = ^?; werase = ^W; ...
icanon icrnl ixon opost onlcr echo echoe ...
Every one of those lowercase words is a flag in struct termios.
Pseudoterminals: the trick that makes terminal emulators possible
A real serial port has hardware on the far end. But xterm, tmux, ssh,
and your GUI terminal have no hardware β so where does /dev/pts/3
come from?
A pseudoterminal (pty) is a pair of connected virtual devices the kernel manufactures on demand:
- The master side (an fd from opening
/dev/ptmx) is held by the terminal emulator. - The slave side (
/dev/pts/N) is what the shell and its children get as stdin/stdout/stderr. To them it is a terminal in every observable way βisatty()says yes,tcsetattr()works, Ctrl+C raises SIGINT.
The two sides are cross-connected through the line discipline:
keyboard/screen side program side
ββββββββββββββββββββ write βββββββββββββββββββ read βββββββββ
β terminal emulatorβ βββββββββΊ β line discipline β βββββββββΊ β shell β
β (master fd) β βββββββββ β (the kernel) β βββββββββ β (pts) β
ββββββββββββββββββββ read βββββββββββββββββββ write βββββββββ
Everything the emulator writes to the master comes out of the slave's
read() as "keyboard input" β after line-discipline processing. And
everything programs write to the slave arrives at the master's
read() as "screen output" for the emulator to draw. When you press
k in a shell running under xterm: xterm writes k to the master β
the line discipline echoes it back and buffers it β the shell reads
it from the slave. Three processes, two fds, one kernel layer.
This is also how ssh works (the remote sshd allocates a pty and
runs your shell on its slave), how tmux keeps sessions alive after
you disconnect (tmux holds the masters; your disconnection kills only
the client drawing them), and how script, expect, and CI systems
fool programs into thinking a human is present.
The modern API for conjuring a pair is small:
#define _XOPEN_SOURCE 600
#include <stdlib.h>
#include <fcntl.h>
int m = posix_openpt(O_RDWR | O_NOCTTY); /* open /dev/ptmx */
grantpt(m); /* fix slave permissions */
unlockpt(m); /* allow slave to open */
char *name = ptsname(m); /* "/dev/pts/N" */
int s = open(name, O_RDWR | O_NOCTTY);
(O_NOCTTY says "don't make this my controlling terminal" β we want a
device to experiment on, not to adopt. Controlling terminals, sessions,
and job control are a deep topic; the extended-reading TTY article is
the best tour.)
For this course the pty is our laboratory: tests can't assume a human at a keyboard, but they can manufacture a real terminal device, apply real termios settings to it, and push real bytes through the real line discipline. Which is exactly what you'll do now.
βΊ Open a Pseudoterminal
15 ptsImplement pty_open_pair(): create a master/slave pty pair and hand
both fds back to the caller. Then prove the plumbing works both ways.
Details that will bite if skipped:
- Open the master with
O_RDWR | O_NOCTTY. grantpt()andunlockpt()must both succeed before the slave is opened βunlockptis the kernel's safety interlock, and opening the slave first fails withEIO.- Close the master fd on any failure path; don't leak it.
The tests then verify the classic pty behaviors through your pair:
the slave is a tty (the master, on Linux, is too β but its name is
the slave's); a line written to the master emerges from the slave's
read() (input direction, through canonical buffering); and bytes
written to the slave emerge from the master (output direction).
Log in to submit a solution and earn points.