What Are Terminal Special Characters?
When you press Ctrl+C at the terminal and a program gets interrupted, or when you press Ctrl+D and the shell sees “end of file”, those keys are called terminal special characters.
The terminal driver intercepts these characters before your program ever sees them. Instead of passing the character to the program, the driver performs a specific action โ like sending a signal, discarding a line, or marking end-of-file.
These special characters are stored in the c_cc array inside the struct termios. Each slot in the array is indexed by a constant starting with the letter V (e.g., VEOF, VINTR, VKILL). You can read, change, or disable each one using tcgetattr()/tcsetattr() or the stty command.
Important: When the terminal driver processes a special character, it is normally discarded โ it does NOT get passed to your program. (Exceptions: CR, EOL, EOL2, NL are passed through.)
The c_cc field of struct termios is an array of bytes. Each byte holds the ASCII value of one special character. You index into the array using the V-prefixed constants (VEOF, VINTR, VERASE, etc.).
#include <termios.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
struct termios tp;
if (tcgetattr(STDIN_FILENO, &tp) == -1) {
perror("tcgetattr");
return 1;
}
/* Print the current special character values */
printf("EOF character : ^%c (ASCII %d)\n",
tp.c_cc[VEOF] + '@', tp.c_cc[VEOF]);
printf("INTR character : ^%c (ASCII %d)\n",
tp.c_cc[VINTR] + '@', tp.c_cc[VINTR]);
printf("ERASE character: ^%c (ASCII %d)\n",
tp.c_cc[VERASE] - 1 + 'A', tp.c_cc[VERASE]);
printf("KILL character : ^%c (ASCII %d)\n",
tp.c_cc[VKILL] + '@', tp.c_cc[VKILL]);
printf("SUSP character : ^%c (ASCII %d)\n",
tp.c_cc[VSUSP] + '@', tp.c_cc[VSUSP]);
return 0;
}
^D
^C
^\
^?
^U
^Z
^Q
^S
1
0
This covers all characters recognized by the Linux terminal driver.
| Character | c_cc Index | Default Key | What it does | Relevant Flag(s) |
|---|---|---|---|---|
| CR | none | ^M (Enter) | Carriage return โ can be mapped to NL via ICRNL | ICANON, IGNCR, ICRNL, OPOST, OCRNL |
| NL | none | ^J | Newline โ terminates a line in canonical mode | ICANON, INLCR, ECHONL, OPOST, ONLCR |
| EOF | VEOF | ^D | End-of-file โ causes read() to return 0 (EOF) | ICANON |
| EOL | VEOL | undefined | Alternate end-of-line terminator (in addition to NL) | ICANON |
| EOL2 | VEOL2 | undefined | Second alternate end-of-line terminator | ICANON, IEXTEN |
| ERASE | VERASE | ^? (Backspace) | Erase the last character typed on the current line | ICANON |
| INTR | VINTR | ^C | Sends SIGINT to the foreground process group | ISIG |
| KILL | VKILL | ^U | Erase the entire current input line | ICANON |
| LNEXT | VLNEXT | ^V | Literal-next โ treat the next character as a literal, not special | ICANON, IEXTEN |
| QUIT | VQUIT | ^\ | Sends SIGQUIT to the foreground process group (also generates core dump) | ISIG |
| REPRINT | VREPRINT | ^R | Reprint the current input line (redisplays what you have typed) | ICANON, IEXTEN, ECHO |
| START | VSTART | ^Q | Resume (start) output that was stopped with STOP character | IXON, IXOFF |
| STOP | VSTOP | ^S | Pause (stop) terminal output (XON/XOFF flow control) | IXON, IXOFF |
| SUSP | VSUSP | ^Z | Sends SIGTSTP โ suspends the foreground process (job control) | ISIG |
| WERASE | VWERASE | ^W | Erase the last word (not just character) typed on the current line | ICANON, IEXTEN |
| DISCARD | VDISCARD | ^O | Toggle discarding of output (not fully implemented on Linux) | โ |
Three special characters send signals to the foreground process group. They are controlled by the ISIG flag. If ISIG is cleared in c_lflag, these characters no longer generate signals โ they are passed to the reading process as data.
INTR
Default action: terminate the process.
QUIT
Default action: terminate + generate core dump.
SUSP
Default action: suspend (stop) the process. Resume with
fg or SIGCONT.#include <termios.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
/* Disable signal generation from INTR, QUIT, SUSP keys */
int main(void) {
struct termios tp;
if (tcgetattr(STDIN_FILENO, &tp) == -1) {
perror("tcgetattr");
return 1;
}
/* Clear ISIG flag โ ^C, ^\, ^Z now pass as raw bytes instead of signals */
tp.c_lflag &= ~ISIG;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) == -1) {
perror("tcsetattr");
return 1;
}
printf("Signals disabled. ^C is now just a byte.\n");
printf("Press ^C (won't kill): ");
fflush(stdout);
int ch = getchar();
printf("Got character with ASCII value: %d\n", ch);
/* Restore */
tp.c_lflag |= ISIG;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp);
return 0;
}
You can disable any special character by setting its c_cc slot to the value returned by:
fpathconf(fd, _PC_VDISABLE)
On most Linux/UNIX systems, this returns 0. Setting a c_cc slot to 0 (or more precisely to _POSIX_VDISABLE) means that character is completely disabled โ pressing the corresponding key has no special meaning at all; it passes through to the reading process as raw data.
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
struct termios tp;
long vdisable;
/* Get the disable value for this terminal */
vdisable = fpathconf(STDIN_FILENO, _PC_VDISABLE);
if (vdisable == -1) {
perror("fpathconf");
return 1;
}
printf("VDISABLE value: %ld\n", vdisable);
if (tcgetattr(STDIN_FILENO, &tp) == -1) {
perror("tcgetattr");
return 1;
}
/* Disable the EOF character (^D will no longer mean EOF) */
tp.c_cc[VEOF] = (cc_t) vdisable;
/* Disable the INTR character (^C won't send SIGINT) */
tp.c_cc[VINTR] = (cc_t) vdisable;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) == -1) {
perror("tcsetattr");
return 1;
}
printf("EOF and INTR characters disabled.\n");
/* Don't forget to restore before exit! */
/* ... restore code ... */
return 0;
}
You can reassign any special character to a different key by writing a new value into c_cc:
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
struct termios tp;
if (tcgetattr(STDIN_FILENO, &tp) == -1) {
perror("tcgetattr");
return 1;
}
/* Change EOF from ^D (ASCII 4) to ^E (ASCII 5) */
tp.c_cc[VEOF] = 5; /* Ctrl+E = ASCII 5 */
/* Change INTR from ^C (ASCII 3) to ^L (ASCII 12) */
tp.c_cc[VINTR] = 12; /* Ctrl+L = ASCII 12 */
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) == -1) {
perror("tcsetattr");
return 1;
}
printf("EOF is now ^E, INTR is now ^L\n");
/* ... restore before exit ... */
return 0;
}
The table shows that CR (carriage return, ^M) and NL (newline, ^J) do NOT have c_cc subscripts. This is because their values are fixed by the ASCII standard and cannot be changed.
However, their behavior can still be controlled through flags:
| Flag | Effect |
|---|---|
| ICRNL (c_iflag) | Map CR to NL on input (default ON โ this is why Enter works as a newline) |
| IGNCR (c_iflag) | Ignore CR on input |
| INLCR (c_iflag) | Map NL to CR on input |
| ONLCR (c_oflag) | Map NL to CR+NL on output (default ON โ ensures cursor moves to column 0) |
| OCRNL (c_oflag) | Map CR to NL on output |
Two special slots in c_cc, VMIN and VTIME, are not character values. Instead, they control how reads behave in noncanonical mode.
In the stty -a output you saw: min = 1; time = 0;
| Parameter | Meaning |
|---|---|
| VMIN | Minimum number of characters that must be available before read() returns |
| VTIME | Timeout in tenths of a second (0 = no timeout, wait indefinitely for VMIN chars) |
These are covered in detail in the noncanonical mode section (Chapter 62.6.2). The key point here is that they live in c_cc but serve a completely different purpose from the other entries.
Q1. What is the c_cc array in struct termios? How many entries does it have?
c_cc is an array of bytes inside struct termios that stores the values of all terminal special characters. Each slot is indexed by a V-prefixed constant (VEOF, VINTR, VERASE, etc.). On Linux, the array has NCCS entries (typically 19 or more). Two special entries, VMIN and VTIME, are used for noncanonical mode control, not as character values.
Q2. What is the difference between INTR (^C), QUIT (^\), and SUSP (^Z)?
All three send signals to the foreground process group, but different ones. INTR sends SIGINT (default: terminate). QUIT sends SIGQUIT (default: terminate + core dump). SUSP sends SIGTSTP (default: suspend the process, which can be resumed later with fg or SIGCONT). All three require the ISIG flag to be set; clearing ISIG disables signal generation for all three.
Q3. What is the difference between ERASE (^?) and KILL (^U)?
ERASE erases only the last character typed in the current input line. KILL erases the entire current input line. Both require ICANON to be enabled. A separate WERASE (^W) erases the last word typed.
Q4. What do START (^Q) and STOP (^S) do?
These implement XON/XOFF software flow control. STOP (^S) pauses terminal output. START (^Q) resumes it. They are controlled by the IXON and IXOFF flags. This is why you might accidentally freeze your terminal by pressing Ctrl+S โ press Ctrl+Q to unfreeze.
Q5. How do you disable a terminal special character from C code?
Set the relevant c_cc slot to the value returned by fpathconf(fd, _PC_VDISABLE), which is typically 0 (also defined as _POSIX_VDISABLE). After calling tcsetattr(), pressing that key will no longer trigger the special behavior and will instead pass the character to the reading process as data.
Q6. Why do CR and NL not have c_cc subscripts unlike other special characters?
CR and NL have fixed ASCII values (13 and 10) that cannot be changed. Because you cannot reassign these characters, there is no c_cc slot for them. Their behavior is controlled indirectly through flags like ICRNL (map CR to NL on input) and ONLCR (map NL to CR+NL on output).
Q7. What is LNEXT (^V) used for?
LNEXT (literal-next) tells the terminal driver to treat the very next character as a literal data character, ignoring any special meaning it may have. For example, if you type ^V followed by ^C, the ^C is passed through as the raw ASCII byte 3 (not as SIGINT). This is how stty lets you type special characters as arguments: stty intr ^V ^L.
Q8. What flag must be set for INTR, QUIT, and SUSP to work?
The ISIG flag in c_lflag must be set. When ISIG is enabled, pressing INTR/QUIT/SUSP sends the corresponding signal. When ISIG is cleared (tp.c_lflag &= ~ISIG), these keys are treated as ordinary data characters โ no signals are generated. This is useful in raw-mode applications like text editors (vim, nano) that handle ^C themselves.
You’ve Covered the Core of Chapter 62!
You now understand how to read/write terminal attributes in C, use stty from the shell, and master the c_cc special character array.
