Chapter 21.5 — Interruption & Restarting of System Calls

 

Chapter 21.5 — Interruption & Restarting of System Calls
EINTR · SA_RESTART · Slow Devices | EmbeddedPathashala
21.5
Section
3
Code Examples
8
Interview Qs

Key Terms
EINTR SA_RESTART Blocking System Call Slow Device siginterrupt() TEMP_FAILURE_RETRY Interrupted System Call read() EINTR select() interruption

What Happens When a Signal Interrupts a System Call?

Consider this scenario: your program calls read() on a terminal — it blocks, waiting for input. While it’s waiting, a signal arrives. The signal handler runs. When the handler returns, what happens to the read()?

By default, the system call fails with errno = EINTR (“Interrupted system call”). Your program receives -1 and must decide what to do.

You have two options: manually restart the call, or use SA_RESTART to have the kernel automatically restart it.

How Signal Interruption Works
1. Process calls blocking syscall: read(fd, buf, len) — goes to sleep waiting for data
↓ signal arrives while sleeping
2. Kernel wakes the process, runs the signal handler
↓ handler returns
Without SA_RESTART
read() returns -1
errno = EINTR
Program must handle this
With SA_RESTART
Kernel re-issues read() automatically
Program sees no interruption
read() eventually returns data
Useful Feature: The EINTR behaviour can be used deliberately. By setting a timer (SIGALRM), you can implement a timeout on a blocking call: the alarm fires, the handler sets a flag, and the read() fails with EINTR — giving you control back to check the timeout.

Which System Calls Are Restarted by SA_RESTART?
✅ Automatically Restarted (SA_RESTART works)
read(), readv(), write(), writev() on slow devices (terminals, pipes, sockets, FIFOs)
ioctl() on slow devices
wait(), waitpid(), wait3(), wait4(), waitid()
open() (when it can block, e.g., on FIFOs)
accept(), connect(), send(), recv() and variants
mq_receive(), mq_send() and timed variants
flock(), fcntl() (file locking)
sem_wait(), sem_timedwait()
pthread_mutex_lock(), pthread_cond_wait() and variants
❌ NEVER Restarted (always return EINTR)
poll(), ppoll()
select(), pselect() (SUSv3 leaves behaviour unspecified)
epoll_wait(), epoll_pwait()
io_getevents()
semop(), semtimedop()
msgrcv(), msgsnd()
sleep(), nanosleep(), clock_nanosleep()
pause(), sigsuspend(), sigtimedwait(), sigwaitinfo()
Key Rule: read()/write() are only restarted for “slow devices” (terminals, pipes, sockets). File I/O on regular disk files is not interruptible (disk I/O uses the buffer cache and completes near-immediately).

Code Example 1 — Manual EINTR Retry Loop
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

static volatile sig_atomic_t got_signal = 0;

static void handler(int sig)
{
    got_signal = 1;
}

/*
 * Safe read() wrapper that retries on EINTR.
 * This is what you must do for all blocking I/O calls
 * when NOT using SA_RESTART.
 */
ssize_t safe_read(int fd, void *buf, size_t count)
{
    ssize_t n;
    while (1) {
        n = read(fd, buf, count);
        if (n == -1 && errno == EINTR) {
            /*
             * read() was interrupted by a signal handler.
             * The handler ran and returned. Try again.
             */
            printf("[safe_read] read() interrupted (EINTR). Retrying...\n");
            continue;
        }
        break;  /* success, EOF, or real error */
    }
    return n;
}

int main(void)
{
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;   /* No SA_RESTART — we handle EINTR manually */
    sigaction(SIGALRM, &sa, NULL);

    /* Send SIGALRM in 2 seconds — will interrupt the read() */
    alarm(2);

    printf("Waiting for input (type something, or wait 2s for SIGALRM)...\n");

    char buf[128];
    ssize_t n = safe_read(STDIN_FILENO, buf, sizeof(buf) - 1);

    if (n > 0) {
        buf[n] = '\0';
        printf("Read %zd bytes: %s", n, buf);
    } else if (n == 0) {
        printf("EOF\n");
    } else {
        perror("read failed");
    }

    if (got_signal)
        printf("(SIGALRM fired during wait)\n");

    return 0;
}

