// 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() in cat doesn'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). cat never 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 cat at all β€” the line discipline swallows it and sends SIGINT to the foreground process group. Ctrl+Z sends SIGTSTP. 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, \n becomes \r\n so 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 pts

Implement 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() and unlockpt() must both succeed before the slave is opened β€” unlockpt is the kernel's safety interlock, and opening the slave first fails with EIO.
  • 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.