15

我是通过 C 在 Unix 中处理信号的新手,我一直在看一些关于它的教程(出于纯粹的兴趣)。

我的问题是,是否可以在处理信号的点之后继续执行程序?

我知道信号处理函数会进行清理,但是本着异常处理的精神(例如在 C++ 中),是否可以以相同的方式处理该信号并使程序继续正常运行?

目前catch进入无限循环(可能退出的一种方法是调用exit(1))。

我的意图是b被分配 1 并让程序优雅地完成(当然,如果可能的话)。

这是我的代码:

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

int a = 5;
int b = 0;

void catch(int sig)
{
    printf("Caught the signal, will handle it now\n");
    b = 1;
}

int main(void)
{
    signal(SIGFPE, catch);

    int c = a / b;

    return 0;
}

此外,由于 C 是程序性的,在违规语句之前声明的信号处理程序如何在后者执行后实际调用?

最后,为了让处理函数正确地进行清理,所有在发生异常时需要清理的变量都需要在函数之前声明,对吧?

如果上述某些内容非常明显,请提前感谢您的回答和道歉。

4

3 回答 3

12

是的,这就是信号处理程序的用途。但是有些信号需要特别处理才能让程序继续运行(例如 SIGSEGV、SIGFPE、……)。

请参阅手册页sigaction

根据 POSIX,进程在忽略不是由 kill(2) 或 raise(3) 生成的 SIGFPE、SIGILL 或 SIGSEGV 信号后,其行为是不确定的。整数除以零有未定义的结果。在某些架构上,它会生成一个 SIGFPE 信号。(同样将最大负整数除以 -1 可能会生成 SIGFPE。)忽略此信号可能会导致无限循环。

现在,你忽略了这个信号,没有做任何事情来阻止它(再次)发生。您需要信号处理程序中的执行上下文并手动修复它,这涉及覆盖一些寄存器。

如果在 sa_flags 中指定了 SA_SIGINFO,则 sa_sigaction(而不是 sa_handler)指定 signum 的信号处理函数。这个函数接收信号编号作为它的第一个参数,一个指向 siginfo_t 的指针作为它的第二个参数,一个指向 ucontext_t(转换为 void *)的指针作为它的第三个参数。(通常,处理函数不使用第三个参数。有关 ucontext_t 的更多信息,请参见 getcontext(2)。)

上下文允许在发生故障时访问寄存器,并且需要更改以允许您的程序继续。请参阅此lkml 帖子。如那里所述,siglongjmp也可能是一种选择。该帖子还提供了一个相当可重用的解决方案来处理错误,而不必将变量设为全局等:

而且因为您自己处理它,所以您可以灵活地处理错误。例如,您可以使故障处理程序跳转到函数中的某个指定点,如下所示:

 __label__ error_handler;   
 __asm__("divl %2"      
         :"=a" (low), "=d" (high)       
         :"g" (divisor), "c" (&&error_handler))     
 ... do normal cases ...

 error_handler:     
     ... check against zero division or overflow, so  whatever you want to ..

然后,您的 SIGFPE 处理程序只需要执行类似的操作

上下文.eip = 上下文.ecx;

于 2013-01-09T11:10:10.073 回答
7

如果您知道自己在做什么,则可以将指令指针设置为指向有问题的指令之后。下面是我的 x86(32 位和 64 位)示例。不要在家里或在实际产品中尝试!!!

#define _GNU_SOURCE /* Bring REG_XXX names from /usr/include/sys/ucontext.h */

#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <ucontext.h>

static void sigaction_segv(int signal, siginfo_t *si, void *arg)
{
    ucontext_t *ctx = (ucontext_t *)arg;

    /* We are on linux x86, the returning IP is stored in RIP (64bit) or EIP (32bit).
       In this example, the length of the offending instruction is 6 bytes.
       So we skip the offender ! */
    #if __WORDSIZE == 64
        printf("Caught SIGSEGV, addr %p, RIP 0x%lx\n", si->si_addr, ctx->uc_mcontext.gregs[REG_RIP]);
        ctx->uc_mcontext.gregs[REG_RIP] += 6;
    #else
        printf("Caught SIGSEGV, addr %p, EIP 0x%x\n", si->si_addr, ctx->uc_mcontext.gregs[REG_EIP]);
        ctx->uc_mcontext.gregs[REG_EIP] += 6;
    #endif
}

int main(void)
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = sigaction_segv;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGSEGV, &sa, NULL);

    /* Generate a seg fault */
    *(int *)NULL = 0;

    printf("Back to normal execution.\n");

    return 0;
}
于 2015-05-11T13:51:37.330 回答
6

一般来说,是的,处理程序返回后继续执行。但是,如果信号是硬件错误(例如浮点异常或分段错误)引起的,则您无法撤消该错误,因此无论如何都会终止您的程序。

换句话说,你必须区分信号和引起信号的事物。信号本身是非常好的和可处理的,但它们并不总是让你修复导致信号的错误

(有些信号是特殊的,比如 ABRT 和 STOP,从某种意义上说,即使你只是用 手动发出这样的信号kill,你仍然不能“阻止它的影响”。当然,甚至根本无法处理 KILL。)

于 2013-01-09T10:50:56.460 回答