【Linux信号】三:信号的捕捉

2024-08-08 17:17:01 浏览数 (2)

信号捕捉主要是为了防止进程意外结束,并得到异常信息,捕捉信号后可以执行我们想要的动作。

1. 信号捕捉函数

1.1 signal函数

  • 包含头文件及函数原型
代码语言:javascript复制
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);
  • 函数功能 The behavior of signal() varies across Unix versions, and has also varied historically across different versions of Linux. Avoid its use: use sigaction(2) instead. See Portability below. 注册一个信号捕捉函数,该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。
  • 函数参数
    • signum:要捕捉的信号编号。
    • handler:捕捉函数,它是一个回调函数,当产生信号signum的时候,执行信号处理函数handler。
  • 函数返回值 signal() returns the previous value of the signal handler, or SIG_ERR on error.

1.2 sigaction函数

  • 包含头文件及函数原型
代码语言:javascript复制
#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • 函数功能 The sigaction() system call is used to change the action taken by a process on receipt of a specific signal. (See signal(7) for an overview of signals.) 注册捕捉函数,所谓的捕捉信号就是指,信号发生时执行什么动作。
  • 函数参数
    • signum:要捕捉的信号编号。
    • act:传入参数(const修饰,不可修改),新的处理方式。
    • oldact:传出参数,旧的处理方式。(用于恢复原始动作)
      • struct sigaction 结构体 struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
    • 结构体解析

结构体成员

作用

sa_handler

是一个函数指针,指定信号捕捉后的处理函数名(即注册函数),也可赋值为SIG_IGN表忽略或SIG_DFL表执行默认动作。实际上是一个回调函数。

sa_mask

调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。实际上就是执行捕捉函数期间临时屏蔽的信号集。

sa_flags

通常设置为0,表示使用默认属性。(sa_flags设置为0时,使用sa_handler动作)

sa_restorer

该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)

sa_sigaction

当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)

  • 函数返回值 sigaction() returns 0 on success and -1 on error.

使用示例:使用sigaction捕获信号

代码语言:javascript复制
/************************************************************
  >File Name  : sigaction_test.c
  >Author     : Mindtechnist
  >Company    : Mindtechnist
  >Create Time: 2022年05月23日 星期一 14时20分42秒
************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>

void m_catch(int signalno)
{
    printf("catch signal: %dn", signalno);
}

int main(int argc, char* argv[])
{
    /*注册信号捕捉函数*/
    struct sigaction mact;
    mact.sa_flags = 0;
    mact.sa_handler = m_catch;
    sigemptyset(&mact.sa_mask);
    sigaction(SIGALRM, &mact, NULL);
    
    /*设置周期性定时器*/
    struct itimerval mit = {{2, 0}, {3, 0}};
    setitimer(ITIMER_REAL, &mit, NULL);
    while(1)
    {
        printf("pid: %dn", getpid());
        sleep(1);
    }
    return 0;
}

2. 信号捕捉的特性和处理

2.1 信号捕捉过程中有什么特性

在信号捕捉的时候,有如下几个特性

  • 进程正常运行时,默认PCB中有一个信号屏蔽字假设为M,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,在捕捉到该信号以后,就要调用该信号捕捉函数,而该函数有可能执行很长时间,在这期间所要屏蔽的信号不由M来指定,而是用sa_mask(临时屏蔽信号集)来指定,等到调用完信号处理函数,再把信号屏蔽字恢复为M。
  • 某个信号sig捕捉函数执行期间,sig信号自动被屏蔽。
  • 阻塞的常规信号不支持排队,如果产生多次,只记录一次。实际上是这样的,未决信号集中使用某一位的0和1来记录信号是否被处理的,所以不管这个信号被发送了几次,未决信号集对应位也只能有一个1,后续也只能处理一次,它不会记录信号屏蔽期间总共发送了几次该信号,解除屏蔽后只会处理一次。后32个实时信号支持排队。

示例分析:

代码语言:javascript复制
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void m_catch(int signo)
{
    printf("m_catch begin... sig: %dn", signo);
    sleep(7);
    printf("m_catch end...n");
}

int main(int argc, char* argv[])
{
    struct sigaction mact;
    mact.sa_flags = 0;
    sigemptyset(&mact.sa_mask);
    sigaddset(&mact.sa_mask, SIGQUIT); /*设置一个临时屏蔽信号
    在调用m_catch的时候,该信号会被屏蔽,m_catch执行完就会恢复原来的屏蔽信号,该信号解除屏蔽*/
    mact.sa_handler = m_catch;
    sigaction(SIGINT, &mact, NULL);
    while(1)
    {
        printf("pid: %dn", getpid());
        sleep(1);
    }
    return 0;
}

编译运行可以看到,虽然按了多次ctrl c,发送了多个信号,但是信号处理函数执行了一次,也就说明阻塞的常规信号不支持排队,如果产生多次,只记录一次,且只处理一次。

如果我们在进入m_catch函数后按ctrl ,程序不会退出,因为在m_catch函数内临时屏蔽了信号SIGQUIT,当执行m_catch函数后,才会处理SIGQUIT信号。可以看到下面的测试结果,在m_catch begin...打印后,也就是函数m_catch执行的时候按ctrl 并不会退出程序,而m_catch end...打印后,也就是m_catch函数结束后,临时信号屏蔽字失效,恢复原来的信号屏蔽字,内核处理SIGQUIT,程序退出。

2.2 内核是如何捕捉信号的

我们拿上面的程序为例,程序正常执行的时候,应该是一直在循环体内打印一句话,直到有信号产生

代码语言:javascript复制
while(1)
{
    printf("pid: %dn", getpid());
    sleep(1);
}

当产生信号的时候,会进入内核态,此时内核会执行信号处理函数,如果有用户自定义信号处理函数会再次返回用户态去执行该函数。执行完信号处理函数后通过系统调用sigreturn再次陷入内核,然后返回用户态从被中断的地方继续执行主控制逻辑。如果上面的程序不是printf打印,而是read读,因为read会阻塞,处理完信号后,只有从下一次while循环的时候才能正常读数据。

整体流程如下图所示

0 人点赞