Special Characters in termios
Beyond the flag fields, struct termios also has an array called c_cc (control characters). This array stores the actual characters (byte values) that trigger special behaviors in the terminal driver β like which character acts as ERASE (Backspace), KILL (erase line), INTR (send SIGINT), etc.
In this final part we look at:
- The
c_ccarray and its key indices - The
VDISABLEmechanism to disable special characters - Complete walkthrough of the
new_intr.cprogram from TLPI - How to change the INTR character at runtime
- Comprehensive interview questions across all four parts
The c_cc array in struct termios maps symbolic index names to the actual byte values that trigger special terminal behaviors. Think of it as a lookup table: “what character value causes ERASE to happen?” β c_cc[VERASE].
| c_cc Index | Default Key | Default Value | What it triggers | Flag needed |
|---|---|---|---|---|
VINTR |
Ctrl+C | 3 (ASCII ETX) | Sends SIGINT to foreground process group | ISIG |
VQUIT |
Ctrl+\ | 28 (ASCII FS) | Sends SIGQUIT (core dump + terminate) | ISIG |
VSUSP |
Ctrl+Z | 26 (ASCII SUB) | Sends SIGTSTP (suspend/stop) | ISIG |
VERASE |
Backspace / DEL | 127 (ASCII DEL) | Erase previous character from line buffer | ICANON |
VKILL |
Ctrl+U | 21 (ASCII NAK) | Erase entire current input line | ICANON |
VEOF |
Ctrl+D | 4 (ASCII EOT) | End of file β flush current line to reader, return 0-length read if at line start | ICANON |
VEOL |
(none) | 0 (disabled) | Additional end-of-line character (besides NL) | ICANON |
VSTART |
Ctrl+Q | 17 (ASCII DC1) | Restart stopped output (XON) | IXON/IXOFF |
VSTOP |
Ctrl+S | 19 (ASCII DC3) | Stop output (XOFF) | IXON/IXOFF |
VMIN |
N/A | 1 | Noncanonical: minimum bytes before read() returns | ~ICANON |
VTIME |
N/A | 0 | Noncanonical: timeout in 0.1-second units for read() | ~ICANON |
c_cc[VINTR] = 1 (ASCII for Ctrl+A).You can completely disable a special character by setting its c_cc entry to the VDISABLE value. This is a special byte value (usually 0xFF = 255) that means “this character is disabled β don’t treat any byte as this special character.”
The portable way to get the VDISABLE value for the current system is to call:
long vdisable = fpathconf(fd, _PC_VDISABLE);
fpathconf() queries file-system or terminal configuration values. The constant _PC_VDISABLE asks “what byte value disables a special character on this terminal?”
| c_cc[VINTR] value | Effect |
|---|---|
3 (default = Ctrl+C) |
Pressing Ctrl+C sends SIGINT |
1 (Ctrl+A) |
Pressing Ctrl+A now sends SIGINT |
VDISABLE (e.g., 0xFF) |
INTR is disabled β no character triggers SIGINT |
This is a program from TLPI (The Linux Programming Interface) that demonstrates how to change the INTR special character at runtime. Let’s walk through it step by step:
/* new_intr.c β Change or disable the INTR (interrupt) character
*
* Usage:
* ./new_intr -- disable INTR completely
* ./new_intr 65 -- set INTR to decimal 65 ('A') β Ctrl+A = SIGINT
* ./new_intr 0x41 -- set INTR to hex 0x41 (also 'A')
* ./new_intr A -- set INTR to the literal character 'A'
*/
#include <termios.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
struct termios tp;
int intrChar;
/* ---- Step 1: Determine the new INTR character ---- */
if (argc == 1) {
/*
* No argument: disable INTR completely.
* fpathconf with _PC_VDISABLE returns the "disable" byte value.
* On Linux this is typically 0 or 0xFF.
*/
intrChar = fpathconf(STDIN_FILENO, _PC_VDISABLE);
if (intrChar == -1) {
perror("fpathconf: Couldn't determine VDISABLE");
exit(EXIT_FAILURE);
}
} else if (isdigit((unsigned char) argv[1][0])) {
/*
* Argument starts with a digit: treat it as a number.
* strtoul with base 0 auto-detects: 0x prefix = hex, 0 prefix = octal,
* no prefix = decimal.
* Example: "65" -> 65, "0x41" -> 65, "0101" -> 65 (all = 'A')
*/
intrChar = strtoul(argv[1], NULL, 0);
} else {
/*
* Argument is a literal character: use its ASCII value.
* Example: "./new_intr A" -> intrChar = 65
* Example: "./new_intr ^" -> intrChar = 94
*/
intrChar = argv[1][0];
}
/* ---- Step 2: Read current terminal attributes ---- */
if (tcgetattr(STDIN_FILENO, &tp) == -1) {
perror("tcgetattr");
exit(EXIT_FAILURE);
}
/* ---- Step 3: Change the VINTR entry in c_cc ---- */
/*
* c_cc[VINTR] holds the byte value that causes SIGINT.
* Setting it to intrChar changes (or disables) the INTR character.
*/
tp.c_cc[VINTR] = intrChar;
/* ---- Step 4: Push the modified settings back ---- */
/*
* TCSAFLUSH: wait for pending output to be written, discard unread input,
* then apply new settings. Best choice for input-related changes.
*/
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) == -1) {
perror("tcsetattr");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
Compile and test:
gcc -o new_intr new_intr.c
# Disable INTR (Ctrl+C won't send SIGINT anymore)
./new_intr
# Change INTR to Ctrl+A (ASCII 1)
./new_intr 1
# Change INTR to the letter 'A' (ASCII 65 = 0x41)
./new_intr A
./new_intr 65
./new_intr 0x41
# Verify: check current INTR character using stty
stty -a | grep intr
stty intr ^C
Or programmatically: tp.c_cc[VINTR] = 3; followed by tcsetattr().
The new_intr.c program uses strtoul(argv[1], NULL, 0). The base = 0 is important β it lets the function auto-detect the number format:
| Input string | Auto-detected base | Parsed value |
|---|---|---|
"65" |
Decimal (no prefix) | 65 |
"0x41" |
Hexadecimal (0x prefix) | 65 |
"0101" |
Octal (0 prefix) | 65 |
This makes the program flexible β users can specify the INTR character value in whatever numeric format they prefer.
In noncanonical mode (ICANON OFF), the special c_cc entries VMIN and VTIME control when read() returns:
| VMIN | VTIME | Behavior of read() | Typical use |
|---|---|---|---|
| > 0 | 0 | Block until at least VMIN bytes are available. No timeout. | Character-by-character input (VMIN=1) |
| 0 | > 0 | Return after VTIME tenths of seconds even if no data. Returns 0 on timeout. | Polling with timeout |
| > 0 | > 0 | Timer starts after first byte arrives. Return when VMIN bytes received OR VTIME expires. | Protocol framing |
| 0 | 0 | Non-blocking: return immediately with whatever is available (0 if nothing). | Event loop / polling |
/* Example: noncanonical with 0.5 second timeout */
struct termios tp;
tcgetattr(fd, &tp);
tp.c_lflag &= ~ICANON;
tp.c_cc[VMIN] = 0; /* Return even if no bytes */
tp.c_cc[VTIME] = 5; /* 5 * 0.1 = 0.5 seconds timeout */
tcsetattr(fd, TCSAFLUSH, &tp);
char c;
int n = read(fd, &c, 1);
if (n == 0) {
printf("Timeout: no input in 0.5 seconds\n");
} else if (n == 1) {
printf("Got char: 0x%02X\n", (unsigned char)c);
}
The stty command is the command-line tool for viewing and modifying termios settings. It is essential for debugging and for disabling flags before testing programs that manipulate them.
# Show all current terminal settings in detail
stty -a
# Show raw termios values (numeric)
stty -g
# Disable ECHO (hide typed characters)
stty -echo
# Re-enable ECHO
stty echo
# Disable canonical mode (raw character input)
stty -icanon
# Restore canonical mode
stty icanon
# Change INTR character to Ctrl+A (^A)
stty intr ^A
# Restore INTR to default Ctrl+C
stty intr ^C
# Disable Ctrl+S / Ctrl+Q (IXON)
stty -ixon
# Save current settings and restore later
saved=$(stty -g)
stty raw -echo
# ... do your raw terminal work ...
stty $saved
bash --noediting to disable readline before experimenting with canonical mode or echo flags.| Field | Controls | Most Important Flags | Where in data flow |
|---|---|---|---|
c_iflag |
Input preprocessing | ICRNL, IXON, BRKINT, INPCK, ISTRIP, IUTF8 | After keyboard β before program read() |
c_oflag |
Output postprocessing | OPOST (master), ONLCR, TABDLY/TAB3 | After program write() β before screen |
c_cflag |
Hardware/serial settings | CSIZE/CS8, PARENB, CSTOPB, CLOCAL, CRTSCTS, baud rate | Physical serial line framing |
c_lflag |
Line discipline behavior | ECHO, ECHOCTL, ECHOE, ICANON, ISIG, IEXTEN, NOFLSH, TOSTOP | Above all β line editing & echo layer |
c_cc |
Special character values | VINTR, VERASE, VKILL, VEOF, VMIN, VTIME | Configures WHAT byte triggers each behavior |
Q1. What is the c_cc array in struct termios? How many entries does it have and what are the most important ones?
c_cc is an array of cc_t (unsigned char) values that stores the byte values triggering special terminal behaviors. Its size is NCCS (typically 32 on Linux). The most important entries are: VINTR (SIGINT trigger, default Ctrl+C), VQUIT (SIGQUIT, Ctrl+\), VSUSP (SIGTSTP, Ctrl+Z), VERASE (backspace, DEL), VKILL (kill line, Ctrl+U), VEOF (EOF, Ctrl+D), VMIN and VTIME (noncanonical read control).
Q2. What is VDISABLE and how do you get its value portably?
VDISABLE is a special byte value stored in a c_cc entry that tells the driver “this special character is disabled β no byte will trigger this function.” The portable way to get it is: long vd = fpathconf(fd, _PC_VDISABLE); On Linux it is typically 0 or 255 (0xFF). Setting e.g. c_cc[VINTR] = vd means no key can trigger SIGINT.
Q3. Explain how the new_intr program changes the INTR character. What argument forms does it accept?
The program reads the new INTR character from the command line. No argument: disables INTR using fpathconf(_PC_VDISABLE). An argument starting with a digit: parses it as a number using strtoul(arg, NULL, 0) which auto-detects decimal, hex (0x prefix), or octal (0 prefix). A non-digit argument: uses the first character’s ASCII value directly. It then calls tcgetattr(), sets tp.c_cc[VINTR] = intrChar, and applies with tcsetattr(TCSAFLUSH).
Q4. What are VMIN and VTIME in noncanonical mode? Give the four combinations and their behavior.
VMIN and VTIME control when read() returns in noncanonical mode. VMIN>0, VTIME=0: block until VMIN bytes available (no timeout). VMIN=0, VTIME>0: return after VTIMEΓ0.1 seconds even if no data (pure timeout). VMIN>0, VTIME>0: timer starts after first byte; return when VMIN bytes arrived OR timer expires. VMIN=0, VTIME=0: non-blocking, return immediately with whatever is available.
Q5. How would you implement a program that reads single keystrokes without the user pressing Enter?
Save original termios, disable ICANON and ECHO in c_lflag, set VMIN=1 and VTIME=0 in c_cc for immediate return on each character, then call tcsetattr(TCSAFLUSH). Each read(STDIN, &c, 1) will now return immediately with one character. Always restore original settings on exit or signal, since failure to restore leaves the terminal unusable.
Q6. What is the purpose of the IEXTEN flag?
IEXTEN enables implementation-defined extended input processing. On Linux, it is needed for the IUCLC flag to work (uppercase-to-lowercase input mapping) and enables additional special characters like LNEXT (literal next, Ctrl+V) and WERASE (word erase, Ctrl+W). If you want strictly POSIX-compliant behavior with no extended processing, you turn it off.
Q7. In embedded Linux serial port programming, what is the typical sequence to configure a UART?
Open with open(device, O_RDWR | O_NOCTTY). Call tcgetattr() to read current settings. Set baud rate with cfsetispeed() and cfsetospeed(). In c_cflag: set CS8, clear PARENB, clear CSTOPB, set CREAD | CLOCAL. In c_iflag: clear ICRNL, IXON, IXOFF, INPCK, ISTRIP. In c_oflag: clear OPOST. In c_lflag: clear ICANON, ECHO, ISIG. Set VMIN and VTIME for desired read behavior. Apply with tcsetattr(TCSANOW).
Q8. What is the staircase problem and when does it occur?
The staircase problem is when output lines print in a diagonal staircase pattern, with each line starting where the previous one ended instead of at column 0. It occurs when OPOST or ONLCR is disabled (e.g., in raw mode) and the program writes only \n without \r. Since \n only moves down one line without returning to column 0, successive lines shift rightward. The fix is to write \r\n explicitly when output processing is disabled.
Q9. Why does bash use –noediting when testing terminal flag changes via stty?
Bash uses GNU readline for command-line editing, which itself manipulates terminal settings (including ICANON mode) when reading each command. When you modify terminal flags using stty and then try to type the next shell command, readline may overwrite your changes before they take effect or may behave unexpectedly. Running bash --noediting disables readline so the shell reads input in a simpler way, leaving your terminal settings intact.
β Part 1: Input Flags Part 2: Output & Control Flags Part 3: Local Flags & Echo
