Linux Terminals Canonical Mode, Noncanonical Mode, and I/O Queues

 

Linux Terminals โ€“ Chapter 62
Part 2 of 3 ยท Canonical Mode, Noncanonical Mode, and I/O Queues
โŒจ๏ธ Canonical Mode
๐Ÿ”ข Noncanonical Mode
๐Ÿ“ฌ I/O Queues

The most important thing the terminal driver does is control how characters are read from the terminal. Should the application receive characters one at a time as the user types? Or should it wait until the user presses Enter and then receive a full line?

This is controlled by the terminal’s input mode. In Part 2 we explore canonical mode (line-by-line reading), noncanonical mode (character-by-character reading), and the internal input/output queues the kernel uses to buffer terminal data.

Key Terms in This Part
Canonical Mode Noncanonical Mode Raw Mode Cooked Mode Input Queue Output Queue Echoing MAX_INPUT MAX_CANON FIONREAD Line Editing Special Characters

1. Canonical Mode (Cooked Mode)

Canonical mode is the default for every terminal. It is sometimes called cooked mode โ€” the terminal driver “cooks” raw input before handing it to your program.

How it works:

  • The terminal driver collects characters as the user types them, building up a line in its internal buffer.
  • Line editing is enabled โ€” the user can press Backspace to fix mistakes, Ctrl+U to erase the whole line, and Ctrl+W to erase the last word, all before pressing Enter.
  • When the user presses Enter, a newline character is added, and the complete line is made available for the application to read.
  • A read() call blocks until a complete line is available, then returns at most one line at a time.

Canonical Mode โ€“ Character Flow
User types:
"Hello\n"
โ†’
TTY driver buffers
the line, allows editing
โ†’ (on Enter) โ†’
read() returns
"Hello\n"
โ„น๏ธ If the user presses Backspace before Enter, the driver removes the last character from its buffer. The application never sees the mistake.

Canonical mode is ideal for shell commands, text prompts, and any situation where the user is expected to type a whole line before anything happens. A basic shell like bash reads user commands in canonical mode.

2. Noncanonical Mode (Raw Mode)

Noncanonical mode turns off line buffering and line editing in the terminal driver. Characters are delivered to the application as soon as they are typed โ€” without waiting for Enter.

When is noncanonical mode used?

  • vi / vim โ€” each keystroke (j, k, :, etc.) must be processed immediately without Enter.
  • more / less โ€” pressing Space or q acts immediately.
  • Interactive menus โ€” press ‘1’ to select option 1, no Enter needed.
  • Games โ€” movement keys must respond instantly.
  • Serial port communication โ€” in embedded systems, raw bytes from a microcontroller must be received without line-buffering interference.

Noncanonical Mode โ€“ Character Flow
User presses:
'j'
โ†’ immediately โ†’
read() returns
1 byte: 'j'
โš ๏ธ No Enter needed. No line editing. Each character goes straight through. Backspace is just another byte (0x7F or 0x08).

In noncanonical mode, the application is responsible for all editing and interpretation. Programs that use noncanonical mode usually also disable echoing so that raw keystrokes are not printed on screen.

Noncanonical mode is also called raw mode, though strictly speaking “raw mode” often implies also disabling signal generation (no SIGINT from Ctrl+C).

3. Special Characters and Signal Generation

In canonical mode (and by default in noncanonical mode), the terminal driver interprets certain key combinations as special characters:

Key Name Default Effect
Ctrl+C INTR ^C (0x03) Sends SIGINT to foreground process group
Ctrl+\ QUIT ^\ (0x1C) Sends SIGQUIT (with core dump)
Ctrl+Z SUSP ^Z (0x1A) Sends SIGTSTP (suspend/stop process)
Ctrl+D EOF ^D (0x04) Signals end-of-file; read() returns 0
Ctrl+S STOP ^S (0x13) Pause output (XON/XOFF flow control)
Ctrl+Q START ^Q (0x11) Resume output (XON/XOFF flow control)
Backspace ERASE 0x7F or 0x08 Erase last character (canonical mode)
Ctrl+U KILL ^U (0x15) Erase entire current line (canonical mode)

Programs that switch to noncanonical mode typically disable signal generation (by clearing the ISIG flag in c_lflag) so that Ctrl+C is received as a plain byte (0x03) rather than generating SIGINT. This is exactly what vim does.

4. Input and Output Queues

The terminal driver maintains two FIFO queues in the kernel:

Terminal Driver โ€“ Input and Output Queues

Process
calls read() / write()

โ†‘ read() returns data
โ†“ write() puts data

TTY Driver (in kernel)
Input Queue
Max 4096 bytes
(kernel limit on Linux)
Output Queue
Auto back-pressure
(writer suspended if full)
If echoing is ON: input chars are also copied โ†’ Output Queue

