有人有非异步安全信号处理程序死锁的示例吗

时间:2019-03-12 18:49:29

标签: c signals deadlock

首先让我作为序言,我理解为什么非重入函数可能会导致信号处理程序中出现死锁,但是无论我怎么努力,我实际上都无法触发该问题。

我有我的第一个程序运行1024个malloc和每个信号printfs,还有其他几个程序每个程序有2个线程运行,并在开始运行信号半小时后仍发出死锁。

>

我正在使用gcc(Ubuntu 4.8.4-2ubuntu1〜14.04.4)4.8.4在64位Ubuntu 14.04.5 LTS(Trusty)上编译并运行这些程序。

第一个程序(应该死锁的程序)是:

// victim.c
#include  <signal.h>
#include  <unistd.h>
#include  <stdlib.h>
#include  <string.h>
#include  <stdio.h>
// global arr to put our malloc results to avoid
// compiler doing any funny business and optimizing
// away the malloc calls, not sure if this is really
// actually necessary or not
void *arr[1024];
// sigint handler to do bad stuff in a loop
void inthandler(int sig)
{
    int i = 0;
    for (i = 0; i < 1024; ++i) {
        // some printf
        printf("Signal loop %d\n", i);
        if (arr[i]) free(arr[i]);
        arr[i] = malloc(1024);
    }
}
void main(void)
{
    // clear out our arr
    memset(arr, 0, sizeof(arr));
    // install our sigint handler
    signal(SIGINT, inthandler);
    // loop and wait for signals
    while (1) {}
}

然后用(O0明确表示我们没有优化)进行编译:

gcc ./victim.c -O0 -o victim

然后是“杀手”,即程序发送信号,该信号最终会在受害者中触发死锁,如下所示:

// killer.c
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
// hack to grab pid of victim
static pid_t __grab_victim_pid()
{
    char line[1024] = {0};
    FILE *command = NULL;
    pid_t pid = 0;
    printf("Getting pid of victim...\n");
    do {
        command = popen("pidof victim", "r");
        memset(line, 0, sizeof(line));
        fgets(line, sizeof(line) - 1, command);
        pid = strtoul(line, NULL, 10);
        pclose(command);
    } while (pid == 0);
    printf("Grabbed pid of victim: [%u]\n", pid);
    return pid;
}
static void *__loop_threadfunc(void *param)
{
    pid_t pid = 0;
    size_t i = 0;
    pid = __grab_victim_pid();
    while (1) {
        kill(pid, SIGINT);
    }
    return 0;
}
int main(int argc, char *argv[])
{
    pthread_t thread1;
    pthread_t thread2;
    // Spawn the threads
    if (pthread_create(&thread1, NULL, __loop_threadfunc, NULL) != 0 ||
        pthread_create(&thread2, NULL, __loop_threadfunc, NULL) != 0) {
        fprintf(stderr, "Failed to create a thread\n");
        return 1;
    }
    // join the threads to wait for them
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    return 0;
}

编译:

gcc ./killer.c -O0 -o killer -lpthread

然后,我在一个终端中运行受害者进程,跳到另一终端并运行多个后台杀手进程,受害者进程自然会从接收到的每个信号中派出很多行到stdout,但它似乎从未死锁。 ..

此外,吐出的行始终是按顺序排列的,也就是说,“信号循环%d”消息永远不会被打断,这向我表明,在活动信号处理程序的执行过程中,永远不会传递信号。这似乎与每个人对信号处理程序的说法背道而驰。

我做错什么了吗?我真的很幸运吗?还是我的操作系统针对此问题进行了加固(甚至可能)?

我尝试将strace附加到受害者身上,我发现它总是报告rt_sigreturn,然后将随后的SIGINT配对在一起:

rt_sigreturn()                          = 0
--- SIGINT {si_signo=SIGINT, si_code=SI_USER, si_pid=11564, si_uid=0} ---

我认为“ SIGINT”需要在rt_sigreturn之前(在信号处理程序中存在之前)传递,但似乎从未发生,它出现,就好像进程阻塞了SIGINT,直到当前的信号处理程序退出为止...(那不可能吗?)

