Linux:详解进程信号(信号的种类、产生、注册、注销以及信号的各种处理方式)(一)(图文并茂)


1. 信号的概念

信号是一个软件中断,相当于是一个口头的约束,对你的限制力比较低,举个例子来说就是当你买的快递到了需要你去取的时候,而你正在打游戏,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取。在这个过程中,你获得了一个取快递的信号,但是你并没有选择立即执行,而是选择了在合适的时候去取,这就是软件中断。

2. 信号的种类

  • 目前Linux的信号数量为62个,分为两种类型:

    ① 非实时信号(非可靠信号),对应信号量为1~31,它的特点是有可能信号会发送丢失
    ② 实时信号(可靠信号),对应信号量为34~64,它的特点是信号不会发送丢失

  • kill -l:可以罗列出具体的信号值。

    在这里插入图片描述

  • man 7 singal:罗列所有的信号的具体信息。

    信号的具体信息:
    在这里插入图片描述
    信号的动作:
    在这里插入图片描述
    如果某一个信号的处理动作是core,那么它默认是需要完成终止进程+产生coredump文件。产生coredump文件依赖于ulimit -a中对应的core file size,并将其设置为unlimited

3. 信号的产生

3.1 硬件产生

  • ctrl + c:使当前运行的进程中断,产生一个值为2号的信号量,是一个SIGINT
  • ctrl + z:使当前运行的进程停止,产生一个值为20号的信号量,是一个SIGTSTP
  • ctrl + |:使当前运行的进程退出,产生一个值为3号的信号量,是一个SIGQUIT

