【Linux系统编程二十四】:(信号3)--信号的捕捉处理与周边细节

2023-12-26 21:49:35

一.信号捕捉

捕捉就是的具体捕捉动作。我们要捕捉信号的处理,其实最简单的做法就是我们的signal系统调用接口。而除了signal接口外还有一个捕捉接口sigaction

1.signal/sigaction

①.sa_handler

在这里插入图片描述
你捕捉信号就是为了未来要修改一个信号对应的处理方法,所以你要设置对这个信号如何进行处理,是自定义捕捉还是什么呢。
sigaction接口有三个参数:
第一个参数,就是要捕捉的信号的编号。
第二个参数,是一个struct sigaction类型的结构变量action,是作为输入型参数。它内部就是封装了具体的信号处理方法。在传入这个参数之前, 你需要将处理信号的方法先初始化给action变量。

1.struct sigaction这个结构是干啥的呢?
它是有负责我们用户要设置的一些自定义捕捉方法。
归根结底我要修改这一个进程所对应的信号的处理动作。本质就是改这三张表,pending表、block表和handler表。
2. struct sigaction它的结构是什么样子的呢?在这里插入图片描述
这个结构体体内部重要的就两个,其他的不需要注意,一个是函数指针sa_handler,一个是sa_mask。
3.sa_handler它就是我们信号的处理方法。当我们要捕捉某个信号时,先定义struct sigaction action变量,然后再将具体信号的处理方法设置进该变量里。最后调用系统调用sigaction时再作为输入型参数,传入进行。
4.sa_mask是干啥的呢?

第三个参数,也是一个struct sigaction类型的结构变量old action,第二个参数是用来修改信号的处理方法的,那么我们在改之前呢,老的对特定信号的处理方法是什么样子,那么他呢就会给我们再返回。
所以是作为输出型参数。保存老的信号处理方法。

所有的保存都是为了最终我们进行恢复,所以他在这里呢就给我们设置了两参数啊,当然你如果不想保存到老的方法,你设nullptr就可以了。

所以最终呢你会发现它的整个调用呢其实跟我们当年讲的signal是一样的,只不过呢差别就在于它需要你自己传入一些结构体对象,然后呢把它字段做一些初始化,设置一下对信号的处理,然后设置进去。

②.sa_mask

当我们正在进行信号处理的时候,倘若我们已经那么进入到了信号的捕捉代码里,那么此时它是先把pending表里对应信号由1置0,然后才调用的对应处理方法。
所以在执行信号捕捉方法的之前,会将pending表里接收到的信号由1置0。

还有一个事实:当进程正在是捕捉处理一个信号时,操作系统一,它会把你当前的信号的pending表的比特位处由1置0。同时,操作系统会自动将当前信号加入到进程的信号屏蔽字当中。当处理函数返回的时候,自动解除对信号的屏蔽。

就比如,捕捉处理2号信号,首先你要接收到2号信号,这是2号信号的pending表比特位由0置1,这是接收的标识。然后开始捕捉处理,在捕捉处理之前,操作系统又会将2号信号的pending表比特位由1置0,同时将2号信号进行屏蔽。等信号处理完毕,就会自动解除对2号信号的屏蔽。

我们进行对二号信号进行捕捉处理的过程,它是会被屏蔽,所以在被捕捉期间,二号信号是不可再被递达的。
这样做主要是为了保证在正在处理某个信号时,如果这种信号再次产生了。那么他呢就只能等当前的处理做完,才能进行下一次处理。
也就是不允许同一个信号不断向进程发送,导致进程不断的忙于进行各种信号处理。

如果不这样干的话,就会导致一个问题:信号处理嵌套调用。

那么就是当我正在执行我对二号信号定义的捕捉的时候,那么在捕捉方法里面有没有可能将进入内核再重新到内核啊,那么答案是完全有可能,而且它是很合理的。
那么说明了如果我们在收到一个二号信号,并且对二号信号正在处理时。
那么我们如果再来一个2号信号,因为我们是先处理它之前,我们是先把它的判pending表由一改成零。
所以正在捕捉二号信号时,还可以再接收对应的二号信号那么在进入二号信号时,那么我们这个函数它本身又可以进入到从内核到用户重新到内核的调用。
所以信号检测条件依旧也可以具备哦,所以此时就会陷入一个非常尴尬的情况,叫做我们正在对二号信号进行捕捉,又来一个信号,此时我们又陷入到这个判断方法里,继续再进行捕捉,相当于我们自己判断方法正在进行不断的重复调用。
在这里插入图片描述

