Setup
In the code below, which simply prints some text until it times out, I added a handler (onintr()) for SIGINT. The handler onintr() does the following:
- Resets itself as the default handler.
- Prints out some text.
- Calls
longjmp().
Issue
It seems only the first Ctrl+C is interpreted correctly.
After pressing Ctrl+C the first time the print statement in onintr() appears on the screen and execution returns to where setjmp() was called. However, subsequent Ctrl+C calls get ignored.
Moreover, resetting onintr() as the handler doesn't seem to make a difference. In other words, if I let SIG_DFL become the default handler after onintr() was called once, subsequent Ctrl+C-s get ignored just as when onintr() was the handler; and I can't terminate the program.
The code signal.c:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>
jmp_buf sjbuf;
void onintr(int);
void
onintr(int i)
{
signal(SIGINT, onintr);
printf("\nInterrupt(%d)\n", i);
longjmp(sjbuf, 0);
}
int
main(int argc, char* argv[])
{
int sleep_t = 1;
int ctr = 0;
int timeout = 10;
if (signal(SIGINT, SIG_IGN) != SIG_IGN)
signal(SIGINT, onintr);
setjmp(sjbuf);
printf("Starting loop...\n");
while (ctr < timeout) {
printf("Going to sleep for %d second(s)\n", sleep_t);
ctr++;
sleep(sleep_t);
}
printf("\n");
return 0;
}
Behavior
On Ubuntu 22.04 I get the following:
gomfy:signal$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
gomfy:signal$ gcc signal.c
gomfy:signal$
gomfy:signal$ ./a.out
Starting loop...
Going to sleep for 1 second(s)
Going to sleep for 1 second(s)
^C
Interrupt(2)
Starting loop...
Going to sleep for 1 second(s)
Going to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
Going to sleep for 1 second(s)
gomfy:signal$
As you can see the subsequent Ctrl+C-s (^C) get ignored. Only the first one seems to call the handler.
For starters, from
signal-safety(7)we learn:This raises two problems:
printfis NOT async-signal-safe, andmixing signal handlers, longjmp(3), and unsafe functions leads to Undefined Behaviour, as described later in the NOTES section:
This means if the delivery of SIGINT happens to interrupt a call to
printfthen the program can no longer be reliably reasoned about.As a possible red herring, the generic Linux manual for
sleep(3)makes the claim:and then ambiguously states:
It is not entirely clear if this is only in reference to the previously mentioned
alarm-based sleeps, but seems likely. The POSIX manual page forsleep(3)would appear to clarify this by making a similar claim:and
nanosleep(2)states:Suffice to say, it does not appear that
sleepcontributes to this problem, at least on Linux.The Linux manual on
signal(2)highlights its issues with portability, and encourages the use ofsigaction(2).An interesting note is:
The portability section later details the differences between System V (reset) and BSD (block) semantics, and notes that glibc 2+ uses BSD semantics by default (by wrapping around
sigaction(2)).So if we
longjmpout of a handler, is the signal ever unblocked?Linux's overview of signals,
signal(7), ultimately clarifies things in a section labeled Execution of signal handlers, detailing a five step process.The last part of step one involves:
While step four and five are:
Again, what happens if we
longjmpout of a handler? Here is the most important piece of information:So the answer is that, by jumping out of the signal handler, the signal mask retains SIGINT, and blocks delivery of subsequent signals. The manual mentions the use of sigprocmask(2) or sigsetjmp(3) and siglongjmp(3) to solve this problem.
Here is a simple example of using the latter.
sigsetjmpneeds a non-zero value as its second argument, which tells the pair of functions to save and restore the signal mask.siglongjmpsimply replaceslongjmp, andsigjmp_bufreplacesjmpbuf.signalis moved aftersigsetjmpto avoid Undefined Behaviour in the event SIGINT is delivered beforesigsetjmpexecutes, which would causesiglongjmpto operate on a garbagesigjmp_buf.Additionally,
ctrmust be declared asvolatile, as otherwise its value is unspecified. Fromsetjmp(3)(applies tosigsetjmpas well):If this still does not work, you may need to define a macro:
_BSD_SOURCEon glibc 2.19 and earlier or_DEFAULT_SOURCEin glibc 2.19 and later. See:feature_test_macros(7).