Linux:进程信号---信号的保存与处理

文章目录

      • [1. 信号的保存](#1. 信号的保存)
        • [1.1 信号的状态管理](#1.1 信号的状态管理)
      • [2. 信号的处理](#2. 信号的处理)
        • [2.1 用户态与内核态](#2.1 用户态与内核态)
        • [2.2 信号处理和捕捉的内核原理](#2.2 信号处理和捕捉的内核原理)
        • [2.3 sigaction函数](#2.3 sigaction函数)
      • [3. 可重入函数](#3. 可重入函数)
      • [4. Volatile](#4. Volatile)
      • [5. SIGCHLD信号](#5. SIGCHLD信号)
  • 序:在上一章中,我们对信号的概念及其识别的底层原理有了一定认识,也知道了信号产生的五种方式,以及core dump是什么,而本章将着重对信号的保存与处理进行讲解,去深入了解信号处理的底层逻辑,去了解什么是用户态和内核态,以及用户态和内核态转换的时机,本章还会浅谈可重入函数以及Volatile关键字

1. 信号的保存

1.1 信号的状态管理

对于普通信号而言,对于进程(是给进程的PCB发)而言,要识别自己有没有收到信号,以及收到了哪一个信号。

复制代码
task_struct{
int signal;// 0000 ...... 0000普通信号,位图管理信号
}

1. 比特位的内容是0还是1,表明是否收到
2. 比特位的位置(第几个),表明信号的编号
3. 所谓的 "发信号" ,本质就是操作系统去修改task_struct的信号位图对应的比特位(写信号!!!)

问题一:信号为什么要保存?

进程收到信号之后,可能不会立即处理这个信号。此时信号不会被处理,就要有一个时间窗口。信号的范围1,31,每一种信号都要有自己的一种处理方法.

信号的几种状态:

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

信号在内核中的表示示意图:

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

sigpending函数:

其中的set参数是一个输出型参数,他会将当前的pending表传出。

sigprocmask函数:

第一个参数how:

参数 含义
SIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号
SIG_UNBLOCK set包含了我们希望从当前信号屏蔽字中解除阻塞的信号
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值

第二个参数:表示要传入的block阻塞表
第三个参数:表示之前的block旧表

问题二:我要是将所以信号都进行屏蔽,信号不就不会被处理了吗?

然而操作系统不会给你这个机会的,比如9号信号和19号信号,用户就屏蔽不了

2. 信号的处理

2.1 用户态与内核态

问题一:信号是什么时候被处理的?

当我们的进程从内核态返回到用户态的时候,进行信号好的检测和处理
当我们在调用系统调用时,操作系统会将我们的用户身份转化为内核身份,然后由操作系统帮我把函数执行完,返回时,再把我的内核身份换回用户身份 ------ 操作系统是自动会做"身份"切换的,用户身份变成内核身份,或者反着来!

问题二:什么是用户态和内核态?

内核态:允许你访问操作系统的代码和数据
用户态:只能访问用户自己的代码和数据

2.2 信号处理和捕捉的内核原理

问题三:信号是如何被处理的?

操作系统不信任用户,不仅仅体现在不让用户访问自己,也体现在操作系统不会访问用户自己写的代码!!!所以当在内核态处理信号时,会先将该信号的pending置0,然后去执行,要是执行到了自定义行为,那么进程就要先由内核状态转化为用户态,去执行这个自定义行为,然后再变为内核态继续在操作系统中执行系统操作,然后再回到用户态,返回值。(基于用户捕捉代码)

问题四:内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

在这个过程中,一共发生了四次状态的转换,所谓的信号的识别,其实就是在进程进入内核态时,顺手完成的任务。CPU内部的信号int 80(是一条汇编语句) 从用户态陷入内核态,这样就有权利去访问操作系统的数据了。

2.3 sigaction函数

sigaction函数:

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);
	};

由于我们目前只研究普通信号,所以,该结构体当中的第二、第四和第五个参数我们不做讨论,感兴趣的小伙伴可以自行去搜索。
该函数的第一个参数是要处理的信号数字,第二个参数表示要传入的sigaction结构体,第三个参数是输出型参数,他会保存上一次的sigaction结构体的数据。

直观的代码和运行结果能让我们直接看到这个函数的作用:

复制代码
#include<iostream>
#include<cstring>
#include<signal.h>
#include<unistd.h>

using namespace std;

void PrintPending()
{
    sigset_t myset;
    sigpending(&myset);
    for(int signo=31;signo>=1;signo--)
    {
        if(sigismember(&myset,signo)){
            cout<<"1";
        }else{
            cout<<"0";
        }
    }
    cout<<endl;
}

void handler(int signo)
{
    while(true)
    {
        PrintPending();
        cout<<"signal : "<<signo<<" acted"<<endl;
        sleep(1);
    }
}

int main()
{
    signal(SIGINT,handler);
    struct sigaction myset,oset;
    memset(&myset,0,sizeof(myset));
    memset(&myset,0,sizeof(oset));
    
    //sigemptyset(&myset.sa_mask);
    //sigaddset(&myset.sa_mask,SIGINT);
    
    myset.sa_handler=handler;
    sigaction(SIGINT,&myset,&oset);
    
    while(true)
    {
        cout<<"i am process: "<<getpid()<<endl;
        sleep(1);
    }
    
    return 0;
}

运行结果如下:

由上图可知,在处理某个信号之前,该信号的pending表对应的值会先置0,然后才会执行对应的处理行动。
总结:如果某个信号正在进行处理,那么,在这个期间,这个信号的信号屏蔽字将会变成1,此时,无论外界再发送多少个该信号,都不会执行,只会将该信号对应的pending表中的值置1,但是不是执行,因为此时该信号已经阻塞了,这就防止了当该信号处理时,被重复执行,之后当这个信号处理完毕,才会取消阻塞,然后继续执行之前发的信号,并在信号执行期间继续阻塞该信号!!!

3. 可重入函数

当我们执行insert函数时,进行到一半,执行完p->next=head;语句后,触发了信号,执行此时信号中也有insert函数,然后执行信号中的insert,执行信号处理后,再继续执行main函数中的语句head=p;此时,由于函数重入,引发节点丢失,导致内存泄漏!!!
如果一个函数,被重复进入的情况下,出错了,或者可能出错,就叫做不可重入函数。否则就叫做可重入函数。

4. Volatile

在g++中是有优化选项的,编译器在编译的时候,有不同的优化级别

源代码:

复制代码
#include<iostream>
#include<cstring>
#include<signal.h>
#include<unistd.h>

using namespace std;

int flag=0;

void handler(int signo)
{
    cout<<" change flag 0->1 "<<endl;
    flag=1;
}

int main()
{
    signal(SIGINT,handler);
    while(!flag);
    cout<<"process quit"<<endl;
    
    return 0;
}

1. O0的优化:

复制代码
sigproc:sigproc.cpp
	g++ -o $@ $^ -O1 -g -std=c++11

结果如图:

2. O1的优化:

复制代码
sigproc:sigproc.cpp
    g++ -o $@ $^ -O1 -g -std=c++11

结果如图:

3. O3的优化:

复制代码
sigproc:sigproc.cpp
    g++ -o $@ $^ -O3 -g -std=c++11

结果如图:

问题一:为什么优化过后,程序退不出去?


此时volatile int flag=0;使用volatile关键字修饰flag就能防止编译器过度优化,保持内存的可见性!!!

5. SIGCHLD信号

当子进程退出时,不是静悄悄的退出,子进程在退出的时候,会主动向父进程发送SIGCHLD(17号)信号。所以在进行进程等待的时候,我们可以采用基于信号的方式进行等待

问题一:等待的好处是什么?

1. 获取子进程的退出状态,释放子进程的僵尸。
2. 虽然不知道父子进程谁先运行(由调度器决定),但是我们清楚,一定是父进程先退出!!!
所以,还是要调用wait/waitpid这样的接口,且父进程必须保证自己是一直在运行的!

问题二:我们必须等待吗?必须调用wait吗?

Signal(17,SIG_IGN);表示忽略17号信号,子进程会自动退出!!!

总结:

本章带大家理解了信号的保存与处理,知道了信号的不同的保存状态,以及处理信号时的内核态和用户态的转化原理,还扩展了可重入函数和Volatile关键字,以及SIGCHLD信号,最后,希望这篇文章对大家有一定帮助。

相关推荐
讯捷蓝达14 分钟前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
且从容.1 小时前
ubuntu安装flink
linux·ubuntu·flink
数字新视界3 小时前
动环监控信创适配在智能机房管理中的应用价值与未来展望
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
爱和冰阔落3 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨3 小时前
WSL2的安装与使用
linux·wsl
微小的鱼3 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng3 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮4 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
上火的金鱼妹4 小时前
k8s生产实际问题排查(网关,数据库读写等)
运维·服务器
孙克旭_4 小时前
k8s基础组件与创建资源
运维·docker·容器·kubernetes