所以操作系统为了避免这样的情况,采用这样的方法:当我正在处理你的时候,我把你对应的信号添加到你的信号屏蔽字里,添加到你的block表里面。这样在信号处理时,就不会再处理该信号了。

那么这些跟sigaction结构体里的sa_mask有什么关系呢?
当正在捕捉处理二号信号时,我们的二号信号呢会自动被系统屏蔽。
好,那如果我还想啊屏蔽更多信号呢?
一旦收到二号信号时,我就要执行二号信号的捕捉方法。正在处理二号信号期间,我只会屏蔽二号信号。但如果我也想同时把其他信号屏蔽呢?
所以这个sa_mask就是用来屏蔽其他信号的,它的类型正好是sigset_t类型也就是位图类型,只要将里面的位图设置好,就可以在处理方法是既可以屏蔽自己也可以屏蔽其他信号了。

就是正在处理一个信号,是不想让这个信号重复的去被处理。你正在处理某一个信号时,系统会自动把当前正要处理的信号给你屏蔽。那么只要你处理完了,这个信号才会被重新再恢复出来


void PrintPending()
{
    sigset_t set;//位图变量,输出型参数

    sigpending(&set);//将penging表带出来

    for(int signo=1;signo<=31;signo++)
    {
        if(sigismember(&set,signo))//如果signo信号在set表里就为真
        cout<<"1";
        else
        cout<<"0";
    }
    cout<<endl;

}
void handler(int signo)
{
    cout<<"catch a signal,signal number is "<<signo<<endl;
    //1.问题:验证什么时候信号由1变成0--->在捕捉处理之前就由1变成0了
    while(true)
    {
      PrintPending();
      sleep(1);
    }
}
int main()
{
   struct sigaction act,oact;
   memset(&act,0,sizeof(act));
   memset(&oact,0,sizeof(oact));//初始化

   act.sa_handler=handler;//act里面的函数指针就是要捕捉使用的方法

   sigaction(2,&act,&oact);//输入型参数和输出型参数 
   
   //sigaction里的sa_mask是一个位图,用来屏蔽其他信号的
   sigemptyset(&act.sa_mask);
   sigaddset(&act.sa_mask,1);
   sigaddset(&act.sa_mask,3);
   sigaddset(&act.sa_mask,7);


   while(true)
   {
    cout<<"l am a process "<<getpid()<<endl;
    sleep(1);
   }
}

二.何时捕捉?

在这里插入图片描述

我们前面一直在将进程接收到信号,并不会立即处理,等到合适的时间再去处理,那么这个合适的时间到底是什么时候呢?

我们的操作进程呢它就会处理对应的信号啊,操作系统设定上呢,它就是给你发信号,他不参与你信号的处理啊,他默认给你设置好,你爱用你用,不用你别用,反正我通知你,这就是操作系统。
那么对于进程来讲,这个信号当然就要由他自己来处理了啊。
其实也是在操作系统的调度之下来处理啊。只不过操作系统给你精准处理信号提供了很大的宽容度啊

结论:就是当我们的进程从内核态返回到用户态时,对信号做检测和处理。
我们在进入操作系统之后,当执行我们定义代码时,操作系统工作做完了,我们在返回的时候对信号做检测和处理。
更重要的是为什么返回的时候呢?
因为决定了我把我当前比较重要的事情一定做完了。那么做完了的时候呢,我在返回时顺手把你的信号检查一次。
在这里插入图片描述

进程是会被调度的
只要你的进程代码一直在跑。那么我们的c p u就会调度你的进程。只要要调度你的时间片,必然会进行消耗完毕。
消耗完毕你的时间片必然是要把你的进程从c p u上剥离下来。
那么也必然的当下次调调你的时候,又要把你的进程什么乱七队列了,或者是什么运行列列拿到c p u开始跑。
当他把你的进程二次在调度的时候,不就是要由操作系统先把你的p c b地址空那个地址空间列表了,包括你的硬件设施全部恢复到CPU上,这肯定是在内核的。
然后把你调度的时候开始执行代码时,他跑的不就是你自己写的代码吗?所以他注定了一定会从内核态再回到用户态。
在你的代码的周期里面,它会有无数次机会,从用户到内核,从内核到用户。

1.陷入内核

CPU在调度你的进程时,执行代码时不只会执行你写的代码。比如说你的代码里还会有系统调用,包括你的代码里还有库函数调用,说明我们的CPU在执行你的代码时,它在运行时不仅仅在跑你写的代码,它也在跑库里曾经写的代码和操作系统曾经写的代码。

