Introduction & PTY I/O Basics

 

Chapter 64: Pseudoterminals (PTY)
Part 1 of 4 โ€” Introduction & PTY I/O Basics
4
Parts
TLPI
Chapter 64
PTY
Core Concept

๐Ÿ“‚ Tutorial Series Navigation

๐Ÿท Key Terms in This Part
pseudoterminal PTY master PTY slave bidirectional pipe terminal emulator canonical mode SIGHUP EIO controlling terminal 4kB capacity

What is a Pseudoterminal?

A pseudoterminal (PTY) is a software-based terminal. It looks and behaves exactly like a real hardware terminal (like the serial port on an old system), but it is completely virtual โ€” everything happens in software inside the Linux kernel.

You use PTYs every day without realising it. When you open a terminal emulator like GNOME Terminal, xterm, or Konsole, it uses a PTY to connect your shell (bash/zsh) to the screen. SSH also uses PTYs so that your remote shell behaves just like a local one.

Think of a PTY as a pair of connected pipes โ€” one end is the master, the other is the slave. The application (terminal emulator, SSH daemon) holds the master end. The shell or user program holds the slave end. But unlike a simple pipe, the slave end behaves like a real terminal.

๐Ÿ“ˆ How a PTY Pair Fits Together

The diagram below shows how a terminal emulator uses a PTY pair to connect to a shell.

๐Ÿ‘ค User
types & sees output
โ†” ๐Ÿ“บ Terminal Emulator
(e.g. xterm, GNOME Terminal)
โ†” PTY
MASTER
KERNEL
PTY
PAIR
PTY
SLAVE
โ†” ๐Ÿ  Shell (bash)
reads stdin, writes stdout
Arrow shows bidirectional data flow โ€” write to master โ†’ readable on slave ย |ย  write to slave โ†’ readable on master

โš– PTY vs Bidirectional Pipe โ€” What is the Difference?

Both a PTY and a pipe let two processes exchange data. But they are very different in one key way:

๐Ÿ”„ A Bidirectional Pipe
  • Just moves raw bytes
  • No special meaning for Control-C
  • No line buffering
  • No echo of input
  • No terminal signals (SIGINT, etc.)
๐Ÿ”ง A PTY (slave side)
  • Moves bytes and interprets them
  • Control-C โ†’ sends SIGINT to shell
  • Canonical mode: line buffered by default
  • Echo: input is copied to output
  • Terminal signals work normally

In short: the slave side of a PTY behaves like a real keyboard/screen terminal. Any program that reads from the slave sees what a program reading from /dev/tty would see.

โš™ Canonical Mode โ€” Line Buffering on the Slave

By default the PTY slave works in canonical mode. This is the same mode a normal terminal uses. It means:

  • The kernel buffers everything you write to the master.
  • The program reading from the slave gets data only when a newline character (\n) is written.
  • Backspace (^H), line-kill (^U), and other line-editing keys work normally.

This is why when you type in a terminal emulator, the shell does not react until you press Enter. The PTY slave is holding the data in its line buffer.

Example: Writing to master, reading from slave
/* Parent holds master fd, child holds slave fd (via ptyFork) */

/* Parent writes to master */
write(masterFd, "hello\n", 6);

/* Child reads from slave - gets "hello\n" only after '\n' is seen */
read(slaveFd, buf, sizeof(buf));
/* buf now contains "hello\n" */

If you write "hel" to the master without a newline, the child reading the slave will block until the newline arrives.

๐Ÿ’พ PTY Capacity โ€” 4kB Limit on Linux

A PTY pair is not unlimited. Like a pipe, it has a finite internal buffer. On Linux this is approximately 4 kB in each direction.

  • If you write more data than this to the master (without the slave reading), the write() call on the master will block.
  • Similarly, if the master is not reading, the slave’s writes block after 4 kB.
  • This back-pressure prevents runaway processes from consuming all memory.
โš  Practical Note: In real applications like SSH or terminal emulators, reads and writes happen in separate threads or via non-blocking I/O (select/poll/epoll) to avoid deadlocks caused by this 4 kB limit.

๐Ÿ”’ What Happens When You Close One Side of the PTY?

Closing the master or slave has well-defined consequences. This is important for writing robust programs.

๐Ÿšซ When ALL master file descriptors are closed:
Operation on Slave Result
Slave has a controlling process SIGHUP is sent to that process group (like a modem hangup)
read() on slave Returns 0 (end-of-file)
write() on slave Fails with EIO

๐Ÿšซ When ALL slave file descriptors are closed:
Operation on Master Result
read() on master Fails with EIO (on Linux)
write() on master Succeeds but bytes are queued; readable if slave reopens
๐Ÿ’ก Why does EIO matter? In a program like SSH, when the remote user closes their connection, the slave side closes. The SSH daemon detects this via EIO on its master read() and knows it is time to clean up.

๐Ÿ“„ Code Example: Detecting Master Close via SIGHUP
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

/* Signal handler - called when master closes the PTY */
void sighup_handler(int sig)
{
    printf("SIGHUP received: master side closed the PTY!\n");
    printf("Shell would normally exit here.\n");
    _exit(0);
}

int main(void)
{
    /* Install SIGHUP handler */
    signal(SIGHUP, sighup_handler);

    printf("Child process (slave side) waiting...\n");
    printf("PID = %d\n", getpid());

    /* Simulate doing work */
    while (1) {
        sleep(1);
    }

    return 0;
}

