Linux Terminals Noncanonical Mode, MIN/TIME

 

Linux Terminals – Part 4
Chapter 62 | Noncanonical Mode, MIN/TIME, Cooked/cbreak/Raw Modes
4
MIN/TIME Cases
3
Classic Modes
Part 4/4
Series

What is Noncanonical Mode?

In canonical mode, your program waits for the user to press Enter before it gets any data. But many programs — text editors, games, terminal applications — need to respond to individual keystrokes immediately, without waiting for Enter.

Noncanonical mode removes line-by-line buffering. Every character typed is made available to the program right away. Two parameters — MIN and TIME — give you precise control over exactly when a read() call returns. Understanding the four combinations of MIN and TIME is the heart of noncanonical mode.

Keywords in This Part

noncanonical mode ICANON off c_cc[VMIN] c_cc[VTIME] MIN == 0, TIME == 0 MIN > 0, TIME == 0 MIN == 0, TIME > 0 MIN > 0, TIME > 0 polling read blocking read timeout read interbyte timeout cooked mode cbreak mode raw mode escape sequences

Canonical vs Noncanonical – Key Differences

Canonical Mode (ICANON set)

  • Input collected into lines
  • read() blocks until newline/EOF
  • Backspace, Ctrl+U work
  • Ctrl+C generates SIGINT
  • Program gets whole edited line

Noncanonical Mode (ICANON clear)

  • No line buffering
  • read() returns based on MIN/TIME
  • No special editing characters
  • Ctrl+C may or may not signal (configurable)
  • Program gets raw keystrokes
Data Flow: Noncanonical Mode
Keystroke arrives
→ immediate
Terminal Driver
→ no line buffer
Input Queue
→ MIN/TIME logic
Program’s read()
No line editing. No waiting for Enter. MIN and TIME determine when read() unblocks.
/* Basic setup for noncanonical mode */
#include <termios.h>
#include <unistd.h>

struct termios tp, save;

tcgetattr(STDIN_FILENO, &tp);
save = tp;   /* Save for restoration */

tp.c_lflag &= ~(ICANON | ECHO);  /* Disable canonical mode and echo */
tp.c_cc[VMIN]  = 1;    /* Read at least 1 char */
tp.c_cc[VTIME] = 0;    /* No timeout */

tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp);

/* ... read single keystrokes ... */

tcsetattr(STDIN_FILENO, TCSANOW, &save);  /* Restore */

The MIN and TIME Parameters

In noncanonical mode, two elements of the c_cc[] array control when read() returns:

MIN — c_cc[VMIN]
Minimum number of bytes that must be available before read() can return.

Type: unsigned char (0–255)
Index: VMIN in c_cc[]
Unit: bytes

TIME — c_cc[VTIME]
Timer value that can limit how long read() waits.

Type: unsigned char (0–255)
Index: VTIME in c_cc[]
Unit: tenths of a second (0.1s each)

Special rule: MIN and TIME only matter in noncanonical mode. If ICANON is set, these values are ignored completely. Also: if enough bytes are already waiting in the input queue when read() is called, read() returns immediately regardless of TIME — it never needs to wait.
/* TIME examples */
tp.c_cc[VTIME] = 0;   /* No timer (wait indefinitely or until MIN met) */
tp.c_cc[VTIME] = 5;   /* 0.5 seconds (5 × 0.1s) */
tp.c_cc[VTIME] = 10;  /* 1.0 second  (10 × 0.1s) */
tp.c_cc[VTIME] = 50;  /* 5.0 seconds (50 × 0.1s) */
tp.c_cc[VTIME] = 255; /* 25.5 seconds maximum */

The Four MIN/TIME Combinations – Complete Reference

MIN and TIME each have two meaningful states (zero or nonzero), giving four cases. Each case gives read() different blocking behavior:

Case 1
MIN == 0, TIME == 0 — Polling Read (Non-blocking)

What happens: read() checks if any data is available right now. If data is there, it returns immediately with however many bytes are available (up to the count requested). If no data is available at all, read() returns 0 immediately — it does not wait.

