Search code examples
csignals

Why can't I update a global sig_atomic_t in this handler?


I have the below C code:

// my_repro.c

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

volatile sig_atomic_t counter = 1;

void handler(int sig) {
    counter = 15;
    return;
}

int main(void) {
    pid_t pid;
    printf("%d\n", counter);
    fflush(stdout);

    signal(SIGINT, handler);
    if ((pid = fork()) == 0) {
        while(1) {};
    }

    kill(pid, SIGINT);
    printf("%d\n", ++counter);
    fflush(stdout);
}

I compile with: gcc -o my_repro.o my_repro.c

and run with: ./my_repro.c

I'm on an x86-64 Mac.

Why does it output:

1
2

instead of:

1
16

?

I thought the purpose of using volatile sig_atomic_t for global variables was so that signal handlers could mutate the state of those global variables.


Solution

  • The sig_atomic_t needs to be stored in a shared memory segment in order for it to be shared between the parent and child process. Some careful synchronization is required before the parent sends the signal to the child. The example code below blocks the signal before forking the child process, and having the child process call sigsuspend to unblock the signal and wait for it. Some synchronization is also required after the parent sends the signal to the child. The example code below simply lets the child process exit gracefully after catching a signal, and the parent waits for the child process to terminate before it uses the shared sig_atomic_t again. A more sophisticated synchronization mechanism could be used if it is desired to keep the child process running.

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <errno.h>
    #include <signal.h>
    #include <unistd.h>
    #include <sys/shm.h>
    #include <sys/wait.h>
    
    volatile sig_atomic_t *counterp;
    
    void handler(int sig) {
        *counterp = 15;
    }
    
    int main(void) {
        enum { MYSIG = SIGUSR1 };    /* Choose the signal to use here. */
        int shmid;      /* shared memory identifier */
        struct shared { /* layout of shared memory */
            sig_atomic_t counter;
        };
        struct shared *shm;     /* pointer to attached shared memory segment */
        static const struct sigaction action = {
            .sa_handler = handler,
        };
        sigset_t blocked;
        sigset_t oldset;
        pid_t pid;
        pid_t wpid;
        int wstatus;
        int rc;
    
        /* Create a shared memory segment. */
        shmid = shmget(IPC_PRIVATE, sizeof *shm, IPC_CREAT | 0666);
        if (shmid < 0){
            perror("an error in shmget");
            exit(EXIT_FAILURE);
        }
    
        /*
         * Attach shared memory segment to process address space.
         *
         * The attachment will be inherited by child processes.
         */
        shm = shmat(shmid, NULL, 0);
        if(shm == (void *)-1){
            perror("an error in shmat");
            exit(EXIT_FAILURE);
        }
    
        /*
         * Point to the sig_atomic_t in the shared memory segment and set a value.
         */
        counterp = &shm->counter;
        *counterp = 1;
    
        printf("%d\n", (int)*counterp);
        fflush(stdout);
    
        /* Block the MYSIG signal before forking. */
        rc = sigemptyset(&blocked);
        if (rc == -1) {
            perror("an error in sigemptyset");
            exit(EXIT_FAILURE);
        }
        rc = sigaddset(&blocked, MYSIG);
        if (rc == -1) {
            perror("an error in sigaddset");
            return EXIT_FAILURE;
        }
        rc = sigprocmask(SIG_BLOCK, &blocked, &oldset);
        if (rc == -1) {
            perror("an error in sigprocmask");
            exit(EXIT_FAILURE);
        }
        
        pid = fork();
        if (pid == -1) {
            perror("an error in fork");
            exit(EXIT_FAILURE);
        } else if (pid == 0) {
            /* Child process.  Set signal handler. */
            rc = sigaction(MYSIG, &action, NULL);
            if (rc == -1) {
                perror("an error in sigaction");
                exit(EXIT_FAILURE);
            }
            /* Unblock and wait for signal. */
            sigsuspend(&oldset);
            /* sigsuspend always returns -1 and normally sets errno = EINTR. */
            if (errno != EINTR) {
                perror("an error in sigsuspend");
                exit(EXIT_FAILURE);
            }
            exit(EXIT_SUCCESS);
        } else {
            /* Parent process.  Unblock signal. */
            rc = sigprocmask(SIG_SETMASK, &oldset, NULL);
            if (rc == -1) {
                perror("an error in sigprocmask");
                exit(EXIT_FAILURE);
            }
            /* Send a signal to child process. */
            rc = kill(pid, MYSIG);
            if (rc == -1) {
                perror("an error in kill");
                exit(EXIT_FAILURE);
            }
            /* Wait for child process to terminate. */
            wpid = wait(&wstatus);
            if (wpid == -1) {
                perror("an error in wait");
                exit(EXIT_FAILURE);
            }
            if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) != EXIT_SUCCESS) {
                fprintf(stderr, "child exited with non-zero exit status %d\n",
                        WEXITSTATUS(wstatus));
                exit(EXIT_FAILURE);
            }
            if (WIFSIGNALED(wstatus)) {
                fprintf(stderr, "child terminated by %s\n",
                        strsignal(WTERMSIG(wstatus)));
                exit(EXIT_FAILURE);
            }
            printf("%d\n", (int)++*counterp);
            fflush(stdout);
            exit(EXIT_SUCCESS);
        }
        return EXIT_SUCCESS;
    }