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
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
/* 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 */
In noncanonical mode, two elements of the c_cc[] array control when read() returns:
Type: unsigned char (0–255)
Index: VMIN in c_cc[]
Unit: bytes
Type: unsigned char (0–255)
Index: VTIME in c_cc[]
Unit: tenths of a second (0.1s each)
/* 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 */
MIN and TIME each have two meaningful states (zero or nonzero), giving four cases. Each case gives read() different blocking behavior:
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.
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 */
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.
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 */
}
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.
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;
}
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.
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]
*/
| 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 |
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.
Signals enabled
MIN=1, TIME=0
Signals still enabled
ISIG=0, MIN=1, TIME=0
All processing off
/* 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;
}
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 */
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. */
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