Case 1 Behavior
Data available in queue:
read() called
→ immediately
returns N bytes (N ≤ requested)
No data in queue:
read() called
→ immediately
returns 0 (no data)

Similar to O_NONBLOCK? Both avoid blocking when no data is available. Key difference: with O_NONBLOCK, read() returns -1 (EAGAIN) when no data. With MIN=0/TIME=0, read() returns 0. They behave differently on end-of-file too.

Typical use: Checking if input is available without blocking — useful in event loops that also monitor other file descriptors.

/* Case 1: Polling read - check for input without blocking */
tp.c_lflag &= ~ICANON;
tp.c_cc[VMIN]  = 0;
tp.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp);

char ch;
ssize_t n = read(STDIN_FILENO, &ch, 1);
if (n > 0)
    printf("Got char: %c\n", ch);
else if (n == 0)
    printf("No input available right now\n");
/* Unlike O_NONBLOCK: n == 0 means "no data", not EAGAIN */

Case 2
MIN > 0, TIME == 0 — Blocking Read (Wait for MIN Bytes)

What happens: read() blocks until at least MIN bytes are available. It then returns the lesser of MIN bytes or the number of bytes requested. It can block indefinitely — there is no timeout.

Case 2 Behavior (MIN=1, TIME=0)
t=0s
read() called. Blocks. No input yet.
t=???
User presses a key. 1 byte arrives in queue. MIN satisfied.
t=???
read() returns 1 byte. No CPU wasted during waiting.

Typical use: Applications like less, simple terminal games, and any program that needs single-keypress responses. Setting MIN=1, TIME=0 is the most common noncanonical configuration — it gives you exactly one character per read(), blocking until a key is pressed.

/* Case 2: Blocking read, single key at a time (like 'less') */
tp.c_lflag &= ~(ICANON | ECHO);
tp.c_cc[VMIN]  = 1;   /* Need at least 1 byte */
tp.c_cc[VTIME] = 0;   /* No timeout - wait forever if needed */
tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp);

char ch;
printf("Press any key (q to quit):\n");
while (1) {
    read(STDIN_FILENO, &ch, 1);  /* Blocks until a key is pressed */
    printf("Key: 0x%02X ('%c')\n", (unsigned char)ch,
           (ch >= 32 && ch < 127) ? ch : '.');
    if (ch == 'q') break;
    /* No CPU wasted here - process sleeps until keystroke */
}
Why not poll in a loop instead? Using MIN=1,TIME=0 is far more efficient than polling with MIN=0,TIME=0 in a loop. With the blocking approach, the process sleeps and uses zero CPU until a key arrives. Polling loops burn CPU constantly.

Case 3
MIN == 0, TIME > 0 — Read with Overall Timeout

What happens: A timer starts as soon as read() is called. read() returns as soon as the first byte arrives, OR when the timer expires (whichever comes first). If the timer expires with no data, read() returns 0. At least 1 byte is returned if data arrives before timeout.

Case 3 Behavior (MIN=0, TIME=10 → 1 second)
Scenario A: Data arrives before timeout
t=0
read() called. Timer starts (1 second).
t=0.3s
User presses key. read() returns immediately with 1 byte.
Scenario B: Timer expires, no data
t=0
read() called. Timer starts (1 second).
t=1.0s
Timer expires. No data came. read() returns 0.

Typical use: Talking to serial devices (modems, microcontrollers). Program sends a command to the device, then calls read() with a timeout. If the device responds, great. If it doesn’t within TIME tenths of a second, read() returns 0 and you can handle the timeout gracefully instead of hanging forever.

/* Case 3: Read with timeout - useful for serial device communication */
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>