Code Example 2 — SA_RESTART: Automatic Restart by the Kernel
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>

static void sigusr1_handler(int sig)
{
    /* Using write() — async-signal-safe */
    const char msg[] = "[Handler] SIGUSR1 fired — read() will be restarted.\n";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}

int main(void)
{
    struct sigaction sa;
    sa.sa_handler = sigusr1_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;  /* Tell kernel to restart interrupted syscalls */
    sigaction(SIGUSR1, &sa, NULL);

    printf("PID=%d. Reading from stdin. Send SIGUSR1 to interrupt:\n", (int)getpid());
    printf("  kill -USR1 %d\n\n", (int)getpid());

    char buf[64];
    /*
     * With SA_RESTART:
     * - You send SIGUSR1 while this read() is blocking.
     * - The handler runs and prints its message.
     * - When the handler returns, the KERNEL automatically restarts read().
     * - You do NOT see EINTR; the read() continues waiting for input.
     * - No special retry loop needed in application code!
     */
    ssize_t n = read(STDIN_FILENO, buf, sizeof(buf) - 1);

    if (n > 0) {
        buf[n] = '\0';
        printf("Read: %s", buf);
    } else {
        perror("read");
    }
    return 0;
}
Test it: Run the program. In another terminal, run kill -USR1 <pid> several times. You’ll see the handler message each time, but the read() keeps waiting for your input — it was automatically restarted.

Code Example 3 — Timed Read: Using EINTR to Implement a Timeout
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

static volatile sig_atomic_t timed_out = 0;

static void alarm_handler(int sig)
{
    timed_out = 1;
    /* Do NOT set SA_RESTART — we WANT read() to return EINTR */
}

/*
 * Read with timeout.
 * Returns:
 *   n > 0  : bytes read
 *   n == 0 : EOF
 *   n < 0  : error or timeout (check errno: EINTR = timed out)
 */
ssize_t read_with_timeout(int fd, void *buf, size_t len, unsigned int secs)
{
    struct sigaction sa, old_sa;
    sa.sa_handler = alarm_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;  /* No SA_RESTART — we want EINTR on timeout */
    sigaction(SIGALRM, &sa, &old_sa);

    timed_out = 0;
    alarm(secs);          /* Set the timeout */

    ssize_t n = read(fd, buf, len);   /* Block here — interrupted by SIGALRM */

    alarm(0);             /* Cancel the alarm if read returned before timeout */
    sigaction(SIGALRM, &old_sa, NULL);  /* Restore old SIGALRM handler */

    return n;
}

int main(void)
{
    printf("You have 5 seconds to type something:\n");
    fflush(stdout);

    char buf[128];
    ssize_t n = read_with_timeout(STDIN_FILENO, buf, sizeof(buf) - 1, 5);

    if (n > 0) {
        buf[n] = '\0';
        printf("You typed: %s", buf);
    } else if (n == 0) {
        printf("EOF\n");
    } else if (timed_out) {
        printf("\nTimeout! No input within 5 seconds.\n");
    } else {
        perror("read");
    }
    return 0;
}
Classic Pattern: This is the standard way to implement I/O timeouts before select()/poll() with timeouts became universal. The key is deliberately NOT using SA_RESTART for the alarm handler, so read() returns EINTR which you interpret as a timeout.

siginterrupt() — Changing SA_RESTART After Installation

siginterrupt() lets you change the SA_RESTART setting for a signal after its handler is already installed, without having to re-register the handler.

#include <signal.h>

