我想知道在多线程环境中是否有可能/建议的方法来捕获SIGSEGV
信号。我特别感兴趣的是处理由SIGSEGV
等提出的*((int *)0) = 0
。
关于这个主题的一些阅读使我找到了signal()
和sigaction()
,它们安装了一个信号处理程序。虽然在多线程环境中似乎都没有前途。然后我尝试了sigwaitinfo()
,在一个线程中接收信号,先前pthread_sigmask()
调用阻止其他信号。它在信号SIGSEGV
被引发的程度上起作用,使用raise(),在一个线程内或当它被kill -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。
答案 0 :(得分:9)
您的代码不会调用sigaction(2),我相信它应该调用它。另请阅读signal(7)和signal-safety(7)。并且信号操作(通过sa_sigaction
字段应该执行某些操作(特定于机器),其siginfo_t
可以跳过违规机器指令,或者mmap
违规地址,或者调用siglongjmp
否则,当从信号处理程序返回时,您将重新获得SIGSEGV
,因为重启了违规的机器指令。
您无法处理另一个线程中的SIGSEGV
,因为同步信号(例如SIGSEGV
或SIGSYS
)是特定于线程的(请参阅this answer),所以您尝试了什么用sigwaitinfo
实现无法工作。特别是 SIGSEGV
被定向到违规线程。
PS。不再维护(在2019年5月)Ravenbrook MPS垃圾收集器库提供了一个聪明的SIGSEGV
处理示例。另请注意Linux特定的和最近的userfaultfd(2)和signalfd(2)系统调用。
答案 1 :(得分:5)
由故障内存访问引起的SIGSEGV
的信号传递是执行无效访问的线程。每POSIX(XSH 2.4.1):
在生成时,应确定是为过程生成了信号还是为过程中的特定线程生成了信号。应该为导致生成信号的线程生成由可归因于特定线程的某些动作(例如硬件故障)生成的信号。应为流程生成与流程ID或流程组ID或异步事件(如终端活动)相关联生成的信号。
在多线程程序中尝试处理SIGSEGV
的问题在于,虽然传递和信号掩码是线程本地的,但信号处置(即调用的处理程序) )是流程全球性的。换句话说,sigaction
为整个进程设置信号处理程序,而不仅仅是调用线程。这意味着每个尝试设置自己的SIGSEGV
处理程序的多个线程都会破坏彼此的设置。
我可以提出的最佳解决方案是使用SIGSEGV
为sigaction
设置全局信号处理程序,最好使用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_create
和pthread_setspecific
/ { {1}})。严格来说,后者不是异步信号安全的,因此如果非法访问发生在标准库中的非异步信号安全函数内,则从信号处理程序调用它们会调用UB。但是,如果它发生在您自己的代码中,您可以确定信号处理程序没有中断任何非异步信号安全函数,因此这些函数具有明确定义的行为(嗯,模拟整个程序的事实)可能已经有任何生成pthread_getspecific
...)的UB。
答案 2 :(得分:1)
“你为什么要抓住SIGSEGV?抓到它后你会做什么?”
最常见的答案是:退出/中止。但那么,甚至将这个信号传递给一个过程而不是随意终止它的原因是什么?
答案是:因为包括SIGSEGV在内的信号只是异常 - 而且对某些应用程序来说非常重要。将硬件输出设置为“安全模式”或确保一些重要数据在终止过程之前保持一致状态。
通常有两种段错误:由写入或读取操作引起。
由读取操作引起的Segfaults在某些情况下(1)可以非常安全地捕获甚至忽略。失败的写入操作需要更多的关注和努力才能安全处理(数据/内存损坏的风险),但这也是可能的(通过避免在段错误后动态分配内存)。
“关键信号”(传递给特定线程,如SIGFPE或SIGSEGV)的问题是通常程序不“知道”信号的上下文是什么 - 即哪个操作或功能触发了信号。
至少有几种可能的方式来获取这些信息,例如:
(1)F.e。 ESRCH和pthread_kill()的一个着名问题是为一个已经退出的线程发出的:)