Normally, a read from the PTY master gives you pure data bytes โ the bytes that the shell or program wrote to the slave. But sometimes the master side needs to know about control events happening on the slave, not just data.
Examples of such events: the user pressed Control-S (stop output), or Control-Q (restart output), or a program flushed the terminal queues. These events relate to software flow control (also called XON/XOFF flow control).
Packet mode is a feature where the PTY master, when enabled, can detect these flow-control events and react to them. Without packet mode, the master sees nothing when Control-S is pressed โ the output just quietly stops. With packet mode, the master is told about it via a special control byte.
This is important for network services like telnet and rlogin that carry terminal sessions over a network. They need to know when output is paused or flushed so they can synchronise the remote display correctly.
Software flow control is a mechanism that lets the terminal (or a program) pause and resume output without dropping data.
Pressing Ctrl-S sends an XOFF character to the terminal driver. The driver stops sending output to the terminal. Characters accumulate in the output buffer.
Pressing Ctrl-Q sends an XON character. The driver resumes output from where it stopped. All buffered characters are sent to the screen.
In a normal terminal emulator, when you press Ctrl-S, the screen freezes. When you press Ctrl-Q, it unfreezes. The PTY slave handles this internally. But if you are forwarding this session over a network (like telnet/rlogin), the remote side needs to know when output stopped, so it does not keep sending data that cannot be displayed.
Packet mode is enabled on the master fd using the TIOCPKT ioctl:
#include <sys/ioctl.h>
int masterFd; /* opened earlier with posix_openpt */
int arg;
/* Enable packet mode */
arg = 1;
if (ioctl(masterFd, TIOCPKT, &arg) == -1) {
perror("ioctl TIOCPKT enable");
return 1;
}
/* Disable packet mode */
arg = 0;
if (ioctl(masterFd, TIOCPKT, &arg) == -1) {
perror("ioctl TIOCPKT disable");
return 1;
}
In normal mode, read(masterFd, buf, N) returns N bytes of data from the slave.
In packet mode, each read() from the master returns one of two things:
| Type of Read Result | What it Means | How to Identify |
|---|---|---|
| Control packet | A flow-control event happened on the slave (e.g., flush, stop, start) | buf[0] != 0 (nonzero control byte, rest is empty) |
| Data packet | Actual data written by the program on the slave | buf[0] == 0, then buf[1..n] contains the data |
So in packet mode, every read from the master starts with one extra byte. If that byte is zero, the rest is data. If it is nonzero, it is a bit mask of control events.
char buf[4096];
int n;
n = read(masterFd, buf, sizeof(buf));
if (n <= 0) {
/* error or EOF */
return;
}
if (buf[0] != 0) {
/* Control event - check which bits are set */
if (buf[0] & TIOCPKT_FLUSHREAD)
printf("Input queue was flushed (Ctrl-C effect)\n");
if (buf[0] & TIOCPKT_FLUSHWRITE)
printf("Output queue was flushed\n");
if (buf[0] & TIOCPKT_STOP)
printf("Output stopped (Ctrl-S pressed)\n");
if (buf[0] & TIOCPKT_START)
printf("Output restarted (Ctrl-Q pressed)\n");
if (buf[0] & TIOCPKT_IOCTL)
printf("Terminal ioctl happened on slave\n");
} else {
/* Data event - buf[1..n-1] contains actual data */
printf("Data received (%d bytes): %.*s\n", n-1, n-1, buf+1);
}
When the control byte (buf[0]) is nonzero, it is a bitmask. Multiple bits can be set at once. The common bit flags defined on Linux (from <sys/ioctl.h>):
| Flag | Triggered By | Meaning |
|---|---|---|
TIOCPKT_FLUSHREAD |
tcflush(TCIFLUSH) on slave | Slave’s input queue was flushed |
TIOCPKT_FLUSHWRITE |
tcflush(TCOFLUSH) on slave | Slave’s output queue was flushed |
TIOCPKT_STOP |
Ctrl-S pressed on slave terminal | Slave output has been stopped |
TIOCPKT_START |
Ctrl-Q pressed on slave terminal | Slave output has been restarted |
TIOCPKT_NOSTOP |
Flow control disabled on slave | XON/XOFF flow control turned off |
TIOCPKT_DOSTOP |
Flow control enabled on slave | XON/XOFF flow control turned on |
TIOCPKT_IOCTL |
ioctl on slave | A terminal control change happened |
tty_ioctl(4) man page for your platform.In a real application, you do not just sit in a blocking read() loop. You multiplex between the master fd and other fds (like a network socket) using select() or poll().
Packet mode integrates with both:
When a control event (STOP/START/FLUSH) occurs on the slave, select() signals an exceptional condition on the master fd. This means the master fd should be added to the exceptfds set in the select() call.
When a control event occurs, poll() sets the POLLPRI bit in the revents field for the master fd. Register the master fd with events = POLLIN | POLLPRI.
#include <poll.h>
#include <stdio.h>
#include <sys/ioctl.h>
void packet_mode_loop(int masterFd, int networkFd)
{
struct pollfd fds[2];
char buf[4096];
int n, arg;
/* Enable packet mode on PTY master */
arg = 1;
ioctl(masterFd, TIOCPKT, &arg);
fds[0].fd = masterFd;
fds[0].events = POLLIN | POLLPRI; /* data + control events */
fds[1].fd = networkFd;
fds[1].events = POLLIN;
while (1) {
if (poll(fds, 2, -1) == -1) {
perror("poll");
break;
}
/* PTY master has something */
if (fds[0].revents & (POLLIN | POLLPRI)) {
n = read(masterFd, buf, sizeof(buf));
if (n <= 0) break; /* slave closed */
if (buf[0] != 0) {
/* Control event - handle flow control */
if (buf[0] & TIOCPKT_STOP) {
/* Tell remote to pause its output */
/* send_xoff_to_network(networkFd); */
}
if (buf[0] & TIOCPKT_START) {
/* Tell remote to resume its output */
/* send_xon_to_network(networkFd); */
}
} else {
/* Data: forward buf[1..n-1] to network */
/* write(networkFd, buf + 1, n - 1); */
}
}
/* Network socket has data */
if (fds[1].revents & POLLIN) {
n = read(networkFd, buf, sizeof(buf));
if (n <= 0) break; /* connection closed */
/* Forward to PTY master */
write(masterFd, buf, n);
}
}
}
#include <sys/select.h>
#include <stdio.h>
void select_packet_mode(int masterFd)
{
fd_set readfds, exceptfds;
char buf[4096];
int n;
while (1) {
FD_ZERO(&readfds);
FD_ZERO(&exceptfds);
FD_SET(masterFd, &readfds); /* normal data */
FD_SET(masterFd, &exceptfds); /* packet mode control events */
if (select(masterFd + 1, &readfds, NULL, &exceptfds, NULL) == -1) {
perror("select");
break;
}
/* Either data or control event - read() handles both */
if (FD_ISSET(masterFd, &readfds) ||
FD_ISSET(masterFd, &exceptfds)) {
n = read(masterFd, buf, sizeof(buf));
if (n <= 0) break;
if (buf[0] != 0) {
printf("Control event: 0x%02x\n", (unsigned char)buf[0]);
} else {
printf("Data (%d bytes): %.*s\n", n-1, n-1, buf+1);
}
}
}
}
read() on the master in the normal way. The read returns a single nonzero control byte. You do not need a separate API to fetch the control data.Consider a user connecting via telnet from a remote machine:
| Without Packet Mode | With Packet Mode |
|---|---|
| User presses Ctrl-S on remote end | User presses Ctrl-S on remote end |
| Local PTY slave stops output internally | Local PTY slave stops output internally |
| telnet daemon does not know output stopped | telnet daemon sees TIOCPKT_STOP control byte |
| telnet keeps sending data, filling network buffers | telnet sends XOFF to remote, pausing remote output too |
| Data accumulates, poor user experience | Clean pause and resume across the network |
This is the exact problem packet mode was designed to solve. Modern SSH does not use XON/XOFF flow control this way (it uses its own windowing), but older protocols like telnet and rlogin relied on packet mode for correct flow-control forwarding.
ioctl(masterFd, TIOCPKT, &arg) with arg = 1 to enable, arg = 0 to disable. It is applied to the master fd, not the slave.revents field for the master fd when a control event occurs. The master fd must be registered with events = POLLIN | POLLPRI to receive both data and control notifications.exceptfds fd_set. When select returns with the master fd set in exceptfds, a read will return a nonzero control byte.