Linux进程信号(产生、保存、处理)/可重入函数概念/volatile理解/SIGCHLD信号
首先区分一下Linux信号跟进程间通信中的信号量,它们的关系就犹如老婆跟老婆饼一样,没有一毛钱的关系。
信号的概念
信号的概念:信号是进程之间事件异步通知的一种方式,属于软中断。比如:红绿灯是一种信号,早上的时候妈妈催我起床是一种信号、下课铃声也是一种信号等等。我们需要有一个共识,那就是信号是给进程发的。
学习Linux进程信号,我们学习的是信号的预备知识+信号是如何产生的+信号是如何保存的+如何处理信号,即信号的整个生命周期。

系统定义的信号
使用kill -l命令,可以查看到Linux中的系统定义的信号。我们可以看到,在这些信号当中,分有[1,31]和[34,64]两个连续区间的信号编号。其中[1,31]的信号称为普通信号,[34,64]的信号称为实时信号。

除此之外,当我们在Shell下启动了一个前台进程后,使用Ctrl+c命令中断这个进程。其中, Ctrl+c便是一个信号!因为操作系统把Ctrl+c解释成kill中的2号信号:SIGINT。
信号的预备知识
红绿灯信号例子:
我们拿红绿灯举例子。红绿灯是一个信号,那么人是能够识别红绿灯的,于是就会产生识别+行为这两个过程,比如说识别到这个是人行道上的绿灯,意味着行人可以过了。
那么就有以下四个关于信号的性质:
①人是如何识别红绿灯的?是因为受过相关教育,当我们的大脑记住了红绿灯的属性以及引导我们判断接下来的行为。---------识别。
②当我们接受到了红绿灯的信号后,我们不一定马上去处理这个信号,立即去执行相应的行为,或许我还得回头跟朋友告别了再去处理这个行为。-------行为。
③跟朋友告别后,我们就会去处理来自红绿灯的信号。在此之前,我们会将这个信号进行保存。--------信号的保存
④对于处理这个红绿灯的信号,我们一般会有三种处理方式:第一种是默认动作,即马上过马路。第二种是自定义动作,如果我们从小就被教育,过马路前要看一看两边再过马路。这个看一看马路两边的行为就是自定义动作。第三种是忽略动作,就是看到了红绿灯,但是我忽视它,因为我不打算过马路。-------信号的处理
接下来我们把红绿灯信号转化成进程信号:
①进程能够识别信号,是通过程序员编程写出来的。因为进程本身就是被程序员编写的属性+逻辑的集合。
②进程在接受信号后,有可能在执行着更重要的的代码,所以信号不一定能够被立即处理。
③进程本身就要有保存信号的能力。
④进程在处理信号的时候,有三种处理方式:默认动作、自定义动作和忽视动作。进程处理信号称为信号被捕捉。
一句话总结:进程能够识别信号通过程序员编码完成的,接受到信号不一定会马上处理,因此就需要有保存信号的能力,当处理这个信号的时候有三种处理方式。
保存信号
保存的地方
信号是给进程发送的,那么进程就应该具备保存信号的能力。而进程保存信号的地方,就是进程PCB中。

保存的方法
我们学习的是kill中的普通信号,即[1,31]区间的信号。因此,有32个信号,在PCB中,使用信号位图的比特位来表示信号的编号和判断是否接受到信号。通过比特位的位置来表示信号的编号,通过比特位的内容来代表是否接受到信号,0代表没有,1代表有。

理解什么是发送信号
发送信号不能理解为从A处发送到B处。发送信号的本质就是对PCB中的信号位图的修改!因此,我们看到发送信号的时候,不要往谁向谁发送了一个信号方向想,而是应该意识到是进程的PCB中的信号位图被修改了!
OS在其中扮演的角色
OS是PCB的管理者,也只有OS有资格对信号位图进行修改!因此,我们学习到的发送信号的方式,本质上都是通过OS提供的系统调用去向目标进程发送信号,由OS去修改位图的比特位。比如说kill命令,其底层就是调用了系统接口。
产生信号
通过终端按键产生信号
一个死循环的进程,我们可以通过按键Ctrl+c,或者Ctrl+\来进行终止进程。也可以通过kill -2 pid 或kill -3 pid终止进程。这些都是终端按键产生信号。

