1

I'm learning about signals and async-signal-unsafe functions. In particular, I learnt that printf is async-signal-unsafe and can cause a deadlock when called from the main program thread and from a signal handler. To check this, I wrote the following program (it is a little ugly):

/*
 * sig-deadlock.c
 */

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void
sigint_handler(int signum)
{
        int i;
        printf("Signal");
        for (i = 0; i < 30; i++) {
                printf("%d\n", i);
        }
}

int
main(void)
{
        int i;
        printf("PID: %d\n", getpid());
        signal(SIGINT, sigint_handler);
        for (i = 0; i < 1e9; i++) {
                printf("a");
                sleep(1);
        }

        return EXIT_SUCCESS;
}

I expect the program to run the inner loop (and hence call printf) several times and each time I send a SIGINT (using kill -INT $PID) to execute the signal handler, which counts to 30.

On running this, however, I observed that the signal handler runs once and the next signal terminates the process. What causes this behavior and how can I fix this?

OS: Linux 4.9.0.

2

signal(2) is part of the old signals API, which isn't always convenient to use, mainly because...

The only portable use of signal() is to set a signal's disposition to SIG_DFL or SIG_IGN. The semantics when using signal() to establish a signal handler vary across systems (and POSIX.1 explicitly permits this variation); do not use it for this purpose.

In the original UNIX systems, when a handler that was established using signal() was invoked by the delivery of a signal, the disposition of the signal would be reset to SIG_DFL, and the system did not block delivery of further instances of the signal. System V also provides these semantics for signal().

On BSD, when a signal handler is invoked, the signal disposition is not reset, and further instances of the signal are blocked from being delivered while the handler is executing.

The situation on Linux is as follows:

  • The kernel's signal() system call provides System V semantics.
  • By default, in glibc 2 and later, the signal() wrapper function [...] calls sigaction(2) using flags that supply BSD semantics.
  • On glibc 2 and later, if the _BSD_SOURCE feature test macro is not defined, then signal() provides System V semantics. (The default implicit definition of _BSD_SOURCE is not provided if one invokes gcc(1) in one of its standard modes (-std=xxx or -ansi) or... see signal(2).

In other words, there is a myriad of cases in which a handler set with signal will not be used more than once: after one signal, the function you defined will be discarded and the handler will be reset to the default, which, for SIGINT, is to kill the process.

While I would encourage you to read the man page I just quoted, the best way to handle this situation is probably to stop using signal and switch to sigaction. The man page will give you all the details you need, but here's a quick sample:

void sighandler(int signum);

int main(void) {
    struct sigaction sa;
    sa.sa_handler = sighandler;
    sigemptyset(&(sa.sa_mask));
    sigaddset(&(sa.sa_mask), SIGINT);
    sigaction(SIGINT, &sa, NULL);

    int i;
    for(i = 0; i < 5; i++) {
        printf("Sleeping...\n");
        sleep(5);
        printf("Woke up!\n");
    }
}

void sighandler(int signum) {
    printf("Signal caught!\n");
}

An important note though: as you may already know (hence your "infinite" loop in your main function), signals will interrupt system calls (such as those sleep(3) makes) upon signal reception, whether that signal is handled by the program or not. In my example above, the program will print Woke up! everytime you kill it because it exits the sleep call prematurely. If you have a look at the man page, you'll see the call should return the number of seconds left to sleep when it is interrupted.

  • However, this is still wrong. You shouldn't call stdio functions from signal handlers, since they aren't reentrant. – Satō Katsura Mar 15 '17 at 7:05
  • @SatoKatsura While I would agree if it were the old signals API, note that because SIGINT is part of the mask passed to sigaction, the handler should never be reentered in the first place. Non-reentrant calls within the handler are therefore not much of an issue ^^ – John WH Smith Mar 15 '17 at 9:21
  • 1
    Consider the following scenario: the user presses Ctrl-C while the program is printing Sleeping... or Woke up!. Highly unlikely to happen, and highly unlikely to overflow anything, but still a potential crash. – Satō Katsura Mar 15 '17 at 9:37
1

One more prerequisite that wasn't emphasized here.

You have to set sa_flags in the sigaction structure to ensure consistent results.

sa.sa_flags = 0 ; // this works for me

I was tripped up for a good several minutes just now because I had 2 different applications that registered the signal in the same way and only one of them had this issue (catching the signal only once).

After staring at my code for a bit and rereading the man page I realized the different behaviour was because of different nuanced function stack side effects of my 2 applications.

one program registered the signal handler w/ sigaction() super early in main() pretty much before any memory was allocated or any nested functions were entered (this was the program w/ irregular sigaction behaviour).

The other program called sigaction() after some other variables had been declared at the top of main that took up some memory etc and it seems like by a fluke gave it consistent behaviour to initialize the memory in sa.sa_flags to zero most likely (or some number that probably didn't have the SA_RESETHAND bit set).

The fact that the inconsistent application would sometimes initialize registering the signal w/ catch once behaviour and sometimes w/ catch every time behaviour tipped me off to some kind of randomness that could be caused by garbage among other things.

The quick sample code snippet's convention in John's answer is still incomplete and may give inconsistent results because it omits setting of sa_flags.

New contributor
NeoGeetz is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
  • I could use my "powers" to edit John's answer, but even with that in it'll be very much broken because it's using the signal-unsafe printf() inside a signal handler and that's a absolutely NO NO EVER, despite appearing on so many examples in forums like this. I'll give it a pass though for the useless sigaddset ;-) – mosvy 2 hours ago
  • I actually first got excited when including sigemptyset/sigaddset into my code thinking that was the solution because it hinted at the same logic of clearing flags/masks for uninitialized memory. And when it didn't work I squinted at the man page again and at least saw that it was sort of on the right track. – NeoGeetz 1 hour ago

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.