【问题标题】:Throwing an exception from within a signal handler从信号处理程序中抛出异常
【发布时间】:2009-11-11 20:53:18
【问题描述】:

我们有一个处理错误报告许多方面的库。我的任务是将此库移植到 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 的问题似乎相关。 我发现了一些相关的谷歌页面。 “智慧”似乎可以归结为。

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

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


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

【问题讨论】:

  • This article 声称它有效;它提到可能需要一些修复,但它似乎没有解释。

标签: c++ exception signals


【解决方案1】:

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

您可以在 POSIX 信号处理程序中进行可移植的操作非常有限。一个常见的策略是有一个sig_atomic_t 类型的全局标志,它将在信号处理程序中设置为1,然后可能将longjmp 设置为适当的执行路径。

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

【讨论】:

  • 设置标志是最安全的事情。也就是说,如果您必须使用信号处理程序。信号的传递非常昂贵。内核必须构造一个特殊的堆栈帧并将各种机器特定的上下文推送到它上面。您的信号处理程序可以在此帧中安全运行,但不能保证堆栈中存在什么。这就是为什么你不能抛出...编译器无法为此上下文生成异常处理代码。
  • 在某种程度上,安全问题不大。要抛出的异常专门用于以有意义的方式中止程序。无意尝试重新启动任何操作。
  • 好的,那么您可能会被longjmp 卡在崩溃处理程序代码中。如果此代码通常位于 catch 块中,您可以将其分解为具有 C 链接的函数。
  • @Charles:又一个小问题。信号不仅仅是一个 POSIX 概念,它们也确实出现在 ISO C 中(它有一个较短的 signal.h),并且在 C++ 中进行了扩展,因为它是规范中作为 引入的 C 头文件之一。跨度>
  • 我猜 OP 知道“信号与 C++ 异常完全不同”。 - 因此他关于如何将一个转换为另一个的问题。谢谢你说的很明显。好奇这样的答案如何值得投票。
【解决方案2】:

此代码演示了一种将异常抛出从信号处理程序移到代码中的技术。我感谢查尔斯的想法。

#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;
}

【讨论】:

  • setjmplongjmp 与异常和 RAII (ctors/dtors) 不兼容。 :( 你可能会因此而导致资源泄漏。
  • @PSkocik - 是的。我知道。没关系。我们基本上想要某种记录的消息和应用程序中止。
  • 不应该是sigsetjmpsiglongjmp吗?据我所知,它们与setjmplongjmp 相同,只是它们应该更好地与信号处理程序一起使用。
  • 是的。我想是的。
【解决方案3】:

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

【讨论】:

  • @EvilTeach,不过我同意 Bastien Leonard 的观点。在sigwait 上有一个单独的线程等待在处理信号时为您提供最大的灵活性。否则,您基本上是在考虑使用全局标志和 longjmp,这不是很漂亮。
  • @Charles 弹出您的 longjmp 建议作为答案
【解决方案4】:

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

必须注意任何由 C++ ABI 使用的寄存器,这些寄存器由信号处理机制保存和重用。

【讨论】:

    【解决方案5】:

    谷歌 g++ 选项

    -fnon-call-exceptions
    

    这基本上就是你想要的。 我认为这是由于苹果对其操作系统的压力而开发的。 我不确定它在 LINUX 上的支持程度。 而且我不确定是否可以捕获 SIGINT -- 但所有 CPU 触发信号(aeh 异常)都可以被捕获。 需要此功能(并且不关心意识形态)的编码人员应该对 LINUX 开发人员社区造成一些压力,以便有一天它也会在 LINUX 上得到支持——在 Windows 上支持了将近 20 年之后。

    【讨论】:

    • 这在 Raspberry Pi 上不起作用。 **给 LINUX 开发者社区带来压力**
    • 什么相当于 MSVC?
    【解决方案6】:

    这是一个潜在的解决方案。实现起来可能相当复杂,当然至少有一部分需要根据 CPU 架构和操作系统和/或 C 库组合重新实现:

    在信号处理程序中,堆栈包含被中断代码的所有寄存器的保存副本。一旦信号处理程序退出,您可以操纵它来修改程序状态。你想在处理程序中做这样的事情:

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

    2) 在堆栈中间的空闲空间中,创建一个新的堆栈帧,就好像在引发信号时已经执行了某个“异常调用”函数。这个框架的布局应该与被中断的代码以正常方式调用这个函数的方式完全相同。

    3) 修改保存的CPU状态的PC指向这个“异常调用”函数。

    4) 退出信号处理程序。

    信号处理程序将返回内核。内核将返回到这个新的堆栈帧(“异常调用”函数)而不是原始代码。这个“异常调用”函数应该简单地引发你想要引发的任何异常。

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

    1) “异常调用”函数可能需要将一堆寄存器保存到通常不会的堆栈中;即被中断代码可能一直在使用的所有被调用者保存的寄存器。您可能需要在汇编中编写(部分?)“异常调用”函数来帮助这里。也许上面的第 2 步可以将寄存器保存为设置堆栈帧的一部分。

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

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

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

    【讨论】:

    • 很酷的想法。杀手锏是,至少在 Linux 中,当信号发生时堆栈会被移动,所以帧上没有你期望的东西。
    • 保存在信号处理程序堆栈上的 CPU 状态信息必须至少包含一个指向原始用户空间堆栈的指针,以便内核可以恢复该堆栈指针。信号处理程序仍然可以找到它,然后去操作原始堆栈。
    【解决方案7】:

    至少在 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;
    #if 0
        // To ensure SIGALRM doesn't remain blocked once the signal handler raises
        // an exception, either enable the following, or add enable the sigprocmask
        // logic in the exception handler below.
        sa.sa_flags = SA_NODEFER;
    #endif
        sigaction(SIGALRM, &sa, NULL);
    
        alarm(3);
    
        try {
            printf("Sleeping...\n");
            sleep(10);
            printf("Awoke\n"); // syscall interrupted
        }
        catch (...) {
            printf("Exception!\n");
    #if 1
            // To ensure SIGALRM doesn't remain blocked once the signal handler
            // raises an exception, either enable the following, or add enable
            // SA_NODEFER when registering the signal handler.
            sigset_t sigs_alarm;
            sigemptyset(&sigs_alarm);
            sigaddset(&sigs_alarm, SIGALRM);
            sigprocmask(SIG_UNBLOCK, &sigs_alarm, NULL);
    #endif
        }
    
        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
    

    【讨论】:

    • 需要添加 sa.sa_flags = SA_NODEFER 才能再次运行
    • 是的,这是真的。但是,如果设置了一个 SIGALRM,这可能会导致信号处理程序被另一个 SIGALRM 中断。这可能是也可能不是您想要的。另一种方法是使用 sigprocmask() 在异常处理程序中解除阻塞 SIGALRM。
    猜你喜欢
    • 1970-01-01
    • 2011-12-12
    • 2011-09-25
    • 1970-01-01
    • 1970-01-01
    • 2014-08-07
    • 2011-08-17
    • 2013-07-24
    • 1970-01-01
    相关资源
    最近更新 更多