Linux Terminals Special Characters, VINTR & new_intr.c Walkthrough

 

Linux Terminals – Chapter 62.5
Terminal Flags | Part 4: Special Characters, VINTR & new_intr.c Walkthrough
πŸ“š Part 4 of 4
πŸ’» Code Walkthrough
🎯 Interview Ready

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_cc array and its key indices
  • The VDISABLE mechanism to disable special characters
  • Complete walkthrough of the new_intr.c program from TLPI
  • How to change the INTR character at runtime
  • Comprehensive interview questions across all four parts

Keywords in This Part:

c_cc array VINTR VQUIT VSUSP VERASE VKILL VMIN VTIME VDISABLE fpathconf _PC_VDISABLE new_intr.c strtoul

The c_cc Array β€” Control Characters

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
Key insight: The c_cc entries store byte values, not key names. You can change any of them. For example, you could make Ctrl+A act as INTR instead of Ctrl+C by setting c_cc[VINTR] = 1 (ASCII for Ctrl+A).

Disabling Special Characters: VDISABLE

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

Code Walkthrough: new_intr.c β€” Changing the INTR Character

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
To restore default Ctrl+C as INTR:

stty intr ^C

Or programmatically: tp.c_cc[VINTR] = 3; followed by tcsetattr().

Understanding strtoul with base 0

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.

VMIN and VTIME β€” Controlling Noncanonical Reads

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);
}

Using stty to Inspect and Change Terminal Flags

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
Shell editing note: When testing terminal flag changes that affect input, shell command-line editing (readline) may interfere because it also manipulates termios. Start bash with bash --noediting to disable readline before experimenting with canonical mode or echo flags.

All Four Flag Fields: Quick Summary Diagram
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

Comprehensive Interview Questions β€” All Topics (Parts 1–4)

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.

Chapter 62.5 Complete!

← Part 1: Input Flags Part 2: Output & Control Flags Part 3: Local Flags & Echo

All 4 parts of Chapter 62.5 β€” Terminal Flags

Leave a Reply

Your email address will not be published. Required fields are marked *