通过系统调用产生信号
系统调用接口
⭐1.kill接口
函数原型:int kill(pid_t pid, int sig);
头文件:#include <sys/types.h> #include<signal.h>
参数:第一个参数pid是接受参数的进程的pid 第二个参数是传入的是几号信号
返回值:返回0代表成功,否则返回-1
测试代码:
①用于调用kill产生信号的源代码:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>static void Usage(const std::string &proc)
{std::cout<<"\nUsage: "<<proc<<"pid signo\n"<<std::endl;
}//在命令行上输入: ./mysignal pid(进程的pid) signo(信号的编号) 此时argc==3
int main(int argc,char* argv[])
{//当argc等于3的时候,跳过下面这条执行语句if(argc != 3){Usage(argv[0]);exit(1);}//将pid和signo转化成整型pid_t pid = atoi(argv[1]);int signo = atoi(argv[2]);//通过kill产生信号int n = kill(pid,signo);//如果失败,就输出killif(n!=0){perror("kill");}return 0;
}
②用于测试的源代码:
#include <iostream>
#include <sys/types.h>
#include <unistd.h>int main()
{while(true){std::cout<<"我是一个正在运行的进程,pid: "<<getpid()<<std::endl; sleep(1);}return 0;
}
结果如下:

结论:信号是由用户通过系统调用发起,由操作系统执行的。
⭐2.raise接口
函数原型:int raise(int sig);
函数功能:将信号发送给调用者,即给自己发送信号
头文件:#include<signal.h>
参数:参数是传入的是几号信号
返回值:返回0代表成功
测试代码:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>int main()
{//raise()给自己发送任意信号int cnt = 0;while(cnt<=10){printf("cnt: %d\n",cnt++);sleep(1);//当到达5的时候,调用raise,发送3号信号SIGQUIT,终止进程if(cnt>=5) {raise(3);}}//kill()可以给任意进程发送任意信号return 0;
}
结果:

其实raise()将信号发送给自己,kill也可以做到。
kill(getpid(),signal);
⭐3.abort()接口
函数原型:void abort(void);
函数功能:给自己发送指定的信号:SIGABRT,6号信号
头文件:#include<stdlib.h>
测试代码:将raise()替换成abort()
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
#include <stdlib.h>
#include <sys/types.h>int main()
{//raise()给自己发送任意信号int cnt = 0;while(cnt<=10){printf("cnt: %d\n",cnt++);sleep(1);//当到达5的时候,调用raise,发送3号信号SIGQUIT,终止进程if(cnt>=5) {abort();//raise(3);}}return 0;
}
结果:

三种系统调用总结:
①kill是给任意进程发送任意信号
kill(pid,signo);
②raise是给自己发送任意信号
kill(getpid(),signo);
③abort是给自己发送指定的信号:SIGABRT
kill(getpid(),SIGABRT);
对于信号的处理,在很多情况下,进程接受到的大部分信号,默认动作都是终止进程。虽然信号有很多种,信号的不同,是代表着事件的不同的,但对事件发生之后的处理动作是一样的。就跟程序抛异常一样,抛异常的原因有很多种,但是最终结果都是让程序终止。因此,我们加下了分析信号产生的另一种情况:硬件异常产生信号。
硬件异常产生信号
除0造成的异常
先来看一个小小的测试代码:
#include <iostream>
#include <unistd.h> int main()
{while(true){std::cout<<"我在运行中..."<<std::endl;sleep(1);int a = 10;a/=0;}return 0;
}
结果如下:

这种错误我们都是知道的,那是因为除0了。那为什么除0就会使进程终止了呢?
当除0的时候,进程会收到来自操作系统的信号,这个信号是8号信号,SIGFPE。下面使用代码来证明这一点:
使用signal接口来捕捉SIGFPE信号并对其自定义,当这个信号被发送给进程,进程处理这个信号的时候,不再是默认终止进程,而是执行自定义动作。
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
#include <stdlib.h>
#include <sys/types.h>void catachSig(int signo)
{std::cout<<"获取到一个信号,信号编号是: "<<signo<<std::endl;
}int main(int argc,char* argv[])
{//自定义信号,将SIGFPE捕捉signal(SIGFPE,catachSig);// int a = 10; //放外面// a/=0;while(true){std::cout<<"我在运行中..."<<std::endl;sleep(1);int a = 10; //放里面a/=0;}return 0;
}
结果如下:会一直打印以下语句。没有终止进程。后续将代码拿出while循环,只执行一次除0操作,但结果依旧如下。

