简体   繁体   中英

How to make POSIX/Linux signal handling safe?

I've just implemented asynchronous file reading using GNU Asynchronous I/O and signals. I handle result using signals with callback handler (SIGUSR1 target):

static
void aioSigHandler(int sig, siginfo_t *si, void *ucontext)
{
    struct aioRequest *request = si->si_value.sival_ptr;
    int bytes_read = aio_return(request->aiocbp);

    printf("I/O completion signal received %d: %.*s\n", bytes_read, bytes_read, request->aiocbp->aio_buf);

    // continue reading if whole buffer was filled
    if(bytes_read == BUF_SIZE) {
        request->aiocbp->aio_offset += bytes_read;

        if (aio_read(request->aiocbp) == -1)
            errExit("aio_read");
    } else {
        request->finished = 1;
    }
}

I wonder what could happen if someone sends SIGUSR1 to my process. Obviosly it won't have siginfo_t *si populated with instance of my structure, therefore I read garbage, and in lucky situation the program would immediately end with segfault. In bad scenario some other data get corrupted and the error wouldn't be detected. How can I protect against that?

Complete source:

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <aio.h>
#include <signal.h>
#include <fcntl.h>

#define BUF_SIZE 4          /* Size of buffers for read operations */
#define IO_SIGNAL SIGUSR1   /* Signal used to notify I/O completion */

#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
#define errMsg(msg)  do { perror(msg); } while (0)

static volatile sig_atomic_t gotSIGQUIT = 0;

struct aioRequest {
    int           finished;
    struct aiocb *aiocbp;
};

static void                 aioSigHandler(int sig, siginfo_t *si, void *ucontext);
static const char *         aioStatusToString(int status);
static struct aioRequest *  aioReadingStart(const char *filename);
static void                 quitHandler(int sig);

