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

26

我们有一个处理许多错误报告方面的库。我被指派将该库移植到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.不能从信号处理程序中抛出异常,因为信号处理程序使用自己的堆栈,所以在其上没有定义处理程序。
2.可以从信号处理程序中抛出异常,只需在堆栈上重建一个虚假帧即可。
3.我们经常这样做。它在平台X上对我有效。
4.以前在gcc中可以使用,但现在似乎不再起作用。尝试使用-fnon-call-exceptions选项,也许会起作用。
该代码在我们的AIX/TRU64/MSVC编译器/环境中按预期工作。但在我们的Linux环境中失败了。

编辑:可能相关 - 从硬件异常处理程序中抛出C ++异常。为什么-fnon-call-exceptions的行为与预期不同?


这篇文章声称它可以工作;虽然它似乎没有解释,但它提到可能需要一些修复。 - Jan Hudec
7个回答

17

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

在可移植性上,您有很大限制,无法在POSIX信号处理程序中执行太多操作。一种常见的策略是,使用sig_atomic_t类型的全局标志,在信号处理程序中将其设置为1,然后可能使用longjmp跳转到适当的执行路径。

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


8
如果必须使用信号处理程序,那么设置标志是最安全的选择。传递信号非常昂贵。内核必须构造一个特殊的堆栈帧,并将各种机器特定的上下文推入其中。您的信号处理程序可以在这个堆栈帧中安全运行,但是不能保证在堆栈的更高层存在什么。实际上这就是为什么您无法抛出异常的原因...编译器无法为此上下文生成异常处理代码。 - David Joyner
3
在某种程度上,安全性不是特别重要的问题。抛出的异常是为了以有意义的方式中止程序。没有尝试重新启动任何操作的意图。 - EvilTeach
2
好的,那么你可能只能使用 longjmp 跳转到崩溃处理器代码。如果这段代码通常在 catch 块中,你可以将其提取为一个具有 C 链接的函数。 - David Joyner
4
我猜OP知道“信号与C++异常完全不同”,因此他问如何将一个转换成另一个。感谢你指出显而易见的事实。好奇这样的答案如何值得点赞。 - user678269
3
这仍然没有解释为什么从信号处理程序中使用throw无效。信号处理程序确实在正常进程堆栈上执行,有一些跳板,并且调试器可以从中提取有效的回溯信息。因此,应该解释为什么无法使用反卷机。 - Jan Hudec
显示剩余4条评论

12

这段代码演示了一种将异常抛出操作从信号处理器移动到代码中的技术。感谢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;
}

9
setjmplongjmp与异常和RAII(ctors/dtors)不兼容。 :( 这可能会导致资源泄漏。 - Petr Skocik
@PSkocik - 是的,我知道。没关系。我们基本上想要一条记录消息和一个应用程序中止。 - EvilTeach
1
这不应该是 sigsetjmpsiglongjmp 吗?据我所知,它们与 setjmplongjmp 相同,只是它们在某种程度上更适合与信号处理程序一起使用。 - Nick Matteo
是的,我也这么认为。 - EvilTeach

6

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


3
@EvilTeach,尽管如此,我同意Bastien Leonard的观点。在sigwait上等待一个单独的线程,可以为处理信号提供最大的灵活性。否则,你基本上只能使用全局标志和longjmp,这不是很美观。 - Charles Salvia
@Charles,请将您的longjmp建议作为答案提供。 - EvilTeach

4
在信号处理函数中抛出异常可能不是一个好主意,因为栈的设置可能与函数调用不同,因此从信号处理程序进行展开可能不能按预期工作。
必须注意C ++ ABI使用的任何寄存器都被信号处理机制保存并重新使用。

3
谷歌g++选项
-fnon-call-exceptions

这基本上是你想要的。

我认为这是由于苹果对他们的操作系统施加压力而开发的。

我不确定它在LINUX上有多少支持。

我也不确定是否可以捕获SIGINT - 但所有CPU触发的信号(即异常)都可以被捕获。

需要这个功能的程序员(并不关心意识形态)应该向LINUX开发社区施加一些压力,以便它将来也能得到在LINUX上的支持 - 在在Windows上支持了近20年之后。


2
这在树莓派上不起作用。施加压力于LINUX开发者社区 - Russell Hankins
MSVC 的等效物是什么? - Peter Quiring

2
这里有一个潜在的解决方案。它可能很难实现,至少需要根据CPU架构和操作系统以及/或C库组合重新实现一部分:
在信号处理程序中,堆栈包含被中断代码的所有寄存器的保存副本。您可以通过操作它来修改程序状态,一旦信号处理程序退出,您就需要在处理程序中执行以下操作:
1)将堆栈的底部部分向下移动(当前堆栈帧,内核保存的CPU状态,任何需要返回到内核的处理程序所需内容)。
2)在堆栈中间的空闲空间中,发明一个新的堆栈帧,就好像某个“异常调用”函数在引发信号时正在执行。此框架应按照与中断代码正常调用此函数相同的方式布置。
3)修改保存的CPU状态的PC,使其指向此“异常调用”函数。
4)退出信号处理程序。
信号处理程序将返回到内核。内核将返回到这个新的堆栈帧(“异常调用”函数),而不是原始代码。此“异常调用”函数应该只是引发您想引发的任何异常。
这里可能有一些细节;例如:
1)“异常调用”函数可能需要将它通常不会保存的一堆寄存器保存到堆栈上;即中断代码可能正在使用的所有被调用者保存的寄存器。您可能需要编写(部分?)汇编语言来协助此处。也许步骤2可以作为设置堆栈帧的一部分保存寄存器。
2)信号处理程序正在操作堆栈。这将使编译器生成的代码混乱不堪。您可能需要使用汇编语言编写异常处理程序(或仅是它调用的某个函数,这将需要移动更多的堆栈帧)才能使其正常工作。
3)您可能需要手动生成一些C ++异常处理程序展开信息,以便C ++异常处理代码知道如何解开此“异常调用”函数的堆栈。如果您可以在C ++中编写该函数,则可能不需要。如果您不能,则几乎肯定需要。
4)我可能忽略了各种恶心的细节 :-)

很棒的想法。致命的是,在Linux中,当信号发生时,堆栈会移动,因此您不会在帧上拥有您期望的内容。 - EvilTeach
1
保存在信号处理程序堆栈上的CPU状态信息必须至少包括指向原始用户空间堆栈的指针,以便内核可以恢复该堆栈指针。信号处理程序仍然可以找到它,并且然后操作原始堆栈。 - Stephen Warren

2

在至少Ubuntu 16.04 x86-64中,从信号处理程序中抛出似乎可以很好地工作。我没有研究过这是否是设计上的(即保证工作而不是某种意外工作)。我使用 g++ -o sig-throw sig-throw.cpp 编译了下面的程序:

Original Answer翻译成“最初的回答”。

#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 才能使其第二次运行。 - sercxjo
1
是的,这是正确的。然而,如果设置了另一个SIGALRM,那么信号处理程序可能会被中断。这可能是您想要的,也可能不是。另一种选择是在异常处理程序中使用sigprocmask()来取消阻塞SIGALRM。 - Stephen Warren
如果你将 sleep 改为 while(true);,这段代码可能应该与 OP 代码相同。也就是说,它以未捕获的异常终止。至少对我来说,只有使用 sleep 才能正常工作。 - Askold Ilvento

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接