深度解析linux下信号的注册和注销原理详解及配合信号更好解决僵尸进程

目录

1.信号的概念:

2.信号的产生

2.1硬件产生(按键盘中的按键):

2.2软件产生:

        1.kill函数

        2.raise函数:

        3.kill- [num] [pid] 可以给进程发送信号

        4.当常见的错误导致程序崩溃时进程收到信号

                1.解引用空指针

                2.除0:

                3.double free:多次释放同一个地址

3.信号的种类

kill -l 可以罗列信号

4.信号的处理方式

5.信号的注册

5.1基础概念了解:

5.2信号的注册:

6.信号的注销:

6.1非可靠信号的注销

6.2可靠信号的注销

7.信号的自定义处理方式

7.1 概念:

7.2 函数

7.3原理:

8.信号的捕捉流程

8.1信号的处理时机:

8.2处理信号的时候:不同的处理方式

8.3常见的进入到内核的方式:

9.信号的阻塞

9.1信号阻塞的特性:

9.2内核代码:

9.3加上信号阻塞之后,理解信号的处理;

9.4接口:

10.配合信号解决僵尸进程:

11.volatile关键字:


1.信号的概念:

  • 信号是一个软中断
    • 软中断:
    • 例如:看到绿灯你可以选择走或选择不走。红绿灯只是一个提醒你可以选择走或不走
    • 1.1只是告诉有这样一个信号,但是具体这个信号怎么处理,什么时候处理由进程决定,所以是软中断。

2.信号的产生

  • 2.1硬件产生(按键盘中的按键):

    • ctrl+c:2号信号 SIGINT,按下ctrl+c其实是进程收到了2号信号,2号信号导致进程的退出。
    • ctrl+z :20号信号SIGTSTP,

    • ctrl+|:3号信号SIGQUIT
  • 2.2软件产生:

    • 1.kill函数

    • 参数:
      • pid_t:要给那个进程发,就填那个进程的pid
      • sig:要给进程发送的信号
    • 使用:我们用getpid获取当前进程pid然后给当前进程发送一个3号信号

    • 我们发现一运行直接终止了

    • 2.raise函数:

    • 作用:给当前进程发送一个信号
    • 参数:给自己发送信号的信号值
    • 使用:给自己发送一个3号信号,退出信号。
      • 可以发现一运行直接退出了。

    • 底层实现原理:
      • 调用kill函数实现将kill函数封装。用getpid获取当前进程的进程号传给kill函数,然后自己只需要获取用户传入的信号值,然后传递给kill函数即可。
    • 3.kill- [num] [pid] 可以给进程发送信号

    • 可以看到进程暂停了

  • 4.当常见的错误导致程序崩溃时进程收到信号

    • 1.解引用空指针

    • 2.除0:

    • 运行一下我们可以看到、

    • 3.double free:多次释放同一个地址

    • 我们gdb调试coredump文件可以发现返回6号信号

3.信号的种类

  • kill -l 可以罗列信号

  • 非实时信号(非可靠信号):
    • 特点:信号可能会丢失(1-31)
  • 实时信号(可靠信号):
    • 特点:信号不会丢失(33-64)

4.信号的处理方式

  • 操作系统对信号的处理方式(man 7 signal)

  • 默认处理方式:
    • SIG_ DFL,操作系统当中已经定义号信号的处理方式了
    • 例如2号信号->终止进程
    • 11->终止进程,并且产生核心转储文件
  • 忽略处理方式:
    • SIG_ IGN, 该信号为忽略处理(僵尸进程)
    • 进程收到忽略处理的方式的信号后,是不进行处理的,例如当子进程先于父进程退出时,就会给父进程发送SIGCHLD信号,而父进程收到这个信号之后,就会忽略处理,导致父进程没有回收子进程的退出状态信息,从而子进程变成了僵尸进程。
  • 自定义处理方式:
    • 程序员可以更改信号的处理方式,定义一 个函数,当进程收到该信号的时候, 调用程序猿自己写的函数。(第7个小点涉及到)

