【Linux信号专题】四、信号的捕捉

简介: 【Linux信号专题】四、信号的捕捉

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

1. 信号捕捉函数

1.1 signal函数

  • 包含头文件及函数原型
#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函数

  • 包含头文件及函数原型
#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捕获信号

/************************************************************
  >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: %d\n", 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: %d\n", getpid());
        sleep(1);
    }
    return 0;
}

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

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

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

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

示例分析:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void m_catch(int signo)
{
    printf("m_catch begin... sig: %d\n", 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: %d\n", 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 内核是如何捕捉信号的

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

while(1)
{
  printf("pid: %d\n", getpid());
  sleep(1);
}

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

整体流程如下图所示


相关文章
|
3天前
|
Ubuntu Linux
【Linux】详解信号产生的方式
【Linux】详解信号产生的方式
|
3天前
|
Unix Linux
【Linux】详解信号的分类&&如何自定义信号的作用
【Linux】详解信号的分类&&如何自定义信号的作用
|
2天前
|
Unix Linux C语言
|
3天前
|
安全 Linux
【Linux】详解用户态和内核态&&内核中信号被处理的时机&&sigaction信号自定义处理方法
【Linux】详解用户态和内核态&&内核中信号被处理的时机&&sigaction信号自定义处理方法
|
3天前
|
存储 Linux C++
【Linux】详解信号的保存&&信号屏蔽字的设置
【Linux】详解信号的保存&&信号屏蔽字的设置
|
3天前
|
存储 Linux
【Linux】对信号产生的内核级理解
【Linux】对信号产生的内核级理解
|
6天前
|
存储 安全 Linux
【探索Linux】P.18(进程信号 —— 信号捕捉 | 信号处理 | sigaction() )
【探索Linux】P.18(进程信号 —— 信号捕捉 | 信号处理 | sigaction() )
7 0
|
6天前
|
存储 算法 Linux
【探索Linux】P.17(进程信号 —— 信号保存 | 阻塞信号 | sigprocmask() | sigpending() )
【探索Linux】P.17(进程信号 —— 信号保存 | 阻塞信号 | sigprocmask() | sigpending() )
9 0
|
6天前
|
算法 Linux C++
【探索Linux】P.16(进程信号 —— 信号产生 | 信号发送 | 核心转储)
【探索Linux】P.16(进程信号 —— 信号产生 | 信号发送 | 核心转储)
6 0
|
7天前
|
存储 Linux 编译器
[Linux打怪升级之路]-信号的保存和递达
[Linux打怪升级之路]-信号的保存和递达