如何使 POSIX/Linux 信号处理安全?

How to make POSIX/Linux signal handling safe?

我刚刚使用 GNU 异步 I/O 和信号实现了异步文件读取。我使用带有回调处理程序(SIGUSR1 目标)的信号处理结果:

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;
    }
}

我想知道如果有人向我的进程发送 SIGUSR1 会发生什么。显然它不会用我的结构实例填充 siginfo_t *si,因此我读取了垃圾,在幸运的情况下,程序会立即以段错误结束。在糟糕的情况下,一些其他数据会损坏,并且不会检测到错误。我该如何防范?

完整来源:

#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;
}

为了专注于所述问题,我将把我的建议限制在信号处理方面。

考虑使用实时信号(SIGRTMIN+0SIGRTMAX-0,包括在内)而不是 SIGUSR1SIGUSR1 等标准信号未排队,因此您可能会丢失它们(如果在另一个相同信号触发时您已经有一个未决信号),但实时信号已排队,并且更可靠。有关详细信息,请参阅 man 7 signal 中的实时信号部分。

还可以考虑在信号处理程序的开头保存 errno,并在返回前恢复它。否则,在某些极端情况下,信号传递可能 "corrupts" errno (因为您的信号处理程序隐式修改了它),这很难调试 - 简单地说,在某些情况下 errno 你认为是由于系统调用失败而分配的,实际上是由你的信号处理程序重置的。

(语言律师可能会指出访问线程局部变量,errno 通常是一个,是非异步信号安全的,至少在理论上是这样。在实践中它是安全的,特别是如果线程局部变量在信号之前已被线程访问。有关 glibc 的更多详细信息,请参阅 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,因此如果它们是异步信号安全的,则恢复 errno 也必须如此。)

man 7 signal 手册页中所述,以及 Andrew Henle 在对原始问题的评论中提到的那样,只有异步信号安全函数才能在信号处理程序中安全使用。 aio_read()printf() 都不是异步信号安全的。

请注意 read(2) and write(2) 是异步信号安全的,可以与例如一起使用。匿名套接字对,用于将信息包(描述事件)传输到事件处理线程,或将信息打印(调试)到标准输出或标准错误(分别为 STDOUT_FILENOSTDERR_FILENO 描述符) .

如果您绝对需要使用非异步信号安全函数,请阻止这些信号,并创建一个使用 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 e.g. sem_post() 唤醒事件处理线程的辅助线程。这里有几个设计选项,到目前为止,即使是我遇到的古怪问题也总是可以使用相对简单的方法解决。

man 2 sigaction 手册页所述,您可以检查 si->code 以找出信号的原因;对于 AIO 完成,它将是 SI_ASYNCIO,对于 SIGIO 信号,它将是 POLL_IN/POLL_OUT/POLL_MSG/POLL_ERR/POLL_PRI/POLL_HUP , SI_KERNEL 用于其他内核发送的信号,依此类推。如果 si->codeSI_USERSI_QUEUE,您可以检查 si->pid 以找出哪个进程发送了信号。

还建议通过例如清除整个 struct sigaction memset(&sa, 0, sizeof sa); 在设置任何字段之前。 (这是因为某些字段可能是也可能不是联合;将整个结构清除为全零可确保 "safe" 未使用字段的值。)

嗯,我是不是忘记了什么?如果您发现我遗漏了什么,请在评论中告诉我,以便我修复此答案。