20

从问题:

在 C 中使用 setjmp 和 longjmp 是一种好的编程习惯吗?

留下的两条评论说:

“你不能在信号处理程序中抛出异常,但你可以安全地执行 longjmp——只要你知道自己在做什么。——Dietrich Epp 8 月 31 日 19:57 @Dietrich:+1 对你的评论。这是一个鲜为人知且完全被低估的事实。如果不使用信号处理程序中的 longjmp,则无法解决许多问题(讨厌的竞争条件)。阻塞系统调用的异步中断就是典型的例子。

我的印象是内核在遇到异常情况(例如除以 0)时会调用信号处理程序。此外,只有在您专门注册它们时才会调用它们。

这似乎暗示(对我而言)它们不是通过您的正常代码调用的。

继续这个想法......我理解的 setjmp 和 longjmp 用于将堆栈折叠到先前的点和状态。我不明白如何在调用信号处理程序时折叠堆栈,因为它是从内核作为一次性情况而不是从您自己的代码调用的。信号处理程序堆栈中的下一件事是什么!?

4

6 回答 6

23

内核“调用”信号处理程序的方式是中断线程,将信号掩码和处理器状态保存在ucontext_t堆栈上的结构中,刚好超出(下面,在增长实现中)被中断代码的堆栈指针,然后重新开始执行信号处理程序的地址。内核不需要跟踪任何“此进程处于信号处理程序”状态;这完全是创建的新调用框架的结果。

如果被中断的线程在系统调用的中间,内核将退出内核空间代码并调整返回地址以重复系统调用(如果SA_RESTART为信号设置并且系统调用是可重新启动的)或放EINTR在返回代码中(如果不可重新启动)。

应该注意的longjmp是异步信号不安全。这意味着如果信号中断了另一个异步信号不安全函数,则从信号处理程序调用它会调用未定义的行为。但是只要被中断的代码不使用库函数,或者只使用标记为异步信号安全的库函数,longjmp从信号处理程序调用是合法的。

最后,我的回答基于 POSIX,因为问题被标记为unix。如果问题只是关于纯 C,我怀疑答案会有所不同,但是如果没有 POSIX,信号就毫无用处......

于 2011-09-07T13:56:05.217 回答
13

longjmp 不执行正常的堆栈展开。相反,堆栈指针只是从保存的上下文中恢复setjmp

这是一个说明,它如何在代码中使用非异步安全的关键部分来咬你。例如,建议在关键代码期间屏蔽违规信号。

于 2011-09-07T13:56:56.240 回答
1

这并没有回答这样做是否“好”的问题,但这是如何做到的。在我的应用程序中,我在自定义硬件、大页面、共享内存、NUMA 锁内存等之间进行了复杂的交互,并且可能会有看起来分配得当但当你触摸它时(在这种情况下写)的内存,它会在应用程序中间引发 BUS 错误或 SEGV 故障。我想想出一种测试内存地址的方法,以确保共享内存没有被节点锁定到没有足够内存的节点上,这样程序就会提前失败并显示优雅的错误消息。因此,这些信号处理程序仅用于这一段代码(5 个字节的小 memcpy),而不用于在应用程序使用时对其进行救援。我觉得这里很安全。

抱歉,如果这不是“正确的”。请发表评论,我会修复它。我根据提示和一些不起作用的示例代码将它拼凑在一起。

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

sigjmp_buf  JumpBuffer;

void handler(int);

int count = 0;

int main(void)
{
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&(sa.sa_mask));
    sigaddset(&(sa.sa_mask), SIGSEGV);
    sigaction(SIGSEGV, &sa, NULL);

    while (1) {
        int r = sigsetjmp(JumpBuffer,1);
        if (r == 0) {
            printf("Ready for memcpy, count=%d\n",count);
            usleep(1000000);
            char buffer[10];
#if 1
            char* dst = buffer; // this won't do bad
#else
            char* dst = nullptr; // this will cause a segfault
#endif
            memcpy(dst,"12345",5); // trigger seg fault here
            longjmp(JumpBuffer,2);
        }
        else if (r == 1)
        {
            printf("SEGV. count %d\n",count);
        }
        else if (r == 2)
        {
            printf("No segv. count %d\n",count);
        }
    }
    return 0;
}

void handler(int  sig)
{
    count++;
    siglongjmp(JumpBuffer, 1);
}

参考

于 2021-08-25T04:40:21.073 回答
1

值得一读: http: //man7.org/linux/man-pages/man2/sigreturn.2.html关于 Linux 如何处理信号处理程序调用,以及在这种情况下它如何管理信号处理程序退出,我对此的阅读建议从信号处理程序执行 longjmp()(导致没有调用 sigreturn())可能充其量是“未定义的”......还必须考虑 setjmp() 在哪个线程(以及用户堆栈)上被调用,以及随后在哪个线程(以及用户堆栈)上调用 longjmp() !

于 2019-02-13T23:47:47.927 回答
0

在大多数系统中,信号处理程序都有自己的堆栈,与主堆栈分开。这就是为什么您可以 longjmp 退出处理程序的原因。我认为这不是明智的做法。

于 2011-09-07T13:28:39.727 回答
-2

你不能longjmp用来摆脱信号处理程序。

这样做的原因是setjmp只保存调用约定指定应该保存在普通函数调用上的资源(进程寄存器)等。

当中断发生时,被中断的函数可能有一个更大的状态,它不会被正确恢复longjmp

于 2011-09-07T13:53:36.680 回答