第二个呢,我们操作系统它既然就是不相信任何用户,所以在很多场景下呢,它是需要我们用户做一下相关的身份切换的。那么才允许你执行的对应的代码。
一般只是你自己写的代码,包括库的代码是啊大部分都是在用户态直接执行的。
有一些情况呢在操作系统,在c p u的调度之下,他要陷入到操作系统内来执行这个任务。
就比如说系统调用。

【自动身份转换】 就是当你们在调用系统调用时,你不要觉得你仅仅是调了这个函数陷入进去了。那么你得有资格陷入到系统调用。
所以操作系统会自动把你这个用户的身份从用户身份变成内核身份。然后由操作系统帮你把函数执行完。然后当返回时再把用户把你的身份从内核身份变成用户身份。

int 80,这个呢就是我们最经典的。x86结构下,英特尔CPU下,从我们对应的用户态陷入内核态。那么它对应的一条汇编语句。
所以一旦调用了之后呢成功了,那么你这个人就有权利去访问操作系统的代码和数据。

2.用户态和内核态

用户态到内核态这两个到底是什么意思呢?简单来说就是内核态是运行访问操作系统的代码和数据,而用户态只能访问用户自己的代码和数据!

电脑刚开机的时候,必须得把操作系统先起来,然后才能启动其他任务,操作系统呢实际这个内核空间啊要像那么我们对应到物理内存当中,它建立映射的时候,那该怎么建立映射?
是不是也需要一个东西叫做页表呀?
答案是是的,它需要有内核页表。
但是实际上操作系统比较复杂啊,那么操作系统呢它呢配置空间里和物理内存之间可以直接建映射。
我们用户这个地方,用户有他自己的用户的页表,映射到自己的代码和数据。
而我们这个内核呢有自己的内核级页表,映射到自己的啊操作性的代码数据。
在这里插入图片描述

整个地址空间是分成2部分,1GB的内核空间,3GB的用户空间。1GB的内核空间就是操作系统物理内存映射到地址空间上的数据。

站在进程视角。

每一个进程它都要有对应的一个自己的用户列表。那么有几个进程就会有几个这个用户级页表。因为我们对应的进程具有独立性
那么内核级页表有几份呢?
在操作系统层面上,内核级页表他要负责让我们进程看到操作系统相关的内容。那么每个进程都有它自己的地址空间,每一个进程都有0到3G是给自己的,3-4G是给操作系统的。
所以内核的页表只有一份。

在整个系统里。那么多进程。进程该怎么切换啊?也就是进程系统的切换啊,上面这一1G内容永远都不变,每一个进程那么想看这里面的内容的时候,那么看到的东西都是一样的。
所以从此往后,你不是有系统调用吗?
操作系统当中所有的方法,我每一个进程在调用这个方法时,系统调用就坏掉。
我呢就相当于在自己的地址空间里面调用该方法,调用完成后直接再返回到地址空间里。
也就是在地址空间里直接调用系统中的方法,哪还有系统调用,
在我看来系统当中的方法啊就是在我自己的地址空间中。
在这里插入图片描述

站在操作系统的角度
那么站在操作系统的角度,那么对应的想管理任何一个进程。
我啊因为那么我们要明白一件事情,在我们的系统当中,任何一个时刻都会有进程在调度。

为什么呢?操作系统啊那么它也会有自己对应的进程哦。 一号进程那么它是在我们开机启动的时候,它自己就起来了。
说明操作系统它本身也是一个,其实它就是一个进程。

它是个进程,它也要地址空间,那么它甚至可以不要这个用户空间,只要自己剩下的空间。
好,另外即便我不执行操作系统代码,我执行任意一个进程,只要它有进程在进行调度执行。
我们想执行想执行什么呢?叫做操作系统的代码,那么就可以啊叫做就可以随时执行。
因为我们拿着每个拿这个进程。
那么它的p c b里面地址空间呢都能够有能让我找到我对应的内核页表,我的映射空间,我就能找到操作系统的数据。
所以我周期性的一些任务,我的调度操作,我的执行,我都可以周期性去做,即便是不做我也可以就是只要操作系统的调度,我就可以随便拿一个进程,那么拿着他的p c b,我拿着他的地址空间找到他的物理内存。找到对应的那个页表,访问到对应的数据。在这里插入图片描述

