Linux Terminals Testing Terminal Modes: cbreak & raw

 

Linux Terminals โ€“ Part 7A
Testing Terminal Modes: cbreak & raw โ€” Full Program Walkthrough
๐Ÿ“˜ Chapter 62
๐Ÿ”ง TLPI Series
โš™๏ธ Systems Programming

What This Tutorial Covers

In earlier parts we learned how to put a terminal into cbreak mode and raw mode using ttySetCbreak() and ttySetRaw(). Now we look at a complete working program test_tty_functions.c that actually uses those functions. This program shows how to handle signals correctly when the terminal is in a non-canonical mode, and how to safely switch modes when the process is stopped and resumed (SIGTSTP/SIGCONT handling).

Understanding this program is critical because in real-world embedded or system tools (like serial terminal emulators, custom shells, or BLE diagnostic tools), you will run into exactly these problems: how to restore terminal settings when something goes wrong, and how to handle job control signals safely.

ttySetCbreak ttySetRaw tcsetattr tcgetattr SIGTSTP SIGCONT SIGINT SIGQUIT SIGTERM signal handler terminal restore job control

1. What Does the Program Do?

The program reads one character at a time from stdin and echoes it back in a modified form:

  • Letters โ†’ printed as lowercase
  • Newline or carriage return โ†’ printed as-is
  • Control characters โ†’ printed as ^A, ^B, etc.
  • Everything else โ†’ printed as *
  • Typing q โ†’ exits the loop

If a command-line argument is given (any argument), it uses cbreak mode. Otherwise it uses raw mode.

Program Flow Diagram
START: main() called
argc > 1 ? โ†’ cbreak mode (ttySetCbreak)
else โ†’ raw mode (ttySetRaw)
Install signal handlers
(SIGINT, SIGQUIT, SIGTSTP, SIGTERM)
Loop: read() one char at a time
Echo transformed character to stdout
If ch == ‘q’ โ†’ break loop
tcsetattr() โ†’ restore original terminal settings
EXIT

2. Why We Save the Original Terminal Settings

The very first thing the program does (inside ttySetCbreak() or ttySetRaw()) is save the current terminal settings into a global variable called userTermios.

This is important. If the program crashes, gets a signal, or exits without restoring settings, the terminal becomes broken โ€” user sees garbage or can’t type. So the original settings must be saved before any modification, and restored at exit or on any signal.

/* Global: stores what terminal looked like BEFORE our changes */
static struct termios userTermios;

/* ttySetCbreak() or ttySetRaw() fills this BEFORE modifying anything */
if (tcgetattr(STDIN_FILENO, &userTermios) == -1)
    errExit("tcgetattr");

The global variable is used by signal handlers. Signal handlers need to restore the terminal before the process exits. If userTermios was a local variable inside main(), the signal handlers could not reach it.

3. The General Signal Handler โ€” handler()

The handler() function is installed for SIGINT (Ctrl+C), SIGQUIT (Ctrl+\), and SIGTERM. All three signals mean “the program should terminate”. So the handler does two things:

  • Restore terminal settings using tcsetattr()
  • Exit with _exit() โ€” not exit()
static void
handler(int sig)
{
    /* Restore terminal to the saved original state */
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &userTermios) == -1)
        errExit("tcsetattr");

    /* _exit() instead of exit() โ€” avoids running atexit() handlers
       or flushing stdio buffers in an async-signal-unsafe context */
    _exit(EXIT_SUCCESS);
}

Why _exit() and not exit()? Signal handlers must only call async-signal-safe functions. exit() calls stdio flush routines which are not signal-safe. _exit() is safe โ€” it terminates immediately without any cleanup.

Signal Triggered By Handler Used Mode
SIGINT Ctrl+C handler() cbreak only
SIGQUIT Ctrl+\ handler() cbreak only
SIGTERM kill command handler() both modes
SIGTSTP Ctrl+Z tstpHandler() cbreak only

Why only in cbreak mode? In raw mode, terminal special characters are disabled. Ctrl+C does not generate SIGINT in raw mode โ€” it arrives as a raw byte (value 3). So there is no need to catch these signals in raw mode.