通过上面的测试,有以下两个问题:
①为什么只执行一次除0操作跟不断执行除0操作的结果是一样的?即我只执行了一次除0操作,为什么进程不断处理SIGFPE信号?
②操作系统怎么知道我除0了?
这一块就跟硬件有关系了。接下来,我们通过硬件来分析除0操作。
在CPU中,进程中的数据运算在其中计算,计算出来的结果存放在寄存器中,此时会判断结果是否合理,即有没有溢出等等。而在CPU内部,有一个叫做状态寄存器的寄存器,当状态寄存器中的溢出标志位从0变为1,说明数据溢出。
在除0的例子中,10除0,是可以被计算的,0被看成无穷小,溢出结果会溢出,结果会非常非常大,寄存器无法保存,于是就随便保存一点或者不保存,然后状态寄存器将其标记溢出。此时就是CPU运算异常了,此时操作系统自然知道了这件事。

状态寄存器中的溢出标记为从0置为1,操作系统就会马上识别到CPU内部出错了,然后操作系统会看看是谁导致CPU出错的,噢,是这个进程,因为正是这个进程正在调度这个CPU,于是,操作系统就知道了:①CPU运算异常。②是除0的进程导致的。于是向这个进程发送8号信号去终止这个进程!
解决了上面的第二个问题,再来看看第一个问题,为什么只执行一次除0操作,信号却一直被自定义处理。
从上面的测试代码的事实看到,收到信号后,进程不一定是终止的,一个没有终止的进程,就可能还会一直调度CPU,一直调度CPU,CPU中的状态寄存器的溢出标志位就会一直从0被置为1!此时操作系统就会不断地向进程发送8号信息。
野指针造成的异常
对空指针进行解引用,即野指针问题,也会使硬件异常产生信号。
测试代码如下:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>int main()
{while(true){std::cout<<"我在运行中..."<<std::endl;sleep(1);int *p = nullptr;*p = 100;}return 0;
}
结果如下:

为什么野指针使进程会崩溃?
野指针的问题,会使进程收到11号信号:SIGSEGV。11号信号的作用也是终止进程,事件为非法的内存引用。我们来证明一下:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>void catachSig(int signo)
{std::cout<<"获取到一个信号,信号编号是: "<<signo<<std::endl;
}
int main()
{//自定义信号,将SIGFPE捕捉signal(11,catachSig);while(true){std::cout<<"我在运行中..."<<std::endl;sleep(1);int *p = nullptr;*p = 100;}return 0;
}
结果如下:

操作系统是如何知道发生了野指针的问题的呢?
因为*p指向的地址是虚拟地址,当需要映射到物理地址空间的时候,是通过页表+CPU中的MMU寄存器去映射访问物理地址空间的。
当访问0号地址的时候,就是越界访问的时候,MMU就会发生异常,此时操作系统就会立马将这个异常接受,并且发送11号信号给进程,使得进程终止!

总结硬件异常从而产生信号:硬件异常是因为进程的不恰当操作,导致进程调度的CPU异常,操作系统通过这个异常情况给调度CPU的进程发送终止信号。这种信号的产生即没有通过终端按键产生,也不是使用系统调用产生的。
软件条件产生信号
软件条件产生信号的场景比如说:在使用管道进行进程间通信的时候,如果将读端关闭,而写端一直在写,操作系统就不允许这样的行为,此时就会发送SIGPIPE信号去终止写端的进程!这种即没有通过终端按键发送信号,也没有通过用户系统调用发送信号,也没有通过硬件的异常发送信号的场景,就是软件条件产生信号的情况。这个是管道的情况。那么接下来,我们使用定时器软件条件来感受一下软件条件产生信号的情况。
我们使用alarm函数来设定一个闹钟,当闹钟响铃后,进程会收到SIGALRM信号,进而终止进程。
函数原型:unsigned int alarm(unsigned int seconds);
函数功能:设置一个时钟来发送信号,发送的信号为SIGALRM
头文件:#include<unistd.h>
参数:参数就一个,按秒为单位
返回值:这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
测试代码:
#include <iostream>
#include <unistd.h> int main(int argc,char* argv[])
{//设立闹钟//执行到alarm函数的时候,并没有立刻发送信号//而是1秒之后再发送信号alarm(1);int cnt = 0;while(true){std::cout<<"cnt: "<<cnt++<<std::endl;}return 0;
}
结果如下:执行程序一秒钟后便发送SIGALRM信号终止进程。

这份程序的意义是统计我们的计算机在一秒钟的时间里能够将数据累加多少次。可以看到,可以累加到6万多。
接下来改变一下这个代码,改变的是先不执行输出,而是先让它不断累积起来,然后捕捉SIGALRM信号,一秒钟后再将结果打印出来。
测试代码:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdio>
#include <stdlib.h>
#include <sys/types.h>int cnt = 0;void catachSig(int signo)
{std::cout<<"获取到一个信号,信号编号是: "<<signo<<"累加结果为: "<<cnt<<std::endl;//exit(1);没有退出
}int main(int argc,char* argv[])
{//设立闹钟//执行到alarm函数的时候,并没有立刻发送信号//而是1秒之后再发送信号//捕捉信号signal(SIGALRM,catachSig);alarm(1);while(true){cnt++;}return 0;
}
结果如下:这次统计出来的数量达到5亿!

这两种情况的计算机速度差了一万倍左右!原因是第一种情况不断地打印,即不断地访问了外设,外设的速度是很慢的!第二个例子是借助了alarm函数来感受了IO的慢。
alarm函数设立的闹钟只会响一次,也就是说只会发送一次信号,即使没有终止进程。
对于闹钟来说,任意一个进程都可以通过alarm系统调用在内核中设置闹钟,因此操作系统中会存在许多闹钟,操作系统就会把这些闹钟管理起来。如图:

进程退出时的核心转储问题
在说核心转储问题前,我先认识到,我是在云服务器上使用的Linux系统。然后我们再去看看信号,终止进程的信号的动作有两种:Term和Core。
Term和Core都是终止进程的意思,不同的是Term将进程终止了就没后续动作了,而Core将进程终止后,会进行核心转储。但是这个我们看不了,因为云服务器默认关闭了core file选项。
使用 ulimie -a查看:

因此,我们可以自己动手设置一下:

我们通过一个小小的测试来看看:
int a[10];a[10000]= 123;

core dumped就是核心转储。核心转储的意思是当进程出现异常的时候,会在进程对应异常的时刻将内存中有效数据转储到磁盘中。
我们可以看到上面的结果中,出现了一个core.17358。其中,core就是核心转储,17358是对应进程的pid。
core.17358的作用是支持调试。在gdb中,可以直接找到出现异常的代码:
core-file core.XXX
信号的保存
阻塞信号
一些概念:
实际执行信号的处理动作称为信号递达(Delivery)。
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
信号在内核中的表示
在内核数据结构中的信号,每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作handler。
pending位图中,比特位的位置代表着信号编号,比特位的内容,即0或1,代表着是否接受到信号。
block位图,比特位的位置也代表着信号编号,比特位的内容则是代表着是否阻塞信号。比如,在block位图中,4号位置的比特位为1,说明4号信号被阻塞了,因此4号信号无法递达,除非解除阻塞。也就是说,阻塞是提前阻塞的,提前将未来不想接受的信号阻塞。
handler数组中,其下标表示信号的编号,下标对应的内容存放的是信号的处理方法。

这就是信号在内核中的基本数据结构构成。
当我们使用signal函数自定义信号的时候,比如signale(signo,handler);就是通过signo找到这个下标,然后把handler的地址填入数组中。
当需要处理信号的时候,操作系统会根据pending位图结构,找到信号的编号,然后根据这个编号去数组中找到这个编号对应的处理方法。
结论:①如果一个信号没有产生,并不妨碍它可以先被阻塞。②进程识别信号,是由程序员在设计信号机制的时候,为进程设计了pending位图、block位图和handler表。这三个结构组合起来,就能去识别信号。
捕捉信号
上文说到,信号产生的时候,是在合适的时候才会去处理信号。那么这个适合的时候,就是内核态返回用户态的时候。
用户态:以用户的身份去使用操作系统自身的资源和硬件资源。
说明:用户要使用这些资源(访问内核或硬件资源),就必须通过系统调用。
内核态:以内核的身份去访问这些资源,但实际上执行这些系统调用的“人”是进程本身。
说明:系统调用往往会比较费时间,因此尽量避免调用系统调用。

进程怎么知道自己是内核态还是用户态呢?
进程的数据代码都会存放在CPU中,CPU中有许多的寄存器,其中一个叫CR3的寄存器就是表征当前进程的允许级别,对应的数字如果为0,那么就是内核态,如果为1那么就是用户态。

进程如何跑到操作系统中执行方法?
内核级页表和用户级页表
在虚拟地址空间中,我们一直所说的栈堆、常量区等等都是在用户空间中的。在虚拟地址空间中,还存在着内核空间。
每一个进程PCB可以通过页表,让虚拟地址空间跟物理空间建立映射关系,其中,用户空间使用的页表称为用户级页表。同样的,内核空间使用的页表称为内核级页表。
对于用户级页表来说,每一个进程都有自己独立的用户级页表,这样就能让每一个进程都能够通过自己的页表访问内存空间。但是内核级页表是让虚拟地址空间与物理地址空间中存放操作系统数据和代码的建立映射关系的,在计算机启动的时候,操作系统作被加载到了内存中,只有一份,是独一无二的。因此,内核级页表只有一份,每一个进程共享这一份。
访问步骤
因此,跟加载动态库,使用动态库的接口一样,当进程要访问OS的接口的时候,只需要在自己的进程空间的用户空间上跳转到内核空间,然后通过内核页表映射到内存中即可,让执行操作之后,返回到原本的空间即可,此时需要把CR3中对应的数字由0改为3。
那么用户能够去访问内核的接口或数据,是因为CPU中的CR3中对应的数字是0.而由用户态转成内核态,从3到0的操作,在调用系统调用的时候自动完成。

于是,我们了解了进程是如何从用户态转化成内核态了。进入内核态后,接下来就看看是如何进行信号捕捉的。
捕捉信号
当进程从用户态转到内核态后,并且执行完系统调用,此时并没有马上返回,本着来了都来了,不能就这么回去,于是会去检查block、pending和handler表。
首先检查block位图,从起始位置开始,如果是1,那么往下找,找到为0的时候,就转到pending位图去找,如果是0,那么直接返回block位图中继续找,如果是1,那么就转到handler表中找相应的信号的处理方法。
处理方法有三种:默认动作、忽略动作和自定义动作。
默认动作就是直接终止进程,忽略动作就什么也不干,就返回回去。若是自定义动作,则会转到这个方法中执行代码。
具体流程图

简化:

找到的图:

使用信号集操作函数实现信号保存的测试代码
接下来将使用信号集操作函数,将上面关于信号保存的理论测试一下,达到知行合一。
代码功能:在开始的时候没有在终端按键产生信号,此时会将pending位图中的比特位打印出来,此时打印的应该是全0。当按下Ctrl+c(或者别的信号)的时候,位图对应的比特位的位置的内容由0变1,接着通过自定义动作,不让进程终止。接着取消对信号的屏蔽,此时再次打印全0。
信号集操作函数:
类型:sigset_t。
sigset_t: 从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
#include <signal.h>
int sigemptyset(sigset_t *set);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有
效信号。int sigfillset(sigset_t *set);
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系
统支持的所有信号int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
sigaddset和sigdelset在该信号集中添加或删除某种有效信号。这四个函数都是成功返回0,出错返回-1。int sigismember(const sigset_t *set, int signo);
sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
sigprocmask()函数的屏蔽字。

测试代码:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#define BLOCK_SIGMAL 2
#define MAX_SIGNUM 31static void show_pending(const sigset_t &pending)
{for(int signo = MAX_SIGNUM;signo >= 1;signo--){if(sigismember(&pending,signo)){std::cout<<"1";}else{std::cout<<"0";}}std::cout<<std::endl;
}static void myhandler(int signo)
{std::cout<<signo<<" 号信号已经被递达!"<<std::endl;
}int main()
{//捕捉信号,自定义动作signal(BLOCK_SIGMAL,myhandler);//1.先尝试屏蔽指定的信号sigset_t block,oblock,pending;//1.1初始化sigemptyset(&block);sigemptyset(&oblock);sigemptyset(&pending);//1.2添加要屏蔽的信号sigaddset(&block,BLOCK_SIGMAL);//1.3开始屏蔽,设置进内核(进程PCB)sigprocmask(SIG_SETMASK,&block,&oblock);//2.遍历打印pending信号集int cnt = 10;//计数while(true){//2.1初始化sigemptyset(&pending);//2.2获取sigpending(&pending);//2.3 打印show_pending(pending);sleep(1);if(cnt--==0)//十秒后,消除对信号的屏蔽{sigprocmask(SIG_SETMASK,&oblock,&block);}}return 0;
}
结果:

sigaction
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
act结构体:

它与signal功能类似,通过修改act.handler表来自定义动作。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。
如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
测试代码如下:
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>void handler(int signo)
{//循环测试while(1){printf("get a signo: %d\n", signo);sleep(1);}
}int main()
{struct sigaction act;memset(&act, 0, sizeof(act));//修改handler表act.sa_handler = handler;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, 3);//本质是修改当前进程的的handler函数指针数组特定内容sigaction(2, &act, NULL);while(1){printf("hello bit!\n");sleep(1);}return 0;
}

可重入函数概念
拿链表的插入操作举例子。当我们进入main函数,进入insert方法对链表进行节点头插,但当执行到head=p,即将新节点的地址交给头节点的这一步之前,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了,从而导致内存泄漏。

因此,对于像insert这样的函数一旦重入,就可能会导致问题的,表示该函数不可重入。而如果某个函数重入后,没有问题发生,表示该函数可重入。
不可重入的情况:
①调用了malloc或free,因为malloc也是用全局链表来管理堆的。
②调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
volatile
我们站在信号的角度上理解volatile关键字吧。
测试代码:
#include <iostream>
#include <signal.h>//全局变量
int flag = 0;void handler(int signo)
{//修改值flag = 1;std::cout<<"change flag 0 to 1"<<std::endl;
}int main()
{signal(2,handler);while(!flag);std::cout<<"这个进程是正常退出的"<<std::endl;return 0;
}
分析一下代码:进入main函数后,捕捉2号信号,自定义动作为handler。然后继续往下执行到while,当这个while循环不断循环的时候,此时我们按下Ctrl+c,就会处理信号2,进入handler方法,修改flag值以及打印输出语句。返回来的时候,while循环条件不满足从而结束循环。

OK,基于这个代码,我们让编译器优化一下代码:
在自动化构建工具makefile中,加上-O3,表示优化一下代码;
test_volatile:test_volatile.ccg++ -o $@ $^ -std=c++11 -O3.PHONY:clean
clean:rm -f test_volatile
此时,我们在执行代码,然后按下Ctrl+c,发现,循环没有退出。我们可以试着猜测一下,循环没退出,那就是flag没有从0置为1。

原因是优化后,flag值直接被放到CPU的寄存器中,不需要再从内存中加载到CPU了,目的是提高效率。但是这样的话,因为flag一开始的值是0,0放到CPU中,即使我们后来的flag被置为1,但这是在内存中的,flag还是在CPU中的那个0.因此,while循环没有退出。
我们用volatile修饰全局变量flag后,最后可以退出循环了。
volatile int flag = 0;

因此,volatile的作用是保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
SIGCHLD信号
子进程退出,而父进程没有退出,导致僵尸进程的情况,其实子进程在终止的时候会给父进程发送SIGCHLD信号。我们可以捕捉这个信号并自定义动作,让其忽略这个信号,此时就可以让子进程退出后,自动释放僵尸进程。
测试代码:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>int main()
{//显示设置忽略17号信号,当进程退出后,自动释放僵尸进程//只在Linux下有效signal(SIGCHLD, SIG_IGN); pid_t id = fork();if(id == 0){//childint cnt = 5;while(cnt){printf("我是子进程: %d\n", getpid());sleep(1);cnt--;}exit(0);}while(1);return 0;
}
相关文章:
Linux进程信号(产生、保存、处理)/可重入函数概念/volatile理解/SIGCHLD信号
首先区分一下Linux信号跟进程间通信中的信号量,它们的关系就犹如老婆跟老婆饼一样,没有一毛钱的关系。 信号的概念 信号的概念:信号是进程之间事件异步通知的一种方式,属于软中断。比如:红绿灯是一种信号,…...
锯齿数组 - 贪心
文章目录锯齿数组 -贪心(不过挺像滑动窗口的)1144. 递减元素使数组呈锯齿状锯齿数组 -贪心(不过挺像滑动窗口的) 1144. 递减元素使数组呈锯齿状 题目链接:1144. 递减元素使数组呈锯齿状 题目大意:给你一个…...
[CVPR 2022] Balanced Contrastive Learning for Long-Tailed Visual Recognition
Contents IntroductionMethodPreliminariesBalanced Contrastive Learning (BCL)Drawbacks of SCLClass-averagingClass-complementLower bound of BCLOptimization with Logit CompensationFrameworkExperimentReferencesIntroduction 作者发现对于在长尾数据集上,Supervised…...
23种设计模式-工厂模式
工厂模式是一种创建型设计模式,它提供了一种创建对象的方式,而无需将具体的对象创建逻辑暴露给客户端。在Java中,工厂模式常常用于创建复杂对象或对象的构造过程涉及到多个步骤的情况。 在Android开发中,工厂模式也经常被使用&am…...
Linux操作系统学习(进程等待)
文章目录进程等待进程等待的必要性如何进程等待waiwaitpid验证进程等待 我们知道fork函数可以创建一个子进程,而子进程通常是替父进程完成一些任务,而父进程在fork之后需要通过wait/waitpid等待子进程退出。这就是进程等待 进程等待的必要性 通过获…...
Docker学习(十八)load 和 import 命令的区别
Docker 中有两个命令可以将本地文件系统中的 tar 文件导入到 Docker 中:docker load 和 docker import。尽管它们的作用类似,但它们之间有一些重要的区别。 1.使用方式的不同: docker load 的使用示例: docker load --input tes…...
mysql中的事务
在日常生活中,我们会遇到一个场景,那就是在转账的时候,A有1000块钱,要给B转账500,那么最后的结果是A有500,B有500,但是也有可能出现A没有钱了,B有1000块,或者在转账过程中卡顿,这是不符合逻辑的,那么这个时候就要使用事务来解决问题 事务就是把一堆sql语句打包成一个整体,要么…...
《C++ Primer Plus》第18章:探讨 C++ 新标准(9)
编程练习 下面是一个简短程序的一部分: int main() {using namespace std;// list of double deduced from list contentsauto q average_list ({15.4, 10.7, 9.0});cout << q << endl;// list of int deduced from list contentscout << averag…...
记录一次PWM信号异常问题
问题我使用单片机输出PWM控制机械臂,但是控制过程中,机械臂总是会出现莫名的抽动。利用示波器测试PWM信号,发现信号正常。过程(1)在反复的测试过程中,队友提出,将示波器的地线放在左侧的GND波形…...
简单了解---性能测试
目录 一、什么是性能测试 二、常见的性能测试指标 1、并发 2、响应时间 3、事务 4、点击率 5、吞吐量 6、资源利用率 三、性能测试的分类 1、一般测试 2、负载测试 3、压力测试 4、稳定性测试 四、为什么要做性能测试? 五、影响性能的因素有哪些&…...
1.机器学习笔记第一周
机器学习利用领域: 1:随着网络数据增大,需要搜集用户的数据,做喜好性偏向判断等。 2:只要有数据的,无论是医疗领域,还是基因领域都是需要机器学习来发现数据密码。 3:机器自我学习…...
若依学习(前后端分离版)——启动时发生了啥?(@PostConstruct)(mybatis log free)
我们可以发现若依启动时执行了一些sql我们可以安装一个插件mybatis log free 来更好的进行sql查看 ,安装后需要修改一下若依的日志配置如下查看日志,我们发现执行了三个方法(),分别查询了一些数据。以第二个方法为例子…...
每日十问9c++-内存模型和名称空间
每日十问9c内存模型和名称空间 1.对于下面的情况,应使用哪种存储方案? a.homer 是函数的形参。 b. secret变量由两个文件共享。 c.topsecret 变量由一个文件中的所有函数共享,但对于其他文件来说是隐藏的。 d. beencalled 记录包含它的函数被调用的次数…...
【python】JSON数据类型与Python数据类型之间的转化
注:最后有面试挑战,看看自己掌握了吗 文章目录JSON格式文件JSON格式序列化与反序列化作用JSON常用数据结构键值对的集合值的有序列表JSON数据类型与Python数据类型之间的转化JSON格式和python的区别读写json文件dump 把python 写到json文件load 把json写…...
Spring——什么是事务?传播行为?事务隔离级别有哪些?
思维导图一、什么是事务?多条DML要么同时成功,要么同时失败Transaction(tx)二、事务的四个过程:开启事务(start transaction)执行核心业务代码提交事务(如果核心业务处理过程中没有出…...
【项目实战】使用Feign服务间相互调用,其实OpenFeign也没有想象中那么难嘛
一、Feign介绍 openfeign是一个java的http客户端,用来简化http调用 二、Feign架构(来自官方) Feign由五大部分组成, 由于刚开始接触 feign ,比较关注的 clients 跟 encoders/decoders 三、OKHTTP与Feign之间的关系 在Feign中,Client是一个非常重要的组件,Feign最终…...
tun驱动之ioctl
struct ifreq ifr; ifr.ifr_flags | IFF_TAP | IFF_NO_PI; ioctl(fd, TUNSETIFF, (void *)&ifr); 上面的代码的意思是设置网卡信息,并将tun驱动设置为TAP模式。在TAP模式下,在用户空间下调用open打开/dev/net/tun驱动文件,发送(调用send函…...
[acwing周赛复盘] 第 93 场周赛20230304
[acwing周赛复盘] 第 93 场周赛20230304 一、本周周赛总结二、 4867. 整除数1. 题目描述2. 思路分析3. 代码实现三、 4868. 数字替换1. 题目描述2. 思路分析3. 代码实现四、4869. 异或值1. 题目描述2. 思路分析3. 代码实现六、参考链接一、本周周赛总结 彩笔了,只A…...
NOIP2022 T4 比赛
P8868 [NOIP2022] 比赛 题目大意 有两个长度为nnn的序列aaa和bbb,有qqq次询问,每次询问给出l,rl,rl,r,求 ∑ilr∑ji1r(maxkijak)(maxlijbl)\sum\limits_{il}^r\sum\limits_{ji1}^r(\max\limits_{ki}^ja_k)\times(\max\limits_{li}^jb_l…...
计算机组成原理
目录 ❤ 控制器 ❤ 运算器 ❤ 控制器运算器(计算机的中央处理器CPU) ❤ 存储器 内存(主存) 外存 内存和外村的区别 ❤ 输入设备 ❤ 输出设备 ❤ 适配器 ❤ 总线 ❤ 启动计算机的流程 ❤ 机械硬盘 ❤ 固态硬盘 python从小白到总裁完整教程目录:https://b…...
深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录
ASP.NET Core 是一个跨平台的开源框架,用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录,以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...
Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
Neo4j 集群管理:原理、技术与最佳实践深度解析
Neo4j 的集群技术是其企业级高可用性、可扩展性和容错能力的核心。通过深入分析官方文档,本文将系统阐述其集群管理的核心原理、关键技术、实用技巧和行业最佳实践。 Neo4j 的 Causal Clustering 架构提供了一个强大而灵活的基石,用于构建高可用、可扩展且一致的图数据库服务…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...
稳定币的深度剖析与展望
一、引言 在当今数字化浪潮席卷全球的时代,加密货币作为一种新兴的金融现象,正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而,加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下,稳定…...
华硕a豆14 Air香氛版,美学与科技的馨香融合
在快节奏的现代生活中,我们渴望一个能激发创想、愉悦感官的工作与生活伙伴,它不仅是冰冷的科技工具,更能触动我们内心深处的细腻情感。正是在这样的期许下,华硕a豆14 Air香氛版翩然而至,它以一种前所未有的方式&#x…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
从面试角度回答Android中ContentProvider启动原理
Android中ContentProvider原理的面试角度解析,分为已启动和未启动两种场景: 一、ContentProvider已启动的情况 1. 核心流程 触发条件:当其他组件(如Activity、Service)通过ContentR…...