int read_with_timeout(int fd, char *buf, int maxlen, int timeout_tenths)
{
    struct termios tp, save;
    tcgetattr(fd, &tp);
    save = tp;

    tp.c_lflag &= ~ICANON;
    tp.c_cc[VMIN]  = 0;                 /* MIN=0: return on first byte OR timeout */
    tp.c_cc[VTIME] = timeout_tenths;    /* TIME in tenths of second */
    tcsetattr(fd, TCSAFLUSH, &tp);

    /* Send command to device (example) */
    write(fd, "AT\r\n", 4);

    /* Wait for response */
    ssize_t n = read(fd, buf, maxlen);

    tcsetattr(fd, TCSANOW, &save);

    if (n == 0) {
        printf("Timeout: device did not respond within %.1f seconds\n",
               timeout_tenths / 10.0);
        return -1;
    }
    return (int)n;
}

int main(void)
{
    char buf[256];
    int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
    if (fd < 0) { perror("open"); return 1; }

    /* 2 second timeout (20 tenths) */
    int n = read_with_timeout(fd, buf, sizeof(buf), 20);
    if (n > 0) {
        buf[n] = '\0';
        printf("Device responded: %s\n", buf);
    }
    return 0;
}

Case 4
MIN > 0, TIME > 0 — Read with Interbyte Timeout

What happens: The timer does NOT start when read() is called. It starts only after the first byte arrives. After each subsequent byte, the timer is reset. read() returns when either MIN bytes have been read, or when TIME tenths of a second pass between successive bytes. At least 1 byte is always returned (since the timer starts only after the first byte). read() can block indefinitely waiting for the first byte.

Case 4 Behavior (MIN=5, TIME=5 → 0.5s interbyte)
t=0
read() called. Waits indefinitely for first byte. Timer NOT running.
t=2.0s
First byte arrives (byte 1). Timer starts (0.5s).
t=2.1s
Bytes 2, 3, 4 arrive quickly. Timer resets each time.
t=2.7s
No more bytes for 0.5s. Timer expires. read() returns 4 bytes (even though MIN=5).
OR: if all 5 bytes arrive quickly (within 0.5s of each other), read() returns all 5.

