【问题标题】:About catching the SIGSEGV in multithreaded environment关于在多线程环境中捕获 SIGSEGV
【发布时间】:2013-04-18 17:29:50
【问题描述】:

我想知道在多线程环境中是否有可能/推荐的方法来捕获SIGSEGV 信号。我对处理 SIGSEGV 之类的 *((int *)0) = 0 之类的东西特别感兴趣。

一些关于这个主题的阅读让我找到了signal()sigaction(),它们安装了一个信号处理程序。虽然在多线程环境中似乎都没有希望。然后我尝试了sigwaitinfo(),在一个线程中接收信号,之前的pthread_sigmask() 调用阻止了其他线程上的信号。它在使用 raise()、在线程内或当它通过类似 kill -SIGSEGV 的东西发送到进程时引发信号 SIGSEGV 的范围内起作用;但是,\*((int*)0) = 0 仍然会终止该进程。我的测试程序如下

void block_signal()
{
        sigset_t set;

        sigemptyset(&set);
        sigaddset(&set, SIGSEGV);
        sigprocmask(SIG_BLOCK, &set, NULL);

        if (pthread_sigmask(SIG_BLOCK, &set, NULL)) {
                fprintf(stderr, "pthread_sigmask failed\n");
                exit(EXIT_FAILURE);
        }
    }

void *buggy_thread(void *param)
{
        char *ptr = NULL;
        block_signal();

        printf("Thread %lu created\n", pthread_self());

        // Sleep for some random time
        { ... }

        printf("About to raise from %lu\n", pthread_self());

        // Raise a SIGSEGV
        *ptr = 0;

        pthread_exit(NULL);
}

void *dispatcher(void *param)
{
        sigset_t set;
        siginfo_t info;
        int sig;

        sigemptyset(&set);
        sigaddset(&set, SIGSEGV);

        for (;;) {
                sig = sigwaitinfo(&set, &info);
                if (sig == -1)
                        fprintf(stderr, "sigwaitinfo failed\n");
                else
                        printf("Received signal SIGSEGV from %u\n", info.si_pid);
        }
}

