SA_RESTART and Signal-Interrupted Syscalls
When a thread is blocked inside a slow system call — say, parked in
read()waiting on an idle TCP socket — and a signal arrives whose handler the thread has installed, the kernel must do something before it can run that handler: it has to unwind the thread out of the blocking call. What happens to the call afterward is governed almost entirely by a single bit,SA_RESTART, passed tosigaction(2)when the handler is established. With the bit set, the kernel transparently restarts the interrupted call after the handler returns, so the program never sees the interruption; without it, the call aborts with the errorEINTRand the program must cope. The catch — and the reason this note exists — is thatSA_RESTARTdoes not restart everything. A large, surprising, and exactly-enumerated set of calls (anything with a timeout, all ofpoll/select/epoll_wait, every System V IPC and sleep interface) always fails withEINTRregardless of the flag, because restarting them cleanly is impossible. This note reproduces thesignal(7)tables faithfully and explains why the line falls where it does.
This note owns the userspace surface of SA_RESTART: what the flag means to sigaction, which calls it does and does not save you from, and how to write code that survives interruption. The kernel-internal mechanism — the ERESTARTSYS, ERESTARTNOHAND, ERESTARTNOINTR, and ERESTART_RESTARTBLOCK magic return codes the handler path inspects, and how the entry code rewinds the instruction pointer — lives in its sibling Restartable System Calls and ERESTARTSYS. The plain “what is EINTR and why do I keep getting it” framing lives in EINTR and Interrupted System Calls. Read those for the layers below and around this one.
Mental Model
The key idea is that interruption is a fork in the road with three outcomes, and which branch is taken depends on (a) which syscall is blocked, (b) whether a user handler is even involved, and (c) whether SA_RESTART was set. SA_RESTART is only one of three inputs, and it only matters on one of the branches.
flowchart TB BLOCK["Thread blocked in a syscall<br/>(read on pipe, accept, futex,<br/>poll, nanosleep, ...)"] SIG["Signal becomes deliverable<br/>to this thread"] BLOCK --> SIG SIG --> KIND{"Is there a user-installed<br/>handler for this signal?"} KIND -->|"No handler<br/>(default action, or via<br/>restart-block path)"| RB["Kernel may auto-restart<br/>via restart_syscall<br/>(nr 219) — never seen<br/>by userspace"] KIND -->|"Handler runs"| CLASS{"Which class is<br/>the blocked call?"} CLASS -->|"Restartable class<br/>(read/write/wait/accept/<br/>recv with NO timeout, ...)"| FLAG{"SA_RESTART set?"} CLASS -->|"Never-restart class<br/>(poll/select/epoll, SysV IPC,<br/>nanosleep, anything with<br/>a timeout, ...)"| EINTR["Call fails with EINTR<br/>regardless of SA_RESTART"] FLAG -->|"Yes"| RESTART["Kernel restarts the call<br/>after handler returns —<br/>program never sees EINTR"] FLAG -->|"No"| EINTR
The three-way fork an interrupted syscall faces. What it shows: SA_RESTART is consulted only on the middle branch — a restartable call with a user handler. If there is no handler, the kernel can use its own restart-block machinery (the restart_syscall path, Restartable System Calls and ERESTARTSYS); if the call is in the never-restart class, EINTR is forced no matter what the flag says. The insight to take: setting SA_RESTART is necessary but never sufficient to make your program EINTR-proof — a correct program still needs an EINTR retry loop for the never-restart class, because no flag will ever silence those.
What SA_RESTART Means to sigaction
SA_RESTART is one of the sa_flags bits in the struct sigaction passed to sigaction(2). The man page’s own definition is deliberately terse (sigaction(2)):
Provide behavior compatible with BSD signal semantics by making certain system calls restartable across signals. This flag is meaningful only when establishing a signal handler. See
signal(7)for a discussion of system call restarting.
Three phrases in that sentence carry the entire weight of the feature. “BSD signal semantics” is the historical anchor: classic System V signal() left interrupted calls failing with EINTR, while 4.2BSD made them restart automatically. Linux’s sigaction() lets you choose per-handler. “certain system calls” — not all — is the qualifier this whole note unpacks. And “meaningful only when establishing a signal handler” means the flag is a property of the handler registration, not of the syscall and not of the process: two handlers for two different signals can disagree on SA_RESTART, and whether a blocked call restarts depends on the disposition of the specific signal that interrupted it.
A subtle and frequently-tripped-over consequence: the obsolete signal(2) wrapper’s restart behavior is not portable. Per signal(2): “By default, in glibc 2 and later, the signal() wrapper function does not invoke the kernel system call. Instead, it calls sigaction(2) using flags that supply BSD semantics” — i.e. glibc’s signal() does set SA_RESTART, but the original System V signal() does not. The man page’s own remedy: “POSIX.1 solved the portability mess by specifying sigaction(2) … use that interface instead of signal().” If you want deterministic restart behavior, never use signal(); always use sigaction() and set the flag yourself.
The full set of sa_flags bits, for context (per sigaction(2)):
| Flag | Meaning |
|---|---|
SA_RESTART | Restart certain interrupted syscalls instead of failing with EINTR (this note). |
SA_NOCLDSTOP | For SIGCHLD: do not receive notification when children stop or resume. |
SA_NOCLDWAIT | For SIGCHLD: do not turn terminated children into zombies. |
SA_NODEFER | Do not automatically block the signal while its own handler runs. |
SA_ONSTACK | Run the handler on the alternate stack set by sigaltstack(2). |
SA_RESETHAND | Reset disposition to SIG_DFL on handler entry (one-shot, BSD SA_ONESHOT). |
SA_SIGINFO | Handler takes three args (siginfo_t *, ucontext_t *) instead of one. |
SA_RESTORER | C-library-internal: address of the signal trampoline. Not for application use. |
SA_UNSUPPORTED | (Linux 5.11+) Probe whether the kernel honors flag bits. |
SA_EXPOSE_TAGBITS | (Linux 5.11+) Preserve arch tag bits in si_addr. |
Uncertain
Verify: the precise kernel version that introduced
SA_UNSUPPORTEDandSA_EXPOSE_TAGBITS(“Linux 5.11”). Reason: taken from the man7.orgsigaction(2)rendering (man-pages 6.18, dated 2026-02), not cross-checked against the kernel commit. To resolve: confirm against the kernel git history forSA_EXPOSE_TAGBITS/SA_UNSUPPORTED. These two are peripheral to this note’s topic. uncertain
The Calls That ARE Restarted by SA_RESTART
The following list is reproduced faithfully from the signal(7) man page, “Interruption of system calls and library functions by signal handlers” section, groff source fetched from the kernel man-pages git tree (man-pages 6.18, dated 2026-02-08). The page’s own framing: “If a blocked call to one of the following interfaces is interrupted by a signal handler, then the call is automatically restarted after the signal handler returns if the SA_RESTART flag was used; otherwise the call fails with the error EINTR.” Internally these all use the ERESTARTSYS return code (see Restartable System Calls and ERESTARTSYS).
read(2),readv(2),write(2),writev(2), andioctl(2)calls on “slow” devices. A “slow” device is one where the I/O call may block for an indefinite time — for example, a terminal, pipe, or socket. If an I/O call on a slow device has already transferred some data by the time it is interrupted, the call returns a success status (normally the number of bytes transferred), not a restart and notEINTR. Crucially: a local disk is not a slow device by this definition — disk I/O is not interrupted by signals at all.open(2), if it can block (e.g. opening a FIFO; seefifo(7)).wait(2),wait3(2),wait4(2),waitid(2), andwaitpid(2).- Socket interfaces:
accept(2),connect(2),recv(2),recvfrom(2),recvmmsg(2),recvmsg(2),send(2),sendto(2), andsendmsg(2)— unless a timeout has been set on the socket (see the never-restart list below). - File locking interfaces:
flock(2)and theF_SETLKWandF_OFD_SETLKWoperations offcntl(2). - POSIX message queue interfaces:
mq_receive(3),mq_timedreceive(3),mq_send(3), andmq_timedsend(3). futex(2)FUTEX_WAIT(since Linux 2.6.22; beforehand it always failed withEINTR).getrandom(2).futex(2)FUTEX_WAIT_BITSET.- POSIX semaphore interfaces:
sem_wait(3)andsem_timedwait(3)(since Linux 2.6.22; beforehand they always failed withEINTR). read(2)from aninotify(7)file descriptor (since Linux 3.8; beforehand it always failed withEINTR).
Two of these entries are genuinely surprising and worth pausing on. mq_timedreceive/mq_timedsend and sem_timedwait are timed interfaces that nevertheless restart — yet the socket calls with SO_RCVTIMEO/SO_SNDTIMEO do not. The difference is the kind of timeout: the POSIX-mq and POSIX-semaphore timed variants take an absolute deadline (a struct timespec against CLOCK_REALTIME), so restarting the call with its original arguments lands on the same deadline — the call does the right thing automatically. A socket SO_RCVTIMEO is a relative duration stored on the socket, with no per-call deadline the kernel can preserve across a restart; restarting would reset the clock. That distinction — absolute deadline restartable, relative timeout not — is the single most useful rule of thumb for predicting which side of the line a timed call lands on.
The Calls That Are NEVER Restarted (always EINTR)
Reproduced faithfully from the same signal(7) section. The page’s framing: “The following interfaces are never restarted after being interrupted by a signal handler, regardless of the use of SA_RESTART; they always fail with the error EINTR when interrupted by a signal handler.” Internally these use ERESTARTNOHAND or EINTR directly (see Restartable System Calls and ERESTARTSYS).
- “Input” socket interfaces, when a receive timeout (
SO_RCVTIMEO) has been set viasetsockopt(2):accept(2),recv(2),recvfrom(2),recvmmsg(2)(also with a non-NULLtimeoutargument), andrecvmsg(2). - “Output” socket interfaces, when a timeout (
SO_RCVTIMEO) has been set viasetsockopt(2):connect(2),send(2),sendto(2), andsendmsg(2). - Interfaces used to wait for signals:
pause(2),sigsuspend(2),sigtimedwait(2), andsigwaitinfo(2). - File-descriptor multiplexing interfaces:
epoll_wait(2),epoll_pwait(2),poll(2),ppoll(2),select(2), andpselect(2). - System V IPC interfaces:
msgrcv(2),msgsnd(2),semop(2), andsemtimedop(2). - Sleep interfaces:
clock_nanosleep(2),nanosleep(2), andusleep(3). io_getevents(2).
And one special case stated separately: sleep(3) is also never restarted if interrupted by a handler, but it gives a success return — the number of seconds remaining to sleep — rather than failing with EINTR.
Uncertain
Verify: the “Output socket interfaces” bullet in the never-restart list cites the timeout as
SO_RCVTIMEO, but the send-side timeout option isSO_SNDTIMEO(and the parallel “stop signals” section of the same page does saySO_SNDTIMEOfor the output case). This looks like a long-standing copy-paste typo insignal(7)itself, not a real semantic claim. Reason: the man page text saysSO_RCVTIMEOfor output sockets, which is almost certainly wrong; the send timeout isSO_SNDTIMEO. To resolve: readnet/socket.c/net/ipv4/tcp.ctimeout handling, or file/track a man-pages report. Treat “output sockets with a send timeout never restart” as the intended meaning. uncertain
The crucial takeaway for application code: poll, select, and epoll_wait always return EINTR on signal interruption. There is no flag that changes this. Event loops built on these must wrap them in a retry loop, or convert signals into file-descriptor events (signalfd(2)) / drain them via the loop’s own self-pipe so the signal never interrupts the wait in the first place. This is one of the most common real bugs in hand-rolled servers. A Linux-specific gotcha makes the naive retry loop subtly wrong for select(2): per select(2), “On Linux, select() modifies timeout to reflect the amount of time not slept,” and “also modifies timeout if the call is interrupted by a signal handler (i.e., the EINTR error return)” — behavior the page notes is “not permitted by POSIX.1.” So a do { } while (errno == EINTR) loop around select() that re-passes the same timeout struct will, on Linux, automatically resume against the shrunken remaining timeout (often what you want) — but the same loop is non-portable, and code that reuses the struct across unrelated select() calls will silently use a near-zero timeout. Use pselect/ppoll or recompute the deadline explicitly.
Why Some Calls Cannot Restart Cleanly
The deep reason behind the never-restart list is that a restarted syscall re-enters the kernel with its original argument registers — the kernel rewinds the user instruction pointer back to the syscall instruction and the same arguments are loaded again (the mechanism is detailed in Restartable System Calls and ERESTARTSYS and The x86-64 Syscall Calling Convention). For a call whose semantics are “block until something happens,” replaying the original arguments is harmless and correct: read(fd, buf, 4096) restarted is still a perfectly good read(fd, buf, 4096).
But for a call whose argument is a relative duration, replaying the original argument is wrong. Consider nanosleep(2) asked to sleep 10 seconds. A signal arrives after 7 seconds. If the kernel naively restarted the call with its original arguments, the thread would sleep another full 10 seconds — 17 in total — silently violating the contract. There is no way to fix this by just retrying the original arguments, so nanosleep cannot use the simple ERESTARTSYS restart path. Linux handles this with a separate restart-block mechanism, surfaced as the internal-only restart_syscall(2) (syscall number 219 on x86-64 — there is no glibc wrapper and userspace must never call it). Per its man page, restart_syscall “is used to restart certain system calls after a process that was stopped by a signal … is later resumed after receiving a SIGCONT,” and when it restarts the call it does so “with a time argument that is suitably adjusted to account for the time that has already elapsed (including the time where the process was stopped).” It applies to the relative-timeout calls — poll(2) (since Linux 2.6.24), nanosleep(2), clock_nanosleep(2), and futex(2) FUTEX_WAIT/FUTEX_WAIT_BITSET — precisely because those are the calls whose timeout must be decremented across the interruption rather than replayed whole. This is why restart_syscall exists at all. See Restartable System Calls and ERESTARTSYS for the ERESTART_RESTARTBLOCK / restart_block internals.
However — and this is the part that surprises people — the restart-block path does not silence interruption when a user handler runs. If you installed a handler for the interrupting signal (even with SA_RESTART), nanosleep does not transparently resume; it returns EINTR to userspace and, if you passed a non-NULL rem argument, writes the remaining time there so you can decide whether to re-sleep. From nanosleep(2): “If the call is interrupted by a signal handler, nanosleep() returns -1, sets errno to EINTR, and writes the remaining time into the structure pointed to by rem unless rem is NULL.” The same holds for clock_nanosleep(2) with a relative request (TIMER_ABSTIME not set), which returns the positive error number EINTR and the unslept remainder in remain. For an absolute clock_nanosleep request (TIMER_ABSTIME set), “the remain argument is unused, and unnecessary,” so there is no remainder to report, and the right retry is simply to call it again with the same absolute deadline — which is exactly why absolute timed calls are easier to make robust. (The restart_syscall deadline-adjustment path described above is what handles the stop/SIGCONT case, where the call is interrupted not by a handler but by the process being stopped and resumed.)
The socket-timeout calls are the same problem in different clothing: the SO_RCVTIMEO duration is relative and lives on the socket, with no per-call deadline the kernel can carry across a restart, so the kernel switches them to forced EINTR rather than risk restarting against a clock that has already partly elapsed.
There is one documented escape hatch in the other direction: the seccomp user-space notification feature can, in certain circumstances, cause restarting of system calls that SA_RESTART would otherwise never restart. Per seccomp_unotify(2): “system call restarting … will occur even for the blocking system calls listed in signal(7) that would never normally be restarted by the SA_RESTART flag.” This happens when a target makes a syscall that triggers a supervisor notification, then is hit by a signal while blocked waiting for the supervisor — the kernel restarts the intercepted call, so the supervisor may see multiple notifications for one logical call. This is a narrow, advanced case; see seccomp and Syscall Filtering.
Code: Establishing SA_RESTART and the Retry Loop You Still Need
#include <signal.h>
#include <errno.h>
#include <unistd.h>
static volatile sig_atomic_t got_signal = 0;
static void on_sig(int signo) { got_signal = 1; }
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = on_sig; /* simple handler */
sigemptyset(&sa.sa_mask); /* don't block extra signals in handler */
sa.sa_flags = SA_RESTART; /* ask the kernel to restart the */
/* restartable class of calls */
sigaction(SIGUSR1, &sa, NULL); /* USE sigaction, never signal() */
char buf[4096];
/* read() on a pipe/socket is in the RESTARTABLE class, so with
SA_RESTART set this loop will essentially never observe EINTR —
the kernel restarts it transparently. */
ssize_t n = read(STDIN_FILENO, buf, sizeof buf);
/* ... but for a NEVER-restart call, the flag does nothing: */
return (n < 0 && errno == EINTR) ? 1 : 0;
}Line-by-line: sa_handler points at the handler; sigemptyset(&sa.sa_mask) declares no additional signals are blocked during the handler (the interrupting signal itself is auto-blocked unless SA_NODEFER); sa.sa_flags = SA_RESTART is the one bit this note is about; and sigaction() installs it. Because read() on a pipe is in the restartable class, that single read() will be auto-restarted and rarely if ever return EINTR.
For the never-restart class — the one SA_RESTART cannot help — the canonical idiom is the EINTR retry loop, often hidden behind a macro (glibc even provides TEMP_FAILURE_RETRY):
int ret;
do {
ret = poll(fds, nfds, timeout_ms); /* poll() NEVER restarts — */
} while (ret == -1 && errno == EINTR); /* SA_RESTART is irrelevant here */This loop is mandatory for poll/select/epoll_wait, the SysV IPC calls, the sleep calls (where you should re-sleep the remainder, not the original duration), and any socket call with a timeout set. Setting SA_RESTART does not let you delete it.
Failure Modes and Common Misunderstandings
- “I set
SA_RESTART, so I don’t need to handleEINTR.” Wrong for the entire never-restart list. The most common production form: anepoll/pollevent loop that spuriously wakes and mis-handles the-1/EINTRreturn when any signal fires (e.g. aSIGCHLDfrom a reaped child, orSIGWINCH). signal()vssigaction()restart drift. Code using the oldsignal()wrapper inherits glibc’s BSD default (SA_RESTARTon), but the same code linked against a different libc, or ported to System V semantics, gets the opposite. Symptom: aread()that “never getsEINTR” on Linux/glibc suddenly does elsewhere. Fix: usesigaction()explicitly.- Partial transfers on slow devices. A
read()/write()on a pipe or socket interrupted after transferring some bytes does not restart and does not returnEINTR— it returns the short count. Code that assumes aread()either fully completes or returnsEINTRmishandles the short-read case. - Sleeping the wrong amount after interruption. Re-calling
nanosleep()with the original duration instead of*remover-sleeps. Always re-sleep the remainder (relative) or call against the same absolute deadline. SO_RCVTIMEO/SO_SNDTIMEOsilently changes restart behavior. Adding a socket timeout movesrecv/send/accept/connectfrom the restartable class into the never-restart class. Code that relied onSA_RESTARTto paper over interruption breaks the moment someone sets a socket timeout.
Alternatives and When to Choose Them
signalfd(2)— turn signals into readable file-descriptor events and add the fd to yourpoll/epollset. The wait call is then never interrupted by these signals because they are blocked and delivered synchronously as fd reads. Preferred for event loops; it sidesteps the wholeSA_RESTART/EINTRquestion for the loop’s wait call. See seccomp and Syscall Filtering’s neighbor topics in Linux IPC MOC.- The self-pipe trick — the pre-
signalfdportable idiom: the handler writes one byte to a pipe whose read end is in theselect/pollset, converting “signal interrupted my wait” into “there is data on the self-pipe.” ppoll(2)/pselect(2)with a signal mask — atomically unblock signals only for the duration of the wait, closing the race between checking a flag and blocking. Still returnsEINTR(they are in the never-restart list), but the atomic mask swap is what you actually want when mixing signals and multiplexing.- Just set
SA_RESTARTand write the loop anyway — for simple programs doing blockingread/write,SA_RESTARTplus a defensiveEINTRloop is the lowest-effort correct approach.
Production Notes
The EINTR-after-SIGCONT wrinkle bites real software: even with no signal handler installed, on Linux certain blocking calls fail with EINTR after the process is stopped (SIGSTOP/SIGTSTP) and resumed via SIGCONT — a behavior signal(7) explicitly notes is not sanctioned by POSIX.1 and does not occur on other systems. The affected set per the man page includes the timeout-bearing socket calls, epoll_wait/epoll_pwait, semop/semtimedop, and sigtimedwait/sigwaitinfo. This is why even a daemon that installs zero handlers must still tolerate EINTR on these calls: a developer suspending it under a debugger or shell job control can inject EINTR out of nowhere. The lesson, repeated across decades of systems code, is that the EINTR retry loop is not optional defensive paranoia — it is part of the contract.
See Also
- Restartable System Calls and ERESTARTSYS — the kernel-side mechanism:
ERESTARTSYS,ERESTARTNOHAND,ERESTARTNOINTR,ERESTART_RESTARTBLOCK, the restart-block, and how the IP is rewound. The layer below this note. - EINTR and Interrupted System Calls — the plain “what is
EINTR” framing and the retry-loop pattern. The layer around this note. - The errno Convention and Negative Return Values — how
EINTR(and every error) crosses the boundary as a small negative return value. - The x86-64 Syscall Calling Convention — why restarting “replays the original argument registers,” the fact that makes relative-timeout calls unrestartable.
- seccomp and Syscall Filtering — the
seccomp_unotifyexception that can restart otherwise-unrestartable calls. - Linux System Call Interface MOC — parent map (§6 Errors, Restart, and Interruption).
- Linux IPC MOC — signals,
signalfd, and the self-pipe trick live here.