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.
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 | ||||||||
Both a PTY and a pipe let two processes exchange data. But they are very different in one key way:
- Just moves raw bytes
- No special meaning for Control-C
- No line buffering
- No echo of input
- No terminal signals (SIGINT, etc.)
- 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.
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.
/* 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.
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.
Closing the master or slave has well-defined consequences. This is important for writing robust programs.
| 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 |
| Operation on Master | Result |
|---|---|
read() on master |
Fails with EIO (on Linux) |
write() on master |
Succeeds but bytes are queued; readable if slave reopens |
EIO on its master read() and knows it is time to clean up.#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.
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
gcc -o pty_open pty_open.c && ./pty_openWhen 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.
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.write() calls on that side block until the other side consumes some data. This is identical to pipe back-pressure.read() on the master returns -1 with errno set to EIO. Some other UNIX implementations return end-of-file (0) instead.posix_openpt(O_RDWR) โ open master2.
grantpt(masterFd) โ grant ownership of slave to calling process3.
unlockpt(masterFd) โ unlock the slave device4.
ptsname(masterFd) + open() โ get slave name and open ittcsetattr(). 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.\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.Part 2: ptyFork() Implementation โ EmbeddedPathashala Home
