What You Will Learn
In Part 6A we understood what Cooked, Cbreak and Raw modes mean. In this part we go hands-on: we implement ttySetCbreak() and ttySetRaw() functions in C, examine every flag change they make, and then study a complete program that uses these functions together with correct signal handling. Proper signal handling is critical โ if your program crashes or gets killed while the terminal is in Raw mode, the shell that launched it will be left in a broken state where keyboard input is invisible and Ctrl-C does not work.
Key Terms
With the old Seventh Edition / BSD terminal driver, switching to Cbreak or Raw mode was a single bit flip. The modern POSIX termios interface replaced those single bits with a full structure of flags, and there is no single “set raw mode” system call. Every application that wants raw or cbreak mode must clear/set the correct combination of flags in struct termios.
Wrapping this in reusable functions achieves three things:
- Correctness โ the exact flags needed are documented and applied once.
- Portability โ differences between Linux, macOS, and embedded RTOS can be isolated inside the functions.
- Safety โ the functions return the previous settings so the caller can restore them.
- Reads current settings via tcgetattr()
- Optionally saves them into *prev
- Clears ICANON, ECHO
- Keeps ISIG (signals work)
- Clears ICRNL (CR stays CR)
- Sets VMIN=1, VTIME=0
- Applies with tcsetattr()
- Reads current settings via tcgetattr()
- Optionally saves them into *prev
- Clears ICANON + ISIG + IEXTEN + ECHO
- Clears ALL input flag processing
- Clears OPOST (output processing off)
- Sets VMIN=1, VTIME=0
- Applies with tcsetattr()
#include <termios.h>
#include <unistd.h>
/*
* ttySetCbreak - place terminal 'fd' into Cbreak mode.
*
* Cbreak = noncanonical, echo off, but signals (Ctrl-C/Z/\) still work.
*
* fd : file descriptor of terminal (typically STDIN_FILENO)
* prevTermios : if non-NULL, the ORIGINAL settings are saved here so
* the caller can restore the terminal later.
*
* Returns 0 on success, -1 on error (errno set).
*/
int ttySetCbreak(int fd, struct termios *prevTermios)
{
struct termios t;
/* Step 1: read current terminal settings into 't' */
if (tcgetattr(fd, &t) == -1)
return -1;
/* Step 2: if caller wants old settings, copy them out now,
BEFORE we modify 't' */
if (prevTermios != NULL)
*prevTermios = t;
/* Step 3: turn OFF line-by-line input and character echo */
t.c_lflag &= ~(ICANON | ECHO);
/* Step 4: keep signal-generating characters ACTIVE
(Ctrl-C โ SIGINT, Ctrl-Z โ SIGTSTP, Ctrl-\ โ SIGQUIT) */
t.c_lflag |= ISIG;
/* Step 5: stop mapping CR (0x0D) to NL (0x0A) on input.
Without this, pressing Enter sends 0x0A not 0x0D,
which confuses programs that watch for raw carriage returns. */
t.c_iflag &= ~ICRNL;
/* Step 6: VMIN=1 means read() returns after at least 1 byte is available.
VTIME=0 means no timeout โ block indefinitely until a byte arrives. */
t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0;
/* Step 7: apply the new settings.
TCSAFLUSH: wait for all output to be sent, then discard
any unread input before applying the change. */
if (tcsetattr(fd, TCSAFLUSH, &t) == -1)
return -1;
return 0;
}
| Step | Code | Why? |
|---|---|---|
| 1 | tcgetattr(fd, &t) |
Read current state so we can modify individual flags without touching others. |
| 2 | *prevTermios = t |
Save BEFORE modifying โ the caller uses this to restore later. |
| 3 | &= ~(ICANON|ECHO) |
Disables line buffering and automatic echo โ we get chars one at a time. |
| 4 | |= ISIG |
Keeps Ctrl-C/Z/\ sending signals. This is what makes Cbreak different from Raw. |
| 5 | &= ~ICRNL |
Stops CRโNL translation so the app sees the real byte the key sent. |
| 6 | VMIN=1, VTIME=0 |
read() blocks until exactly 1 byte arrives; no timer polling needed. |
| 7 | tcsetattr(TCSAFLUSH) |
Flush queued input (prevents stale keystrokes from before mode switch). |
/*
* ttySetRaw - place terminal 'fd' into Raw mode.
*
* Raw = noncanonical, no signals, no echo, no input/output processing.
* Every byte typed reaches the program unmodified.
*
* fd : file descriptor of terminal
* prevTermios : if non-NULL, original settings saved here for later restore
*
* Returns 0 on success, -1 on error.
*/
int ttySetRaw(int fd, struct termios *prevTermios)
{
struct termios t;
if (tcgetattr(fd, &t) == -1)
return -1;
if (prevTermios != NULL)
*prevTermios = t;
/* ---- c_lflag (local / line discipline flags) ---- */
t.c_lflag &= ~(ICANON /* no line-by-line buffering */
| ISIG /* no signals from Ctrl-C/Z/\ */
| IEXTEN /* no extended processing (Ctrl-V) */
| ECHO); /* no automatic echo */
/* ---- c_iflag (input flags) ---- */
t.c_iflag &= ~(BRKINT /* BREAK does not send SIGINT */
| ICRNL /* no CRโNL mapping */
| IGNBRK /* do not ignore BREAK */
| IGNCR /* do not drop CR characters */
| INLCR /* no NLโCR mapping */
| INPCK /* no parity checking */
| ISTRIP /* do not strip 8th bit */
| IXON /* no START/STOP output control */
| PARMRK); /* no parity-error marking */
/* ---- c_oflag (output flags) ---- */
t.c_oflag &= ~OPOST; /* disable ALL output processing */
/* ---- noncanonical read parameters ---- */
t.c_cc[VMIN] = 1; /* read() returns when 1 byte ready */
t.c_cc[VTIME] = 0; /* no timeout: block until byte */
if (tcsetattr(fd, TCSAFLUSH, &t) == -1)
return -1;
return 0;
}
Key differences between ttySetCbreak and ttySetRaw:
| Flag | Cbreak | Raw | Implication |
|---|---|---|---|
ISIG |
ON (kept) | OFF (cleared) | Ctrl-C sends SIGINT in Cbreak; passes as byte 0x03 in Raw |
IEXTEN |
ON | OFF | Ctrl-V (literal next char) works in Cbreak; disabled in Raw |
BRKINT/IGNCR/INLCR etc. |
May be ON | ALL OFF | Raw disables every input transformation โ true binary pass-through |
OPOST |
ON (by default) | OFF | NLโCR+NL expansion happens in Cbreak output; not in Raw |
#ifndef TTY_FUNCTIONS_H
#define TTY_FUNCTIONS_H
#include <termios.h>
/*
* Set terminal 'fd' to Cbreak mode.
* Saves previous settings into *prevTermios if non-NULL.
* Returns 0 on success, -1 on error.
*/
int ttySetCbreak(int fd, struct termios *prevTermios);
/*
* Set terminal 'fd' to Raw mode.
* Saves previous settings into *prevTermios if non-NULL.
* Returns 0 on success, -1 on error.
*/
int ttySetRaw(int fd, struct termios *prevTermios);
#endif /* TTY_FUNCTIONS_H */
Always pair each ttySetCbreak() or ttySetRaw() call with a tcsetattr(fd, TCSANOW, &saved) on exit. The next section shows how to do this correctly in the presence of signals.
Once your program puts the terminal into Cbreak or Raw mode, the terminal driver no longer echoes input or interprets Backspace. If your program exits abnormally โ killed by a signal, segfault, or uncaught exception โ and never runs the restore code, the user’s shell session is left in an unusable state:
- Nothing typed appears on screen.
- In Cbreak mode, Ctrl-C still sends SIGINT to the shell, but in Raw mode even that breaks.
- The user must type
resetblindly, or close the terminal.
โ exit()
โ exit()
โ exit()
โ re-raise SIGTSTP
โ on SIGCONT: reapply mode
The SIGTSTP case is the trickiest. When the user presses Ctrl-Z, the process must:
- Save the program’s current terminal settings (
ourTermios). - Restore the user’s original settings (
userTermios) so the shell prompt works normally. - Re-raise SIGTSTP (with the default handler active) to actually suspend the process.
- When
fgis typed, the process receives SIGCONT and resumes. - On SIGCONT: save the current (possibly user-modified) terminal state as
userTermios, then reapplyourTermios.
The following program demonstrates all the concepts. It places the terminal in Cbreak or Raw mode (depending on a command-line argument), reads characters, echoes them transformed (lowercase letters as-is, control chars as ^X notation, others as *), and handles all signals correctly.
/*
* demo_tty_modes.c
*
* Usage:
* ./demo_tty_modes -- uses Cbreak mode
* ./demo_tty_modes raw -- uses Raw mode
*
* Press 'q' to quit. Watch Ctrl-C behavior differ between modes.
*
* Compile:
* gcc -Wall -o demo_tty_modes demo_tty_modes.c tty_functions.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <termios.h>
#include "tty_functions.h"
/*
* Two terminal state snapshots kept in global scope so signal handlers
* can access them without extra arguments.
*
* userTermios : terminal state as the USER left it (what we restore to)
* ourTermios : terminal state our program set (used after SIGCONT)
*/
static struct termios userTermios;
static struct termios ourTermios;
/* ------------------------------------------------------------------ */
/* Handler for SIGINT, SIGQUIT, SIGTERM */
/* ------------------------------------------------------------------ */
static void restoreAndExit(int sig)
{
/*
* Restore the terminal to the state the user had BEFORE our program ran.
* TCSANOW: apply immediately (don't wait for output drain โ we are exiting).
*/
tcsetattr(STDIN_FILENO, TCSANOW, &userTermios);
/* Print a newline so the next shell prompt appears on a fresh line */
write(STDOUT_FILENO, "\n", 1);
/*
* Re-raise the signal with the default handler so the process exits
* with the right status/core-dump behavior (SIGQUIT generates a core).
*/
signal(sig, SIG_DFL);
raise(sig);
}
/* ------------------------------------------------------------------ */
/* Handler for SIGTSTP (Ctrl-Z / job control suspend) */
/* ------------------------------------------------------------------ */
static void tstpHandler(int sig)
{
/* Step 1: Save our program's terminal settings for later resumption */
tcgetattr(STDIN_FILENO, &ourTermios);
/*
* Step 2: Restore the terminal to what the user had.
* This makes the shell prompt work normally while we are stopped.
*/
tcsetattr(STDIN_FILENO, TCSANOW, &userTermios);
write(STDOUT_FILENO, "\n", 1); /* cosmetic newline */
/*
* Step 3: Re-raise SIGTSTP with the DEFAULT handler so the kernel
* actually stops this process.
* The process is now suspended here until SIGCONT arrives.
*/
signal(SIGTSTP, SIG_DFL);
raise(SIGTSTP);
/*
* ---- execution resumes here after 'fg' sends SIGCONT ----
*
* Step 4: Re-install our custom SIGTSTP handler (it was reset to SIG_DFL above).
*/
signal(SIGTSTP, tstpHandler);
/*
* Step 5: The user may have changed terminal settings while we were stopped
* (e.g. via 'stty'). Save whatever state they left it in.
*/
tcgetattr(STDIN_FILENO, &userTermios);
/*
* Step 6: Reapply our program's terminal mode (cbreak or raw).
*/
tcsetattr(STDIN_FILENO, TCSANOW, &ourTermios);
}
/* ------------------------------------------------------------------ */
/* main */
/* ------------------------------------------------------------------ */
int main(int argc, char *argv[])
{
int useRaw;
char ch;
ssize_t numRead;
/* Decide mode based on presence of any command-line argument */
useRaw = (argc > 1);
/* Install signal handlers BEFORE switching terminal mode */
signal(SIGINT, restoreAndExit);
signal(SIGQUIT, restoreAndExit);
signal(SIGTERM, restoreAndExit);
signal(SIGTSTP, tstpHandler);
/*
* Switch to the chosen mode.
* Pass &userTermios so the ORIGINAL settings are saved there.
* Signal handlers will use userTermios to restore the terminal.
*/
if (useRaw) {
if (ttySetRaw(STDIN_FILENO, &userTermios) == -1) {
perror("ttySetRaw");
exit(EXIT_FAILURE);
}
write(STDOUT_FILENO, "[Raw mode]\r\n", 12);
} else {
if (ttySetCbreak(STDIN_FILENO, &userTermios) == -1) {
perror("ttySetCbreak");
exit(EXIT_FAILURE);
}
write(STDOUT_FILENO, "[Cbreak mode]\r\n", 15);
}
/* Save our terminal settings for the SIGTSTP handler */
tcgetattr(STDIN_FILENO, &ourTermios);
/* ---- Main character read loop ---- */
while (1) {
numRead = read(STDIN_FILENO, &ch, 1);
if (numRead <= 0)
break;
if (ch == 'q') /* 'q' quits the loop */
break;
/*
* Echo transformation rules:
* lowercase letters โ output as-is
* \n and \r โ output unchanged
* other control chars โ display as ^X (e.g. Ctrl-A โ ^A)
* everything else โ display as *
*/
if (ch >= 'a' && ch <= 'z') {
write(STDOUT_FILENO, &ch, 1);
} else if (ch == '\n' || ch == '\r') {
write(STDOUT_FILENO, &ch, 1);
} else if (ch < 0x20 || ch == 0x7f) {
/* Control character: print as ^X */
char ctrl[3];
ctrl[0] = '^';
ctrl[1] = (ch == 0x7f) ? '?' : (ch + '@');
ctrl[2] = '\0';
write(STDOUT_FILENO, ctrl, 2);
} else {
write(STDOUT_FILENO, "*", 1);
}
}
/* ---- Normal exit: restore terminal ---- */
tcsetattr(STDIN_FILENO, TCSANOW, &userTermios);
write(STDOUT_FILENO, "\r\n[Terminal restored]\r\n", 23);
return EXIT_SUCCESS;
}
| # | What happens | Why it is done this way |
|---|---|---|
| 1 | Install all signal handlers | Must be done BEFORE mode switch, otherwise a signal delivered immediately after ttySetRaw() but before signal() would leave the terminal broken. |
| 2 | ttySetRaw/Cbreak(fd, &userTermios) |
Switches mode AND saves original settings in one call. Never call tcgetattr separately for saving โ the mode might change between the two calls. |
| 3 | tcgetattr(fd, &ourTermios) |
Saves what our program set. Used by SIGTSTP handler to reapply after fg. |
| 4 | Read loop: read(fd, &ch, 1) |
VMIN=1, VTIME=0 means this blocks until exactly 1 byte arrives. No polling needed. |
| 5 | Control char echoed as ^X |
In Raw mode control chars are raw bytes (e.g. Ctrl-A = 0x01). Displaying ^A makes them visible and debuggable. |
| 6 | SIGINT/SIGQUIT/SIGTERM handler | Restores terminal, then re-raises with SIG_DFL so the process exits with the correct signal status (important for parent processes and shell scripts). |
| 7 | SIGTSTP handler: restore โ re-raise โ on resume: save user’s state โ reapply ours | The five-step SIGTSTP dance ensures: (a) shell works while suspended, (b) user’s stty changes while suspended are respected, (c) our program gets its mode back on fg. |
| 8 | Normal exit: tcsetattr(TCSANOW, &userTermios) |
Clean path: restore terminal immediately when the loop exits normally (user pressed ‘q’). |
The second argument to tcsetattr(fd, when, &t) controls when the change takes effect:
| when constant | Takes effect | Best used for |
|---|---|---|
TCSANOW |
Immediately | Restore on exit, SIGTSTP handler โ speed matters, output already finished. |
TCSADRAIN |
After all pending output written | Changing baud rate โ ensures previous output was sent at old speed before switch. |
TCSAFLUSH |
After output written, then discards unread input | Entering Cbreak/Raw โ discard keystrokes typed before the switch; prevents them from being misinterpreted in new mode. |
Our ttySetCbreak() and ttySetRaw() use TCSAFLUSH for mode entry. Signal handlers use TCSANOW for restoration because we need it to take effect before the next shell prompt appears.
# Compile (all three .c files together)
gcc -Wall -Wextra -o demo_tty_modes demo_tty_modes.c tty_functions.c
# Run in Cbreak mode (Ctrl-C still sends SIGINT)
./demo_tty_modes
# Run in Raw mode (Ctrl-C passes as byte ^C to the program)
./demo_tty_modes raw
# Expected output (Cbreak mode, typing 'hello', Ctrl-A, then 'q'):
[Cbreak mode]
hello^Aq
[Terminal restored]
Experiment ideas:
- Run in Cbreak mode, press Ctrl-C โ the program exits via SIGINT handler.
- Run in Raw mode, press Ctrl-C โ you see
^Cechoed as output; the program does NOT exit. - Run in Cbreak mode, press Ctrl-Z, then type
fgโ the program resumes in its mode correctly. - Send SIGTERM from another terminal:
kill <pid>โ terminal is restored cleanly.
If you handle SIGINT but forget to call tcsetattr(saved) before exit, the shell is left with no echo. The user must type reset blindly.
With OPOST off, \n in printf is sent as byte 0x0A โ the cursor moves down but NOT back to column 0. Use "\r\n" explicitly, or use write() directly.
A signal delivered between ttySetRaw() and signal(SIGINT, handler) will use the default handler, which calls exit() without restoring the terminal.
If you simply reapply the same ourTermios after every SIGCONT, you ignore any stty changes the user made while suspended. Always re-read userTermios first.
Q1. Why does ttySetCbreak() use TCSAFLUSH rather than TCSANOW?
TCSAFLUSH waits for all pending output to be written to the terminal and then discards any unread input before applying the new settings. This prevents keystrokes that the user typed before the mode switch (in Cooked/canonical mode) from being misinterpreted once the terminal enters Cbreak mode where each byte is immediately available to the program. TCSANOW would apply immediately without flushing, risking stale input being read under the new mode.
Q2. Why must signal handlers be installed BEFORE calling ttySetRaw()?
There is a race window between calling ttySetRaw() and installing the signal handler. If a signal (e.g. SIGINT) is delivered in that window, the process’s default handler runs, which calls _exit() or abort() without restoring the terminal. Installing handlers first eliminates this window because once Raw mode is active all subsequent signals are caught and the terminal is restored before exit.
Q3. Explain the five steps in a correct SIGTSTP handler for a terminal application.
(1) Save the program’s current terminal settings (ourTermios). (2) Restore the user’s original settings (userTermios) so the shell works while the process is stopped. (3) Reset SIGTSTP to SIG_DFL and re-raise it โ this causes the kernel to actually stop the process. (4) On return from the suspension (i.e. after SIGCONT is received and the process resumes): reinstall the custom SIGTSTP handler. (5) Read the current terminal state into userTermios (the user may have run stty while the process was stopped), then reapply ourTermios.
Q4. Why does the demo program use write() instead of printf() for output in Raw mode?
In Raw mode OPOST is cleared, so the kernel does not expand \n (0x0A) to \r\n (0x0D 0x0A). A printf("\n") would move the cursor down one line but NOT return to column 0, causing each output line to appear stair-stepped to the right. The program uses write(STDOUT_FILENO, "\r\n", 2) to explicitly send both the carriage return and newline, and uses write() because it is async-signal-safe (safe to call from signal handlers), whereas printf() is not.
Q5. What happens when SIGINT is received in Raw mode and the handler calls signal(SIGINT, SIG_DFL) then raise(SIGINT)?
After restoring the terminal, the handler resets SIGINT to its default action and then raises SIGINT again. The default action for SIGINT is to terminate the process and set the exit status to indicate it was killed by SIGINT (exit status = 130 in shell convention, or the parent’s waitpid() reports WIFSIGNALED true with signal number SIGINT). This is important for shell scripts using set -e or pipelines โ simply calling exit(1) inside the handler would not propagate the correct termination reason to the parent.
Q6. In the context of an embedded Linux BLE dongle tool, when would you use Raw mode vs Cbreak mode for the UART terminal?
Use Raw mode when forwarding HCI commands and events over the UART โ bytes must pass through completely unmodified (8 bits, no CR/NL mapping, no 8th-bit strip, no flow control interpretation) because BLE HCI packets contain binary data where any substitution corrupts the packet. Use Cbreak mode for an interactive configuration CLI on the embedded device where the user presses single keys to navigate menus but still expects Ctrl-C to abort the tool gracefully (SIGINT) and Ctrl-Z to background it (SIGTSTP). The key difference: Cbreak preserves signal semantics; Raw gives true byte transparency.
Q7. How would you write a minimal “terminal pass-through” in C using ttySetRaw()?
/* Conceptual outline โ reads stdin raw, writes to stdout raw */
struct termios saved;
ttySetRaw(STDIN_FILENO, &saved);
char buf[256];
ssize_t n;
while ((n = read(STDIN_FILENO, buf, sizeof(buf))) > 0)
write(STDOUT_FILENO, buf, n);
tcsetattr(STDIN_FILENO, TCSANOW, &saved);
This is the skeleton of tools like minicom or picocom that relay bytes between a local terminal and a serial/UART device.
| Topic | Key Point |
|---|---|
ttySetCbreak(fd, prev) |
Clears ICANON+ECHO, keeps ISIG; VMIN=1 VTIME=0; TCSAFLUSH |
ttySetRaw(fd, prev) |
Clears ALL processing flags inc. ISIG, OPOST; VMIN=1 VTIME=0; TCSAFLUSH |
| TCSAFLUSH vs TCSANOW | Use TCSAFLUSH for entry (flushes stale input); TCSANOW for restore in handlers |
| Signal handler order | Install handlers BEFORE calling ttySetRaw/Cbreak |
| SIGTSTP handling | Restore terminal โ SIG_DFL โ raise โ on SIGCONT: update userTermios โ reapply ourTermios |
| printf in Raw mode | Dangerous โ use write() with explicit “\r\n” because OPOST is off |
| ncurses equivalent | cbreak() = our Cbreak; raw() = our Raw; handles restore automatically |
Continue Learning
Review the theory behind these implementations
