Search code examples
csignalsposixsystem-calls

SIGINT caught only one time


Given this code:

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

void sigint_handler(int h)
{
    printf("Hey! I caught a SIGINT! :)\n");
}

int main()
{
    struct sigaction act;
    act.sa_handler = &sigint_handler;

    if (0 != sigaction(SIGINT, &act, NULL)) {
        perror("unable to setup the SIGINT handler!\n");

        return 1;
    }

    while(1) { }

    return 0;
}

compiled with gcc 7.2.0 (kernel 4.13.12) using the following options: -Wall -pedantic -ansi -std=gnu11.

The first signal is always caught, but sometimes, the second one is not caught, and sometimes it is.

I encountered this bug while spamming Ctrl-C at process' startup.

What did I miss to catch all signals?


Solution

  • As Martin James observed in a comment:

    Your SIGINT handler has only one line - a call to a function that is not async-signal safe:(

    Somewhat later, I observed:

    You've no idea what the other fields in the struct sigaction are set to because you didn't initialize act. Maybe you will get better behaviour if you set the documented fields to known values. You can use write() in a POSIX signal handler (not in standard C, but fortunately you're not using standard C). You shouldn't use printf(), though in this context it is unlikely to cause any trouble. One minor advantage of write() — there's no application level buffering to worry about.

    The question How to avoid using printf() in a signal handler discusses which functions can be used in a signal handler. Note that the functions from the <string.h> header such as strlen() and strchr() are not listed amongst those that are async-signal safe. I find that omission puzzling, but that's what POSIX (2008 and earlier) says. (This was accurate for POSIX 2008. One of the changes in POSIX 2016 is that a number of signal-safe routines have been added to the list in Signal Concepts, including both strlen() and strchr() — this makes a lot of sense to me.)

    I adapted your code like this:

    #include <signal.h>
    #include <stdio.h>
    #include <unistd.h>
    
    static void sigint_handler(int h)
    {
        char message[] = "Hey! I caught a SIGINT x! :\n";
        char *x = message;
        while (*x != 'x' && *x != '\0')
            x++;
        if (*x != '\0')
            *x = (h % 10) + '0';
        write(1, message, sizeof(message) - 1);
    }
    
    int main(void)
    {
        struct sigaction act = { 0 };
        act.sa_handler = &sigint_handler;
    
        if (0 != sigaction(SIGINT, &act, NULL))
        {
            perror("Unable to setup the SIGINT handler!\n");
            return 1;
        }
    
        while (1)
        {
            printf("Pausing for a moment...\n");
            pause();
            printf("You interrupted my dozing\n");
        }
    
        return 0;
    }
    

    The code uses pause() rather than spinning in a busy-loop. It's an unusual system call; it never returns normally (the exec*() family of functions never return normally either).

    I compile with stringent warning options:

    $ gcc -O3 -g -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes \
    >     -Wstrict-prototypes sig13.c -o sig13
    $
    

    If I didn't use h (the argument to the signal handler), the code wouldn't compile, so I used it. The code avoids using string handling functions (char *x = strchr(message, 'x'); if (x != 0) *x = (h % 10) + '0'; would be clearer). The function is static because it won't be used outside this file — so there isn't a header to declare it.

    When executed (on a Mac running macOS High Sierra 10.13.2, using GCC 7.2.0), it produces output like:

    $ ./sig13
    Pausing for a moment...
    ^CHey! I caught a SIGINT 2! :
    You interrupted my dozing
    Pausing for a moment...
    ^CHey! I caught a SIGINT 2! :
    You interrupted my dozing
    Pausing for a moment...
    ^CHey! I caught a SIGINT 2! :
    You interrupted my dozing
    Pausing for a moment...
    ^CHey! I caught a SIGINT 2! :
    You interrupted my dozing
    Pausing for a moment...
    ^CHey! I caught a SIGINT 2! :
    You interrupted my dozing
    Pausing for a moment...
    ^\Quit: 3
    $
    

    The main moral to this is "make sure your variables are properly initialized". A secondary moral is to make sure your signal handler is clean.