在此先感谢您的澄清,

Edit1:我已经在一个受害进程上运行了10个杀手级进程,如果发生任何事情,它将发布结果。

Edit2:这与我在虚拟机上运行这些测试有关吗?

2 个答案:

答案 0 :(得分:2)

由于两个原因,您没有看到任何死锁或其他故障。首先,也是最重要的是,您的程序处于自旋循环中,等待信号被传递。

// loop and wait for signals
while (1) {}

这意味着信号处理程序永远不会有“有趣”的中断。如果您将其更改为以下内容:

while (1) {
  size_t n = rand();
  char *p = malloc(n);
  free(p);
}

然后您就有机会让信号处理程序调用{​​{1}}中断malloc 内部的正常执行流程,这是异步信号处理程序可以执行的一种方法导致死锁。

另一个原因是,在您的系统上,malloc正在安装一个处理程序,该处理程序的执行不能被另一个signal(SIGINT, handler) 中断。 C标准没有说明SIGINT是否这样做,但是大多数现代的Unix都这样做。您可以通过降低到较低级别的signal来获得信号处理程序,可以中断其执行 :将您的sigaction调用替换为

signal

这还将使信号有可能进入struct sigaction sa; sa.sa_handler = inthandler; sa.sa_flags = SA_NODEFER | SA_RESTART; sigemptyset(&sa.sa_mask); sigaction(SIGINT, &sa, 0); 内。

信号是底层Unix API中最令人困惑和困难的方面之一。我鼓励您获得W. Richard Stevens的书Advanced Programming in the Unix Environment的副本,并阅读有关信号处理的章节。这是一本昂贵的书,但您应该可以在当地的公共图书馆索取。

答案 1 :(得分:2)

问题似乎可以归结为最后,关于信号处理程序是否可以在接收到已经处理过的相同信号时被打断。

  

我做错什么了吗?我真的很幸运吗?或者也许是我的   操作系统针对此问题进行了强化(甚至可能)?

     

我尝试将strace附加到受害者身上,我发现它总是   报告rt_sigreturn,然后报告随后配对的SIGINT:

     

rt_sigreturn()= 0   --- SIGINT {si_signo = SIGINT,si_code = SI_USER,si_pid = 11564,si_uid = 0} ---

     

我认为“ SIGINT”需要在   rt_sigreturn(在信号处理程序中存在之前),但永远不会   似乎发生了,似乎该进程正在阻止SIGINT   直到当前的信号处理程序退出...(那不可能是对的   可以吗?)

实际上,很有可能在处理SIGINTSIGINT被阻塞。 Linux的signal(2)手册页在功能描述的开头带有以下警告:

  

signal()的行为在UNIX版本之间有所不同,并且还具有          历史上在不同版本的Linux中有所不同。避免其          用途:改为使用sigaction(2)。

可移植性说明描述了通过signal()安装信号处理功能后程序行为的这些变化:

  • 为处理程序调用信号时,其信号处理将重置为SIG_DFL。并且信号没有被阻塞。这是UNIX signal()的原始行为,也是在System V中实现的。

  • 调用处理程序时,信号的设置保持不变, ,并且在执行处理程序时阻止信号 。这是BSD实现的行为,如果收到信号中断,也会导致某些系统调用重新启动。

您有可能在具有后者的系统上进行测试,因为Mac OS是BSD,并且尽管Linux内核的signal() syscall实现了System V语义,但是GLIBC的signal()包装函数提供了BSD语义。 Windows对信号和信号处理的实现对于您的测试来说太微不足道,因此System V语义的唯一可能来源将是System V的后代,例如Solaris或HP-UX(在这里我不确定它们的行为)。当然,还有其他操作系统,但是我提到的操作系统涵盖了通用计算机已安装的绝大部分。

如果要避免信号在其处理程序运行时被阻塞,请使用sigaction()进行安装,并指定适当的标志。例如,

struct sigaction action = {
    .sa_handler = inthandler,
    .sa_flags = SA_NODEFER
};

int result = sigaction(SIGINT, &action, NULL);