简体   繁体   中英

C Signal handler doesn't uninstall when installed from another function

I'm experiencing what I believe to be some bizarre behavior with the below code snippet. When I call addHandler() to install a signal handler, the signal handler gets called every single time I press CTRL+C (send SIGINT) in the terminal, however if I replace the call to addHandler() with the contents of the addHandler() function (currently commented out), the handler is only called a single time (from what I know, this is the expected behavior), and subsequent SIGINT's will actually terminate the process since there is no installed user handler. Am I missing something basic here? Why is it that installing the handler via another function, seemingly permanently installs it?

I'm sure it is more nuanced than that...but here's the code:

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

void sigHandler(int signal) {
  printf("Signal handler beiing called!\n");
}

void addHandler() {
  struct sigaction actionStruct;
  actionStruct.sa_handler = sigHandler;
  sigaction(SIGINT, &actionStruct, NULL);
}

int main() {
  addHandler();
  /*struct sigaction actionStruct;
  actionStruct.sa_handler = sigHandler;
  sigaction(SIGINT, &actionStruct, NULL);*/

  int i = 0;
  while (1) {
    printf("In while with i: %d\n", i++);
    sleep(1);
  }

  return 0;
}

Thanks!

You don't clear the memory of struct sigaction actionStruct when you declare it. You set a single value. All the rest of the struct will contain values that were on the stack from a previous function. This is probably why you have different behavior from different functions.

You need to declare it with struct sigaction actionStruct = {}; or use memset

Am I missing something basic here?

Yes.

  1. You did not properly initialize the struct sigaction actionStruct . Essentially, you provided random .sa_flags , which causes the issues OP observed.

    The recommended way to initialize it is to use memset() and sigemptyset() :

     memset(&actionStruct, 0, sizeof actionStruct); sigemptyset(&actionStruct.sa_mask);

    memset() clears the entire structure to zeroes, including any padding. sigemptyset() clears the set of signals blocked while the signal handler itself is run; ie, it initializes it to the empty set).

  2. You did not set the actionStruct.sa_flags member.

    Zero is a perfectly valid value for it, but setting it explicitly is important for us humans, because then we can read the intent .

    For example, if you only want the handler to run once, you can set actionStruct.sa_flags = SA_RESETHAND; . After the first signal is delivered, the SA_RESETHAND flag causes the handler to be reset to the default. For SIGINT , this is Term (terminate the process), as described in the man 7 signal man page.

  3. printf() is not an async-signal safe function (as listed in the man 7 signal-safety man page in newer systems, in the man 7 signal man page in older systems).

    Depending on the exact C library implementation (there are many POSIXy systems), it may work, it may garble output, or it may even crash the process. So, don't do that.

In the hopes that you, dear reader, are really interested in writing robust, portable, POSIX signal handling C99 or later programs, let me show you an example. breakme.c :

#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <limits.h>
#include <string.h>
#include <signal.h>
#include <stdio.h>
#include <errno.h>

/* Low-level, async-signal safe write routines.
*/

static int wrpp(const int descriptor, const char *ptr, const char *end)
{
    while (ptr < end) {
        ssize_t  n = write(descriptor, ptr, (size_t)(end - ptr));
        if (n > 0)
            ptr += n;
        else
        if (n != -1)
            return EIO; /* Should not occur */
        else
        if (errno != EINTR)
            return errno;
    }

    return 0;
}

static int wrs(const int descriptor, const char *s)
{
    if (descriptor == -1)
        return EBADF;
    else
    if (!s)
        return EINVAL;
    else {
        /* Note: strlen() is not listed as an async-signal safe function. */
        const char *end = s;
        while (*end)
            end++;
        return wrpp(descriptor, s, end);
    }
}

static int wrn(const int descriptor, const char *ptr, const size_t len)
{
    if (descriptor == -1)
        return EBADF;
    else
        return wrpp(descriptor, ptr, ptr + len);
}

static int wri(const int descriptor, const long value)
{
    char           buffer[4 + (sizeof value) * (CHAR_BIT * 10) / 3];
    char *const    end = buffer + sizeof buffer;
    char          *ptr = buffer + sizeof buffer;
    unsigned long  u = (value < 0) ? -value : value;

    if (descriptor == -1)
        return EBADF;

    do {
        *(--ptr) = '0' + (u % 10);
        u /= 10uL;
    } while (u);

    if (value < 0)
        *(--ptr) = '-';

    return wrpp(descriptor, ptr, end);
}