5.信号的注册

  • 5.1基础概念了解:

    • 一个进程收到一个信号,这个过程称之为注册信号的注册和注销并不是一个过程,是两个独立的过程

    • 内核中信号注册位图以及s igqueue队列的的了解
      • task_ struct结构体内部
      • struct S igpending pending;
      • siget_ t
  • 5.2信号的注册:

    • 位图更改为1,添加Sigqueue节点到sigqueue队列:
    • 信号在注册的时候,会将信号对应的比特位从0修改为1,表示当前进程收到了该信号。
    • 还需要哎sigqueue队列中添加一个sigqueue节点,队列在操作系统内核当中本质上是一个双向链表(先进先出的特性)

    • 实时信号和非实时信号在注册时的区别:

    • 非实时信号(非可靠信号)的注册
      • 第一次注册:修改sig位图(0-1),修改sigqueue队列。
      • 第二次注册:相同信号值的信号:修改sig位图(1->1),并不会添加sigqueue节点。
      • 总结:再次添加,不会添加sigqueue节点
    • 实时信号(可靠信号)的注册
      • 第一次注册:修改sig位图(0-1),修改sigqueue队列。
      • 第二次注册:相同信号值的信号:修改sig位图(1->1),添加sigqueue节点到sigqueue队列中。
      • 再次添加,会再次添加siquque节点

6.信号的注销:

  • 6.1非可靠信号的注销

    • 1.将信号对应的s ig位图当中的比特位置为0(1-0)
    • 2.将对应的信号的sigqueue节点进行出队操作
  • 6.2可靠信号的注销

    • 1.将对应的信号的sigqueue节点进行出队操作
    • 2.判断s igqueue队列当中还有相同信号的S igqueue节点吗
      • 如果有:则比特位不变
      • 如果没有:则比特位改变位0

7.信号的自定义处理方式

  • 7.1 概念:

  • 自定义处理方式, 就是让程序猿自己定义某一个信号的处理方式,例如原来我们的2号信号是一个终止命令我们可以通过自己定义来让他做其他的事,例如打印一句话。

7.2 函数

  • 1.sighandler_t signal(int signum, sighandler_t handler);

  • 作用:
  • 在调用signal函数的时候,我们给函数的第二个参数传递一个回调函数的地址,当我们收到第一个参数所定义的信号值时,就会调用回调函数,执行回调函数的功能。
  • 参数:
  • signum:信号值
  • handler:更改为哪一 个函数处理,接受一 个函数地址,函数指针(回调函数)。
  • typedef void (*sighandler_ t)(int);
  • 代码验证:
  • 我们写一个代码测试当进程收到2号信号和9号信号的时候,是否会调用回调函数

  • 我们运行代码按下ctrl+c向进程发送2号信号,可以发现执行回调函数将收到的2号信号打印出来了。

  • 我们向信号传递9号信号

  • 可以发现函数并不会执行回调函数,而是直接将进程强杀了,所以可以得知9号信号是无法被用户更改处理方式的。

  • 2.int sigemptyset(sigset_t *set);(将信号位图初始化为全0)

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

    • 参数:
    • signum:要更改的信号值
    • act:要将信号的处理方式更改为act
    • oldact:原来信号的处理方式,要依赖于函数填充。
    • struct sigaction
    • 使用方式:
    • void (*sa_ handler)(int); //保存信号处理方式(默认)的函数指针
    • void (*sa_ sigaction)(int, siginfo _t *,void *);// (也是保存原来信号的处理方式的函数指针),但是没有使用。当要使用的时候,配合sa_ flags- 起使用。当sa_ falgs的值为SA_ SIGINF0的时候,信号按照sa_ sigaction保存的函数地址进行处理
    • sigset_ t a_ mask;//当进程在处理信号的时候,如果还在收到信号, 则放到该信号位图当中,后续再放到进程的信号位图当中使用时需要用int sigemptyset(sigset_t *set);函数将位图每一位置为0
    • int sa_ f lags;
    • void (*sa_ restorer)(void); //保留字段
    • 代码测试:

    • 我们运行代码:

7.3原理:

内核当中的结构体

8.信号的捕捉流程

  • 当我们的进程从内核态切换回用户态时,会调用do_signal,检查进程是否收到了信号。

  • 8.1信号的处理时机:

  • 当从内核态切换会用户态的时候,会调用do_ S igna l函数处理信号
    • 有,就处理信号(信号的处理方式(默认,“ 忽略, , 自定义) )
    • 没有,就直接返回会用户态
  • 8.2处理信号的时候:不同的处理方式

  • 默认:
  • 忽略:直接在内核就处理结束。
  • 自定义处理:
  • 调用程序猿自己定义的处理函数进行处理
  • 执行用户自定义的处理函数(用户空间)
  • 调用sigreturn( )再次回到操作系统内核(内核空间)
  • 再次调用会调用do_ signa L函数处理信号
  • 调用sys_ sigreturn函数回到用户空间, 继续执行代码
  • 8.3常见的进入到内核的方式:

    • 调用系统调用函数,
    • 内存访问越界,”访问空指针
    • 调用库函数