Typical use: Handling terminal escape sequences. When you press the left-arrow key on most terminals, it sends a 3-byte sequence: ESC (0x1B), then ‘[‘, then ‘D’. These three bytes arrive nearly simultaneously. Case 4 lets you read them together as one unit by waiting for a brief inter-byte gap rather than dealing with one byte at a time.

/* Case 4: Interbyte timeout - reading escape sequences from terminal */
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void setup_interbyte_timeout(int fd)
{
    struct termios tp;
    tcgetattr(fd, &tp);

    tp.c_lflag &= ~(ICANON | ECHO);
    tp.c_cc[VMIN]  = 1;   /* Need at least 1 byte to start */
    tp.c_cc[VTIME] = 1;   /* 0.1s interbyte gap triggers return */
    tcsetattr(fd, TCSAFLUSH, &tp);
}

int main(void)
{
    struct termios save;
    tcgetattr(STDIN_FILENO, &save);

    setup_interbyte_timeout(STDIN_FILENO);

    printf("Press keys (Ctrl+C to quit):\n");

    unsigned char buf[16];
    ssize_t n;

    while ((n = read(STDIN_FILENO, buf, sizeof(buf))) > 0) {
        printf("Read %zd bytes: ", n);

        for (ssize_t i = 0; i < n; i++)
            printf("0x%02X ", buf[i]);

        /* Identify common escape sequences */
        if (n == 3 && buf[0] == 0x1B && buf[1] == '[') {
            switch (buf[2]) {
                case 'A': printf(" [UP arrow]");    break;
                case 'B': printf(" [DOWN arrow]");  break;
                case 'C': printf(" [RIGHT arrow]"); break;
                case 'D': printf(" [LEFT arrow]");  break;
            }
        } else if (n == 1 && buf[0] == 0x1B) {
            printf(" [ESC key]");
        } else if (n == 1) {
            printf(" ['%c']", (buf[0] >= 32 && buf[0] < 127) ? buf[0] : '?');
        }
        printf("\n");

        if (n == 1 && buf[0] == 3)  /* Ctrl+C = 0x03 */
            break;
    }

    tcsetattr(STDIN_FILENO, TCSANOW, &save);
    return 0;
}
/* Output example:
   Press LEFT arrow: Read 3 bytes: 0x1B 0x5B 0x44 [LEFT arrow]
   Press 'a':        Read 1 bytes: 0x61 ['a']
   Press ESC:        Read 1 bytes: 0x1B [ESC key]
*/

MIN/TIME Cases – Quick Reference Table
Case MIN TIME Timer starts read() returns when Returns 0 if Use case
1 0 0 Never Immediately (any data or none) No data available Polling / non-blocking check
2 >0 0 Never MIN bytes available (blocks forever) Never returns 0 Single keypress response (less, games)
3 0 >0 On read() call 1st byte arrives OR timer expires Timer expires before any data Serial device with response timeout
4 >0 >0 After 1st byte MIN bytes OR interbyte gap > TIME Never (at least 1 byte guaranteed) Escape sequence reading
Universal rule for all cases: If sufficient bytes to satisfy MIN are already in the input queue when read() is called, read() returns immediately regardless of TIME. The timer is only relevant when bytes have not yet arrived.

The Three Classic Terminal Modes – Cooked, cbreak, Raw

These three terminal modes come from Seventh Edition UNIX (1979). Modern Linux does not have a single system call to set them — instead, you set the appropriate flags manually. But the concepts are still widely used and referenced.

Cooked Mode
ICANON=1, ECHO=1
Signals enabled
Full line editing. Characters echoed. Ctrl+C works. Normal shell behaviour.
cbreak Mode
ICANON=0, ECHO=0
MIN=1, TIME=0
Signals still enabled
Single-char at a time. No echo. Ctrl+C still sends SIGINT. Used by screen-oriented apps.
Raw Mode
ICANON=0, ECHO=0
ISIG=0, MIN=1, TIME=0
All processing off
Total control. Every byte goes directly to program. Ctrl+C is just 0x03. Used by terminal emulators.
/* Setting up cbreak mode */
void set_cbreak_mode(int fd, struct termios *saved)
{
    struct termios tp;
    tcgetattr(fd, &tp);
    *saved = tp;                          /* Save for restore */

    tp.c_lflag &= ~(ICANON | ECHO);      /* No line mode, no echo */
    tp.c_lflag |= ISIG;                   /* Keep signals (Ctrl+C works) */
    tp.c_cc[VMIN]  = 1;                   /* One char at a time */
    tp.c_cc[VTIME] = 0;                   /* No timeout */

    tcsetattr(fd, TCSAFLUSH, &tp);
}

/* Setting up raw mode */
void set_raw_mode(int fd, struct termios *saved)
{
    struct termios tp;
    tcgetattr(fd, &tp);
    *saved = tp;

    tp.c_iflag &= ~(BRKINT | ICRNL | IGNBRK | IGNCR | INLCR |
                    INPCK  | ISTRIP | IXON   | PARMRK);
    tp.c_oflag &= ~OPOST;                 /* Disable output processing */
    tp.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
    tp.c_cflag &= ~(CSIZE | PARENB);
    tp.c_cflag |= CS8;                    /* 8-bit chars */
    tp.c_cc[VMIN]  = 1;
    tp.c_cc[VTIME] = 0;

    tcsetattr(fd, TCSAFLUSH, &tp);
}

int main(void)
{
    struct termios saved;

    set_cbreak_mode(STDIN_FILENO, &saved);

    printf("Press keys (Escape to quit):\n");
    char ch;
    while (read(STDIN_FILENO, &ch, 1) == 1 && ch != 27)  /* ESC = 27 */
        printf("Got: 0x%02X\n", (unsigned char)ch);

    /* Restore terminal */
    tcsetattr(STDIN_FILENO, TCSANOW, &saved);
    printf("Restored to cooked mode\n");
    return 0;
}
cbreak vs raw: The difference is signals. In cbreak mode, ISIG is set — so Ctrl+C still sends SIGINT and kills your program (or jumps to your signal handler). In raw mode, ISIG is cleared — so Ctrl+C is just byte 0x03, and your program must handle everything itself. Terminal emulators (like xterm) use raw mode to have full control.

O_NONBLOCK vs MIN=0, TIME=0 — What Is the Difference?

Both make read() return immediately when no data is available. The differences are subtle but important:

Aspect O_NONBLOCK MIN=0, TIME=0
No data available Returns -1, errno = EAGAIN Returns 0
End of file Returns 0 Returns 0 (ambiguous with “no data”)
Where configured open() or fcntl() — file descriptor flag termios c_cc[] — terminal setting
Scope Affects the fd — inherited across dup() Affects the terminal device itself
Works on Any fd (pipes, sockets, files) Terminal fds only
/* O_NONBLOCK approach */
#include <fcntl.h>
#include <errno.h>

int flags = fcntl(STDIN_FILENO, F_GETFL);
fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);

