When you type on a keyboard connected to a Linux terminal, the characters don’t go straight to your program. They first pass through a layer in the kernel called the terminal driver (also called the line discipline). This driver can buffer characters, perform editing (backspace, Ctrl+U to erase a line), translate special key combinations into signals, and echo characters back to the screen.
All of these behaviours are controlled by a data structure called struct termios. Using the functions tcgetattr() and tcsetattr() your program can read and change these settings at runtime.
On early UNIX systems, terminals were real hardware devices (typewriter-like machines, then video display terminals) connected via serial lines. Each vendor made their own terminal with different escape sequences. Modern Linux still supports all this via virtual consoles and pseudoterminals used by terminal emulators.
Key Terms in This Section
All terminal settings are held in a struct termios (defined in <termios.h>). It has four bit-mask fields that control different aspects of terminal behaviour, plus a character array for special key definitions.
| Field | Type | Controls |
c_iflag |
tcflag_t (bitmask) | Input โ parity checking, NL/CR translation, flow control signals on input |
c_oflag |
tcflag_t (bitmask) | Output โ NL/CR translation, tab expansion, output post-processing |
c_cflag |
tcflag_t (bitmask) | Control โ baud rate (legacy way), character size, parity, stop bits, hardware flow control |
c_lflag |
tcflag_t (bitmask) | Local โ canonical mode (ICANON), echo (ECHO), signal generation (ISIG), etc. |
c_cc[] |
cc_t array | Special characters โ EOF (Ctrl+D), ERASE (Backspace), KILL (Ctrl+U), INTR (Ctrl+C), MIN, TIME |
#include <termios.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
struct termios t;
/* Read current terminal settings */
if (tcgetattr(STDIN_FILENO, &t) == -1) {
perror("tcgetattr");
return 1;
}
/* Print a few flag bits */
printf("ICANON (canonical mode) : %s\n",
(t.c_lflag & ICANON) ? "ON" : "OFF");
printf("ECHO (echo input chars) : %s\n",
(t.c_lflag & ECHO) ? "ON" : "OFF");
printf("ISIG (signal on Ctrl+C) : %s\n",
(t.c_lflag & ISIG) ? "ON" : "OFF");
printf("VEOF char : 0x%02X (usually Ctrl+D = 0x04)\n",
t.c_cc[VEOF]);
printf("VINTR char : 0x%02X (usually Ctrl+C = 0x03)\n",
t.c_cc[VINTR]);
return 0;
}
In canonical mode (enabled when the ICANON flag is set in c_lflag), the terminal driver buffers everything you type until you press Enter (or another line-delimiter character). Only then does it hand the complete line to the reading program. This is the mode used by shell prompts, the Python REPL, etc.
characters
buffers line
allows editing
Enter
complete line
Line delimiter characters in canonical mode include: newline (\n), end-of-file (Ctrl+D), end-of-line (c_cc[VEOL]). The ICANON flag being set in c_lflag is what turns canonical mode on.
In noncanonical mode (ICANON cleared), the terminal driver does not buffer lines. Your program can read individual characters as they are typed. Line editing keys (backspace, Ctrl+U) are not processed โ they are just characters like any other. This mode is used by programs like vim, games, and any program that needs immediate keypress response.
| Case | MIN | TIME | Behaviour |
| Case A | > 0 | 0 | Block until MIN characters available, no timeout |
| Case B | 0 | > 0 | Timer starts immediately; return whatever arrived before timeout (even 0 chars) |
| Case C | > 0 | > 0 | Timer starts on first char received; return when MIN chars or timeout reached |
| Case D | 0 | 0 | Non-blocking: return immediately with whatever is available (0 chars if none) |
TIME is in tenths of a second (e.g. TIME=10 means 1.0 second timeout)
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
/* Read one character without waiting for Enter */
int read_one_char(void)
{
struct termios old, new_t;
int ch;
/* Save current settings */
tcgetattr(STDIN_FILENO, &old);
new_t = old;
/* Disable canonical mode and echo */
new_t.c_lflag &= ~(ICANON | ECHO);
new_t.c_cc[VMIN] = 1; /* return after 1 char */
new_t.c_cc[VTIME] = 0; /* no timeout */
/* Apply new settings immediately */
tcsetattr(STDIN_FILENO, TCSANOW, &new_t);
ch = getchar();
/* Restore original settings */
tcsetattr(STDIN_FILENO, TCSANOW, &old);
return ch;
}
int main(void)
{
printf("Press any key (no Enter needed): ");
fflush(stdout);
int c = read_one_char();
printf("\nYou pressed: '%c' (ASCII %d)\n", c, c);
return 0;
}
#include <termios.h>
int tcgetattr(int fd, struct termios *termios_p);
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
Both return 0 on success, -1 on error
| Value | Meaning |
TCSANOW |
Apply changes immediately, right now |
TCSADRAIN |
Wait until all queued output has been transmitted, then apply |
TCSAFLUSH |
Wait for output, discard pending input, then apply |
A good practice: always save the old termios with tcgetattr() before changing anything, and restore it with tcsetattr() before your program exits. If you don’t, the terminal stays in whatever mode you left it in, which can make the shell unusable.
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
struct termios saved, t;
/* Always save first */
if (tcgetattr(STDIN_FILENO, &saved) == -1) {
perror("tcgetattr"); return 1;
}
t = saved;
/* Turn off echo only */
t.c_lflag &= ~ECHO;
/* Apply (TCSAFLUSH = wait for output, discard pending input) */
tcsetattr(STDIN_FILENO, TCSAFLUSH, &t);
printf("Echo is OFF. Type something (won't show): ");
fflush(stdout);
char buf[128];
if (fgets(buf, sizeof(buf), stdin) != NULL) {
printf("\nYou typed: %s", buf);
}
/* Always restore */
tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved);
printf("Echo is restored.\n");
return 0;
}
For real serial terminals, the baud rate (bits per second) matters because data travels over a physical wire. For pseudoterminals in terminal emulators, it is mostly irrelevant, but the API still exists. The functions use symbolic constants like B9600, B115200.
#include <termios.h>
speed_t cfgetispeed(const struct termios *termios_p); /* get input speed */
speed_t cfgetospeed(const struct termios *termios_p); /* get output speed */
int cfsetispeed(struct termios *termios_p, speed_t speed); /* set input */
int cfsetospeed(struct termios *termios_p, speed_t speed); /* set output */
int cfsetspeed(struct termios *termios_p, speed_t speed); /* set both (Linux) */
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
struct termios t;
tcgetattr(STDIN_FILENO, &t);
speed_t ispeed = cfgetispeed(&t);
speed_t ospeed = cfgetospeed(&t);
printf("Input baud rate code: %u\n", (unsigned)ispeed);
printf("Output baud rate code: %u\n", (unsigned)ospeed);
/* Set both speeds to 115200 baud (meaningful on real serial ports) */
cfsetspeed(&t, B115200);
/* Then call tcsetattr() to apply... */
return 0;
}
Beyond reading and writing settings, there are functions that perform specific actions on the terminal line. These are especially useful when working with serial ports.
| Function | What it does |
tcsendbreak(fd, duration) |
Send a break condition (series of zero bits) on the line โ used to reset serial devices |
tcdrain(fd) |
Wait (block) until all queued output has been physically transmitted to the device |
tcflush(fd, queue_selector) |
Discard (flush) queued input, queued output, or both |
tcflow(fd, action) |
Suspend or resume transmission or reception of data (software flow control) |
| Value | Flushes |
TCIFLUSH |
Received but unread input data |
TCOFLUSH |
Written but untransmitted output data |
TCIOFLUSH |
Both input and output queues |
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
/* Wait until all output on stdout has been sent to the terminal */
printf("This line will be fully transmitted before we proceed.\n");
tcdrain(STDOUT_FILENO);
/* Discard any input the user typed ahead */
tcflush(STDIN_FILENO, TCIFLUSH);
printf("Flushed pending input. Now reading fresh:\n");
char buf[64];
fgets(buf, sizeof(buf), stdin);
printf("You typed: %s", buf);
return 0;
}
Older UNIX systems (Seventh Edition and BSD) defined three named input modes. Modern POSIX replaced them with the termios flags, but you will still see these names in documentation and code comments.
| Feature | Cooked (canonical) | Cbreak | Raw |
| Line buffering | โ Yes | โ No | โ No |
| Line editing (Backspace, Ctrl+U) | โ Yes | โ No | โ No |
| Signal chars (Ctrl+C, Ctrl+Z) | โ Yes | โ Yes | โ No |
| Echo input | โ Yes | โ No | โ No |
| NL/CR translation | โ Yes | โ Yes | โ No |
| Modern equivalent | ICANON set | ICANON cleared, ISIG set | ICANON + ISIG cleared |
To emulate cbreak or raw mode in modern code, you simply set the appropriate combination of flags in the termios structure rather than using any named mode API.
#include <termios.h>
#include <unistd.h>
/* Emulate "raw" mode โ no processing at all */
void set_raw_mode(struct termios *saved)
{
struct termios t;
tcgetattr(STDIN_FILENO, saved);
t = *saved;
/* Clear ALL processing flags */
t.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
| INLCR | IGNCR | ICRNL | IXON);
t.c_oflag &= ~OPOST;
t.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
t.c_cflag &= ~(CSIZE | PARENB);
t.c_cflag |= CS8;
t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &t);
}
/* Emulate "cbreak" mode โ char-at-a-time, but signals still work */
void set_cbreak_mode(struct termios *saved)
{
struct termios t;
tcgetattr(STDIN_FILENO, saved);
t = *saved;
t.c_lflag &= ~(ICANON | ECHO); /* no buffering, no echo */
t.c_lflag |= ISIG; /* Ctrl+C still sends SIGINT */
t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &t);
}
Q1. What are the four fields of struct termios and what does each control?
c_iflag controls input processing (parity, NL/CR translation), c_oflag controls output processing (tab expansion, NL/CR mapping), c_cflag controls hardware settings (baud rate, character size, parity, stop bits), and c_lflag controls local line discipline behaviour (canonical mode via ICANON, echo via ECHO, signal generation via ISIG). The c_cc[] array defines special characters.
Q2. What is the difference between canonical and noncanonical mode?
In canonical mode (ICANON set), input is gathered into lines and line editing is enabled. A read() call returns only when a complete line is available. In noncanonical mode, line buffering and editing are disabled. The program reads characters as they arrive, controlled by the MIN and TIME values in the c_cc[] array.
Q3. What do MIN and TIME do in noncanonical mode?
MIN (c_cc[VMIN]) is the minimum number of characters a read() should return. TIME (c_cc[VTIME]) is a timeout in tenths of a second. Their combination gives four cases: MIN>0, TIME=0 (block until MIN chars); MIN=0, TIME>0 (timer starts immediately, return on timeout or first char); MIN>0, TIME>0 (timer after first char, return on MIN or timeout); MIN=0, TIME=0 (non-blocking, return whatever is there).
Q4. What is the difference between TCSANOW, TCSADRAIN, and TCSAFLUSH?
TCSANOW applies new terminal settings immediately. TCSADRAIN waits for all queued output to be transmitted before applying. TCSAFLUSH also waits for output, but additionally discards any unread input before applying. TCSADRAIN or TCSAFLUSH are preferred when changing output settings to avoid garbled output.
Q5. What does tcdrain() do and when would you use it?
tcdrain() blocks until all data queued for output on the given file descriptor has been physically transmitted to the device. You use it when you need to ensure that all output has reached the device before taking some action (like sending a break condition with tcsendbreak(), or before closing the port).
Q6. What is the difference between cooked, cbreak, and raw modes?
These are historical terms from early UNIX. Cooked (canonical) mode does full line buffering, line editing, signal characters, echo, and NL translation. Cbreak mode disables buffering and editing but keeps signal characters active (Ctrl+C still works). Raw mode disables all processing โ characters are passed to the program exactly as received with no interpretation. In modern POSIX, these are emulated by setting the appropriate bits in the termios structure.
Q7. Why should you always restore terminal settings before your program exits?
If your program changes terminal settings (for example disables canonical mode or echo) and then exits or crashes without restoring them, the shell or next program will inherit those changed settings. The terminal may stop echoing typed characters, refuse line editing, or behave in other confusing ways. The fix is to save settings with tcgetattr() at startup and restore them with tcsetattr(TCSAFLUSH) before exit, including in signal handlers.
Q8. What does tcflush() do?
tcflush() discards data in the terminal’s input queue, output queue, or both. TCIFLUSH discards received but unread input, TCOFLUSH discards written but unsent output, and TCIOFLUSH discards both. It is often used before changing terminal settings to clear stale buffered data.
Next: Chapter 62 Exercises โ implement isatty, ttyname, getpass, detect canonical mode