9.信号的阻塞

  • 9.1信号阻塞的特性:

  • 信号的注册是信号注册, 信号阻塞是信号阻塞。信号的阻塞并不会干扰信号的注册,而是说进程收到这个信号之后,由于阻塞, 暂时不处理该信号 。
  • 9.2内核代码:

    • struct task_ struct{
    • sigset_ t blocked; (位图)它和前面的sig位图一样都是当信号阻塞时将相关的bit位置为1。

  • 9.3加上信号阻塞之后,理解信号的处理;

    • 进入内核,返回之前, 会调用do_ signa 1函数处理信号
    • 有信号要处理,则先判断该信号是否阻塞,如果没阻塞, 在处理信号。 如果阻塞,则不处理 。当不阻塞再处理。
  • 9.4接口:

  • int sigprocmask(int how, const sigset_ t *Set, sigset_ t *oldset);(无法阻塞9号和19号信号)
  • 参数:
    • how:想让s igp rocmask做什么事情
    • SIG_ BLOCK: 设置某个信号为阻塞状态
    • SIG_ UNBL.OCK :设置 某个信号为非阻寒状态
    • SIG_ SETMASK :用第二个参数“set”,替换原来的阻寨位图。 ( 替换的意思)
    • set :新设置的阻塞位图
    • 根据传递进的函数变量计算新的阻塞位图:
      • 1.阻塞单个信号,阻塞多个信号(只要将相应的信号位图设置为1即可)
      • 2.接触阻塞单个信号/解除阻塞多个信号。
    • oldset :原来老的阻塞位图
  • 原理解析:
    • 当how为SIG_ _BLOCK时,函 数会根据set,计算新的阻塞位图, 方式为:
    • block(new) = block(old) | set;新的block位图和旧的block位图按位或运算。
    • 当how为为SIG_ _UNBLOCK时,函数会根据set,计算新的阻塞位图, 方式为:
    • block(new) = block(old) & (^ 'set);将传入新的位图先取反,然后和老的位图进行按位与操作。
    • 当how为为SIG_ SETMASK时,函 数会根据set,计算新的阻塞位图,方式为:
    • block (new) = set;
  • 测试代码:

  • 运行结果我们发现给当前进程无论发送什么信号都没有作用。

  • 我们给进程发送2号信号发现没有反应

  • 发送9号信号

  • 我们接下来验证实时信号和非实时信号这里

  • 我们运行之后看结果

10.配合信号解决僵尸进程:

  • 在我们之前解决僵尸进程只能调用wait函数或者waitpid函数,但是在调用这两个函数都面临一个问题那就是在调用时,父进程要不一直处于阻塞等待子进程退出状态,要不一直要配合循环和waitpid的非阻塞状态使用,我们的父进程就无法做任何的事情。这里我们可以运用信号,对子进程进行回收。

  • 我们看运行结果

11.volatile关键字:

  • 作用:
  • 保证内存可见性
  • 每次CPU要计算的数据都是从内存中获取,拒绝编译时优化的方案(从寄存器当中获取)gcc/gt+的编译选项“-00, 01, -02,-03,优化级别时越来越高。(理解优化级别越高,程序可能执行的越快)优化级别越高就从寄存器中取值的可能性越大
  • 我们写一个代码,验证一下

  • 我们运行发现按下ctrl+c向进程输入2号信号,进程直接结束了,这是因为我们在编译时没有对程序进行优化

  • 我们优化为O1级别

  • 运行发现仍然可以退出也就是从内存中读取数据

  • 但是当我们优化到一定级别CPU在计算时就为了提高运行效率不从内存中取数据了,就直接从寄存器中取数据,导致我们更改的g_val的数值在程序运行时显示不改变。这样就会导致一定错误产生。
  • 我们在变量前边加上volatile关键字就会保证内存可见性,CPU在处理数据时每次从内存中读取。

看到这里如果觉得有用不如点个赞再走吧!!!

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月半木斤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值