从信号处理程序中抛出异常

时间:2009-11-11 20:53:18

标签: c++ exception signals

我们有一个库,可以处理错误报告的许多方面。我的任务是将此库移植到Linux。当我的小测试套件运行时,其中一个测试失败了。测试的简化版本如下所示。

// Compiler: 4.1.1 20070105 RedHat 4.1.1-52
// Output: Terminate called after throwing an instance of 'int' abort

#include <iostream>
#include <csignal>
using namespace std;

void catch_signal(int signalNumber)
{
    signal(SIGINT, SIG_DFL);
    throw(signalNumber);
}

int test_signal()
{
    signal(SIGINT, catch_signal);

    try
    {
        raise(SIGINT);
    }
    catch (int &z)
    {
        cerr << "Caught exception: " << z << endl;
    }
    return 0;
}

int main()
{
    try
    {
        test_signal();
    }
    catch (int &z)
    {
        cerr << "Caught unexpected exception: " << z << endl;
    }
    return 0;
}

我的期望是会显示 Caught exception:消息。实际发生的是程序终止,因为抛出的int似乎没有出现catch处理程序。

关于SO的一些问题似乎有关。 我找到了许多相关的Google网页。 “智慧”似乎归结为。

  1. Ya不能从信号处理程序中抛出异常,导致信号 handler使用自己的堆栈运行,因此没有定义处理程序。
  2. Ya可以从信号处理程序中抛出异常,只需重建一个假的     堆栈上的框架,你很高兴。
  3. 雅,我们一直这样做。它适用于平台X
  4. 雅,曾经可以使用gcc,但似乎没有任何效果 更多。尝试使用 -fnon-call-exceptions 选项,这可能会起作用

    代码在AIX / TRU64 / MSVC编译器/环境中按预期工作。它在我们的Linux环境中失败了。


  5.   我正在寻找有助于解决此问题的建议,因此Linux上的库行为将与我的其他平台或可能实现相同功能的某种类型或解决方法相匹配。
    让程序核心转储信号,不是一个可行的选择。

7 个答案:

答案 0 :(得分:14)

信号完全与C ++异常不同。您不能使用C ++ try / catch块来处理信号。具体来说,信号是POSIX概念,而不是C ++语言概念。信号由内核异步传递给您的应用程序,而C ++异常是由C ++标准定义的同步事件。

在POSIX信号处理程序中可以轻松实现的功能非常有限。一个常见的策略是使用sig_atomic_t类型的全局标志,它将在信号处理程序中设置为1,然后可能longjmp设置为适当的执行路径。

请参阅here以获取有关编写正确信号处理程序的帮助。

答案 1 :(得分:9)

此代码演示了一种将异常抛出信号处理程序的技术。感谢Charles的想法。

#include <iostream>
#include <csignal>
#include <csetjmp>

using namespace std;

jmp_buf gBuffer;        // A buffer to hold info on where to jump to

void catch_signal(int signalNumber)
{
    //signal(SIGINT, SIG_DFL);          // Switch to default handling
    signal(SIGINT, catch_signal);       // Reactivate this handler.

    longjmp             // Jump back into the normal flow of the program
    (
        gBuffer,        // using this context to say where to jump to
        signalNumber    // and passing back the value of the signal.
    );
}


int test_signal()
{
    signal(SIGINT, catch_signal);

    try
    {
        int sig;
        if ((sig = setjmp(gBuffer)) == 0) 
        {
            cout << "before raise\n";
            raise(SIGINT);
            cout << "after raise\n";

        }
        else
        {
            // This path implies that a signal was thrown, and
            // that the setjmp function returned the signal
            // which puts use at this point.

            // Now that we are out of the signal handler it is
            // normally safe to throw what ever sort of exception we want.
            throw(sig);
        }
    }
    catch (int &z)
    {
        cerr << "Caught exception: " << z << endl;
    }

    return 0;
}

int main()
{
    try
    {
        test_signal();
    }
    catch (int &z)
    {
        cerr << "Caught unexpected exception: " << z << endl;
    }
    return 0;
}

答案 2 :(得分:5)

我将屏蔽每个线程中的所有信号,除了那些等待sigwait ()信号的信号。 该线程可以无限制地处理信号,例如,抛出异常或使用其他通信机制。