โ†‘ chars from keyboard
โ†“ chars to display

Terminal Device
(physical terminal, PTY, or serial port)

Key points about these queues:

Input Queue

Holds characters typed by the user but not yet read by the process. On Linux, the kernel limits this to 4096 bytes (regardless of the POSIX MAX_INPUT value of 255). If the queue fills up, additional typed characters are discarded.

Output Queue

Holds characters written by the process but not yet sent to the terminal. If the output queue is full (the terminal is slow), the kernel automatically suspends the writing process until space is available โ€” the application doesn’t need to handle this.

Echoing: When a user types a character, the terminal driver also places a copy in the output queue. That is why you see what you type โ€” the kernel echoes it, not the application. Programs that read passwords typically turn echoing off.

5. POSIX Limits: MAX_INPUT and MAX_CANON

POSIX defines two limits for terminal input queues:

  • MAX_INPUT โ€” maximum number of bytes that can be stored in the terminal’s input queue.
  • MAX_CANON โ€” maximum number of bytes in a single line of input in canonical mode.

You can query these using sysconf():

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    long max_input = sysconf(_SC_MAX_INPUT);
    long max_canon = sysconf(_SC_MAX_CANON);

    printf("MAX_INPUT  = %ld\n", max_input);
    printf("MAX_CANON  = %ld\n", max_canon);

    return 0;
}

On Linux, both return 255, but the actual kernel limit is 4096 bytes. The POSIX values are just what the kernel reports via the API; the internal buffer is larger.

You can also check how many unread bytes are in the input queue right now using ioctl():

#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>

int main(void)
{
    int unread_bytes;

    /* FIONREAD = "how many bytes can be read without blocking?" */
    if (ioctl(STDIN_FILENO, FIONREAD, &unread_bytes) == -1) {
        perror("ioctl FIONREAD");
        return 1;
    }

    printf("Unread bytes in input queue: %d\n", unread_bytes);
    return 0;
}

FIONREAD is useful when you want to check if data is available without actually blocking on a read() call. Note: this is a Linux extension, not specified in POSIX/SUSv3.

6. Demonstrating the Two Modes in Code

The following example shows the difference clearly. It reads one character in both canonical and noncanonical mode.

/* demo_modes.c
 * Demonstrates the difference between canonical and noncanonical terminal input.
 * Compile: gcc demo_modes.c -o demo_modes
 * Run: ./demo_modes
 */
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
#include <string.h>

/* Save original settings so we can restore them */
static struct termios original_termios;

/* Switch terminal to noncanonical (raw) mode */
void enter_raw_mode(void)
{
    struct termios raw;

    /* Get current attributes */
    tcgetattr(STDIN_FILENO, &original_termios);

    raw = original_termios;

    /* ICANON flag controls canonical vs noncanonical mode.
     * Clearing it enables noncanonical mode.
     * ECHO controls whether typed chars are echoed. Clearing hides them. */
    raw.c_lflag &= ~(ICANON | ECHO);

    /* MIN=1: block until at least 1 character is available */
    raw.c_cc[VMIN]  = 1;
    /* TIME=0: no timeout */
    raw.c_cc[VTIME] = 0;

    /* Apply immediately (TCSANOW) */
    tcsetattr(STDIN_FILENO, TCSANOW, &raw);
}

/* Restore original terminal settings */
void exit_raw_mode(void)
{
    tcsetattr(STDIN_FILENO, TCSANOW, &original_termios);
}

int main(void)
{
    char buf[128];
    int  n;

    /* --- Part 1: Canonical mode (default) --- */
    printf("=== CANONICAL MODE ===\n");
    printf("Type some text and press Enter: ");
    fflush(stdout);

    n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
    buf[n] = '\0';
    printf("read() got %d bytes: [%s]\n\n", n, buf);

    /* --- Part 2: Noncanonical mode --- */
    printf("=== NONCANONICAL MODE ===\n");
    printf("Press any key (no Enter needed): ");
    fflush(stdout);

    enter_raw_mode();

    n = read(STDIN_FILENO, buf, 1);   /* reads ONE char immediately */

    exit_raw_mode();

    printf("\nread() got %d byte(s): ASCII code = %d (char = '%c')\n",
           n, (unsigned char)buf[0],
           (buf[0] >= 32 && buf[0] < 127) ? buf[0] : '?');

    printf("\nTerminal restored to canonical mode.\n");
    return 0;
}

Sample run:

=== CANONICAL MODE ===
Type some text and press Enter: Hello World
read() got 12 bytes: [Hello World
]

=== NONCANONICAL MODE ===
Press any key (no Enter needed): 
read() got 1 byte(s): ASCII code = 106 (char = 'j')

Terminal restored to canonical mode.