Also notice: before installing SIGINT and SIGQUIT handlers, the program first checks whether those signals are already being ignored (e.g., the parent shell may have set SIG_IGN). It only installs a handler if the signal is not already ignored. This is standard practice.

/* Check existing disposition before overriding */
if (sigaction(SIGQUIT, NULL, &prev) == -1)
    errExit("sigaction");

if (prev.sa_handler != SIG_IGN)   /* Only install if not ignored */
    if (sigaction(SIGQUIT, &sa, NULL) == -1)
        errExit("sigaction");

4. The SIGTSTP Handler โ€” Handling Ctrl+Z (Job Control)

This is the most complex part of the program. When the user presses Ctrl+Z, the kernel sends SIGTSTP to the foreground process group. Normally this suspends the process. But if our terminal is in cbreak or raw mode at that moment, the terminal settings remain modified even while the process is stopped โ€” which breaks any other foreground process.

So tstpHandler() does the following sequence:

SIGTSTP Handler Step-by-Step
1
Save errno (signal handlers can corrupt errno)
2
tcgetattr() โ†’ save our current terminal settings into ourTermios
3
tcsetattr() โ†’ restore user’s original terminal settings (userTermios) so the terminal is clean while we are stopped
4
Reset SIGTSTP disposition to SIG_DFL (default), then call raise(SIGTSTP) to actually stop ourselves
5
Unblock SIGTSTP so the raised signal actually stops the process
โ€” Process is now suspended here โ€”
6
Execution resumes here after SIGCONT (user types fg)
7
Reblock SIGTSTP (restore previous signal mask)
8
Reinstall tstpHandler as the SIGTSTP handler (we reset it to SIG_DFL in step 4)
9
Re-read terminal settings into userTermios โ€” user may have changed them while we were stopped
10
Restore our terminal settings (ourTermios) so the program continues working correctly
static void
tstpHandler(int sig)
{
    struct termios ourTermios;
    sigset_t tstpMask, prevMask;
    struct sigaction sa;
    int savedErrno;

    savedErrno = errno;  /* Step 1: save errno */

    /* Step 2: save our current terminal settings */
    if (tcgetattr(STDIN_FILENO, &ourTermios) == -1)
        errExit("tcgetattr");

    /* Step 3: restore original user terminal settings */
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &userTermios) == -1)
        errExit("tcsetattr");

    /* Step 4a: reset SIGTSTP to default */
    if (signal(SIGTSTP, SIG_DFL) == SIG_ERR)
        errExit("signal");

    /* Step 4b: raise SIGTSTP */
    raise(SIGTSTP);

    /* Step 5: unblock SIGTSTP so the raise() actually stops us */
    sigemptyset(&tstpMask);
    sigaddset(&tstpMask, SIGTSTP);
    if (sigprocmask(SIG_UNBLOCK, &tstpMask, &prevMask) == -1)
        errExit("sigprocmask");

    /* === PROCESS IS STOPPED HERE ===
       execution resumes below after SIGCONT === */

    /* Step 7: reblock SIGTSTP */
    if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1)
        errExit("sigprocmask");

    /* Step 8: reinstall our handler */
    sa.sa_handler = tstpHandler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGTSTP, &sa, NULL) == -1)
        errExit("sigaction");

    /* Step 9: re-read terminal settings (user may have changed them) */
    if (tcgetattr(STDIN_FILENO, &userTermios) == -1)
        errExit("tcgetattr");

    /* Step 10: restore our working terminal settings */
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &ourTermios) == -1)
        errExit("tcsetattr");

    errno = savedErrno;
}

5. The main() Function โ€” Putting It All Together

The main function is straightforward once you understand the signal handlers. Here is the full flow:

int
main(int argc, char *argv[])
{
    char ch;
    struct sigaction sa, prev;
    ssize_t n;

    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;

    if (argc > 1) {
        /* ---- CBREAK MODE ---- */
        if (ttySetCbreak(STDIN_FILENO, &userTermios) == -1)
            errExit("ttySetCbreak");

        /* Catch SIGQUIT only if not ignored */
        sa.sa_handler = handler;
        if (sigaction(SIGQUIT, NULL, &prev) == -1)
            errExit("sigaction");
        if (prev.sa_handler != SIG_IGN)
            if (sigaction(SIGQUIT, &sa, NULL) == -1)
                errExit("sigaction");

        /* Catch SIGINT only if not ignored */
        if (sigaction(SIGINT, NULL, &prev) == -1)
            errExit("sigaction");
        if (prev.sa_handler != SIG_IGN)
            if (sigaction(SIGINT, &sa, NULL) == -1)
                errExit("sigaction");

        /* Catch SIGTSTP */
        sa.sa_handler = tstpHandler;
        if (sigaction(SIGTSTP, NULL, &prev) == -1)
            errExit("sigaction");
        if (prev.sa_handler != SIG_IGN)
            if (sigaction(SIGTSTP, &sa, NULL) == -1)
                errExit("sigaction");

    } else {
        /* ---- RAW MODE ---- */
        if (ttySetRaw(STDIN_FILENO, &userTermios) == -1)
            errExit("ttySetRaw");
    }

    /* Both modes catch SIGTERM */
    sa.sa_handler = handler;
    if (sigaction(SIGTERM, &sa, NULL) == -1)
        errExit("sigaction");

    setbuf(stdout, NULL);  /* Disable buffering โ€” important in raw mode */

    /* Read loop */
    for (;;) {
        n = read(STDIN_FILENO, &ch, 1);
        if (n == -1) { errMsg("read"); break; }
        if (n == 0) break;  /* Terminal disconnected */

        if (isalpha((unsigned char) ch))
            putchar(tolower((unsigned char) ch));   /* A-Z, a-z โ†’ lowercase */
        else if (ch == '\n' || ch == '\r')
            putchar(ch);                            /* Newline/CR as-is */
        else if (iscntrl((unsigned char) ch))
            printf("^%c", ch ^ 64);                /* Ctrl-A โ†’ ^A */
        else
            putchar('*');                           /* Everything else โ†’ * */

        if (ch == 'q')   /* q exits */
            break;
    }

    /* Restore terminal before exit */
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &userTermios) == -1)
        errExit("tcsetattr");

    exit(EXIT_SUCCESS);
}

Notice setbuf(stdout, NULL) is called to disable stdout buffering. In raw mode, characters arrive one at a time and we want them echoed immediately. If stdout is line-buffered (the default), output would be delayed until a newline appears.

The ch ^ 64 trick converts a control character to its printable letter: Ctrl+A has value 1, 1 ^ 64 = 65 = 'A', so we print ^A.

6. Sample Program Output

Raw Mode Run:

$ stty                      # terminal is normal (cooked) before we start
speed 38400 baud; line = 0;
$ ./test_tty_functions       # no args โ†’ raw mode
abc                          # type abc and Ctrl+J
 def                         # type DEF, Ctrl+J, Enter
^C^Z                         # type Ctrl+C, Ctrl+Z (shown as ^C^Z, no signal!)
 q$                          # type q to exit โ€” shell prompt on same line

In raw mode, Ctrl+C does not generate SIGINT โ€” it arrives as ASCII 3, which is a control character, so it prints as ^C. No signal is delivered, no special behavior happens.

Cbreak Mode Run:

$ ./test_tty_functions x     # arg given โ†’ cbreak mode
XYZ                          # type XYZ
^Z                           # type Ctrl+Z โ€” SIGTSTP delivered!
[1]+ Stopped ./test_tty_functions x
$ stty                       # terminal IS restored (handler worked!)
speed 38400 baud; line = 0;
$ fg                         # resume foreground
./test_tty_functions x
***                          # type 123 โ†’ all shown as *
 $                           # Ctrl+C โ†’ SIGINT โ†’ handler โ†’ terminal restored
$ stty                       # confirm terminal is normal again
speed 38400 baud; line = 0;

7. Key Concepts Summary
Concept Why It Matters
Save terminal settings globally Signal handlers need access to restore on any exit path
Use _exit() in signal handlers exit() is not async-signal-safe
Check SIG_IGN before installing handlers Parent may have ignored signal for a reason โ€” respect it
SIGTSTP needs terminal restore before stopping Other processes need a working terminal while we are stopped
Disable stdout buffering Characters must appear immediately, not wait for newline
In raw mode, no signal handlers needed for Ctrl+C etc. Terminal special character processing is disabled in raw mode
Re-read userTermios after SIGCONT User might have run stty while we were stopped