When the parent process (holding the PTY master) exits or closes the master fd, the kernel sends SIGHUP to the child’s process group. The child catches it and cleans up. This is exactly what happens when you close a terminal window โ€” the shell receives SIGHUP.

๐Ÿ“„ Code Example: Opening a PTY Pair with posix_openpt()

The modern, portable way to open a PTY pair on Linux uses these functions:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
    int masterFd;
    char *slaveName;

    /* Step 1: Open the PTY master */
    masterFd = posix_openpt(O_RDWR | O_NOCTTY);
    if (masterFd == -1) {
        perror("posix_openpt");
        exit(EXIT_FAILURE);
    }

    /* Step 2: Grant access to slave device */
    if (grantpt(masterFd) == -1) {
        perror("grantpt");
        exit(EXIT_FAILURE);
    }

    /* Step 3: Unlock the slave device */
    if (unlockpt(masterFd) == -1) {
        perror("unlockpt");
        exit(EXIT_FAILURE);
    }

    /* Step 4: Get the name of the slave device (e.g. /dev/pts/5) */
    slaveName = ptsname(masterFd);
    if (slaveName == NULL) {
        perror("ptsname");
        exit(EXIT_FAILURE);
    }

    printf("Master fd = %d\n", masterFd);
    printf("Slave device = %s\n", slaveName);

    /* Step 5: Open the slave */
    int slaveFd = open(slaveName, O_RDWR);
    if (slaveFd == -1) {
        perror("open slave");
        exit(EXIT_FAILURE);
    }

    printf("Slave fd = %d\n", slaveFd);

    /* Now masterFd and slaveFd form a connected PTY pair */
    /* Write to master, read from slave */
    write(masterFd, "hello\n", 6);

    char buf[64] = {0};
    int n = read(slaveFd, buf, sizeof(buf));
    printf("Read from slave: %.*s", n, buf);

    close(masterFd);
    close(slaveFd);
    return 0;
}

Expected Output:

Master fd = 3
Slave device = /dev/pts/5
Slave fd = 4
Read from slave: hello
Compile & Run:
gcc -o pty_open pty_open.c && ./pty_open

๐Ÿ”„ How Input Echo Works on the PTY Slave

When you type in a terminal, you see what you type. This is called echo and it happens in the kernel’s terminal driver, not in your shell.

Step What Happens
1 Terminal emulator writes your keypress to the master
2 Kernel PTY driver copies it to the slave input queue
3 Because echo is ON (default), the same byte is also written to the slave output queue
4 The terminal emulator reading the master sees this echoed byte and displays it on screen
5 The shell reads from the slave (after newline) to get the user input

When a program disables echo (e.g., for password input using tcsetattr()), step 3 is skipped โ€” so passwords are never sent to the terminal screen or to a script recording file.

๐ŸŽ“ Interview Questions โ€” PTY Intro & I/O
Q1. What is a pseudoterminal and why is it needed?
A pseudoterminal is a software pair of connected file descriptors (master and slave) where the slave behaves exactly like a real terminal device. It is needed so that programs like SSH, terminal emulators, and script can connect to shells and other programs that expect to run on a terminal, while the actual data flow goes through software rather than hardware.
Q2. How is a PTY different from a regular bidirectional pipe?
A pipe is a raw byte channel with no special semantics. A PTY slave acts like a terminal: it supports canonical line buffering, input echo, terminal signal generation (SIGINT for ^C, SIGHUP on master close), and all the line-discipline processing that a real terminal provides.
Q3. What is the PTY buffer capacity on Linux and what happens when it is full?
Approximately 4 kB in each direction. When the buffer is full, further write() calls on that side block until the other side consumes some data. This is identical to pipe back-pressure.
Q4. What error does read() on the PTY master return when all slave fds are closed?
On Linux, read() on the master returns -1 with errno set to EIO. Some other UNIX implementations return end-of-file (0) instead.
Q5. What happens to the shell process when you close the terminal window?
Closing the terminal window closes all master fds for that PTY. The kernel sends SIGHUP to the shell (which is the controlling process of the PTY slave). Unless the shell ignores SIGHUP, it exits. Background jobs in that session also receive SIGHUP.
Q6. What are the four steps to open a PTY master using POSIX APIs?
1. posix_openpt(O_RDWR) โ€” open master
2. grantpt(masterFd) โ€” grant ownership of slave to calling process
3. unlockpt(masterFd) โ€” unlock the slave device
4. ptsname(masterFd) + open() โ€” get slave name and open it
Q7. Why are passwords not captured by the script(1) program?
Programs reading passwords disable terminal echo using tcsetattr(). With echo disabled, the PTY kernel driver does not copy slave input bytes to the slave output queue. Since script reads the master output, it never sees the password bytes.
Q8. In canonical mode, when does a program reading from the PTY slave actually receive data?
Only when a newline character (\n) or end-of-file marker (^D) is written to the PTY master. Until then, the data sits in the kernel’s line buffer and the read() on the slave blocks.

Next: ptyFork() โ€” Splitting Master and Slave Across Processes
Learn how to implement ptyFork() that forks a child, gives it the slave side, and sets up a proper session using setsid() and dup2().

Part 2: ptyFork() Implementation โ†’ EmbeddedPathashala Home

Leave a Reply

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