Nonblocking getchar() in C
Getting characters one at a time with getchar() is useful.
Sometimes a program needs to do other things while waiting for input.
Sometimes blocking is problematic.
This post covers nonblocking getchar().
The problem consists of three parts.
- Unbuffered
getchar(). By default,getchar()buffers until input followed by a newline is available. - Nonblocking
getchar(). By default,getchar()blocks until input is available. - Sleeping when input is not available. There is no need to run the CPU full throttle.
Software Versions
# Date (UTC)
$ date -u "+%Y-%m-%d %H:%M:%S +0000"
2026-01-17 23:51:32 +0000
# OS and Version
$ uname -vm
Darwin Kernel Version 23.6.0: Mon Jul 29 21:14:30 PDT 2024; root:xnu-10063.141.2~1/RELEASE_ARM64_T6000 arm64
# Hardware Information
$ system_profiler SPHardwareDataType | sed -n '8,10p'
Chip: Apple M1 Max
Total Number of Cores: 10 (8 performance and 2 efficiency)
Memory: 32 GB
# Shell and Version
$ echo "${SHELL}"
/bin/bash
$ "${SHELL}" --version | head -n 1
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin23)
# C Compiler Version
$ clang --version
Apple clang version 16.0.0 (clang-1600.0.26.6)
Target: arm64-apple-darwin23.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
Instructions
To achieve non-blocking input, we must interact with the Unix terminal interface termios and file control fcntl.
1. Disable Canonical Mode
Input is usually processed line-by-line (canonical mode). We clear the ICANON and ECHO flags to allow getchar() to read keys immediately without a newline.
2. Set O_NONBLOCK
By setting the O_NONBLOCK flag on stdin, getchar() returns EOF immediately if no key is in the buffer, rather than hanging the thread.
3. Safety First: Signal Handling
Because we are modifying the terminal state, a crash or a Ctrl+C interrupt could leave your shell “broken” (no echo, no newlines). We use sigaction to catch interrupts and restore the original state before exiting.
The Implementation
main.c
#include <fcntl.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
// Original state restoration variables
struct termios original_termios;
int original_fcntl_flags;
void async_print(const char *format, ...) {
char message_buffer[128];
va_list args;
va_start(args, format);
int length = vsnprintf(message_buffer, sizeof(message_buffer), format, args);
va_end(args);
if (length > 0) {
// Use write() to bypass stdio buffering
write(STDOUT_FILENO, message_buffer, (size_t)length);
}
}
void restore_terminal_settings(void) {
tcsetattr(STDIN_FILENO, TCSANOW, &original_termios);
fcntl(STDIN_FILENO, F_SETFL, original_fcntl_flags);
}
void handle_termination_signal(int signal_number) {
// Cleanup and exit
restore_terminal_settings();
async_print("\nInterrupted by signal %d. Terminal settings restored.\n", signal_number);
exit(0);
}
int main(void) {
struct termios modified_termios;
struct timespec sleep_duration = {0, 100000000L}; // 100ms
int input_char;
// Set up signal handling for safe exit (Ctrl+C)
struct sigaction signal_action;
signal_action.sa_handler = handle_termination_signal;
sigemptyset(&signal_action.sa_mask);
signal_action.sa_flags = 0;
sigaction(SIGINT, &signal_action, NULL);
// Save current terminal settings and file flags
tcgetattr(STDIN_FILENO, &original_termios);
original_fcntl_flags = fcntl(STDIN_FILENO, F_GETFL, 0);
// Modify terminal: Disable canonical mode (line buffering) and echo
modified_termios = original_termios;
modified_termios.c_lflag &= (tcflag_t)~(ICANON | ECHO);
tcsetattr(STDIN_FILENO, TCSANOW, &modified_termios);
// Modify file descriptor: Set stdin to non-blocking mode
fcntl(STDIN_FILENO, F_SETFL, original_fcntl_flags | O_NONBLOCK);
async_print("Non-blocking loop started. Press 'q' to quit or Ctrl+C to interrupt.\n");
while (1) {
input_char = getchar();
if (input_char != EOF) {
async_print("\nYou pressed: %c", input_char);
if (input_char == 'q') {
break;
}
async_print("\n");
} else {
// No input currently available: visually indicate activity and sleep
async_print(".");
nanosleep(&sleep_duration, NULL);
}
}
// Cleanup and exit
restore_terminal_settings();
async_print("\nNormal exit. Terminal settings restored.\n");
return 0;
}
Compilation and Output
To compile and run:
BIN="getchar_nonblocking"
clang -O3 main.c -o "${BIN}"
"./${BIN}"
Expected Output: You will see a sequence of dots appearing every 100ms. If you press a key, it is captured immediately without hitting Enter.
Non-blocking loop started. Press 'q' to quit or Ctrl+C to interrupt.
.......
You pressed: a
.....
You pressed: s
....
You pressed: d
.....
You pressed: f
....
You pressed: q
Normal exit. Terminal settings restored.
If you terminate with Ctrl+C, the signal handler triggers, ensuring your terminal doesn’t stay in a bugged, non-echoing state.