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

Does anybody have an Example of A Non-Async Safe Signal Handler Deadlock

首先让我先说明一下 我理解为什么 不可重入函数可能会导致信号处理程序出现死锁,但是无论我多么努力,我都无法真正触发该问题。

我有我的第一个程序 运行ing 1024 malloc 和 printfs 每个信号,我还有其他几个程序 运行 每个程序 2 个线程在第一个甚至之后触发信号 运行连续半小时我没有看到死锁。

我正在 64 位 Ubuntu 14.04.5 LTS (Trusty) 和 gcc (Ubuntu 4.8.4-2ubuntu1~14.04.4) 4.8 上编译和 运行ning 这些程序.4.

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

// 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" 即程序发送信号,最终应该在受害者中触发死锁如下:

// 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

然后我 运行 一个终端中的受害进程,跳转到另一个终端和 运行 几个后台杀手进程,受害进程自然地从每个终端吐出很多行到标准输出它收到信号,但从未出现死锁...

此外,吐出的行总是有序的,也就是说,"Signal loop %d" 消息似乎从未被打断,这向我表明信号从未在活动信号处理程序的执行过程中传递.这似乎与每个人对信号处理程序的看法背道而驰。

我做错了什么吗?我只是非常幸运吗?或者我的 OS 是否对这个问题更加坚定(甚至可能)?

我尝试将 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 个杀手进程 运行ning,如果发生任何事情,将会 post 结果。

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

I understand why non-reentrant functions may cause a deadlock in a signal handler, however I cannot actually trigger the issue no matter how hard I try.

我不明白为什么您会认为您的特定示例会以死锁告终。以信号为目标的进程在其主循环中没有做任何事情。因此,在其信号处理程序中调用可重入函数是相当安全的。

此外,在 Linux 上,signal 的语义实际上与 BSD 语义相同。这意味着,虽然信号处理程序是 运行,但同一信号的更多实例将被阻止。 "killer" 发出的所有信号都由 "victim" 按顺序处理。

调用非可重入函数的潜在问题比仅仅陷入僵局更隐蔽。例如,如果 malloc 在操作其数据结构的过程中被信号中断,那么在信号处理程序中调用 malloc 就像在损坏的堆上调用它一样。它不一定会导致死锁,您可能只是发现了无法解释的分段违规,或者只是损坏了数据。但是,我要强调的是,这不是您所看到的情况,因为您只在信号处理程序中调用 malloc

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

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

这意味着信号处理程序永远不会中断任何 "interesting"。如果你把它改成这样:

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

然后你的信号处理程序调用 malloc 中断正常的执行流程 malloc 中,这是一种方式异步信号处理程序可能会导致死锁。

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

替换您的 signal 调用
struct sigaction sa;
sa.sa_handler = inthandler;
sa.sa_flags = SA_NODEFER | SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, 0);

这也将使信号到达 malloc 内部的可能性成为可能。

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

问题似乎归结为末尾的一点,关于信号处理程序是否可以在收到它已经在处理的相同信号时被中断。

Am I doing something wrong? Am I just extremely lucky? Or is maybe my OS hardened against this issue (is that even possible)?

I tried attaching strace to the victim and I am seeing that it always reports rt_sigreturn and then a subsequent SIGINT paired together:

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

I would imagine that the "SIGINT" needs to be delivered before the rt_sigreturn (before it exists from the signal handler) but that never seems to happen, it appears as if the process is blocking the SIGINT until the current signal handler has exited... (That can't be right can it?)

事实上,完全 有可能 SIGINT 在处理 SIGINT 时被阻塞。 signal(2) 的 Linux 手册页在函数描述的开头有此警告:

The behavior of signal() varies across UNIX versions, and has also varied historically across different versions of Linux. Avoid its use: use sigaction(2) instead.

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

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

  • 当处理程序被调用时,信号的处理没有改变,并且信号在处理程序执行时被阻塞 。这是 BSD 实现的行为,如果因收到信号而中断,它也会导致某些系统调用重新启动。

您很可能在展示后者的系统上进行测试,因为 Mac OS 是 BSD,尽管 Linux 内核的 signal() 系统调用实现 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);