Linux System Programming · beginner · ~8 min
- By the end you can arm a one-shot countdown with alarm(n) and cancel it with alarm(0). - By the end you can install a SIGALRM handler so the timeout interrupts a blocking call instead of killing your process. - By the end you can detect a timeout by testing for read()/accept()/connect() returning -1 with errno == EINTR. - By the end you can explain why SA_RESTART must be OFF for this pattern and why you save errno before cleanup. - By the end you can name the sharper alternatives (setitimer, timerfd, poll/select) and when to reach for them.
You already know from safe-signal-handlers that a signal handler runs asynchronously at an arbitrary point in your program, that it must only touch async-signal-safe state (a volatile sig_atomic_t flag, at most), and that sigaction() is the modern, portable way to install one. This lesson puts that knowledge to work for one very common job: putting a deadline on a call that would otherwise block forever.
alarm(n) asks the kernel to deliver SIGALRM to your process after n seconds. If that signal arrives while you are parked inside a blocking system call, the call is interrupted and returns early. Combined with the handler discipline you already learned, this gives you a tiny, dependency-free timeout mechanism — no threads, no event loop, just one syscall and a flag.
Real programs talk to things that can stall: a socket peer that never replies, a pipe with no writer, a terminal waiting on a human. Without a deadline a single stuck read() or connect() freezes the whole process. In defensive/security terms, an unbounded blocking call is a denial-of-service foothold — a slow or malicious peer (think slowloris-style clients) can pin your workers indefinitely just by connecting and going quiet. A timeout converts "hang forever" into "fail cleanly and move on," which is the difference between a resilient service and one a single client can wedge.
unsigned alarm(unsigned seconds) sets a per-process countdown timer. When it expires the kernel posts one SIGALRM to your process. It is one-shot (it does not repeat) and there is exactly one alarm timer per process — a second call replaces the first. Calling alarm(0) cancels any pending alarm. The return value is the number of seconds that were left on a previously set alarm (0 if none), which is occasionally handy for saving and restoring a deadline.
time ->
t0 arm: alarm(3)
|------------ read() blocks, waiting for data ------------|
t0 t0+1 t0+2 t0+3 <-- kernel posts SIGALRM
|
+--> handler runs (sets flag)
+--> read() returns -1, errno=EINTR
The default disposition of SIGALRM is Term — it kills the process. So alarm(3) on its own is a 3-second self-destruct button. Installing a handler changes the disposition to "run my function," and the side effect we actually want is that delivering the signal interrupts the blocking syscall. The handler body can be empty; its mere existence is what saves you. Following safe-signal-handlers, the handler does the minimum: set volatile sig_atomic_t timed_out = 1; and return.
When a signal is delivered to a process sitting in a slow syscall, the kernel unblocks the call. What happens next depends on a flag you set in sigaction:
sa_flags setting |
Behaviour on signal during a slow syscall |
|---|---|
0 (no SA_RESTART) |
The syscall returns -1 with errno == EINTR. This is what the timeout pattern needs. |
SA_RESTART |
The kernel silently restarts the syscall — so it keeps blocking and your timeout never fires. |
So the whole mechanism hinges on EINTR surfacing. Your success test is: n < 0 && errno == EINTR. Pair that with your own timed_out flag to be certain this EINTR came from the alarm and not from some other signal (e.g. a SIGCHLD or SIGWINCH).
Knowledge check: You run alarm(5) but never install a SIGALRM handler, then call read() on an empty pipe. What happens after 5 seconds?
Your process is terminated. The default action for
SIGALRMis Term, so with no handler the alarm doesn't interrupt the read — it kills the program. You mustsigaction(SIGALRM, ...)first, and withsa_flags = 0so the read returns EINTR instead.
The canonical sequence is arm → blocking call → immediately save errno → disarm → interpret. alarm(0) (and any library call) can itself change errno, so you must snapshot errno the instant the blocking call returns, before doing cleanup. Always disarm on every exit path (success too): a leftover alarm from the success case will fire later and interrupt some unrelated call, producing a baffling "random EINTR" bug.
Alarm-based timeouts only help with slow (blocking) syscalls — ones that wait on external events.
| Syscall | Interruptible by SIGALRM? | Typical timeout use |
|---|---|---|
read/recv on a pipe/socket/tty |
Yes | Bound a stalled reader |
accept |
Yes | Bound waiting for a connection |
connect |
Yes | Bound a slow TCP handshake |
write on a full pipe/socket |
Yes | Bound a stalled writer |
read on a fast regular file |
No (rarely blocks) | N/A — completes immediately |
alarm() counts in whole seconds and offers only one timer. For sub-second deadlines, repeating ticks, or multiple independent timers, reach for setitimer() (microsecond resolution, still SIGALRM-based) or, on Linux, timerfd_create() (a timer you read()/poll() like any FD — no signals at all). For "wait on several FDs with a deadline" the cleaner answer is usually poll()/select()/epoll with a timeout argument, which sidesteps the whole EINTR dance.
#include <unistd.h>
unsigned alarm(unsigned seconds);
// seconds: fire SIGALRM after this many whole seconds; 0 cancels a pending alarm.
// returns: seconds LEFT on a previously set alarm (0 if none). Cannot fail.
// Only ONE alarm per process; a new call REPLACES the old one.
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// signum : SIGALRM here.
// act : new disposition. Set act->sa_handler to your function,
// sigemptyset(&act->sa_mask), and act->sa_flags = 0
// (NOT SA_RESTART, or the syscall auto-restarts and never times out).
// oldact : previous disposition, or NULL if you don't need it.
// returns: 0 on success, -1 + errno on failure.
// Inside the handler, only touch async-signal-safe state:
volatile sig_atomic_t timed_out; // the ONLY kind of variable safe to set
// The blocking call reports interruption via errno:
// ssize_t n = read(fd, buf, len);
// if (n < 0 && errno == EINTR) { /* interrupted (by our alarm) */ }
No memory is allocated by any of these, so there is nothing to free. Do close any file descriptors you open. alarm itself never fails and needs no error check.
alarm() doesalarm(n) asks the kernel to send the signal SIGALRM to your process after n seconds. A signal is an asynchronous notification the kernel delivers to a process.
Some system calls block — they pause your program until they have something to return. Examples are read(), accept(), and connect().
alarm() lets such a call give up after a deadline. The pattern is:
SIGALRM handler that does nothing useful (or just sets a flag).alarm(n) to start the countdown.When SIGALRM fires, it interrupts the blocked call. The call then returns -1 and sets errno to EINTR ("interrupted system call"). That is your signal that the timeout was hit.
alarm(0) cancels a pending alarm.setitimer(), or — on modern Linux — timerfd_create().#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
/* Set by the handler so main can tell a timeout from a real error. */
static volatile sig_atomic_t timed_out = 0;
static void on_alarm(int sig) {
(void)sig; /* unused; a handler receives the signal number */
timed_out = 1; /* async-signal-safe: only touch a sig_atomic_t */
}
/* Read up to cap bytes from fd, giving up after `seconds`.
Returns bytes read, or -1 on timeout/error (errno set on real errors). */
static ssize_t read_with_timeout(int fd, void *buf, size_t cap, unsigned seconds) {
timed_out = 0;
alarm(seconds); /* arm: SIGALRM will fire in N sec */
ssize_t n = read(fd, buf, cap); /* blocking call we want to bound */
int saved = errno; /* snapshot errno BEFORE cleanup */
alarm(0); /* disarm on every exit path */
if (n < 0 && saved == EINTR && timed_out) {
fprintf(stderr, "read: timed out after %u s\n", seconds);
return -1;
}
errno = saved;
return n;
}
int main(void) {
/* Install a SIGALRM handler. WITHOUT this, the default action for
SIGALRM is to KILL the process, not just interrupt the read. */
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = on_alarm;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; /* NO SA_RESTART: we WANT EINTR */
if (sigaction(SIGALRM, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
/* Hermetic demo: a pipe with no writer of data. read() on the empty
read-end blocks, so the alarm is what unblocks it. */
int fds[2];
if (pipe(fds) == -1) { perror("pipe"); return 1; }
char buf[64];
/* --- Case 1: nothing is ever written -> the alarm fires. --- */
printf("Case 1: waiting up to 1s for data that never comes...\n");
ssize_t n = read_with_timeout(fds[0], buf, sizeof buf, 1);
printf("Case 1 result: n=%zd (timed_out=%d)\n\n", n, (int)timed_out);
/* --- Case 2: data is already present -> read returns before the alarm. --- */
const char *msg = "hello";
if (write(fds[1], msg, strlen(msg)) == -1) { perror("write"); return 1; }
printf("Case 2: data is ready, 5s budget should not be needed...\n");
n = read_with_timeout(fds[0], buf, sizeof buf, 5);
if (n > 0) printf("Case 2 result: read %zd bytes: \"%.*s\"\n", n, (int)n, buf);
else printf("Case 2 result: n=%zd\n", n);
close(fds[0]);
close(fds[1]);
return 0;
}
static volatile sig_atomic_t timed_out — the one variable the handler and main share. volatile stops the compiler from caching it; sig_atomic_t guarantees the write is atomic with respect to signal delivery. This is the discipline from safe-signal-handlers.on_alarm — the handler. (void)sig silences the unused-parameter warning; the body does nothing but set the flag. Its existence is what changes SIGALRM's disposition away from "kill the process."read_with_timeout — reusable wrapper. timed_out = 0 resets the flag before each attempt.alarm(seconds) — arms the one-shot timer.read(fd, buf, cap) — the blocking call. On an empty pipe with no data, it parks here until either data arrives or SIGALRM interrupts it.int saved = errno — captures errno the instant read returns, before alarm(0) (or anything else) can overwrite it. This ordering is the subtle, important bit.alarm(0) — disarms unconditionally, so a surviving alarm can't fire later and interrupt an unrelated call.if (n < 0 && saved == EINTR && timed_out) — the timeout test. All three parts: the call failed, it failed because it was interrupted, and our alarm is the reason (not some other signal).main, the sigaction block installs the handler with sa_flags = 0 — deliberately NOT SA_RESTART, because SA_RESTART would auto-restart the read and defeat the timeout.pipe(fds) — creates a hermetic blocking source: fds[0] (read end) blocks because nobody has written yet. No network or terminal needed.n=-1 (timed_out=1)."hello" first, so read returns 5 immediately, well inside the 5s budget, and the alarm is cancelled unused.1. No handler installed — the alarm kills you.
alarm(3);
read(fd, buf, n); // 3s later: process TERMINATED, not interrupted
The default action for SIGALRM is Term. Fix: install a handler first.
struct sigaction sa; memset(&sa,0,sizeof sa);
sa.sa_handler = on_alarm; sigemptyset(&sa.sa_mask); sa.sa_flags = 0;
sigaction(SIGALRM, &sa, NULL);
alarm(3); read(fd, buf, n);
2. Using SA_RESTART — the timeout never fires.
sa.sa_flags = SA_RESTART; // kernel silently restarts read(); it blocks forever
Fix: use sa.sa_flags = 0 so the interrupted read returns EINTR.
3. Forgetting to cancel on the success path.
alarm(10);
ssize_t n = read(fd, buf, sizeof buf); // returns in 1s
// ...no alarm(0)... 9s later SIGALRM interrupts some UNRELATED call
Fix: always alarm(0); right after the guarded call, on every path.
4. Reading errno after other calls clobber it.
ssize_t n = read(fd, buf, sizeof buf);
alarm(0);
printf("...\n");
if (n < 0 && errno == EINTR) { ... } // errno may now be from alarm/printf
Fix: int saved = errno; immediately after read, then test saved.
strace -f ./prog (Linux) and watch for the --- SIGALRM --- line and the read(...) = -1 EINTR (Interrupted system call) that follows. On macOS use dtruss/sudo dtruss.sa_flags. If you see the read restart in strace (a second read() right after the signal), you left SA_RESTART on.+++ killed by SIGALRM +++. Or you overwrote SIGALRM's disposition later.alarm(0). Add a temporary fprintf(stderr, "alarm fired\n") in the handler? No — that's not async-signal-safe; instead set the flag and print from main.time ./prog; a 1s timeout demo should take ~1s of real time.handle SIGALRM nostop pass so the debugger delivers the signal to your program instead of trapping it.volatile sig_atomic_t. Do not call printf, malloc, or anything not on the async-signal-safe list from inside it — that risks reentrancy corruption and deadlocks.alarm(0), and library calls all write errno. Snapshot it immediately or your timeout test reads a stale/overwritten value.alarm is per process, and a signal is delivered to an arbitrary thread. In multithreaded code use timer_create with SIGEV_THREAD, or per-FD timeouts via poll, instead of alarm.alarm(n) and entering read, the flag is set but the read still blocks (nothing interrupts it). For tight deadlines this is why poll(fds, n, timeout_ms) — one atomic call with a built-in deadline — is safer than the arm-then-block pattern.close() the pipe FDs.connect, accept, and recv so a dead or malicious peer can't wedge a worker — a core defence against slow-client DoS. Production code usually prefers poll/epoll timeouts or SO_RCVTIMEO, but alarm is the minimal building block.timeout(1) utility, watchdogs, and health-check probes use SIGALRM (via alarm/setitimer) to cap how long a child runs.setitimer, timerfd_create, or poll/select with a timeout. Reserve raw alarm for simple, single-threaded, whole-second deadlines — and always pair it with a handler and unconditional alarm(0).Modify the demo so the timeout is configurable from argv[1] (seconds). Print the timeout used and verify Case 1 takes about that long with time ./prog.
Add a Case 3 that forks a child which sleeps 2s then writes to the pipe, with a 4s timeout in the parent — confirm the read succeeds; then drop the timeout to 1s and confirm it times out.
Deliberately set sa.sa_flags = SA_RESTART, run under strace, and document exactly what you observe (the read restarting). Then revert and explain the difference in one sentence.
Wrap accept() on a listening socket bound to 127.0.0.1 with read_with_timeout's pattern so a server gives up waiting for a connection after N seconds and logs a clean timeout instead of hanging.
Reimplement the same 1-second read timeout using poll(fds, 1, 1000) instead of alarm, then write two sentences comparing the two approaches (granularity, EINTR handling, thread safety).
alarm(n) schedules a single SIGALRM n whole seconds from now; alarm(0) cancels it; there is only one alarm per process.sigaction FIRST — its existence is what turns a kill into an interrupt.sa_flags = 0 (never SA_RESTART) so the interrupted blocking call returns -1 with errno == EINTR — that's your timeout signal.errno the instant the call returns, then alarm(0) on every path; combine the EINTR test with your own volatile sig_atomic_t flag.setitimer, timerfd_create, or poll/select with a timeout.