3.2 软件产生

  • kill 命令

    kill [PID]:终止一个进程。
    kill -[num] [PID]:给进程号为PID的进程发送一个信号值为num的信号。

  • kill 函数(包含在#include<signal.h>中,是一个系统调用函数)

    int kill(pid_t pid, int sig);
    功能是:给pid进程发送sig的信号。

  • raise函数(包含在#include<signal.h>中,是一个库函数)

    int raise(int sig)
    功能:谁调用给谁发送signal信号。

4. 信号的注册

  • 一个进程接收到一个信号,这个过程就被称之为信号的注册。
  • 信号的注册和信号的注销并不是一个过程,是两个独立的过程。
  • 信号的注册分为两种情况:可靠信号的注册和非可靠信号的注册

在说可靠信号的注册和非可靠信号注册之前,我们先来看看信号在操作系统内核中到底是怎样存储的。

4.1 信号注册在内核中的存储表示

① 首先我们查看源码中的struct task_struct结构体(PCB)中关于信号的处理程序即signal handlers,我们本节就看其中的一个变量struct sigpending pending
在这里插入图片描述
② 然后我们转到该结构体的内部定义对其进行查看
在这里插入图片描述
发现有一个sigset_t类型的变量,这个sigset_t类型应该是被typedef出来的。

③ 我们使用grep在内核源码中对sigset_t进行搜索,发现其包含在signal.h文件中。
在这里插入图片描述
④ 在signal.h头文件中对sigset_t进行查看
在这里插入图片描述
说明sigset_t类型是一个结构体,它包含了一个无符号长整型的数组。

至此,我们可以清楚的知道信号注册时在内核中到底是如何存储的。

总结一下就是:

  • 在操作系统内核的task_struct结构体内部有一个变量struct sigpending pending
  • 内核定义的结构体struct sigpending当中有两个变量:一个是内核定义的双向链表,一个是sigset_t signal
  • 内核定义的类型sigset_t是一个结构体,而该结构体内部有一个变量,该变量为一个数组,是一个无符号长整形的数组。

因此:

  • 信号的注册本质上是在使用sig数组,但是并不是按照数组类型的方式来使用,而是按照位图(比特位)的方式在使用,即是将整个数组看做是一个位图,若某一个信号被注册,则某个信号的对应数组中的比特位就会被置为1。一般在信号注册时候,称之为操作sig位图。
  • sig数组的比特位数远远是大于62的,那些剩下的比特位会被置位保留位
  • 操作系统内核对于注册的时候,还有一个sigqueue队列,信号的注册逻辑位,将信号对于的sig位图中的比特位置为1,并在sigqueue队列中添加一个sigqueue节点。因此,我们可以通过注册同一信号两次,来区分可靠信号和非可靠信号的注册逻辑

4.2 非可靠信号的注册

第一次注册信号:

  • 更改信号对应在sig位图中的比特位,将其从0变为1。
  • 在sigqueue队列中添加一个sigqueue节点。

第二次注册同样的信号:

  • 更改信号对应在sig位图中的比特位,将其从1变为1。
  • 对于第二次的信号,不添加sigqueue节点到sigqueue队列中。

总结:

如果有多次同一的非可靠信号来注册,对于非可靠信号而言,只会添加一次sigqueue节点,换而言之,就是只注册了一次

4.3 可靠信号的注册

第一次注册信号:

  • 更改信号对应在sig位图中的比特位,将其从0变为1。
  • 在sigqueue队列中添加一个sigqueue节点。

第二次注册同样的信号:

  • 更改信号对应在sig位图中的比特位,将其从1变为1。
  • 在sigqueue队列中添加sigqueue节点。

总结:

如果有多次同一可靠信号来注册,那么会添加多次sigqueue节点,换言之,就是注册了多次

5. 信号的注销

5.1 非可靠信号的注销

  • 将信号对应到sig位图当中的比特位置为0。
  • 将对应的非可靠信号的sigqueue节点进行出队操作。

5.2 可靠信号的注销

  • 先将可靠信号对应的sigqueue节点进行出队操作
  • 再判断sigqueue队列中是否有同类的可靠信号的sigqueue节点

    ① 若有:则不会将sig位图中对应的比特位置为0.
    ② 若没有:则将信号对应到sig位图当中的比特位置为0。

6.信号的处理方式

6.1 默认处理方式

默认处理方式:已经在操作系统内核当中定义好了。

对应的是宏:SIG_DFL,注意是宏,而不是信号。

6.2 忽略处理方式

忽略处理方式:操作系统定义进程收到某一个信号之后,忽略掉。

对应的宏是:SIG_IGN

问题:为什么子进程先于父进程退出的时候,子进程就会变成僵尸进程?

解答:子进程先于父进程退出,子进程就会向父进程发送一个SIGCHLD信号。而父进程并不会做任何事情,导致子进程资源没有进程可以回收,导致僵尸进程的产生。

6.3 自定义处理方式

自定义处理方式:程序员可以定义某个信号的处理方式。

6.3.1 signal函数

sighandler_t signal(int signum, sighandler_t handler)

参数:

  • signum:待要更改的信号的值
  • handler:是一个函数指针,接收的是一个函数地址,并且该函数没有返回值,但有一个int类型的参数。

sighandler_t 对应的类型是:typedef void (*sighandler_t)(int);

返回值:

在这里插入图片描述
也就是说 signal 的返回值是指向之前的信号处理程序的指针,之前的信号处理程序,也就是在执行signal(signo,func)之前,对信号signo的信号处理程序。我们一般不需要来接收它,操作系统会自动进行相应的操作。

举个例子:

我们需要编写一个文件,实现当我们给定一个2号信号(即ctrl + c)的时候,让它去执行我们自己定义好的一个sigCallBack函数,该函数中用printf函数打印一句话,“It’s test to processing the SIGINT signal”。

代码实现:

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

void sigCallBack(int sig)
{
    printf("It's test to processing the SIGINT signal\n");
}

int main()
{
    printf("test start!\n");
    signal(2,sigCallBack);

    while(1)
    {
        sleep(1);
    }
    return 0;
}

那么,当该代码运行的时候,在终端按下ctrl + c,就会回调去调用sigCallBack函数去执行相应的内容。

运行结果

在这里插入图片描述

自定义signum这个信号的处理方式,定义为handler这个函数指针保存的函数地址对应的函数。换句话来说就是当进程收到signum这个信号的时候就会调用handler中保存的函数。

用图来理解就是:
在这里插入图片描述
注意:是从内核处调用的回调函数,不是从main函数处调用

小结:

signal函数向内核注册了一个信号的处理函数,调用signal函数的时候,并没有调用注册的函数(注册的函数在进程收到信号之后才调用),这种方法称为回调

6.3.2 sigaction函数

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

参数:

  • signum:待要自定义处理的信号。
  • act:要将信号处理方式更改为act。
  • oldact:原来的处理方式。

struct sigaction结构体定义

在这里插入图片描述
结构体参数解释:

  • void (*sa_handler)(int):默认的信号处理函数保存的函数指针
  • void (*sa_handler)(int ,siginfo_t * ,void *):函数指针,但要配合sa_flags一起使用,当sa_flags当中的值为SA_SIGINFO的时候,信号处理是按照sigaction结构体当中保存的函数地址来处理的(默认的),而不是传递进来的那个函数指针。
  • sigset_t sa_mask:注意上面讲过sigset_t是一个位图,那么sa_mask所实现的是,当进程在处理某一个信号的时候,有可能还会收到其他的信号,此时其他的信号就暂时存在sa_mask当中。
  • int sa_flags:指定了一组修改信号行为的标志。当其为SA_SIGINFO的标志的时候,就按照默认的信号处理函数处理,当为0的时候,就按照我们传递进来的信号处理函数处理。
  • void (*sa_restorer)(void):sa_restorer元素已经过时了,不应该被使用。它作为一个保留字段保留了下来。

返回值:

若是修改成功则返回0,失败返回-1。

举个例子:
还是上面signal函数中所实现的例子,只不过这次讲signal函数修改为sigaction函数。

代码实现:

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

void sigCallBack(int sig)
{
    printf("It's test to processing the SIGINT signal\n");
}


int main()
{
    printf("test start!\n");

    struct sigaction sa;
    sa.sa_handler = sigCallBack;
    //将位图中的比特位情况全部设置为0
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(2,&sa,NULL);

    while(1)
    {
        sleep(1);
    }
    
    return 0;
}

需要注意的是,该代码中有一个sigemptyset函数,是将对应的位图全部设置为0。

运行结果:

在这里插入图片描述

6.4 从内核角度去分析信号的处理

① 首先,我们还是从struct task_struct结构体开始看起,我们可以找到一个struct signal_struct* sighand1的变量。
在这里插入图片描述
② 查看 struct sighand_struct结构体的定义。
在这里插入图片描述
在该结构体的内部有一个struct k_sigaction action[_NSIG]的变量。注意他不是一个指针,而是一个数组,因此它会直接展开在sighand_struct结构体的内部。
③ 查看struct k_sigaction结构体的定义。
在这里插入图片描述
该结构体内部就是我们使用sigaction函数中所提到的struct sigaction结构体。
④我们来看看struct sigaction结构体的定义。
在这里插入图片描述
注意这里的__sighandler_tvoid (*sa_handler)(int)typedef出来的。

signal函数和sigaction函数区别

  • signal函数的内部也是在调用sigaction函数的,它修改的是一个__sighandler_t 的函数指针
  • 而sigaction函数是直接对struct sigaction结构体进行相应的修改。

画图解释如下:

在这里插入图片描述

  • 10
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值