Sauron
Sauron

Reputation: 453

Robust graceful shutdown of an application

To ensure that all destructors are properly called if the program is terminated from keyboard (Ctrl+C), the approach with signals are used:

The problem is that SIGINT can arrive between check for exit flag (while (!finish)) and calling read(). In this case, read() will be blocked until the signal is sent once again.

This is a minimal working example:

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

enum { STDIN, STDOUT, STDERR };

static unsigned char finish=0;

static void handleSignal(int signal) {
    finish=1;
}

int main(int argc, char ** e) {
    struct sigaction action;
    memset(&action, 0, sizeof(action));
    action.sa_handler=handleSignal;
    action.sa_flags=0;
    sigaction(SIGINT, &action, NULL);
    
    char buffer[256];
    puts("<<");
    while (!finish) {
        sleep(2);
        ssize_t n=read(STDIN, buffer, sizeof(buffer));
        if (n==0) {
            // End of stream
            finish=1;
        }
        else if (n<0) {
            // Error or interrupt
            if (errno!=EINTR)
                perror("read");
        }
        else {
            // Convert data to hexadecimal format
            for (size_t i=0; i<n; i++)
                printf("%02x", buffer[i]);
        }
    }
    puts(">>\n");
    
    return 0;
}

sleep(2) is added for visibility (a real program may perform some preparational work before reading from file descritor).

If there any way of reliable handling of signals without using non-crossplatform things like signalfd()?

Upvotes: 2

Views: 693

Answers (1)

Nate Eldredge
Nate Eldredge

Reputation: 58473

The pselect(2) system call was invented to solve this exact problem. It's POSIX, so hopefully cross-platform enough for you.

The purpose of pselect is to atomically unblock some signals, wait for I/O as select() does, and reblock them. So your loop can look something like the following pseudocode:

sigprocmask(SIG_BLOCK, {SIGINT});
while (1) {
    if (finish)
        graceful_exit();
    int ret = pselect(1, {STDIN}, ..., { /* empty signal set */});
    if (ret > 0) {
        read(STDIN, buf, size); // will not block
        // process data
        // If you like you can do
        sigprocmask(SIG_UNBLOCK, {SIGINT});
        // work work work
        if (finish)
            graceful_exit();
        // work work work
        sigprocmask(SIG_BLOCK, {SIGINT});
    } else {
        // handle timeout or other errors
    }
}

There is no race here because SIGINT is blocked for the time in between checking the finish flag and the call to pselect, so it cannot be delivered during that window. But the signal is unblocked while pselect is waiting, so if it arrives during that time (or already arrived while it was blocked), pselect will return without further delay. We only call read when pselect has told us it was ready for reading, so it cannot block.

If your program is multithreaded, use pthread_sigmask instead of sigprocmask.

As was noted in comments, you have to make your finish flag volatile, and for best compatibility it should be of type sig_atomic_t.

There is more discussion and another example in the select_tut(2) man page.

Upvotes: 2

Related Questions