Interview Questions โ€” Terminal Mode Signal Handling
Q1: Why is the original terminal settings stored in a global variable and not a local variable in main()?
Signal handlers are separate functions that run asynchronously. They do not have access to local variables in main(). To let the signal handlers restore the terminal, userTermios must be in a scope both main() and the handlers can see โ€” which means file-scope (global) or static.
Q2: Why does the signal handler use _exit() instead of exit()?
Signal handlers must only call async-signal-safe functions. exit() calls stdio flush routines and atexit handlers, which are not async-signal-safe and can deadlock or corrupt state if called from a signal handler. _exit() terminates immediately without any library cleanup, so it is safe to call from a signal handler.
Q3: What happens if you don’t handle SIGTSTP specially when the terminal is in cbreak or raw mode?
If the process is stopped (Ctrl+Z) without restoring the terminal, the terminal remains in cbreak or raw mode while the process is suspended. Any other foreground process (like the shell) will see a broken terminal โ€” it won’t echo input correctly, line editing won’t work, and the user’s experience will be very confusing.
Q4: In the SIGTSTP handler, why is SIGTSTP set to SIG_DFL before calling raise(SIGTSTP)?
If our custom handler is still installed when raise(SIGTSTP) is called, the signal would invoke our handler again (recursion) instead of stopping the process. By first setting the disposition to SIG_DFL, the raised SIGTSTP causes the default action which is to stop the process.
Q5: Why must SIGTSTP be unblocked after raise(SIGTSTP)?
When a signal handler is entered, the kernel automatically blocks that signal. So when tstpHandler() is running, SIGTSTP is blocked. Calling raise(SIGTSTP) while SIGTSTP is blocked delivers the signal but does not stop the process โ€” the signal is just pending. We must unblock it with sigprocmask(SIG_UNBLOCK) to allow the raised signal to actually take effect and stop the process.
Q6: Why is setbuf(stdout, NULL) called in this program?
In cbreak and raw mode, characters are read one at a time and should be echoed immediately. By default, stdout is line-buffered when connected to a terminal, meaning output is held until a newline character appears. Disabling buffering with setbuf(stdout, NULL) ensures every character is output immediately as it is processed.
Q7: How does the program detect control characters and print them as ^A, ^B, etc.?
Control characters have ASCII values 1โ€“31. The expression ch ^ 64 XORs the value with 64 (0x40). For Ctrl+A (value 1): 1 ^ 64 = 65 = 'A'. For Ctrl+B (value 2): 2 ^ 64 = 66 = 'B'. So printf("^%c", ch ^ 64) prints the human-readable representation of any control character.
Q8: Why does the SIGTSTP handler re-read userTermios after resuming?
While the process is stopped, the user may have run stty to change terminal settings manually, or another program may have changed them. If we kept our old copy of userTermios, restoring it at program exit would put the terminal back to a state that no longer matches what the user expects. Re-reading after resume ensures we always restore to the current “user normal” state.
Q9: Why are SIGINT and SIGQUIT only installed in cbreak mode and not raw mode?
In raw mode, the terminal’s signal-generating special character processing is disabled (the ISIG flag is cleared). This means Ctrl+C does not generate SIGINT and Ctrl+\ does not generate SIGQUIT. These keys arrive as raw bytes (values 3 and 28 respectively). Since no signals are generated from these keys in raw mode, there is no need to install handlers.
Q10: What is the role of SA_RESTART in this program?
The SA_RESTART flag tells the kernel to automatically restart any slow system call (like read()) that was interrupted by the signal, instead of returning with EINTR. Without this flag, the main read loop would need to explicitly check for EINTR and retry the read. With SA_RESTART, the loop is simpler.

Next: Terminal Line Speed (Bit Rate)
Explore cfgetispeed, cfsetispeed, cfgetospeed, cfsetospeed โ€” how to read and change terminal baud rate.

embeddedpathashala.com

Leave a Reply

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