【Linux】信号的保存&信号的捕捉&信号集&零碎知识点总结


在这里插入图片描述

在这里插入图片描述

一、信号的保存

1.1 信号几种概念

实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

1.2 信号在内核中的表示

原理图,一图速览内核中信号的基本数据结构构成
内核中信号基本数据结构构成

在进程内部要保存信号周边的信息,有3种数据结构与之是强相关的,pending表,Block表,Handler表

  1. 进程可能在任何时候收到OS给它发送的信号,该信号可能暂时不被处理,所以需要暂时被保存,进程为了保存信号采用位图来保存,这个位图就是pending位图,被置于pending位图的信号处于未决状态。OS向进程发信号就是向目标进程的pending位图设置比特位,从0到1就是当前进程收到该信号,所以发信号应该是写信号,PCB属于OS内核结构,只有OS有权力修改pending位图,所以发送信号的载体只能是OS。
  2. block位图:block位图比特位的位置代表信号标号,比特位的内容代表是否阻塞了该信号
  3. Handler表,内核中有指针指向该数组,这个数组称为当前进程所匹配的信号递达的所有方法,数组的位置(下标)代表信号的编号,数组下标对应的内容表示对应信号的处理方法.

⚠️重点:我们之前所谈到的信号接口signal(signo,handler)的本质是:根据信号编号,找到数组对应的下标,然后将用户层设置的handler函数的地址填到数组对应下标处,等信号产生时候,修改pending表比特位,根据Block表判断该比特位是否被阻塞。信号被递达时,OS拿到信号根据信号位置得到信号的编号,进而访问数组得到信号的处理方法。

二、信号的捕捉

信号产生的时候,可能不会立即处理,会在合适的时候从内核态返回用户态的时候处理。

了解用户态和内核态

  • 用户为了访问内核或者硬件资源必须要使用系统调用,系统调用是OS提供的接口,而普通用户不能以用户态的身份执行系统调用,必须让自己的身份切换成成内核态。

  • 一个进程在执行时必须把上下文信息切换到CPU中,CPU中有大量的寄存器,寄存器可分为可见寄存器(eax,ebx…),不可见寄存器(状态寄存器…),凡是和当前进程强相关的,是上下文数据。

    寄存器中还有非常多的寄存器在进程中有特定作用,寄存器可以指向进程PCB,也可以保存当前用户级的页表,指向页表起始地址

    寄存器中还有CR3寄存器:表征当前进程的运行级别:0表示内核态,3表示用户态

进程如何去OS中执行方法
在这里插入图片描述

  • 以前所说的进程地址空间0-3G是用户级页表,通过用户级页表映射到不同的物理空间处,而除了用户级页表之外,还有内核级页表,OS为了维护从虚拟地址到物理地址之间的OS级别的代码所构成的内核级映射表。
  • 3G-4G是OS内部的映射,所以进程建立映射的时候不仅仅把用户的代码、数据和进程产生关联,每一个进程都要通过内核级页表和OS产生关联,而每一个进程都有自己的地址空间,其中用户级空间自己占有,而内核空间是被映射到了每一个进程的3-4G空间,每一个进程都可以通过页表映射到OS,而且每个进程看到的OS都是一样的,所以进程要访问OS的接口,其实只需要在自己的地址空间上进行跳转就可以了
  • 每一个进程都有3-4GB,都会共享一个内核级页表,无论进程如何切换,都不会更改任何的3-4GB。

用户通过什么能够执行访问内核的接口或者数据

  • 当要访问3-4G任何一个地址时,OS立马读取CPU中的CR3寄存器,得到运行状态。所以系统调用接口起始的位置会帮我们把用户态变成内核态,从3号状态改成0号状态。
    OS是如何通过系统调用把用户态变成内核态的
    中断汇编指令int 80就是陷入内核,简单理解把状态由用户态改成内核态。调用结束时再切回来

2.1 捕捉过程