答案 3 :(得分:4)

抛出信号处理程序可能不是一个好主意,因为堆栈没有必要以与函数调用相同的方式设置,因此从信号处理程序展开可能无法按预期工作。

必须对C ++ ABI使用的信号处理机制保存和重用的寄存器进行重要说明。

答案 4 :(得分:3)

google g ++选项

-fnon-call-exceptions

这基本上就是你想要的。 我认为这是由于苹果对其操作系统的压力而产生的。 我不确定它对LINUX的支持程度如何。 而且我不确定是否能抓住SIGINT - 但是可以捕获所有CPU触发的信号(例外情况)。 需要此功能的编码器(并不关心意识形态)应该给LINUX开发人员社区带来一些压力,以便有一天也能在LINUX上支持它 - 自从近二十年来在Windows上得到支持之后。

答案 5 :(得分:1)

这是一个潜在的解决方案。它的实现可能相当复杂,并且肯定至少有一部分需要根据CPU架构和OS和/或C库的组合重新实现:

在信号处理程序中,堆栈包含已中断代码的所有寄存器的已保存副本。一旦信号处理程序退出,您就可以操纵它来修改程序状态。您想在处理程序中执行以下操作:

1)在内存中向下移动堆栈的底部(当前堆栈帧,内核保存的CPU状态,以及处理程序返回内核所需的所有内容)。

2)在堆栈中间的备用空间中,发明一个新的堆栈帧,好像在发出信号时已经执行了某些“异常调用”功能。该帧的布局应与被中断的代码以正常方式调用此函数的方式完全相同。

3)修改保存的CPU状态的PC,使其指向此“异常调用”功能。

4)退出信号处理程序。

信号处理程序将返回内核。内核将返回到新的堆栈框架(“异常调用”功能),而不是原始代码。此“异常调用”功能应简单地引发您想要引发的任何异常。

这里可能有一些细节;例如:

1)“异常调用”功能可能需要将一堆寄存器保存到堆栈中,而这通常是不希望的;即该中断代码可能一直在使用的所有保存被调用者的寄存器。您可能需要在汇编中编写(部分?)“异常调用”功能以在此处提供帮助。也许上面的步骤2可以将寄存器保存为设置堆栈框架的一部分。

2)信号处理程序正在处理堆栈。这将使编译器生成的代码非常混乱。您可能必须在汇编中编写异常处理程序(或者也许只是它调用的某些函数,这需要移动更多的堆栈框架)才能完成这项工作。

3)您可能需要手动生成一些C ++异常处理程序展开信息,以便C ++异常处理代码知道如何从该“异常调用”功能中展开堆栈。如果您可以用C ++编写该函数,则可能无法。如果不能,那么几乎可以肯定。

4)可能是我忽略的各种令人讨厌的细节:-)

答案 6 :(得分:0)

至少在Ubuntu 16.04 x86-64中,抛弃信号处理程序似乎可以正常工作。无论这是设计使然(即保证能正常工作,而不是偶然工作),我都没有进行过研究。我使用g++ -o sig-throw sig-throw.cpp编译了以下程序:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

extern "C" void handler(int sig, siginfo_t *info, void *xxx)
{
    throw "Foo";
}

int main(int argc, char **argv)
{
    struct sigaction sa = {0};

    sa.sa_sigaction = handler;
    sigaction(SIGALRM, &sa, NULL);

    alarm(3);

    try {
        printf("Sleeping...\n");
        sleep(10);
        printf("Awoke\n"); // syscall interrupted
    }
    catch (...) {
        printf("Exception!\n");
    }

    return 0;
}

它正在运行:

[swarren@swarren-lx1 sig-throw]$ ./sig-throw 
Sleeping...
Exception!

供参考:

[swarren@swarren-lx1 sig-throw]$ lsb_release -a
...
Description:    Ubuntu 16.04.6 LTS
...

[swarren@swarren-lx1 sig-throw]$ dpkg -l libc6
...
ii  libc6:amd64  2.23-0ubuntu11  amd64  GNU C Library: Shared libraries

[swarren@swarren-lx1 sig-throw]$ g++ --version
g++ (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609