Timeouts on Blocking Operations

โฐ Timeouts on Blocking Operations
Chapter 23 | Part 3 of 9 โ€” Interrupt Blocked System Calls with Timers
๐Ÿ”” Signal
SIGALRM
โŒ Error
EINTR
๐Ÿ“ž Syscall
read() / write()

The Problem: Stuck System Calls

Many system calls block indefinitely โ€” read() on a terminal waits forever if no input arrives. accept() on a socket blocks until a client connects. How do you impose a maximum wait time?

The classic solution: set a timer, make the system call, handle EINTR when it is interrupted by the signal.

๐Ÿ”„ Pattern: Timeout on a Blocking Call
Step 1
sigaction(SIGALRM) โ€” install handler, omit SA_RESTART
โ†“
Step 2
alarm(N) โ€” set timeout timer
โ†“
Step 3
Make blocking syscall: read(), accept()โ€ฆ
โ†“
Path A: Syscall succeeds
alarm(0) to cancel timer
โ†’ use result normally
Path B: Timer fires
SIGALRM โ†’ handler โ†’ syscall returns -1
errno == EINTR โ†’ timeout!
Step 4 (both paths)
Check errno == EINTR for timeout detection

โš ๏ธ Critical: SA_RESTART Flag Must NOT Be Set
WITHOUT SA_RESTART (correct for timeouts)

When SIGALRM fires, the blocking syscall returns -1 with errno = EINTR. You detect the timeout.

WITH SA_RESTART (NOT what you want here)

The syscall is automatically restarted after the signal. Your timer fires, but the program keeps waiting โ€” timeout is missed!

๐Ÿ The Race Condition

There is a theoretical race: if the timer fires AFTER alarm() but BEFORE read() starts, the signal is handled early and read() will block again โ€” uninterrupted.

Timeline of the race (rare but possible):
alarm(5)
โ€”
SIGALRM fires here!
โ€”
read() starts (blocks forever)

In practice: timeout values are typically seconds, making this race extremely unlikely. For sub-second precision use select()/poll() which have built-in timeout parameters.

๐Ÿ’ป Example 1: Timed read() from stdin

If the user does not type input within 5 seconds, print “Timed out!”

#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>

#define BUF_SIZE 256
#define TIMEOUT_SECS 5

static void alarm_handler(int sig) {
    /* Just return to interrupt the syscall */
    (void)sig;
}

int main(void) {
    struct sigaction sa;
    char buf[BUF_SIZE];
    ssize_t n;
    int saved_errno;

    /* Install SIGALRM handler WITHOUT SA_RESTART */
    sa.sa_handler = alarm_handler;
    sa.sa_flags   = 0;          /* NO SA_RESTART โ€” important! */
    sigemptyset(&sa.sa_mask);
    if (sigaction(SIGALRM, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    printf("You have %d seconds to type input: ", TIMEOUT_SECS);
    fflush(stdout);

    /* Set timer */
    alarm(TIMEOUT_SECS);

    /* Blocking read โ€” will be interrupted by SIGALRM */
    n = read(STDIN_FILENO, buf, BUF_SIZE - 1);
    saved_errno = errno;

    /* Always cancel the timer after the call */
    alarm(0);
    errno = saved_errno;

    if (n == -1) {
        if (errno == EINTR) {
            printf("\nTimed out! No input received.\n");
        } else {
            perror("read");
        }
        return 1;
    }

    buf[n] = '\0';
    printf("You typed: %s", buf);
    return 0;
}

/* Compile: gcc timed_read.c -o timed_read
   Run: ./timed_read
   - Type within 5s: prints input
   - Don't type: "Timed out!" after 5s
*/

๐Ÿ’ป Example 2: Timed connect() to a Server

Impose a 3-second maximum wait for a TCP connection to succeed.

#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define TIMEOUT_SECS 3

static void timeout_handler(int sig) { (void)sig; }

int connect_with_timeout(const char *ip, int port, int timeout_s) {
    int sock;
    struct sockaddr_in addr;
    struct sigaction sa;
    int ret;

    /* Install handler without SA_RESTART */
    sa.sa_handler = timeout_handler;
    sa.sa_flags   = 0;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGALRM, &sa, NULL);

    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) return -1;

    memset(&addr, 0, sizeof(addr));
    addr.sin_family      = AF_INET;
    addr.sin_port        = htons(port);
    inet_pton(AF_INET, ip, &addr.sin_addr);

    /* Set timer before blocking connect() */
    alarm(timeout_s);

    ret = connect(sock, (struct sockaddr *)&addr, sizeof(addr));
    int saved = errno;

    alarm(0);         /* Cancel timer */
    errno = saved;

    if (ret == -1) {
        close(sock);
        if (errno == EINTR) {
            fprintf(stderr, "connect() timed out after %ds\n", timeout_s);
        } else {
            perror("connect");
        }
        return -1;
    }

    return sock;  /* Connected successfully */
}