通过系统调用,陷入内核,OS返回用户态之前,会在进程的上下文中搜索可以传递的信号,OS能从CPU寄存器中拿到task_struct找到进程,查3张表,先查block表:block为0说明没被阻塞,继续看pending,pending为0继续下一个…

理论上是可以从内核态访问用户态(waitpid),但是实际上我们不能以内核态去访问用户态,OS不相信任何用户

在这里插入图片描述


如果Linux内核确定某个进程的未阻塞信号正在等待处理,则在该进程的下一次转换回到用户模式时(例如,从系统调用返回时或将该进程重新安排到CPU时),它将创建用户空间堆栈上的新帧,其中保存了各种过程上下文(处理器状态字,寄存器,信号掩码和信号堆栈设置)。

内核还安排,在转换回用户模式期间,将调用信号处理程序,并且在从处理程序返回时,控制权将传递给通常称为"信号蹦床"的一段用户空间代码。信号蹦床代码依次调用sigreturn()。

此sigreturn()调用撤消已完成的所有操作-更改过程的信号掩码,切换信号堆栈(sigaltstack(2))-以调用信号处理程序。使用先前保存在用户空间堆栈中的信息sigreturn()可还原进程的信号掩码,切换堆栈并还原进程的上下文(处理器标志和寄存器,包括堆栈指针和指令指针),以便进程恢复在信号中断的地方执行。—来自linux手册(了解)


简化图:
在这里插入图片描述

2.2 信号的捕捉方法

signal:

#include <signal.h>
typedef void (*sighandler_t)(int);//函数指针
sighandler_t signal(int signum, sighandler_t handler);

sigaction:
act:结构体对象,输入型参数;oldact:输出型参数,获取特定信号老的处理方法

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
               void     (*sa_handler)(int);//回调方法
               void     (*sa_sigaction)(int, siginfo_t *, void *);//处理实时信号,不关心
               sigset_t   sa_mask;//信号集
               int        sa_flags;//设置0就行
               void     (*sa_restorer)(void);
           };
RETURN VALUE:返回值
     sigaction() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error.

2.3 信号捕捉规则

  1. 当我们进行正在递达某一个信号期间,同类型信号无法递达:当前信号正在被捕捉,系统会自动将当前信号加入到进程的信号屏蔽字(即BLOCK表),自动将2号信号屏蔽。
  2. 而当信号完成捕捉动作,系统又会自动解除对该信号的屏蔽
  3. 一般一个信号被解除屏蔽的时候,如果该信号已经被pending的话,会自动进行递达当前屏蔽信号,没有就不做任何动作
  4. 进程处理信号的原则是串行的处理同类的信号,不允许递归式处理
  5. 信号的自定义捕捉行为在执行之前就会把pending对应的信号比特位由1置0,然后再去执行行为,避免重复抵达问题

在这里插入图片描述

2.4 多信号屏蔽问题

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt:%d\r",cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    cout<<endl;
}

void handler(int signo)
{
    cout<<"get a signo"<<signo<<"正在处理信号"<<endl;
    Count(20);//倒计时打印
}

