Pseudoterminals Terminal Attributes & Window Size Sharing

 

Chapter 64 – Pseudoterminals
Part 2: Terminal Attributes & Window Size Sharing
Topic
termios + winsize
Level
Advanced
Book
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

Key Terms in This Part

termios winsize tcsetattr() tcgetattr() TIOCGWINSZ TIOCSWINSZ SIGWINCH ioctl() terminal emulator window resize

How termios Is Shared Between Master and Slave

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 with tcgetattr(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.

How winsize Is Shared and Why It Matters

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 here
draws output for 80×30
→ garbled display!
Fix: script / terminal emulator must propagate SIGWINCH and call TIOCSWINSZ on master fd

Reading and Writing Window Size with ioctl()

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;
}
    

Handling Window Resize with SIGWINCH

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 the script process.
  • The script process should handle SIGWINCH, read the new size from the real terminal, and write it to the PTY slave via TIOCSWINSZ on 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., vi running inside script).
  • vi handles 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.

Putting It Together: script with Window Resize Support

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.

Why Can the Master Control the Slave’s Terminal Settings?

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.

Quick Reference: Key API Calls for PTY Attributes

/* ====== 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 */
    

Interview Questions – Terminal Attributes & Window Size

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.

← Part 1: script Program EmbeddedPathashala.com

Leave a Reply

Your email address will not be published. Required fields are marked *