Notice that in canonical mode, the Enter key is included in the returned string (as \n). In noncanonical mode, pressing ‘j’ immediately returned โ€” no Enter needed.

โš ๏ธ Always restore terminal settings! If your program crashes while in noncanonical/raw mode, the terminal stays in that broken state. Best practice: use atexit() or signal() handlers to always call your restore function before exiting. The reset or stty sane command can recover a broken terminal from the shell.

7. Terminal Echoing

By default, the terminal driver echoes every character you type back to the output queue, so you can see what you are typing. This echo happens inside the kernel โ€” your application does not do it.

The most common reason to disable echo is reading passwords. Here is a minimal example:

#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>

int read_password(const char *prompt, char *buf, int maxlen)
{
    struct termios old, noecho;
    int n;

    /* Print the prompt */
    printf("%s", prompt);
    fflush(stdout);

    /* Save current terminal settings */
    tcgetattr(STDIN_FILENO, &old);
    noecho = old;

    /* Clear the ECHO flag to stop echoing */
    noecho.c_lflag &= ~ECHO;

    /* Apply new settings immediately */
    tcsetattr(STDIN_FILENO, TCSANOW, &noecho);

    /* Read one line (still in canonical mode, just without echo) */
    n = read(STDIN_FILENO, buf, maxlen - 1);
    if (n > 0) {
        buf[n] = '\0';
        /* Strip trailing newline */
        if (buf[n-1] == '\n') buf[n-1] = '\0';
    }

    /* Restore original settings */
    tcsetattr(STDIN_FILENO, TCSANOW, &old);

    /* Print newline since the Enter key was not echoed */
    printf("\n");
    return (n > 0) ? 0 : -1;
}

int main(void)
{
    char password[128];

    if (read_password("Password: ", password, sizeof(password)) == 0) {
        printf("You entered: [%s]\n", password);
    }
    return 0;
}

This is essentially what getpass() (a legacy POSIX function) does internally.

Interview Questions โ€“ Part 2

Q1. What is canonical mode? What is the default terminal mode?

Canonical mode is the default input mode where the terminal driver processes input line by line. The driver buffers characters as the user types, enables line editing (Backspace, Ctrl+U, etc.), and only makes a complete line available to the application when Enter is pressed. A read() call returns at most one complete line. This is sometimes called cooked mode.

Q2. What is noncanonical (raw) mode? Name two programs that use it.

Noncanonical mode disables line buffering and line editing in the terminal driver. Characters are delivered to the application immediately, without waiting for Enter. Programs that use it: vim (every keystroke is a command), less (spacebar/q act instantly), minicom (raw serial communication), and any interactive menu-driven application.

Q3. What is the purpose of the terminal’s input queue? What is its size limit on Linux?

The input queue holds characters typed by the user that have not yet been read by the application. It acts as a FIFO buffer between the terminal device and the reading process. On Linux, the kernel imposes a hard limit of 4096 bytes on the input queue. Characters typed beyond this limit are discarded. (POSIX defines MAX_INPUT = 255, but Linux uses 4096 internally.)

Q4. What happens when a process writes to the terminal faster than the terminal can display?

The kernel automatically suspends the writing process until space becomes available in the output queue. This is transparent back-pressure โ€” the application does not need to handle this situation explicitly. The write() call will simply block until the terminal driver has room for more data.

Q5. How does terminal echoing work? Who is responsible for it?

Echoing is handled by the kernel’s terminal driver, not the application. When a character arrives in the input queue and echoing is enabled, the driver automatically places a copy in the output queue so the character appears on screen. The application never sees the echo happen. Programs that need to hide input (e.g. password prompts) disable echo by clearing the ECHO flag in c_lflag of the termios structure.

Q6. What signal does Ctrl+C generate, and how can a program receive Ctrl+C as a plain byte instead?

By default, Ctrl+C causes the terminal driver to send SIGINT to the foreground process group. To receive Ctrl+C as a plain byte (0x03) instead of a signal, a program must clear the ISIG flag in c_lflag (part of the termios structure). This is what text editors like vim do โ€” they need to handle Ctrl+C themselves as part of their key mapping, not let the kernel turn it into a signal.

Q7. What does FIONREAD do and when would you use it in embedded Linux?

ioctl(fd, FIONREAD, &count) returns the number of bytes currently in the terminal’s (or socket’s) input queue, without consuming them. In embedded Linux, this is useful when you have a serial port open and want to check non-blocking whether data has arrived from a microcontroller, before committing to a read() call that might block.

Continue to Part 3
Dive deep into the termios structure: all flags, tcgetattr/tcsetattr, and how to configure every aspect of terminal behaviour.

Part 3 โ†’ termios Attributes and Flags โ† Part 1: Terminal Overview

Leave a Reply

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