【问题标题】:How to make POSIX/Linux signal handling safe?如何使 POSIX/Linux 信号处理安全?
【发布时间】:2016-01-31 17:51:18
【问题描述】:

我刚刚使用 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;
}

【问题讨论】:

  • C 不是 C++ 不是 C!
  • 好吧,对不起。我可能错过了点击并忽略了它,因为我更专注于这个主题。但是,为什么要拒​​绝投票和关闭请求?我知道 C 和 C++ 之间的区别,它们是非常不同的语言,具有一些相似功能的子集,但它们具有不同的 ABI。
  • 如何使其信号安全?不要在信号处理程序中调用非异步信号安全函数,例如 printf()aio_read()
  • @AndrewHenle 好吧,这只是一点,并不是我要问的。谢谢你的提醒。在实际代码中,我会将结果放入队列中,稍后再进行处理。
  • 时间巧合并不意味着因果关系。

标签: c linux signals posix


【解决方案1】:

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

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

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

(语言律师可能会指出,访问线程局部变量,errno 通常是一个,至少在理论上是非异步信号安全的。实际上它是安全的,特别是如果线程局部变量在信号之前已被线程访问。有关 glibc 的更多详细信息,请参阅 libc-alpha 邮件列表中的this thread。就我个人而言,我创建的 pthreads 小于默认堆栈(对于典型工作人员来说,默认值太大线程),并确保线程函数首先读取和写入线程局部变量,避免在实践中出现任何线程局部非异步信号安全问题。这也适用于主线程。简而言之,如果线程- 局部变量已知在信号传递之前已分配和可用,它们的使用是,在实践中,异步信号安全。最后,异步信号安全函数,例如 read()write() 修改 errno internally,不带 any special handling,所以如果它们是 async-s信号安全,恢复errno 也必须如此。)

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

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

如果您绝对需要使用非异步信号安全函数,请阻止这些信号,并创建一个使用 sigwaitinfo() 处理信号的辅助线程。这不一定适用于 Linux 上的线程目标信号,我个人会使用信号处理程序、GCC atomic builtins(幸运的是,大多数 C 编译器都支持它们)来维护事件队列,例如sem_post() 唤醒事件处理线程。这里有几个设计选项,到目前为止,即使是我遇到的奇怪问题也总是可以使用相对简单的方法来解决。

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

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

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

【讨论】:

  • 谢谢。所以,简而言之:只使用异步信号安全功能;并确保si-&gt;si_value.sival_ptr 是我的结构struct aioRequest 的实例,我必须检查si-&gt;si_codesi-&gt;si_pid。我对吗?因此,没有其他方法可以保证 si-&gt;si_value.sival_ptr 是我的结构 struct aioRequest 的实例?
  • 检查si-&gt;si_pid == getpid() &amp;&amp; si-&gt;si_code == SI_ASYNCIO是否足够。如果为真,那么信号是由于 AIO 完成而引发的,并且您应该在 si-&gt;si_value.sival_ptr 中有一个指向您的结构的指针(在创建 AIO 请求时设置)。我认为没有理由进行额外检查。除了异步信号安全功能,考虑使用实时信号(我推荐SIGRTMIN+0)而不是SIGUSR1,并在信号处理程序中保存和恢复errno
  • 好吧。感谢您的回答,以及我没想到的其他有用信息。我明白了,我必须学习很多关于信号的知识。
  • @NominalAnimal 感谢您的回答。您是否知道 access-thread-locals-at-the-start-of-a-thread 方法是否也应该保证非 Linux 平台上的 AS 安全性(我主要对 Cygwin 和 MacOS 感兴趣)?
猜你喜欢
  • 2012-02-18
  • 1970-01-01
  • 1970-01-01
  • 2011-01-28
  • 1970-01-01
  • 1970-01-01
  • 2015-02-15
  • 2018-06-30
  • 1970-01-01
相关资源
最近更新 更多