/* 'Done' signal handler.
*/

static volatile sig_atomic_t  done = 0;

static void handle_done(int signum)
{
    int saved_errno;

    /* Note: Most commonly, we just use
                 done = 1;
             here. In practice, we could also just use
                 done = signum;
             because current POSIXy systems don't have a signal 0.
             The following uses signum if it is nonzero,
             and -1 for (signum == 0).
    */
    done = (signum) ? signum : -1;

    /* Before running functions that affect errno, save it. */
    saved_errno = errno;

    wrs(STDERR_FILENO, "handle_done(): Caught signal ");
    wri(STDERR_FILENO, signum);
    wrn(STDERR_FILENO, "\n", 1);

    /* Restore errno to its saved value. */
    errno = saved_errno;
}

/* Helper function for installing the signal handler.
*/

static int install_done(int signum)
{
    struct sigaction  act;

    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);

    act.sa_handler = handle_done;
    act.sa_flags = 0;

    if (sigaction(signum, &act, NULL) == -1)
        return errno;

    return 0;
}


int main(void)
{
    int  i = 0;

    if (install_done(SIGINT) ||
        install_done(SIGHUP) ||
        install_done(SIGTERM)) {
        fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    printf("Run\n");
    printf("    kill -HUP %ld\n", (long)getpid());
    printf("    kill -INT %ld\n", (long)getpid());
    printf("    kill -TERM %ld\n", (long)getpid());
    printf("in another terminal window, or press Ctrl-C.\n");
    fflush(stdout);

    while (!done) {
        printf("(%d) ", i++);
        fflush(stdout);
        sleep(1);
    }

    printf("Terminated with done = %ld.\n", (long)done);
    return EXIT_SUCCESS;
}

Compile and run it using eg

gcc -Wall -O2 breakme.c -o breakme
./breakme

Note that the four wr*() functions are async-signal safe functions that output a string ( wrs() ), the specified number of chars ( wrn() ), or a signed (long) integer ( wri() ) to the specified low-level descriptor; here, standard error ( STDERR_FILENO ). You should not mix these with <stdio.h> functions to the same.

(Note that breakme.c uses fprintf(stderr, ..) only if (some of) the signal handlers cannot be installed, and immediately exits (with failure exit status). Sure, we could use three wrs() calls instead, first grabbing the error string into a temporary variable like const char *msg = strerror(errno); since the wr*() functions may modify errno , but I don't think going that far is really sensible. I believe it is enough for the program to try to report the exact problem, then exit as soon as possible. However, I would not use fprintf(stderr,) during the normal operation of the program, to avoid messing up the standard error output.)

Pay particular attention to the install_done() function. It returns 0 if it successfully installs the handle_done function as the specified signal handler, and errno otherwise.

I recommend you experiment with the program. For example, change the done = line to done++; , and the while (!done) to while (done < 3) for example, so that only the third signal caught will cause the program to exit.

Finally, do note that standard POSIX signals like INT are not technically "reliable" : their delivery is not guaranteed. In particular, the signals are not queued, so if you managed to send two INT signals before the first one is delivered, only one would be delivered. The OS/kernel does do its best to ensure the signals are delivered, but a developer should know the technical limitations.

Note that POSIX realtime signals -- SIGRTMIN+0 to SIGRTMAX-0 , inclusive; there being at least 8, or SIGRTMAX-SIGRTMIN+1 >= 8 -- are queued, although they are not perfectly reliable either. They too support passing one int or void pointer worth of payload, via the sigqueue() function. You need to use a signal handler with SA_SIGINFO to catch the payload, or sigwaitinfo() / sigtimedwait() to catch blocked signals in a loop. I believe it would be a fun, while very easy, exercise to modify the above program to detect and show the payload, and a second program (run separately by the user at the same time) to send a signal and an integer as a payload to the specified process; I recommend writing that program as taking exactly three parameters (process ID, signal number, and the payload integer).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM