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信号,最后,希望这篇文章对大家有一定帮助。

相关推荐
19778354622 分钟前
Linux系统安全
linux
FBI HackerHarry浩29 分钟前
Linux云计算训练营笔记day13[CentOS 7 find、vim、vimdiff、ping、wget、curl、RPM、YUM]]
linux·运维·笔记·centos·云计算
*星星之火*30 分钟前
【GPT入门】第39课 OPENAI官方API调用方法
java·服务器·gpt
煤灰24233 分钟前
简单的基于sqlite的服务器和客户端实现
服务器·sqlite
wqqqianqian43 分钟前
国产linux系统(银河麒麟,统信uos)使用 PageOffice实现PDF文件加盖印章和签字功能
linux·pdf·签字·国产·pageoffice·盖章
小诸葛的博客1 小时前
Flannel后端为UDP模式下,分析数据包的发送方式(一)
linux·运维·服务器
一只码代码的章鱼1 小时前
操作系统 第四章 -1
linux·服务器·数据库
末央&1 小时前
【Linux】进程状态
android·linux·运维
独行soc1 小时前
2025年渗透测试面试题总结-快手[实习]安全工程师(题目+回答)
linux·安全·web安全·网络安全·面试·职场和发展·渗透测试
XY.散人1 小时前
初识Linux · NAT 内网穿透 内网打洞 代理
linux·服务器·网络