/*
 * siginterrupt(sig, flag):
 *   flag = 1: handler for 'sig' WILL interrupt blocking syscalls (EINTR)
 *   flag = 0: handler for 'sig' will NOT interrupt (SA_RESTART behaviour)
 *
 * Note: SUSv4 marks siginterrupt() as obsolete.
 * Prefer setting sa_flags in sigaction() directly.
 */
int siginterrupt(int sig, int flag);

/* Example: make SIGALRM interrupt blocking syscalls */
siginterrupt(SIGALRM, 1);   /* equivalent to NOT having SA_RESTART */

/* Example: make SIGUSR1 NOT interrupt blocking syscalls */
siginterrupt(SIGUSR1, 0);   /* equivalent to SA_RESTART */
Internals: siginterrupt() works by fetching the current signal disposition with sigaction(), tweaking the SA_RESTART bit, and calling sigaction() again. This means it is not atomic; prefer setting sa_flags in your initial sigaction() call.

Interview Questions — System Call Interruption & EINTR
Q1. What is EINTR and when does it occur?
EINTR (“Interrupted system call”) is set in errno when a blocking system call returns -1 because it was interrupted by a signal handler. The signal handler ran and returned normally, but the system call was not restarted. The application must handle this by either retrying the call or treating it as an error.
Q2. What is SA_RESTART and how does it work?
SA_RESTART is a flag in sigaction()‘s sa_flags. When set, the kernel automatically re-issues certain interrupted system calls after the signal handler returns, instead of making them fail with EINTR. It is a per-signal flag — different signals can have different restart behaviour.
Q3. Does SA_RESTART work for select(), poll(), and epoll_wait()?
No. select(), poll(), ppoll(), epoll_wait(), and epoll_pwait() are NEVER automatically restarted, even if SA_RESTART is set. They always return EINTR when interrupted. This is by design — the caller should re-evaluate the file descriptor set after a signal. You must always handle EINTR for these calls in a retry loop.
Q4. Write a wrapper function that retries read() on EINTR.
ssize_t safe_read(int fd, void *buf, size_t n) { ssize_t r; while ((r = read(fd, buf, n)) == -1 && errno == EINTR) continue; return r; } — This loop retries on EINTR and returns on any other result (success, EOF, or a different error).
Q5. What is a “slow device” in the context of signal-interrupted I/O?
A slow device is one where I/O operations can block for an indefinite time: terminals, pipes, FIFOs, sockets, and some character devices. Disk files are NOT slow devices because their I/O is served from the buffer cache near-instantaneously. The SA_RESTART restart guarantee for read()/write() applies only to slow devices.
Q6. How can you implement a read() with a timeout using signals?
Install a SIGALRM handler WITHOUT SA_RESTART, call alarm(n) to schedule SIGALRM after n seconds, then call read(). If the user doesn’t respond within n seconds, SIGALRM fires, the handler runs, and read() returns -1 with errno == EINTR. Check a flag set by the handler to distinguish timeout from other interruptions. Call alarm(0) to cancel if read succeeds in time.
Q7. Can SA_RESTART cause problems? When might you want EINTR instead?
Yes. If you are deliberately using signals to interrupt long-running I/O (e.g., a timeout with SIGALRM, or a graceful shutdown via SIGTERM), then SA_RESTART will silently restart the syscall, defeating the signal’s purpose. In such cases, you want EINTR — the interrupted syscall tells you to check your flag and respond to the signal.
Q8. What is TEMP_FAILURE_RETRY?
TEMP_FAILURE_RETRY(expr) is a glibc macro (from <unistd.h>, available when _GNU_SOURCE is defined) that evaluates the expression in a loop, retrying as long as the result is -1 and errno is EINTR. It is equivalent to the manual while-loop EINTR retry pattern. Usage: ssize_t n; TEMP_FAILURE_RETRY(n = read(fd, buf, len));

All Chapters Complete! → Review & Summary

Back to Chapter Index ← Previous

Leave a Reply

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