termios + winsize
Advanced
TLPI Ch.64
Why Do Terminal Attributes Matter for PTYs?
A pseudoterminal (PTY) has two ends — a master and a slave. Applications like terminal emulators talk to the master. Programs like the shell talk to the slave.
The master and slave share two important structures:
- termios — controls how the terminal processes input/output (echo, line editing, special characters like Ctrl+C).
- winsize — stores the number of rows and columns the terminal window has.
Because they share these structures, whoever controls the master fd can change terminal settings for the slave simply by calling tcsetattr(masterFd, ...) or ioctl(masterFd, TIOCSWINSZ, ...). The slave sees the updated settings immediately.
| PTY Master (terminal emulator / script) masterFd |
Shared in kernel PTY driver:
struct termios tty_attr struct winsize ws |
⇄ | PTY Slave (shell / application) /dev/pts/N |
| tcsetattr(masterFd, …) or tcsetattr(slaveFd, …) — both update the same kernel structure | |||
Inside the Linux kernel’s PTY driver, a single termios structure is maintained for each PTY pair. Both the master and slave file descriptors refer to this same structure.
This means:
- If you call
tcsetattr(masterFd, TCSANOW, &new_settings), the slave immediately sees those new settings. - If the shell calls
tcsetattr(slaveFd, TCSANOW, &raw_settings)(for example, a text editor switching to raw mode), the master side can read that change withtcgetattr(masterFd, ...).
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
/*
* Example: read termios from the master fd
* and print whether echo is enabled on the slave.
*/
void check_echo_on_slave(int masterFd)
{
struct termios t;
if (tcgetattr(masterFd, &t) == -1) {
perror("tcgetattr on masterFd");
return;
}
if (t.c_lflag & ECHO)
printf("Slave: echo IS enabled\n");
else
printf("Slave: echo is DISABLED\n");
}
Practical significance: A terminal emulator (like xterm or GNOME Terminal) uses this mechanism. When a user changes font size or theme, the emulator may want to update the slave’s terminal settings — it does so through the master fd without needing to know what process is using the slave.
The winsize structure stores the terminal’s dimensions:
struct winsize {
unsigned short ws_row; /* rows (characters) */
unsigned short ws_col; /* columns (characters) */
unsigned short ws_xpixel; /* width in pixels (optional) */
unsigned short ws_ypixel; /* height in pixels (optional) */
};
Screen-oriented programs — like vi, nano, top, htop — rely on ws_row and ws_col to know how many lines to draw, where to position the cursor, and how to wrap output. If winsize is wrong, these programs produce garbled, mis-aligned output.
| Problem: xterm window resized by user | ||
| xterm Knows new size: 80×30 → 120×40 Updates its own kernel record for /dev/pts/1 (real PTY) |
✗ | PTY slave (/dev/pts/24) Still thinks size is 80×30 vi running heredraws output for 80×30 → garbled display! |
| Fix: script / terminal emulator must propagate SIGWINCH and call TIOCSWINSZ on master fd | ||
Two ioctl() commands handle window size:
- TIOCGWINSZ — Get (read) the current window size.
- TIOCSWINSZ — Set (write) a new window size.
These work on both master and slave fds. The script program uses TIOCGWINSZ at startup to copy the real terminal’s size to the PTY slave.
#include <sys/ioctl.h>
#include <termios.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
struct winsize ws;
/* --- Read current size from real terminal --- */
if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == -1) {
perror("TIOCGWINSZ");
return 1;
}
printf("Current size: %d rows x %d cols\n", ws.ws_row, ws.ws_col);
/* --- Example: set a new size on the PTY master fd --- */
/* (In reality masterFd comes from ptyFork()) */
int masterFd = 3; /* placeholder */
ws.ws_row = 40;
ws.ws_col = 120;
if (ioctl(masterFd, TIOCSWINSZ, &ws) == -1) {
perror("TIOCSWINSZ");
return 1;
}
printf("Updated slave to: %d rows x %d cols\n", ws.ws_row, ws.ws_col);
return 0;
}
When the terminal window is resized, the kernel sends the SIGWINCH (Window Change) signal to the foreground process group of that terminal.
In the context of script:
- When the user resizes the xterm, xterm updates its PTY device (
/dev/pts/1) with the new size and sends SIGWINCH to thescriptprocess. - The
scriptprocess should handle SIGWINCH, read the new size from the real terminal, and write it to the PTY slave viaTIOCSWINSZon the master fd. - Writing the new size to the master causes the kernel to also send SIGWINCH to the foreground process group of the slave (e.g.,
virunning insidescript). vihandles SIGWINCH by re-querying its terminal size and redrawing itself correctly.
| User resizes xterm | → | xterm updates /dev/pts/1 winsize + sends SIGWINCH to script process |
→ | script reads new size from real terminal, calls TIOCSWINSZ on masterFd |
→ | Kernel sends SIGWINCH to vi (slave side) → vi redraws correctly |
#include <signal.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
static int g_masterFd; /* set this before installing the handler */
/*
* SIGWINCH handler: read new window size from real terminal
* and push it to the PTY slave via the master fd.
*/
static void sigwinch_handler(int sig)
{
struct winsize ws;
/* Read new size from the real terminal (STDIN is the real terminal) */
if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == -1) {
return; /* ignore errors in signal handler */
}
/* Push new size to PTY slave via master fd.
* The kernel will also deliver SIGWINCH to the slave's
* foreground process group (e.g., vi running on pts/24).
*/
if (ioctl(g_masterFd, TIOCSWINSZ, &ws) == -1) {
return;
}
}
void install_sigwinch(int masterFd)
{
g_masterFd = masterFd;
struct sigaction sa;
sa.sa_handler = sigwinch_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; /* restart interrupted syscalls */
if (sigaction(SIGWINCH, &sa, NULL) == -1) {
perror("sigaction SIGWINCH");
}
}
Note: The default disposition of SIGWINCH is to ignore it. Programs like vi install their own SIGWINCH handler to trigger a screen redraw. The shell typically also handles SIGWINCH to update its COLUMNS and LINES environment variables.
The basic script program (Part 1) does not handle window resize — if you run vi inside script and resize the xterm, vi will look wrong. Here is how to add SIGWINCH support:
#include <signal.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#include <sys/select.h>
#include <stdio.h>
#include <stdlib.h>
#include "pty_fork.h"
#include "tty_functions.h"
#define BUF_SIZE 256
#define MAX_SNAME 1000
static int g_masterFd;
static struct termios ttyOrig;
static void ttyReset(void) { tcsetattr(STDIN_FILENO, TCSANOW, &ttyOrig); }
/* Propagate new window size from real terminal to PTY slave */
static void sigwinch_handler(int sig)
{
struct winsize ws;
if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) != -1)
ioctl(g_masterFd, TIOCSWINSZ, &ws);
}
int main(int argc, char *argv[])
{
char slaveName[MAX_SNAME];
char *shell;
int masterFd, scriptFd;
struct winsize ws;
struct sigaction sa;
fd_set inFds;
char buf[BUF_SIZE];
ssize_t numRead;
pid_t childPid;
/* Step 1: save real terminal state */
tcgetattr(STDIN_FILENO, &ttyOrig);
ioctl(STDIN_FILENO, TIOCGWINSZ, &ws);
/* Step 2: fork via PTY */
childPid = ptyFork(&masterFd, slaveName, MAX_SNAME, &ttyOrig, &ws);
g_masterFd = masterFd;
if (childPid == 0) {
shell = getenv("SHELL");
if (!shell || !*shell) shell = "/bin/sh";
execlp(shell, shell, (char *)NULL);
_exit(1);
}
/* Install SIGWINCH handler BEFORE entering relay loop */
sa.sa_handler = sigwinch_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGWINCH, &sa, NULL);
/* Open script file */
scriptFd = open((argc > 1) ? argv[1] : "typescript",
O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (scriptFd == -1) { perror("open"); return 1; }
ttySetRaw(STDIN_FILENO, &ttyOrig);
atexit(ttyReset);
/* Relay loop */
for (;;) {
FD_ZERO(&inFds);
FD_SET(STDIN_FILENO, &inFds);
FD_SET(masterFd, &inFds);
if (select(masterFd + 1, &inFds, NULL, NULL, NULL) == -1) {
if (errno == EINTR) continue; /* interrupted by SIGWINCH — loop again */
break;
}
if (FD_ISSET(STDIN_FILENO, &inFds)) {
numRead = read(STDIN_FILENO, buf, BUF_SIZE);
if (numRead <= 0) break;
write(masterFd, buf, numRead);
}
if (FD_ISSET(masterFd, &inFds)) {
numRead = read(masterFd, buf, BUF_SIZE);
if (numRead <= 0) break;
write(STDOUT_FILENO, buf, numRead);
write(scriptFd, buf, numRead);
}
}
return 0;
}
Key addition: When select() returns -1 with errno == EINTR, it means a signal (SIGWINCH) interrupted it. We use continue to restart the loop — the handler has already propagated the new size.
This is a fundamental design feature of PTYs. The PTY master is meant to represent the “physical” side of the terminal — the side that a terminal emulator or network daemon controls. The PTY slave is meant to represent the “keyboard and screen” from the application’s point of view.
Just as a physical terminal’s hardware settings (baud rate, parity, word length) affect how characters are sent to/received from the connected process, the PTY master’s tcsetattr() affects how the PTY driver processes characters flowing to the slave.
| Physical Terminal World | PTY World |
|---|---|
| Physical keyboard + screen hardware | PTY master fd (terminal emulator) |
| UART / serial driver | PTY kernel driver (shared termios/winsize) |
| Application (/dev/ttyS0) | Application (/dev/pts/N) — PTY slave |
The key insight: the terminal emulator (or script) is the “operator” of the terminal. It makes sense that the operator can configure the terminal’s behavior, just like a sysadmin configuring a serial console’s settings.
/* ====== Reading / Writing Terminal Line Discipline (termios) ====== */
struct termios t;
/* Read from master or slave — both get same termios */
tcgetattr(masterFd, &t);
tcgetattr(slaveFd, &t);
/* Write — updates the shared structure for both */
tcsetattr(masterFd, TCSANOW, &t);
tcsetattr(slaveFd, TCSANOW, &t);
/* ====== Reading / Writing Window Size ====== */
struct winsize ws;
/* Read current size */
ioctl(masterFd, TIOCGWINSZ, &ws); /* from master */
ioctl(slaveFd, TIOCGWINSZ, &ws); /* from slave */
/* Set new size (and trigger SIGWINCH to slave's foreground pgrp) */
ioctl(masterFd, TIOCSWINSZ, &ws); /* via master — common in terminal emulators */
ioctl(slaveFd, TIOCSWINSZ, &ws); /* via slave — also valid */
/* ====== Handle Window Change Signal ====== */
signal(SIGWINCH, my_handler); /* default action is SIG_IGN */
Q1. Do the PTY master and slave have separate termios structures?
No. They share a single termios structure maintained by the PTY kernel driver. Calling tcsetattr() on either the master fd or the slave fd updates the same structure. Both sides see the change immediately.
Q2. Why would a terminal emulator call tcsetattr(masterFd, ...)?
To change how the PTY driver processes characters — for example, to enable or disable software flow control (XON/XOFF), or to match the terminal settings a user configures in the emulator’s preferences. Since the master represents the physical terminal side, setting attributes via the master is the natural approach.
Q3. What happens if the PTY slave’s winsize is not updated when the terminal emulator window is resized?
Screen-oriented programs (vi, top, nano) running on the slave will still think the terminal has the old dimensions. They will draw output for the wrong number of rows and columns — resulting in garbled, overwritten, or clipped output.
Q4. What signal is sent when a terminal window is resized, and who handles it?
SIGWINCH (Signal Window Change) is sent to the foreground process group of the terminal. In a PTY setup, when the master’s winsize is updated with TIOCSWINSZ, the kernel also delivers SIGWINCH to the foreground process group of the slave. Programs like vi install a SIGWINCH handler to redraw their screen.
Q5. In the script program, how should SIGWINCH be propagated to the shell running on the PTY slave?
The script process should install a SIGWINCH handler. When it receives SIGWINCH (because the user resized the real terminal), it reads the new size from the real terminal using TIOCGWINSZ and applies it to the PTY slave using TIOCSWINSZ on the master fd. This causes the kernel to send SIGWINCH to the slave’s foreground process group.
Q6. Can you call tcsetattr() on the PTY master fd with TCSADRAIN?
Yes. The same flags (TCSANOW, TCSADRAIN, TCSAFLUSH) work on the master fd just as they do on a regular terminal fd. TCSADRAIN waits for all queued output to be written before applying the change.
Q7. What is the difference between TIOCGWINSZ and TIOCSWINSZ?
TIOCGWINSZ is a Get operation — it reads the current winsize structure from the kernel into a user-space struct winsize. TIOCSWINSZ is a Set operation — it writes a user-space struct winsize into the kernel and, if the size changed, sends SIGWINCH to the foreground process group of that terminal.
Q8. What is the default signal disposition of SIGWINCH?
The default disposition is to ignore it (SIG_IGN). Programs that care about window size changes (text editors, pagers, shells) explicitly install a SIGWINCH handler. Programs that don’t handle it simply never know the window was resized.
Q9. How does xterm or GNOME Terminal set the initial window size for a newly spawned shell?
The terminal emulator calls ioctl(masterFd, TIOCSWINSZ, &ws) with the desired dimensions before (or immediately after) the child shell starts. The child shell then reads this size with TIOCGWINSZ when it initialises and uses those values for its prompt, tab completion width, etc.
Chapter 64 Complete!
You have covered the script program and PTY terminal attribute sharing — the core of Chapter 64.