int main()
{
    struct sigaction act,oact;
    act.sa_handler=handler;
    act.sa_flags=0;
    sigemptyset(&act.sa_mask);
    //屏蔽多个信号可以把信号添加到sa_mask
    sigaddset(&act.sa_mask,3);
    

    sigaction(SIGINT,&act,&oact);

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

在这里插入图片描述

三、信号集

3.1 概念

每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态

3.2 信号集(sigset_t)操作函数

使用者只能调用以下函数来操作sigset_ t变量

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

sigprocmask :读取或更改进程的信号屏蔽字(阻塞信号集)Block表
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信
号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后
根据set和how参数更改信号屏蔽字

sigpending :读取pending信号集

#include <signal.h>
int sigpending(sigset_t  *set)
调用成功则返回0,出错则返回-1

关于这些函数的具体使用可参考我的码云

四、可重入函数

在信号层面解释:如果在main中和在handler中,该函数被重复调用,此时出现冲突问题,则该函数(比如list.insert)称为不可重入函数;相反如无冲突,则是可重入函数。

例如:main函数调用insert,向链表head插入Node1,insert只做了第一步,然后就被中断(或者因为信号原因执行信号捕捉),此时进程挂起,然后回到用户态之前检查有信号待处理,于是执行sighandler方法,sighandler也调用了insert函数,把Node2头插到链表里,Node2的next结点指向下一个结点位置,下一步就是head指向Node2,完成Node2的头插,信号捕捉完之后就成功把Node2插入,接下来回到main执行流,对Node1完成插入的第二步动作,此时把head指向Node1,最后只有Node1真正插入到链表之中。
在这里插入图片描述

而我们目前大部分情况下用的接口,全部都是不可重入的,重入不重入是特性。

识别不可重入函数
调用了malloc或free,因为malloc也是用全局链表来管理堆的。

调用了标准I/O库 函数。标准I/O库的汗多实现都以不可重入的方式使用全局数据结构。

五、volatile关键字

volatile保持内存可见性

#include <stdio.h>
#include <signal.h>
int quit = 0;
void handler(int signo)
{
    printf("%d号信号,正在被捕捉\n",signo);
    printf("quit:%d",quit);
    quit = 1;
    printf("->%d\n",quit);
}
int main()
{
    signal(2,handler);
    while(!quit);
    printf("注意,我是正常退出的\n");
    return 0;
}

以上代码正常编译是以下情况
在这里插入图片描述

但是增加了-O3优化后变成以下情况

mysignal:mysignal.c
	gcc -o $@ $^  -O3 

在这里插入图片描述
具体原因:
寄存器的存在遮盖了物理内存当中quit变量存在的事实

在这里插入图片描述
解决办法:int quit = 0;增加关键字 volatile-->volatile int quit=0

六、SIGCHLD

父进程可以用wait和waitpid函数清理僵尸进程【子进程先于父进程退出后,子进程的PCB需要其父进程释放,但是父进程并没有释放子进程的PCB,这样的子进程就称为僵尸进程】,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式),这两种方式都是局限性。
子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略。

要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGNsignal(SIGCHLD, SIG_IGN);,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。
虽然信号SIGCHID的默认动作也是忽略,但这个忽略是实实在在的无视了这个信号;signal(SIGCHLD, SIG_IGN)这种方法子进程退出时不会给发送父进程发送信号,但子进程会被操作系统回收,这就是区别所在

SIGCHLD应用

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

void handler(int signo)
{

    // 1. 多个子进程,在同一个时刻退出了
    // 2. 多个子进程,在同一个时刻只有一部分退出了
    while(1)//等待所有要退出的子进程
    {
        pid_t ret = waitpid(-1, NULL, WNOHANG);//非阻塞式调用
        if(ret == 0) break;
    }

}

int main()
{
    // 显示的设置对SIGCHLD进行忽略
    //signal(SIGCHLD, SIG_IGN); // 忽略
    // signal(SIGCHLD, SIG_DFL);//默认
	signal(SIGCHLD, handler)
    printf("我是父进程, %d, ppid: %d\n", getpid(), getppid());

    pid_t id = fork();
    if (id == 0)
    {
        printf("我是子进程, %d, ppid: %d,我要退出啦\n", getpid(), getppid());
     
        exit(1);
    }

    while (1)
        sleep(1);

    return 0;
}

如果两个子进程几乎同时终止,它们各自发送的 SIGCHLD 信号可能会被合并为一个信号,导致信号处理函数只被调用一次。然而,通过在信号处理函数中使用 waitpid() 函数并设置 WNOHANG 选项

这个循环会在有子进程终止时执行。只要 waitpid() 函数返回的值大于 0,意味着有子进程已经终止,循环将继续执行。当没有更多子进程终止时,waitpid() 会返回 0,并退出循环

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值