char ch;
ssize_t n = read(STDIN_FILENO, &ch, 1);
if (n == -1 && errno == EAGAIN)
    printf("No input available (O_NONBLOCK)\n");
else if (n == 0)
    printf("EOF\n");

/* MIN=0/TIME=0 approach (noncanonical terminal) */
/* returns 0 for BOTH "no data" and EOF -- harder to distinguish */

Complete Example – Simple Interactive Menu Using Noncanonical Mode

This example shows a real use of noncanonical mode (MIN=1, TIME=0) to build a simple keyboard-driven menu that responds immediately to keypresses:

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

static struct termios orig_termios;

void disable_raw_mode(void)
{
    tcsetattr(STDIN_FILENO, TCSANOW, &orig_termios);
    printf("\r\n[Terminal restored]\r\n");
}

void enable_raw_mode(void)
{
    struct termios raw;
    tcgetattr(STDIN_FILENO, &orig_termios);
    atexit(disable_raw_mode);  /* Auto-restore on exit */

    raw = orig_termios;
    raw.c_lflag &= ~(ECHO | ICANON | ISIG);
    raw.c_cc[VMIN]  = 1;
    raw.c_cc[VTIME] = 0;
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

void clear_screen(void)
{
    write(STDOUT_FILENO, "\033[2J\033[H", 7);  /* ANSI clear screen */
}

int main(void)
{
    enable_raw_mode();

    int selected = 0;
    const char *options[] = { "Option A", "Option B", "Option C", "Quit" };
    int num_options = 4;

    while (1) {
        clear_screen();
        write(STDOUT_FILENO, "=== Menu ===\r\n", 14);

        for (int i = 0; i < num_options; i++) {
            if (i == selected)
                printf("  \033[7m %s \033[0m\r\n", options[i]);  /* Highlighted */
            else
                printf("    %s\r\n", options[i]);
        }
        write(STDOUT_FILENO, "\r\nUse UP/DOWN arrows. Enter to select.\r\n", 40);

        unsigned char buf[3];
        ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));

        if (n == 3 && buf[0] == 0x1B && buf[1] == '[') {
            if (buf[2] == 'A')      /* UP arrow */
                selected = (selected - 1 + num_options) % num_options;
            else if (buf[2] == 'B') /* DOWN arrow */
                selected = (selected + 1) % num_options;
        } else if (n == 1) {
            if (buf[0] == '\r' || buf[0] == '\n') {  /* Enter */
                if (selected == 3) break;  /* Quit */
                printf("\r\nYou selected: %s\r\n", options[selected]);
                write(STDOUT_FILENO, "Press any key to continue...", 28);
                read(STDIN_FILENO, buf, 1);  /* Wait for key */
            } else if (buf[0] == 'q') {
                break;
            }
        }
    }

    return 0;
}
/* Note: This example uses VTIME=0, VMIN=1 (Case 2) 
   but reads up to 3 bytes for escape sequences.
   For proper arrow key handling, Case 4 (interbyte timeout)
   is more robust - see the escape sequence example above. */