int main(void) {
    /* Try connecting to a non-responsive host to see timeout */
    int fd = connect_with_timeout("192.0.2.1", 80, TIMEOUT_SECS);
    if (fd == -1) {
        printf("Connection failed (expected on unreachable host).\n");
    } else {
        printf("Connected! fd=%d\n", fd);
        close(fd);
    }
    return 0;
}

/* Compile: gcc timed_connect.c -o timed_connect
   The connect attempt will abort after 3 seconds with EINTR */

๐Ÿ’ป Example 3: Reusable timed_read() Wrapper Function

A clean wrapper that abstracts the alarm/read/cancel pattern.

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

static void noop_handler(int sig) { (void)sig; }

/**
 * Read from fd with a timeout.
 * Returns:
 *   >0  : bytes read
 *    0  : EOF
 *   -1  : error (check errno)
 *   -2  : TIMEOUT
 */
ssize_t timed_read(int fd, void *buf, size_t count, unsigned int secs) {
    struct sigaction sa, old_sa;
    ssize_t n;
    int saved_errno;
    unsigned int old_alarm;

    /* Install noop handler without SA_RESTART */
    sa.sa_handler = noop_handler;
    sa.sa_flags   = 0;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGALRM, &sa, &old_sa);

    /* Save any existing alarm and set ours */
    old_alarm = alarm(secs);

    n = read(fd, buf, count);
    saved_errno = errno;

    /* Cancel our alarm, restore old one */
    alarm(0);
    sigaction(SIGALRM, &old_sa, NULL);
    if (old_alarm > 0)
        alarm(old_alarm);    /* Restore previous alarm if any */

    errno = saved_errno;

    if (n == -1 && errno == EINTR)
        return -2;   /* Timeout */

    return n;
}

int main(void) {
    char buf[256];
    ssize_t n;

    printf("Type something (3s timeout): ");
    fflush(stdout);

    n = timed_read(STDIN_FILENO, buf, sizeof(buf) - 1, 3);

    if (n == -2) {
        printf("\nTimeout! Nothing typed.\n");
    } else if (n <= 0) {
        printf("\nError or EOF.\n");
    } else {
        buf[n] = '\0';
        printf("Got: %s", buf);
    }
    return 0;
}

/* Clean, reusable, production-worthy pattern.
   Returns -2 specifically for timeout, -1 for real errors. */

๐Ÿ’ก Better Alternatives for I/O Timeouts
select() / poll()

Have native timeout parameters. No race condition, simpler, and work on multiple fds at once. Preferred over alarm+read.

SO_RCVTIMEO socket option

For socket reads specifically, setsockopt(SO_RCVTIMEO) sets a read timeout at the socket level. Very clean API.

alarm()+read() pattern

Simple and works for non-socket fds (like terminals, pipes). Has the theoretical race condition but practical in most cases.

๐ŸŽ“ Interview Questions
Q1. How do you implement a timeout on a blocking read() call?

Install a SIGALRM handler WITHOUT SA_RESTART, call alarm(N) before read(), then check for EINTR when read returns -1. Always call alarm(0) after to cancel.

Q2. Why must SA_RESTART NOT be set when using alarm for timeouts?

SA_RESTART causes the syscall to restart automatically after being interrupted by a signal. With SA_RESTART, the SIGALRM fires but read() restarts silently โ€” the timeout has no effect.

Q3. What is the race condition in the alarm()+read() pattern?

If the timer fires after alarm() but before read() begins, the signal is handled before the syscall starts. read() then blocks indefinitely with no pending alarm.

Q4. What errno is set when a syscall is interrupted by a signal?

EINTR โ€” “Interrupted system call”. Check errno == EINTR to distinguish a timeout from a real I/O error.

Q5. Why should you always call alarm(0) after the blocking call?

To cancel the pending timer. If the syscall succeeds before the timer fires, the pending SIGALRM would later interrupt an unrelated operation if not cancelled.

Next: Sleeping โ€” sleep() & nanosleep() โ†’

Part 4: Sleeping APIs โ† Part 2: Scheduling

Leave a Reply

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