What Will You Learn?
In this tutorial we will understand what a pseudoterminal (PTY) is, why it was invented, and how the master and slave devices work together. This is fundamental knowledge for anyone working on terminal emulators, SSH, serial consoles over network, or any Linux systems programming involving interactive programs.
Think of a PTY as a “fake terminal” — it looks and behaves exactly like a real hardware terminal from the program’s point of view, but it is entirely virtual and implemented in the Linux kernel.
Key Terms in This Tutorial
Before understanding what a PTY is, let us understand the problem it solves.
Imagine you want to run a text editor like vi on a remote server from your local laptop over a network. You have sockets and TCP/IP to transfer data. But there is a big problem: vi is a terminal-oriented program. It does not just read and write plain text. It expects to be connected to a real terminal device.
Program (vi, bash)
socket directly!
A terminal-oriented program needs three things from its environment:
read() return 0 bytes./dev/tty and receive job-control signals like SIGINT, SIGTSTP, SIGTTIN.None of these things work when stdin/stdout/stderr are connected to a raw socket. The pseudoterminal is the solution to all three problems at once.
A pseudoterminal (PTY) is a pair of virtual kernel devices that together act as a communication channel:
- PTY Master — the controlling side, opened by the driver/relay program
- PTY Slave — looks exactly like a real terminal, used by the terminal-oriented program
Think of it like a bidirectional pipe with terminal features:
/dev/ptmxhandles terminal
line discipline
/dev/pts/Nas if it’s a real terminal
The key rule to remember: whatever you write to the master appears as input on the slave, and whatever the slave program writes appears as output readable from the master. The kernel PTY driver in the middle applies the full terminal line discipline — all the special character processing (Ctrl+C sends SIGINT, Ctrl+D is EOF, etc.).
The PTY slave silently ignores operations that don’t make sense for a virtual device (like setting baud rate or parity). This allows real terminal programs to work without any modifications.
The term terminal-oriented program is broader than you might think. It covers almost every interactive program you use in a shell session:
These programs share common behaviors that require a real terminal:
- They call
tcsetattr()to change terminal mode (raw, noncanonical, etc.) - They call
tcgetattr()to save and restore terminal settings - They use
ioctl(TIOCGWINSZ)to get window size for screen layout - They open
/dev/ttyto directly read passwords or control the terminal - They rely on the terminal driver to deliver signals on special key presses
If you try to run such a program with its stdin/stdout connected to a plain socket or pipe, these operations return errors like ENOTTY (“Not a typewriter”) and the program either crashes or behaves incorrectly.
#include <stdio.h>
#include <termios.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
struct termios t;
/* Try to get terminal attributes on stdin */
if (tcgetattr(STDIN_FILENO, &t) == -1) {
/* If stdin is a pipe or socket, this fails with ENOTTY */
fprintf(stderr, "tcgetattr failed: %s\n", strerror(errno));
/* errno will be ENOTTY = 25 = "Inappropriate ioctl for device" */
} else {
printf("stdin is a terminal - terminal programs work fine\n");
}
/* Check using isatty() before calling terminal functions */
if (isatty(STDIN_FILENO)) {
printf("stdin is a TTY\n");
} else {
printf("stdin is NOT a TTY - PTY slave would fix this\n");
}
return 0;
}
gcc -o check_tty check_tty.c && ./check_tty — runs with a real terminal, prints “is a TTY”echo hello | ./check_tty — stdin is a pipe, prints “NOT a TTY”./check_tty < /dev/null — stdin is /dev/null, prints “NOT a TTY”A pseudoterminal always comes as a matched pair: one master device and one slave device. You cannot use one without the other.
| Property | PTY Master | PTY Slave |
|---|---|---|
| Device file | /dev/ptmx (POSIX multiplexer) |
/dev/pts/0, /dev/pts/1, … (auto assigned) |
| Who opens it | Driver program (ssh server, terminal emulator) | Terminal-oriented program (shell, vi, etc.) |
| Looks like | A regular file descriptor (no special terminal features) | A full terminal device — supports all terminal ioctls |
| Data flow | Write to master → appears as input on slave | Write to slave → appears as output readable from master |
| Terminal processing | Passes through kernel PTY line discipline | Full line discipline applied (echo, Ctrl+C → SIGINT, etc.) |
| Controlling terminal | Cannot be a controlling terminal | Becomes the controlling terminal for the child process |
The kernel PTY line discipline sits between master and slave. It does all the same work as a real serial terminal driver: echoing characters, translating newlines, processing Ctrl+C into SIGINT, Ctrl+Z into SIGTSTP, and so on. This is why terminal-oriented programs work transparently on the PTY slave.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int master_fd;
char *slave_name;
/*
* posix_openpt() opens /dev/ptmx and returns a file descriptor
* for a new PTY master device. This is the POSIX standard way.
* Equivalent to: open("/dev/ptmx", O_RDWR)
*/
master_fd = posix_openpt(O_RDWR | O_NOCTTY);
if (master_fd == -1) {
perror("posix_openpt");
exit(EXIT_FAILURE);
}
/* grantpt(): sets ownership and permissions of the slave device */
if (grantpt(master_fd) == -1) {
perror("grantpt");
exit(EXIT_FAILURE);
}
/* unlockpt(): unlocks the slave so it can be opened */
if (unlockpt(master_fd) == -1) {
perror("unlockpt");
exit(EXIT_FAILURE);
}
/* ptsname(): returns the pathname of the slave device */
slave_name = ptsname(master_fd);
if (slave_name == NULL) {
perror("ptsname");
exit(EXIT_FAILURE);
}
printf("PTY master fd = %d\n", master_fd);
printf("PTY slave device = %s\n", slave_name);
/* slave_name will be something like: /dev/pts/3 */
close(master_fd);
return 0;
}
PTY master fd = 3PTY slave device = /dev/pts/3
Every time you open a new terminal tab or SSH connection, the kernel allocates a new PTY pair. Run ls /dev/pts/ in your terminal to see all active PTY slaves on your system.
This is the most important concept in the entire chapter. The PTY slave device is indistinguishable from a real hardware terminal from the point of view of the terminal-oriented program running on it.
- Physical serial port
- Terminal line discipline in kernel
- isatty() returns 1
- tcsetattr() works
- TIOCGWINSZ ioctl works
- Ctrl+C → SIGINT to fg process
- Can be controlling terminal
- Virtual kernel device
- Same line discipline in kernel
- isatty() returns 1 ✓
- tcsetattr() works ✓
- TIOCGWINSZ ioctl works ✓
- Ctrl+C → SIGINT to fg process ✓
- Can be controlling terminal ✓
Some operations that don’t make sense for a virtual device (like setting baud rate or parity bits) are simply silently ignored by the PTY slave. The program gets a success return code and moves on, never knowing it’s not on real hardware.
tty in your shell — it will print something like /dev/pts/2.tcsetattr(), tcgetattr(), or any terminal-related ioctl. isatty(fd) returns 0 when the fd is a socket or pipe./dev/ptmx) is opened by the driver program (like an SSH server or terminal emulator). It is a regular file descriptor with no terminal features itself. The PTY slave (/dev/pts/N) is connected to the terminal-oriented program. It behaves like a complete terminal device — supports all terminal ioctls, can be a controlling terminal, and has the kernel line discipline applied. Data written to the master appears as input on the slave, and vice versa.posix_openpt(O_RDWR) — opens /dev/ptmx and returns master fd.grantpt(master_fd) — sets correct ownership/permissions on slave device.unlockpt(master_fd) — unlocks the slave so it can be opened.ptsname(master_fd) — returns the pathname of the slave (e.g., /dev/pts/3).2. Terminal driver processing: the kernel line discipline must process special characters (Ctrl+D for EOF, Ctrl+C for SIGINT, echo, etc.).
3. Controlling terminal: must have a controlling terminal so it can open
/dev/tty and receive job-control signals (SIGINT, SIGTSTP, SIGTTIN).isatty(fd) — returns 1 if fd refers to a terminal, 0 otherwise. Alternatively call tcgetattr(fd, &t) — if it returns -1 with errno ENOTTY, the fd is not a terminal. Running the shell command tty also tells you the terminal device name if stdin is a terminal.Up Next: Part 2
How Programs Use PTYs — fork/exec workflow, SSH use case, and complete coding example