Interview Questions – Noncanonical Mode
Q1. What is noncanonical terminal mode and when is it used?
Noncanonical mode (ICANON cleared) disables line buffering in the terminal driver. Characters typed by the user are made available to the reading process immediately, without waiting for a line delimiter. No special editing characters (backspace, Ctrl+U) are processed by the driver. It is used by applications that need to respond to individual keystrokes — text editors like vi, pagers like less, terminal games, and terminal emulators.
Q2. Explain the four combinations of MIN and TIME in noncanonical mode.
MIN=0/TIME=0 is a polling read — read() returns immediately with available data or 0 if none. MIN>0/TIME=0 is a blocking read — read() waits until MIN bytes are available (potentially forever). MIN=0/TIME>0 is a timed read — a timer starts on read() and it returns on first byte or when the timer expires (returning 0 on timeout). MIN>0/TIME>0 is an interbyte-timeout read — the timer starts only after the first byte and resets after each subsequent byte; read() returns when MIN bytes are received or when the gap between bytes exceeds TIME tenths of a second.
Q3. Why is MIN=1, TIME=0 preferred over a polling loop with MIN=0, TIME=0?
With MIN=1/TIME=0, read() blocks in the kernel until a byte arrives. The process sleeps and consumes zero CPU during the wait. With MIN=0/TIME=0, read() returns 0 immediately if no data is available, so you need a loop that keeps calling read() — this is busy-waiting and burns 100% CPU unnecessarily. Blocking reads are always preferred over polling loops for waiting on input.
Q4. What is interbyte timeout (MIN>0, TIME>0) and why is it useful for escape sequences?
With MIN>0/TIME>0, the TIME timer starts only after the first byte arrives and resets with each subsequent byte. If bytes stop arriving for TIME tenths of a second, read() returns whatever it has collected. This is ideal for terminal escape sequences because arrow keys and function keys generate multi-byte sequences (e.g., ESC [ A for the up arrow) that arrive in rapid succession. The interbyte timeout lets the program collect the entire sequence as a unit, while also detecting when only a single ESC byte was sent (user pressed the ESC key alone) because no further bytes follow.
Q5. What is the difference between cbreak mode and raw mode?
Both cbreak and raw mode disable canonical input (ICANON off) and echo (ECHO off). The key difference is signal generation: in cbreak mode, ISIG remains set, so Ctrl+C still generates SIGINT and Ctrl+Z generates SIGTSTP — the program can be interrupted normally. In raw mode, ISIG is also cleared, so all special signal-generating characters are delivered as plain bytes. Additionally, raw mode typically also disables all output processing (OPOST off). Raw mode is used by terminal emulators and programs that need complete byte-level control.
Q6. How does read() with MIN=0/TIME=0 differ from read() with O_NONBLOCK?
Both return immediately when no data is available, but they differ in the return value for the “no data” case: O_NONBLOCK causes read() to return -1 with errno set to EAGAIN, which is unambiguous. MIN=0/TIME=0 causes read() to return 0, which is ambiguous because 0 also means end-of-file. O_NONBLOCK is an fd-level flag set via fcntl() that works on any file descriptor type, while MIN/TIME are terminal-level settings in termios that only apply to terminal fds.
Q7. In Case 3 (MIN=0, TIME>0), when exactly does the timer start?
The timer starts at the moment read() is called — not when the first byte arrives. This is the critical distinction between Case 3 and Case 4. In Case 3, you get a fixed window of TIME tenths of a second from the start of the read() call to receive at least one byte. If no byte arrives within that window, read() returns 0. This makes it suitable for command-response protocols where you know roughly how long a device takes to respond.

Series Complete!

You have covered all topics from TLPI Chapter 62.5 — terminal flags, tcgetattr/tcsetattr, canonical mode, and noncanonical MIN/TIME modes.

← Part 1: Terminal Flags ← Part 3: Canonical Mode EmbeddedPathashala

Leave a Reply

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