. 站在CPU角度
在这里插入图片描述

3.操作系统的本质

在我们系统当中每一个进程都可以让我找到操作系统。
好,因为它的地址空间全部3-4G每一个进程都映射到我们的操作系统当中了。

那么操作系统的本质是什么呢?
是一个基于叫做时钟中断的一个死循环。

在我们的计算机里面呢,它都有一个叫做芯片单元。 它以非常高的频率定期向操作系统发送时钟中断。它向操作系统或CPU发送时钟中断。
那么而CPU一旦接收到了时钟中断,就要执行我们的中断所对应的方法。那么这个中断所对应的方法就是操作系统的代码。

一旦收到了你对应的这个使用中断的这个信息了,操作系统内会执行我们所对应的中断向量表。
操作系统它就是前期在启动的时候,把它该做的工作全做完。
然后往后它自己就执行一个叫死循环。
那么当它执行一个while死循环的时候呢,那么它其实在那里干什么呢?
他就要帮我们去检测当前有没有时钟了。
一旦有时钟到来了,我们对应的CPU它就会执行我们对应的时间中断对应的方法。
那这个方法是什么呢?我们叫做比如说检查之类的工作。

往后整个操作系统它都靠着你对应的时钟中断来驱动。
好,也就是说你有外设中断了来了,那么我们操作系统就去执行对应的中断一段时间时钟中断给我发来的时候,然后我的操作系统就要执行对应的方法。
比如多调度多执行好,换句话说有硬件我们就能执行。
所以操作系统的本质就是一个死循环。
它的核心代码,最后操作系统会卡在我们对应的for循环这里。
收到之后呢就会逼着你操作系统一直在,你不是在等吗?
CPU就会一直跑过来执行你的调度,执行调度,执行调度。
所以正因为硬件它一直在推着操作系统在走,所以你的操作系统才推着你的进程在走。

它会推着你的进程带走,所以你的代码才会得以推进。

所以操作系统它本质是基于时钟段设置的一个死循环。
在这里插入图片描述

三.周边细节问题

1.pending位图什么时候由1->0?

在执行信号捕捉方法的之前啊,操作系统就会将该信号的pending表对应的比特位由1置0。
在这里插入图片描述

3.可重入函数

什么叫重入呀?就是多个执行流同时访问该函数。就叫做重入了。
在这里插入图片描述
一个函数在被重复进入的情况下,那么如果出问题,这样的函数我们称之为不可重入函数。大部分函数百分之九十,全都是不可重入的。

4.volatile

只要是计算,那么就要在c p u当中判断的。
那么c p u呢它呢在计算判断的时候呢,就发现你这个地方只对变量做检测,并不计算。就会对该变量做优化条件。变量啊就可能被直接优化到我们c p u那个寄存器中。
因为变量只是单纯的在做检测,并没有对变量做任何修改。CPU它就会发现这个变量只是做读取。所以就会把它优化到计算机当中,让c p u直接不用缓存,而在c p u寄存器当中直接读取。

这个变量它本身就是我们定义的全局变量。那么它一定在内存里会存在。
它的内容在优化的时候,无非就是未来我们不要循环执行的时候,把这个变量从内存获取检测。而是读取变量的寄存器,然后呢我们做逻辑检测。

所以编译器优化的时候,他就直接不再把数据从我们对应的呃变量的内容从内存里来了。
而是在计算器里直接用,每次都是读取这样的一个我们叫做我们的寄存器啊检测到。因为这个工作不需要保存了,所以它的效率就高了,所以它这个优化好。
c p u已经优化了,每次检测都只会在计算机里检测就可以了。
所以此时我们在做c p u检测时,他每次都只读取c p u寄存器的值,不再读内存。
不过这样就会出现一些问题:在这里插入图片描述

虽然你的信号已经把存量值改成一了,只是改内存不容易对寄存器没做任何修改。
所以你最终发现你的代码在优化的情况下退不出来了。
而volatile的作用就是保存内存的可见性,不让CPU直接从寄存器上获取,而还是从内存中获取;在这里插入图片描述

5.SIGCHILD信号

在这里插入图片描述
子进程在退出之前,会发送17号信号给父进程的!
这样我们就可以实现一个基于信号捕捉进行进程等待的工作了,将进程等待封装在信号捕捉里,那么当子进程退出后,发送信号就会自动捕捉信号实现等待。
在这里插入图片描述
在这里插入图片描述

文章来源:https://blog.csdn.net/Extreme_wei/article/details/135203207
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。