int
main(int argc, char *argv[]) {

    if (argc < 2) {
        fprintf(stderr, "Usage: %s <pathname>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    struct sigaction sa;
    sa.sa_flags = SA_RESTART;
    sigemptyset(&sa.sa_mask);

    sa.sa_handler = quitHandler;
    if (sigaction(SIGQUIT, &sa, NULL) == -1)
        errExit("sigaction");

    sa.sa_flags = SA_RESTART | SA_SIGINFO;
    sa.sa_sigaction = aioSigHandler;
    if (sigaction(IO_SIGNAL, &sa, NULL) == -1)
        errExit("sigaction");

    struct aioRequest *request = aioReadingStart(argv[1]);

    while (1) {
        sleep(3);       /* Delay between each monitoring step */

        if(request->finished) {
            break;
        }

        int status = aio_error(request->aiocbp);
        if (status != EINPROGRESS) {
            printf("aio_error() for request (descriptor %d): ", request->aiocbp->aio_fildes);
            printf("%s\n",aioStatusToString(status));
            break;
        }

        if (gotSIGQUIT) {
            /* On receipt of SIGQUIT, attempt to cancel I/O requests,
             * and display status returned
             * from the cancellation request */
            printf("got SIGQUIT; canceling I/O request: ");

            int s = aio_cancel(request->aiocbp->aio_fildes, request->aiocbp);
            if (s == AIO_CANCELED)
                printf("I/O canceled\n");
            else if (s == AIO_NOTCANCELED)
                printf("I/O not canceled\n");
            else if (s == AIO_ALLDONE)
                printf("I/O all done\n");
            else
                errMsg("aio_cancel");

            gotSIGQUIT = 0;
        }
    }

    printf("File reading completed\n");

    exit(EXIT_SUCCESS);
}

static
void aioSigHandler(int sig, siginfo_t *si, void *ucontext)
{
    struct aioRequest *request = si->si_value.sival_ptr;
    int bytes_read = aio_return(request->aiocbp);

    printf("I/O completion signal received %d: %.*s\n", bytes_read, bytes_read, request->aiocbp->aio_buf);

    // continue reading if whole buffer was filled
    if(bytes_read == BUF_SIZE) {
        request->aiocbp->aio_offset += bytes_read;

        if (aio_read(request->aiocbp) == -1)
            errExit("aio_read");
    } else {
        request->finished = 1;
    }
}

static
const char * aioStatusToString(int status) {
    switch (status) {
        case 0:
            return "I/O succeeded\n";
        case EINPROGRESS:
            return "In progress\n";
        case ECANCELED:
            return "Canceled\n";
        default:
            errMsg("aio_error");
            return 0;
    }
}

static
struct aioRequest * aioReadingStart(const char *filename) {
    struct aioRequest *request = malloc(sizeof(struct aioRequest));
    struct aiocb *aiocbInstance = malloc(sizeof(struct aiocb));

    if (request == NULL || aiocbInstance == NULL)
        errExit("malloc");

    request->finished = 0;
    request->aiocbp = aiocbInstance;

    request->aiocbp->aio_fildes = open(filename, O_RDONLY);
    if (request->aiocbp->aio_fildes == -1)
        errExit("open");
    printf("opened %s on descriptor %d\n", filename,
           request->aiocbp->aio_fildes);

    request->aiocbp->aio_buf = malloc(BUF_SIZE);
    if (request->aiocbp->aio_buf == NULL)
        errExit("malloc");

    request->aiocbp->aio_nbytes = BUF_SIZE;
    request->aiocbp->aio_reqprio = 0;
    request->aiocbp->aio_offset = 0;
    request->aiocbp->aio_sigevent.sigev_notify = SIGEV_SIGNAL;
    request->aiocbp->aio_sigevent.sigev_signo = IO_SIGNAL;
    request->aiocbp->aio_sigevent.sigev_value.sival_ptr = request;

    if (aio_read(request->aiocbp) == -1)
        errExit("aio_read");

    return request;
}

static void
quitHandler(int sig) {
    gotSIGQUIT = 1;
}

To focus on the stated question, I shall limit my suggestions to the signal handling aspect.

Consider using a realtime signal ( SIGRTMIN+0 to SIGRTMAX-0 , inclusive) instead of SIGUSR1 . Standard signals such as SIGUSR1 are not queued, so you may lose them (if you have one already pending when another same signal triggers), but realtime signals are queued, and much more reliable. See Real-time signals section in man 7 signal for details.

Also consider saving errno at the beginning of your signal handler, and restoring it before returning. Otherwise, it is possible that in some corner cases the signal delivery "corrupts" errno (because your signal handler modifies it implicitly), which is very hard to debug -- simply put, in some cases the errno you think was assigned due to a failed syscall, was actually reset by your signal handler.

(Language-lawyers might point out that accessing thread-local variables, errno typically being one, is non-async-signal safe, at least in theory. In practice it is safe, especially if the thread-local variables have been accessed by the thread prior to the signal. For further details regarding glibc, see this thread at the libc-alpha mailing list. Personally, I create my pthreads with smaller than default stacks (the default being way too large for typical worker threads), and ensure the thread function reads and writes thread-local variables as the first thing, avoiding any thread-local non-async-signal-safe issues in practice. This also applies to the main thread. In short, if the thread-local variables are known to be allocated and available prior to the signal delivery, their use is, in practice , async-signal-safe. Finally, async-signal-safe functions such as read() and write() do modify errno internally , without any special handling , so if they are async-signal-safe, restoring errno has to be too.)

As described in the man 7 signal man page, and mentioned by Andrew Henle in a comment to the original question, only async-signal-safe functions are safe to use in a signal handler. Neither aio_read() nor printf() are async-signal-safe.

Note that read(2) and write(2) are async-signal-safe, and can be used with eg. an anonymous socket pair to transfer an information package (describing the event) to an event-processing thread, or to print (debugging) information to standard output or standard error ( STDOUT_FILENO and STDERR_FILENO descriptors, respectively).

If you absolutely need to use non-async-signal-safe functions, block those signals, and create a helper thread that uses sigwaitinfo() to handle the signals. This won't necessarily work for thread-targeted signals on Linux, and I personally would use a signal handler, GCC atomic builtins (they're supported by most C compilers, fortunately) to maintain a queue of events, and eg sem_post() to wake up the event-processing thread. There are several design options here, and thus far even the oddball problems I've come across have always been solvable using a relatively straightforward approach.

As described in the man 2 sigaction man page, you can examine si->code to find out the reason for the signal; it will be SI_ASYNCIO for AIO completions, POLL_IN / POLL_OUT / POLL_MSG / POLL_ERR / POLL_PRI / POLL_HUP for SIGIO signals, SI_KERNEL for other kernel-sent signals, and so on. If si->code is SI_USER or SI_QUEUE , you can examine si->pid to find out which process sent the signal.

It is also recommended to clear the entire struct sigaction via eg memset(&sa, 0, sizeof sa); prior to setting any of the fields. (This is because some of the fields may or may not be unions; clearing the entire structure to all zeros ensures "safe" values for the unused fields.)

Hmm, did I forget something? If you notice something I missed, please let me know in the comments, so I can fix this answer.

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