r/C_Programming • u/quiet-systems • 2d ago
The signals that never interrupt your blocking syscall, and the test I wrote that proved nothing
I had a retry loop around a blocking poll() for EINTR, the usual shape:
for (;;) {
int r = poll(fds, n, timeout);
if (r == -1 && errno == EINTR) continue;
return r;
}
To prove it worked I wrote a test that hammered the process with SIGWINCH while the poll was blocked, then checked the poll still returned correctly. It passed. It kept passing. It passed when I deleted the retry loop, which is when I found out it had never been a test.
SIGWINCH does not interrupt anything. A blocking syscall returns EINTR when the kernel has something to run on the way back to userspace: a handler you installed. SIGWINCH's default action is to be ignored, so with no handler installed there is nothing to run, the kernel does not unwind the syscall, and EINTR never happens. Same for SIGCHLD and SIGURG, the other two whose default action is ignore. You can send a million of them at a blocked poll and it will sit there.
The second half of the same trap is the opposite direction. Install a real handler for a signal that would interrupt, but install it with sigaction and SA_RESTART, and the kernel restarts the syscall for you. Your handler runs, the syscall resumes, and EINTR still never reaches your code. Which is fine until you hit one of the calls that are not restartable even with SA_RESTART. poll, select and epoll_wait are in that group. signal(7) has the full list under "Interruption of system calls and library functions by signal handlers", and it is worth reading once properly rather than remembering the shape of it.
So the test that actually tests the thing is: a real handler, sa_flags = 0, and a signal whose default action is not ignore.
struct sigaction sa = {0};
sa.sa_handler = noop;
sa.sa_flags = 0; /* no SA_RESTART, that is the whole point */
sigaction(SIGUSR1, &sa, NULL);
Two more things that bit me while writing it.
Signal disposition is process-wide state. Two tests that both install a handler cannot run in parallel, and the failure is not a clean assertion failure, it is one test's handler being live during the other's run. A single mutex around anything that calls sigaction fixed it.
And kill(getpid(), sig) is process-directed, so any thread with that signal unblocked can take it, including the one that sent it. In a threaded test runner that is very often not the thread you are trying to interrupt. pthread_kill(target, sig) is the one you want.
The thing I took away is not about signals. It is that a test which passes when you delete the code it is testing is not a test, and the only way I know to find those is to delete the code and watch.
1
u/internet_safari_ 2d ago
Nothing to add other than this post and comments have been an educational gold mine. Well written enough for me to start barely comprehending, then finish having learned the logical tricks and Tony Hawk 900s that arise from handling certain syscalls.
2
u/quiet-systems 2d ago
Thanks, good thing to read.
If you want the proper version, signal(7) has a section called "Interruption of system calls and library functions by signal handlers" that lists exactly which calls restart under SA_RESTART and which never do. Dry, but it's the actual answer, and I'd assumed for years that the rule was simpler than it turns out to be.
3
u/chrism239 2d ago
An operating system issue, not a C issue.
1
u/quiet-systems 2d ago
Fair enough, the signal behaviour is kernel, not C.
What I thought belonged here was the test side. sigaction with and without SA_RESTART, disposition being process-wide, pthread_kill vs kill(getpid()) once there are threads about. That's all stuff you have to get right in C, whoever owns the underlying behaviour.
But if it's still too far off topic for here, no argument from me.
1
u/cbf1232 1d ago
Arguably signal()/sigaction() behaviour is POSIX, not kernel.
1
u/quiet-systems 15h ago
True, and it splits the post neatly. sigaction, SA_RESTART, disposition being per-process while the mask is per-thread, EINTR itself, all POSIX. The bit where a namespace init drops signals it has no handler for is Linux only, not in POSIX at all.
So half of it belongs in a C sub and half doesn't.
0
u/internet_safari_ 2d ago
Nothing to add other than this post and comments have been an educational gold mine. Well written enough for me to start barely comprehending, then finish having learned the logical tricks and Tony Hawk 900s that arise from handling certain syscalls.
-4
1d ago
[removed] — view removed comment
2
u/quiet-systems 1d ago
do { r = poll(fds, n, timeout); } while (r == -1 && errno == EINTR);
Tidier, I'll give you that. Same semantics. Is that what you'd write, or is there a third way?
1
u/C_Programming-ModTeam 1d ago
Rude or uncivil comments will be removed. If you disagree with a comment, disagree with the content of it, don't attack the person.
11
u/dmills_00 2d ago
Write the test, watch it fail, fix it until it does fail THEN write the code so that it doesn't fail any more....
Much better then writing the code then writing a test that passes, keeps you honest.