ptyFork() is a utility function that combines two operations into one:
- Open a PTY pair (master + slave)
- Fork a child process and give the child the slave side as its controlling terminal (stdin, stdout, stderr)
The parent keeps the master fd. The child gets the slave as its standard streams. After ptyFork() returns, the parent and child can communicate through the PTY pair just as a terminal emulator communicates with a shell.
This function is used internally by programs like SSH, screen, tmux, expect, and script.
#include "pty_fork.h" /* from TLPI pty/ directory */
pid_t ptyFork(int *masterFd,
char *slaveName,
size_t snLen,
const struct termios *slaveTermios,
const struct winsize *slaveWS);
| Parameter | Direction | Purpose |
|---|---|---|
masterFd |
Output | Filled with the PTY master file descriptor (parent only) |
slaveName |
Output | Buffer to receive the slave device name (e.g. /dev/pts/7) |
snLen |
Input | Size of the slaveName buffer |
slaveTermios |
Input | Terminal attributes to apply to slave (NULL = defaults) |
slaveWS |
Input | Window size to set on slave (NULL = defaults) |
Return value: Behaves like fork(). Returns child PID to parent, 0 to child, -1 on error.
| 1 | posix_openpt() โ Open the PTY master device. Get back mfd. |
| 2 | grantpt() + unlockpt() โ Set permissions and unlock the slave device. |
| 3 | ptsname() โ Get the slave device name string (e.g. /dev/pts/5). Copy to slaveName buffer. |
| 4 | fork() โ Create a child process. Parent gets child PID. Child continues below. |
| 5 | [CHILD] setsid() โ Child becomes a new session leader with no controlling terminal. |
| 6 | [CHILD] close(mfd) โ Child does not need the master fd. |
| 7 | [CHILD] open(slaveName) โ Opening the slave makes it the controlling terminal of the new session (on Linux). On BSD, needs ioctl(TIOCSCTTY) as well. |
| 8 | [CHILD] tcsetattr() / ioctl(TIOCSWINSZ) โ Optionally set terminal attributes and window size. |
| 9 | [CHILD] dup2(slaveFd, 0/1/2) โ Make the slave stdin, stdout, and stderr of the child. |
- Has
masterFd(PTY master) - Does NOT have slaveFd
- Returns child PID from ptyFork()
- Reads/writes masterFd to talk to shell
- Responsible for waiting (waitpid)
- masterFd is CLOSED
- stdin (fd 0) = PTY slave
- stdout (fd 1) = PTY slave
- stderr (fd 2) = PTY slave
- New session leader (setsid done)
- PTY slave is controlling terminal
- Returns 0 from ptyFork()
The child calls setsid() before opening the slave device. Here is why this order matters:
| Without setsid() | With setsid() first |
|---|---|
| Child is still in parent’s session | Child starts a brand new session |
| Child still has parent’s controlling terminal | Child has NO controlling terminal yet |
| Opening slave does NOT make it controlling terminal | Opening slave automatically becomes controlling terminal on Linux |
| Signals go to wrong process group | Signals (SIGINT, SIGHUP) go to correct child session |
setsid() in the child before opening the PTY slave. This is the correct and portable way to set up a new terminal session.On Linux, simply opening the slave device as a session leader (after setsid) automatically makes it the controlling terminal.
On BSD (FreeBSD, macOS), you also need to explicitly call:
#ifdef TIOCSCTTY
if (ioctl(slaveFd, TIOCSCTTY, 0) == -1)
err_exit("ptyFork: ioctl TIOCSCTTY");
#endif
The #ifdef guard means this code compiles on both Linux (where TIOCSCTTY may still be defined but is optional) and BSD (where it is required). This is standard practice in portable system-programming code.
#include <fcntl.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include "pty_fork.h" /* declares ptyFork */
#include "tlpi_hdr.h" /* err_exit macro */
#define MAX_SNAME 1000
pid_t ptyFork(int *masterFd, char *slaveName, size_t snLen,
const struct termios *slaveTermios,
const struct winsize *slaveWS)
{
int mfd, slaveFd, savedErrno;
pid_t childPid;
char slname[MAX_SNAME];
/* --- Open PTY master --- */
mfd = posix_openpt(O_RDWR | O_NOCTTY);
if (mfd == -1)
return -1;
if (grantpt(mfd) == -1) {
savedErrno = errno;
close(mfd);
errno = savedErrno;
return -1;
}
if (unlockpt(mfd) == -1) {
savedErrno = errno;
close(mfd);
errno = savedErrno;
return -1;
}
/* Get slave name */
if (ptsname_r(mfd, slname, MAX_SNAME) != 0) {
savedErrno = errno;
close(mfd);
errno = savedErrno;
return -1;
}
/* Copy slave name to caller's buffer if requested */
if (slaveName != NULL) {
if (strlen(slname) < snLen) {
strncpy(slaveName, slname, snLen);
} else {
close(mfd);
errno = EOVERFLOW;
return -1;
}
}
/* --- Fork --- */
childPid = fork();
if (childPid == -1) {
savedErrno = errno;
close(mfd);
errno = savedErrno;
return -1;
}
/* --- PARENT returns here --- */
if (childPid != 0) {
*masterFd = mfd;
return childPid; /* parent gets child PID */
}
/* === CHILD from here === */
/* Start a new session - child has no controlling terminal yet */
if (setsid() == -1)
err_exit("ptyFork: setsid");
/* Child does not need master fd */
close(mfd);
/* Open slave - on Linux this also sets it as controlling terminal */
slaveFd = open(slname, O_RDWR);
if (slaveFd == -1)
err_exit("ptyFork: open slave");
/* BSD portability: explicitly acquire controlling terminal */
#ifdef TIOCSCTTY
if (ioctl(slaveFd, TIOCSCTTY, 0) == -1)
err_exit("ptyFork: ioctl TIOCSCTTY");
#endif
/* Set terminal attributes on slave if caller provided them */
if (slaveTermios != NULL)
if (tcsetattr(slaveFd, TCSANOW, slaveTermios) == -1)
err_exit("ptyFork: tcsetattr");
/* Set window size on slave if caller provided it */
if (slaveWS != NULL)
if (ioctl(slaveFd, TIOCSWINSZ, slaveWS) == -1)
err_exit("ptyFork: ioctl TIOCSWINSZ");
/* Make slave stdin, stdout, stderr of child */
if (dup2(slaveFd, STDIN_FILENO) != STDIN_FILENO)
err_exit("ptyFork: dup2 STDIN");
if (dup2(slaveFd, STDOUT_FILENO) != STDOUT_FILENO)
err_exit("ptyFork: dup2 STDOUT");
if (dup2(slaveFd, STDERR_FILENO) != STDERR_FILENO)
err_exit("ptyFork: dup2 STDERR");
/* Close original slaveFd - no longer needed (stdin/out/err cover it) */
if (slaveFd > STDERR_FILENO)
close(slaveFd);
return 0; /* child returns 0 like fork() */
}
After opening the slave device, slaveFd might be file descriptor 5 or 7 or any number. The child’s shell will read from fd 0 (stdin), write to fd 1 (stdout), and print errors to fd 2 (stderr).
dup2(slaveFd, 0) means: “make fd 0 point to the same file as slaveFd“. It closes fd 0 first (the old stdin) and replaces it.
/* Before dup2: */
/* fd 0 = pipe or /dev/null or parent's terminal */
/* fd 5 = slaveFd (PTY slave) */
dup2(slaveFd, STDIN_FILENO); /* fd 0 now = PTY slave */
dup2(slaveFd, STDOUT_FILENO); /* fd 1 now = PTY slave */
dup2(slaveFd, STDERR_FILENO); /* fd 2 now = PTY slave */
if (slaveFd > STDERR_FILENO) /* slaveFd is now redundant */
close(slaveFd); /* close fd 5 - no leak */
/* After dup2: */
/* fd 0 = PTY slave (stdin) */
/* fd 1 = PTY slave (stdout) */
/* fd 2 = PTY slave (stderr) */
if (slaveFd > STDERR_FILENO) guard prevents accidentally closing fd 0, 1, or 2 if slaveFd happened to be one of those (very rare but possible).ptyFork() optionally accepts terminal attributes (struct termios) and window size (struct winsize) to apply to the slave. This lets the caller match the slave to the actual user terminal.
#include <stdio.h>
#include <termios.h>
#include <sys/ioctl.h>
#include "pty_fork.h"
int main(void)
{
int masterFd;
pid_t childPid;
struct termios tios;
struct winsize ws;
char slaveName[100];
/* Read the current terminal settings from the real terminal */
if (tcgetattr(STDIN_FILENO, &tios) == -1) {
perror("tcgetattr");
return 1;
}
/* Read window size from the real terminal */
if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == -1) {
perror("ioctl TIOCGWINSZ");
return 1;
}
printf("Terminal: %d rows x %d cols\n", ws.ws_row, ws.ws_col);
/* Fork with matching terminal config for the slave */
childPid = ptyFork(&masterFd, slaveName, sizeof(slaveName),
&tios, &ws);
if (childPid == -1) {
perror("ptyFork");
return 1;
}
if (childPid == 0) {
/* Child: exec a shell */
execlp("bash", "bash", NULL);
perror("execlp");
return 1;
}
/* Parent: masterFd talks to the shell */
printf("Parent: child PID=%d, slave=%s\n", childPid, slaveName);
/* ... read/write masterFd ... */
return 0;
}
If the child forgets to close the master fd, then the child holds both ends of the PTY. When the parent closes its master fd, the child still has it open. This means SIGHUP is never sent to the child and the PTY never properly closes.
If you open the slave before setsid(), the kernel may not make it the controlling terminal (or may assign the wrong terminal). Always call setsid() first.
After dup2() makes fd 0/1/2 point to the slave, the original slaveFd is still open. It must be closed to avoid leaking a file descriptor. The guard
if (slaveFd > STDERR_FILENO) handles this safely.If fork() fails, you must close masterFd before returning -1, otherwise the master fd leaks. The TLPI implementation saves errno before close() because close() could change errno.
*masterFd. To the child it returns 0.if (slaveFd > STDERR_FILENO) prevents accidentally closing fd 0, 1, or 2 if slaveFd happened to be one of those numbers.