int main()
{
        int i;
        pthread_t tid;
        pthread_t disp_tid;

        block_signal();

        if (pthread_create(&disp_tid, NULL, dispatcher, NULL)) {
                fprintf(stderr, "Cannot create dispatcher\n");
                exit(EXIT_FAILURE);
        }

        for (i = 0; i < 10; ++i) {
                if (pthread_create(&tid, NULL, buggy_thread, NULL) {
                        fprintf(stderr, "Cannot create thread\n");
                        exit(EXIT_FAILURE);
                }
        }

        pause();
}

出乎意料的是,程序因分段错误而死,而不是打印引发者的线程 ID。

【问题讨论】:

  • 为什么要抓SIGSEGV?抓到之后怎么办?

标签: c linux signals


【解决方案1】:

您的代码没有调用sigaction(2),我相信它应该调用它。另请阅读signal(7)signal-safety(7)。并且信号操作(通过sa_sigaction 字段应该使用其siginfo_t 执行某些操作(特定于机器)以跳过有问题的机器指令,或者到mmap 有问题的地址,或者调用siglongjmp,否则从信号返回时处理程序您将再次获得SIGSEGV,因为有问题的机器指令重新启动。

您无法在另一个线程中处理SIGSEGV,因为同步信号(例如SIGSEGVSIGSYS)是特定于线程的(请参阅this answer),因此您尝试使用sigwaitinfo 实现的目标无法工作.特别是 SIGSEGV 被定向到有问题的线程

另请阅读all about Linux signals

PS。不再维护(2019 年 5 月)Ravenbrook MPS 垃圾收集器库提供了一个巧妙的 SIGSEGV 处理示例。还要注意 Linux 特定的和最近的 userfaultfd(2)signalfd(2) 系统调用。

【讨论】:

  • 信号处理不是线程本地的。只有信号掩码,在某些情况下是信号传递,是线程本地的。
  • 感谢您提供丰富的参考资料。但是当我用 'raise(SIGSEGV)' 替换有问题的行 '*ptr = 0' 时,上面的代码就起作用了。不知道为什么 '*ptr = 0' 不会产生相同的结果。
  • 因为像*ptr = 0;这样的分隔违规是线程特定的,所以SIGSEGV被发送到有问题的线程。这和raise(SIGSEGV)不一样
  • 其实不是,raise() 也可以给调用线程发送信号。在多线程程序中 raise(sig) 等价于 pthread_kill(pthread_self(), sig)。请参阅手册页。
【解决方案2】:

由错误的内存访问引起的SIGSEGV 的信号传递到执行无效访问的线程。根据 POSIX (XSH 2.4.1):

在生成时,应确定该信号是为进程还是为进程内的特定线程生成的。由可归因于特定线程的某些动作(例如硬件故障)生成的信号应为导致生成信号的线程生成。应为进程生成与进程 ID 或进程组 ID 或异步事件(例如终端活动)相关联的信号。

尝试在多线程程序中处理 SIGSEGV 的问题在于,虽然传递和信号掩码是线程本地的,但信号 disposition(即要调用的处理程序)是进程全局。换句话说,sigaction 为整个进程设置了一个信号处理程序,而不仅仅是调用线程。这意味着每个尝试设置自己的SIGSEGV 处理程序的多个线程将破坏彼此的设置。

我能提出的最佳解决方案是使用sigactionSIGSEGV 设置一个全局信号处理程序,最好使用SA_SIGINFO,这样您就可以获得有关故障的更多信息,然后为处理程序设置一个线程局部变量具体线程。然后,实际的信号处理程序可以是:

_Thread_local void (*thread_local_sigsegv_handler)(int, siginfo_t *, void *);
static void sigsegv_handler(int sig, siginfo_t *si, void *ctx)
{
    thread_local_sigsegv_handler(sig, si, ctx);
}

请注意,这使用了 C11 线程本地存储。如果您没有可用的,您可以退回到“GNU C”__thread 线程本地存储,或 POSIX 线程特定数据(使用 pthread_key_createpthread_setspecific/pthread_getspecific)。严格来说,后者不是异步信号安全的,因此如果非法访问发生在标准库中的非异步信号安全函数内,则从信号处理程序调用它们会调用 UB。但是,如果它发生在您自己的代码中,您可以确定没有非异步信号安全函数被信号处理程序中断,因此这些函数具有明确定义的行为(嗯,以您的整个程序为模可能已经从它生成SIGSEGV...的任何操作中获得了UB。

【讨论】:

  • 我不太明白拥有 thread_local_sigsegv_handler 的利润是多少。如果我理解正确,这是一个全局函数指针(专门为每个线程分配)。但是如何让引发信号的线程调用该函数呢?
  • 同步生成的信号总是会发生这种情况。请参阅 XSH 2.4.1 信号生成和传递pubs.opengroup.org/onlinepubs/9699919799/functions/…在生成时,应确定信号是为进程生成还是为内部的特定线程生成过程。由可归因于特定线程的某些操作(例如硬件故障)生成的信号应为导致生成信号的线程生成。
  • 根据gnu.org/software/libc/manual/html_node/… pthread_getspecific 是异步信号安全的(pthread_setspecific 不是),因此使用 POSIX 线程特定的数据存储应该没问题(至少使用 glibc)。另一方面,我找不到任何关于声明的变量 __thread(或 C++ 11 中的 thread_local)是否实际上是异步信号安全的信息。任何指向有关此问题的文档的链接将不胜感激。
【解决方案3】:

“你为什么要抓 SIGSEGV ?抓到后你会怎么做?”

最常见的答案是:退出/中止。但是,有什么理由甚至将这个信号传递给一个进程而不是随意终止它呢?

答案是:因为包括 SIGSEGV 在内的信号只是例外 - 对于某些应用程序来说,f.e. 非常重要。将硬件输出设置为“安全模式”或在终止进程之前确保某些重要数据保持一致状态。

一般有2种segfaults:由写或读操作引起。

read 操作引起的段错误在某些情况下可以完全安全地捕获甚至忽略 (1)。失败的写入操作需要更多的关注和努力才能安全处理(数据/内存损坏的风险),但这也是可能的(通过避免在段错误后动态分配内存)。

“关键信号”(传递给特定线程,如 SIGFPE 或 SIGSEGV)的问题是程序通常不“知道”信号的上下文是什么——即,哪个操作或函数已触发信号。

至少有几种可能的方式来获取这些信息,例如:

  1. 每个线程只能执行一类小操作 - 因此,如果它收到信号,则很容易判断发生了什么 -> 终止线程,验证已处理的数据等 -> 安全终止。
  2. 使用 C 异常 - 很少有现成的解决方案,我的是:libcxc

(1) F.e. ESRCH 和 pthread_kill() 的著名问题是针对已经自行退出的线程发出的 :)

【讨论】:

  • "你为什么要抓 SIGSEGV ?抓了之后怎么办?"在其他线程完成其当前工作部分之前推迟整个进程终止(这对于程序的逻辑可能很重要)。我不明白为什么如果一个线程访问了外部内存,那么其他所有线程都必须无条件地同时死掉,特别是如果它不影响除一个线程之外的整个进程的状态。
猜你喜欢
  • 1970-01-01
  • 2022-01-16
  • 2013-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多