Syscall / ELF · Linux userspace / kernel ABI

errno, EINTR, and short returns

Explains how to decide from both the amount of work completed and the syscall restart policy, rather than applying a blanket rule to retry every error.

Series
03 / 38
Build
cc -std=c17 -Wall -Wextra -O2 eintr_read.c -o eintr_read
Run
./eintr_read
Kernel
Linux 6.18.37 LTS

Does EINTR always make it safe to call the same syscall again?

A syscall interrupted by a signal has not necessarily done nothing. A read/write-family call may return the number of bytes processed as a successful result; it returns EINTR when no progress was made.

SA_RESTART lets the kernel and libc automatically restart some blocking syscalls, but it does not apply to every syscall. For poll-family calls with a timeout, or operations that change user-visible state, the caller must recompute the remaining time.

Structure diagram

Figure 1. Three ways a signal meets I/O progress
T0 start read requested=128
T1 wait queue processed bytes=0
T2 signal pending check mask/action
T3-A EINTR no progress · retry/abort decision
T3-B short return partial progress · byte count takes precedence
T3-C restart SA_RESTART condition satisfied

Even for the same signal, the observed return value depends on whether the syscall started, processed some bytes, or qualifies for automatic restart.

Call path

Figure 2. From userspace code to observable results
blocking read wait for data
signal schedule handler
restart rule SA_RESTART/errno
caller loop check progress and deadline
result complete or explicitly abort

Whether to retry depends not on errno alone, but on bytes already processed, the absolute deadline, and side effects of the call.

Figure 3. Major points along the kernel-internal path
read syscall enter wait queue
signal_pending check the reason for wakeup
-ERESTARTSYS internal restart code
arch return rewind instruction / errno
userspace EINTR or success

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
fs/read_write.c vfs_read(), vfs_write() Short returns and file-position updates
kernel/signal.c get_signal(), signal_setup_done() Pending signals and the syscall restart decision
arch/x86/entry/common.c syscall_exit_to_user_mode() Signal handling before return to userspace

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 eintr_read.c -o eintr_read
01#define _POSIX_C_SOURCE 200809L
02#include <errno.h>
03#include <signal.h>
04#include <stdio.h>
05#include <unistd.h>
06
07static volatile sig_atomic_t timed_out;
08
09static void on_alarm(int signo)
10{
11    (void)signo;
12    timed_out = 1;
13}
14
15int main(void)
16{
17    struct sigaction sa = { .sa_handler = on_alarm };
18    char buffer[128];
19
20    sigemptyset(&sa.sa_mask);
21    sigaction(SIGALRM, &sa, NULL);
22    alarm(3);
23
24    for (;;) {
25        ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer));
26        if (n > 0)
27            return write(STDOUT_FILENO, buffer, (size_t)n) < 0;
28        if (n == 0)
29            return 0;
30        if (errno == EINTR && !timed_out)
31            continue;
32        if (errno == EINTR && timed_out)
33            return 2;
34        perror("read");
35        return 1;
36    }
37}

Code notes

Source line 7volatile sig_atomic_t timed_out

This is the smallest integer type that C guarantees can be shared between a signal handler and main. volatile prevents the compiler from eliding accesses, but does not provide thread synchronization.

Source line 17struct sigaction sa

SA_RESTART is deliberately omitted so that read() returning EINTR can be observed.

Source line 22alarm(3)

After 3 seconds, SIGALRM becomes pending and wakes the blocking read from its wait.

Source line 28if (n == 0)

EOF is not an error. It is the normal completion condition after all writers of a pipe or file are closed.

Source line 32errno == EINTR && timed_out

Even for the same EINTR, this timeout policy stops rather than retries and reports the reason to the caller above.

Detailed behavior

01

Kernel restart codes are not errno values

Kernel-internal values such as -ERESTARTSYS and -ERESTARTNOHAND are not exposed directly as errno. The signal disposition and architecture return path decide whether to reexecute the syscall instruction or convert the value to EINTR.

This is why strace may display restart_syscall or ERESTARTSYS.

02

A relative timeout can grow on every retry

If the original timeout is reused after EINTR, frequent signals can extend the total wait without bound. Establish an absolute CLOCK_MONOTONIC deadline at the start and calculate the time remaining immediately before each new call.

ppoll() and pselect() combine waiting and a signal-mask change in one kernel operation to avoid a wakeup race.

03

Check state before retrying a call with side effects

For calls such as write, send, and accept that may create an object or consume some data, first check whether a successful byte count or new fd was already returned.

Blindly closing the same fd number again after close() returns EINTR is dangerous. On Linux, the fd slot may be released early and reused by another thread.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
pending signalRecorded in a thread/process signal queue when generated and consumed at deliveryblocked mask, disposition
restart_blockSome syscalls preserve the arguments required for restart in the taskrestart_fn, remaining time
deadlineCreated by userspace at the start and kept until the operation completes or times outMeasured against CLOCK_MONOTONIC

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
The timeout is longer than expectedThe relative timeout is reset for every EINTRLog the absolute deadline and remaining time
Duplicate requests occurA syscall whose side effect completed is retried blindlyCheck the return value and the server request id
read keeps returning EINTRSA_RESTART is not used, or the syscall cannot be restartedCheck the sigaction flags and strace

Verify it yourself

  1. Wait without entering input and verify that SIGALRM leads to exit status 2.
  2. Add sa.sa_flags = SA_RESTART and compare in strace whether read continues waiting.
  3. Have a separate process send SIGALRM every 100ms and compare total elapsed time between relative-timeout and absolute-deadline implementations.
Run./eintr_read
Tracestrace -e trace=read,rt_sigaction,alarm ./eintr_read

Primary sources