Linux——进程信号

news2024/9/28 15:26:14

进程信号

  • 信号的基础
    • 生活中
    • 技术上
  • 信号的产生
    • 信号捕捉接口
    • 信号发送接口
      • 向任意进程发送信号
      • 向自己发送信号
    • 进程退出时——核心转储
  • 信号的保存
    • 信号其它相关概念
    • 信号如何实现捕捉的
  • 信号的处理
    • sigset_t
    • 信号集操作函数
    • sigprocmask
    • sigpending
    • 对于信号保存更深入的理解
    • sigaction
  • 可重入函数
  • volatile
  • SIGCHLD信号

信号的基础

生活中

生活中的信号:红绿灯,手机的来电通知等。
为什么这些是信号呢?因为我们知道这些信号的意义代表着什么。
例如:红绿灯
有人教育过我们,让我们的大脑记住了红绿灯属性对应的行为。
但是,我们就算知道这个信号,也不一定要立刻去处理,因为可能正在做另一间更重要的事情。
所以我们也会有对应的三个动作:
默认动作(看到红灯停),自定义动作(看到红灯不是立刻停下,而而是后退一步或者是其他操作),忽略动作(看到红灯不停)。

技术上

首先要清楚一点,信号是OS发给进程的。
例如:kill -9 进程的pid
那么进程是如何识别信号的呢?
认识+动作。
进程本身就是被程序员编写出来的。
当进程收到某个信号的时候,它可能无法第一时间作出处理,有可能在执行更重要的代码。这也就说明进程对于信号要有保存的能力。
进程对于处理信号有三种动作:默认,自定义,忽略。这里有一个专业名词,叫做信号被捕捉。

那么信号是保存在了哪里呢?
是保存在了进程的PCB中。里面用的是位图结构,假如说有32个比特位,那么就可以保存32种信号。0表示没收到,1表示没有。
也就是说,给进程发送信号的本质其实就是修改PCB中的信号位图而已。
我们还能得出一个结论,一个进程的PCB是内核数据结构对象,PCB是的管理者是OS,也之后OS有权利去修改PCB中的位图结构。
结论:信号发送的各种方式,都是通过OS给进程发送信号,那么OS必须提供发送信号处理信号的相关系统调用。

再来看看之前见过的信号:
一个程序在运行的时候,如果用ctrl+c,进程就立刻终止了,这里其实就是相当于给进程发送了一个信号。
在这里插入图片描述
在这里插入图片描述
其实这个本质就是像这个进程发送了2号信号,这里用kill -l来查看所有信号。
在这里插入图片描述
上面说过,每个信号都有对应的动作,那么如何查看2号信号的对应动作呢?

man 7 signal

在这里插入图片描述
这个默认行为就是终止进程。
如果我想看到是如何向这个进程发送2号信号怎么办呢?

信号的产生

信号捕捉接口

在这里插入图片描述
这里的参数第一个是对于当前进程几号信号进行捕捉,第二个参数是一个函数指针,这个相对应的函数内容是对于当前进程自定义动作。(自己实现)

#include<iostream>
#include<unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)//参数是对应信号的编号
{
    cout << "进程捕捉到了要给信号,信号编号是:" << signo << endl;
}
int main()
{
    //这里是signal函数调用,不是handler函数调用
    //这里只是设置对于2号信号会进行捕捉而已,只有收到对应的信号才会执行handler函数中对应的内容
    signal(2,handler);
    while(true) 
    {
        cout << "进程" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
这个时候ctrl+c和kill -2 pid都不会使这个进程停止下来。
如果想退出可以用kill -9 pid或者ctrl+\(也是默认终止当前进程)。

信号发送接口

向任意进程发送信号

信号发送第一种方法是通过键盘发送,上面的组合键就是。
第二种方法是系统调用向目标进程发送信号。
在这里插入图片描述
这个接口就是向目标进程发送信号。
首先要清楚,OS才有权力向进程发送信号,对用户提供向进程发送信号的服务要通过系统调用才可以。
第一个参数是要向哪个进程发送pid,第二个参数是要向该进程发送几号信号。
成功返回0,失败返回-1。
我们可以利用这个系统接口来实现一个对另外进程发送信号的进程。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以向任意进程发送任意信号。

向自己发送信号

在这里插入图片描述
参数就是信号编号。

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

int main(int argc, char *argv[])
{
    int count = 0;
    while(true)
    {
        if(count == 5)
        {
            cout << "向进程发送2号信号" << endl;
            raise(2);
        }
        cout << "进程计数:" << count << endl;
        count++;
    }
    return 0;
}

在这里插入图片描述
在这里插入图片描述
这个接口是给自己发送指定的信号。(STGABRT,6号信号)
在这里插入图片描述
在这里插入图片描述
这里我们来说一下如何理解信号处理的行为:
有很多的情况,进程收到大部分的信号默认动作都是终止进程。
信号的意义:如果进程有异常,遇到异常终止了进程,那么是因为什么种类的异常停止了呢?这个时候就需要发送一个信号来判断是什么异常。

第三种方式,硬件异常产生的信号。
在这里插入图片描述
在这里插入图片描述
浮点数错误。
那么为什么除0就会终止进程呢?
因为当前进程会收到OS的信号。
在这里插入图片描述
然后我们来捕捉一下8号信号,并且略微改动一下代码:
在这里插入图片描述
在这里插入图片描述
我们发现,这里一直在打印接受到8号信号,可是我们这里只除了一次0,为什么一直在发送呢?
下面说一说对于这段代码的理解:
在这里插入图片描述
CPU在计算的时候会有很多个寄存器,其中有一个是状态寄存器,这个是用来衡量这一次计算的结果,如果发现数据计算异常,比如说除0,等于除无穷大,这个时候状态寄存器中的数据溢出的位置就会由0置为1。
CPU运算发生了异常,OS就会知道,所以OS立刻就知道是当前进程出问题了,立刻向这个进程发送8号信号。
所以,收到信号不一定就会退出,如果没退出,有可能还会被调度。
CPU的寄存器只有一份,但是寄存器中的内容属于当前进程的上下文。
我们也无法将CPU中的状态寄存器修改,当进程被进行切换的时候,就有无数次状态寄存器就有被保存和恢复的过程。
所以每一次恢复的时候,OS就会识别到CPU内控部的状态寄存器溢出标志位。

同理,野指针也硬件异常,我们访问地址是先去虚拟地址空间然后通过页表映射到物理内存,一旦发生野指针,页表就会拦截,OS也会注意到,然后直接向当前进程发送信号。

第四种,软件也可以产生信号:
比如说之前的管道,读端关闭,写端也会关闭,然后导致这个软件触发条件,发生信号。
在Linux下有一个叫定时器的软件,可以设定一个闹钟,如果时间到了,会给当前进程发送编号为14的信号。(闹钟只会响一次)
在这里插入图片描述
参数是按照秒为单位设置一个信号。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>
using namespace std;

int main(int argc, char *argv[])
{
    int count = 0;
    alarm(1);
    while(true)
    {
    	count++;
        cout << count << endl;
    }
    return 0;
}

在这里插入图片描述
这段代码的功能是统计1S左右能让我们的计算机数据累加多少次。
其实正常来说CPU不会这么慢,可以改进一下代码:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>
using namespace std;
int count = 0;

void catchSig(int sig)
{
    cout << count <<endl;
    exit(1);
}

int main()
{
    signal(14, catchSig);
    alarm(1);
    while(true)
    {
        count++;
    }
    return 0;
}

在这里插入图片描述
那么为什么差距这么大呢?
因为打印是一种外设输出,访问外设的时候是很慢的,需要大量的时间,第一段代码一直在通过外设进行打印,所以很慢,第二段之后结束的时候才会通过外设打印。
如果是服务器还要经过网络IO,会更慢。

”闹钟“其实就是用软件实现的:
任何一个进程都可以通过alarm系统调用在内核中设计闹钟,OS内可能会存在很多的闹钟,OS也一定要管理这些闹钟,先描述再组织。
用struct alarm类型的对象去描述各个进程的闹钟数据:

struct alarm
{
	uint64_t when;//未来的超时时间
	int type;//闹钟类型,一次性的还是周期性的
	tasl_struct *p;//和哪个进程相关
	struct alarm *next;
}

然后OS用特定堆的数据结构方式管理,struct alarm *head
OS会周期性的检测这些闹钟,如果发现超时了OS就会给对应的进程发SIGALARM信号。

上面所说的所有信号的产生,都是由OS来执行,但是信号不一定立即处理,那么是什么时候被处理的呢?

进程退出时——核心转储

先来看一段代码:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>
using namespace std;

int main()
{
    while(true)
    {
        int arr[10];
        arr[100] = 106;//这里数组是越界的
    }
    return 0;
}

在这里插入图片描述
这里并没有显示越界的报错。
在这里插入图片描述
改成一千也没报错,但是i改成一万就报错了
在这里插入图片描述

在这里插入图片描述
这里是什么情况呢?因为开辟的栈区是合法的,只有到了为开辟的栈区才会进行报错。
在这里插入图片描述
像这种,Term这种是正常退出,而Core是退出之后还要做其他工作。
在云服务器上,默认如果进程是core退出的暂时看不到现象,想看到需要打开一个选项:
在这里插入图片描述
第一个core file size是0,这是云服务器默认的。
在这里插入图片描述
这里设置一下。
然后再次运行上面的段错误的代码:
在这里插入图片描述
在这里插入图片描述
并且还多出来了一个文件。
第一个后面多出来的core dumped就是核心转储操作,多出来的文件就是核心转储的内容。
多出来的文件.后缀是引起core问题进程的pid。
核心转储:当进程出现异常是hi后,我们将进程的对应时刻,在内存中的有效数据转储到磁盘中。(二进制临时文件)
作用就是为了更方便调试:
在这里插入图片描述
在这里插入图片描述
这里直接就帮助我们找到了问题。(这里叫做事后调试)

core-file core.xxx

信号的保存

有一个问题,如果所有信号都被捕捉了,那么这个信号是不是就无法停下来了呢?

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>
using namespace std;
void catchSig(int signo)
{
    cout << "信号拦截:" << signo << endl;
}
int main()
{
    for(int signo = 1; signo <= 31; signo++)
    {
        signal(signo,catchSig);
    }
    while(true)
    {
        cout << "运行中" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
在这里插入图片描述
最后用了kill -9才将这个进程杀掉。
OS中9号信号是无法进行捕捉的。

信号其它相关概念

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

在这里插入图片描述
并且,PCB中还有一个信号的函数指针数组,里面都是处理信号的方法。
我们使用的信号捕捉也只是将该数组中对应信号的方法给替换了,也就是替换了函数地址。
在这里插入图片描述
也就是说,如果要给信号产生,不妨碍他可以先被阻塞。

信号如何实现捕捉的

之前说信号只会在合适的时候才会被处理,不然就一直被保存在pending位图中。
从内核态返回用户态的时候,进行信号的处理。

我们平时是用户态,但是难免会去通过OS访问系统自身的资源和硬件资源,这个时候就要去进行系统调用才能完成:
在这里插入图片描述
也就是说,系统调用还要进行身份切换,会比调用用户层本身的方法慢。
所以避免频繁的使用系统调用。

并且,CPU中由寄存器会存储以下相关数据。
在这里插入图片描述

那么,一个进程怎么跑到OS中执行方法呢?
在这里插入图片描述
因为进程的独立性,所以每个进程都有一个用户级页表。
在开机的时候,操作系统要加载到内存中,因为操作系统只有一份,在内存中也只有一份,相对应的内核级页表也只有一份就够了。
CPU中也会有一个寄存器储存内核级页表,每个进程都会通过内核空间访问内核页表,然后去找到物理内存中的操作系统的代码和数据。
也就是说,进程要访问OS的接口,其实只需要在自己的地址空间上进行跳转就可以了。

如果想访问内核级数据,CPU的CR3要变成0才有权限。
那么是怎么进行切换的呢?是系统调用接口的起始位置会帮助我们进行切换。
也就会说前半段代码可能是用户态跑的,但是这里突然就变成内核态跑。
在Linux中,有一个叫Int 80 —— 陷入内核。
这个是汇编指令,这个就是修改当前进程在寄存器中CR3的身份状态。

信号的处理

那么,从内核态返回用户态的时候,才会进行信号处理,也就是说很可能进行了系统调用或者是进程切换(进程切换需要进程切换到内核态,因为进程被切换的时候一定没有被执行完,放在运行或者是等待队列的时候一定就要切换到内核态,然后再继续调度下面代码的时候就要切换回用户态)

在这里插入图片描述

sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

pengding位图和block位图的统一类型就是sigset_t,是为了更方便用户,定义的用级数据结构的类型。
一般将block信号集叫做信号屏蔽字

信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。
对于信号集位图不要私自修改,要用对应的接口。

#include <signal.h>
int sigemptyset(sigset_t *set);//清空位图中的所有位置,全都变成0
int sigfillset(sigset_t *set);//位图全都置为1
int sigaddset (sigset_t *set, int signo);//添加特定信号
int sigdelset(sigset_t *set, int signo);//删除特定信号
int sigismember(const sigset_t *set, int signo);//判断一个信号是否在这个信号集中

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1

第一个参数是下面这些选项。
在这里插入图片描述
第三个选项是重置信号屏蔽字。
第二个参数是你要修改的位图结构,也就是信号集。
第三个参数是第二个参数修改之前的信号集。(输出行参数)
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

sigpending

在这里插入图片描述
这个函数参数是一个输出型参数,在哪个进程调用就返回哪个进程的pengding位图。
返回成功0,失败-1。

对于信号保存更深入的理解

这里用起来上面介绍的接口,然后来写一段程序。
条件:

先屏蔽2号信号,发送一个信号2,在发生2号信号之前打印出pengding位图,发送之后再次打出pengding位图

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>
using namespace std;

#define BLOCK_STGNAL 2
void show_pending(const sigset_t& pengding)
{
    for(int i = 31; i >= 1; i--)
    {
        if(sigismember(&pengding, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << "\n";
}
int main()
{
    //1.屏蔽指定信号
    sigset_t block, oblock, pengding;
    //初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pengding);
    //屏蔽
    sigaddset(&block, BLOCK_STGNAL);//添加屏蔽的信号
    sigprocmask(SIG_BLOCK, &block, &oblock);//正式屏蔽,这里才是真正通过OS设置进当前进程的PCB中
    //2.打印pengding信号集
    int count = 5;
    while(true)
    {
        sigpending(&pengding);//获取他
        show_pending(pengding);
        sleep(1);
        //3.解除信号的屏蔽
        if(count-- == 0)
        {
            sigprocmask(SIG_SETMASK, &oblock, &block);
            cout << "不屏蔽信号" << endl;
        }
    }
    return 0;
}

在这里插入图片描述
在这里插入图片描述
我们发现,如果一旦解除信号屏蔽,进程立刻就会退出,后续的代码不会被执行。
因为一旦信号屏蔽解除,一般OS要立马递达一个信号。(处理完一个信号,该比特位立刻清零)

sigaction

在这里插入图片描述
这个函数和signal函数差不多,第一个参数是对于该信号进行捕捉,第二个参数是一个结构体对象指针,传入的就是结构体的对象;
在这里插入图片描述
第一个成员是对于处理这个信号的方法。
第三个成员是信号集。
也就是说第二个参数是要对于该信号做一些列结构体中内容的设置的,是一个输入性参数。
第三个参数是一个输出型参数,获取对应信号老的处理方法。
成功返回0,失败返回-1。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>
using namespace std;

void handler(int sig)
{
    cout << "get a signo" << sig << endl;
    sleep(10);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT, &act, &oact);
    while(true) sleep(1);
    return 0;
}

在这里插入图片描述
第一次立刻打印,第二次和第三次只打印了一次,两次我一起按的,但是打印出来的结果只有一个,这是为什么呢?

当我们进行正在递达第一个信号期间,同类型信号无法被递达,因为当前信号正在被捕捉,系统会自动将当前信号加入到该进程的信号屏蔽字。
当信号完成捕捉动作时,OS又会自动解除对该信号的屏蔽。
上面的现象可以这样解释,2号比特位被第一次置为1的时候,相对应的block位图2号也被置为了1,那么处理这个2号信号的时候,pengding位图对应的比特位又被置为0了,但是紧接着又来了一个2号信号,该比特位又变成了1,最后又来了一个2号信号,这个时候就不会再让pengding为途中2号信号中的比特位继续改变了,因为已经没有能力保存了。
在一个信号被解除屏蔽的时候,会自动递达当前屏蔽信号,没有就不做任何动作。

也就是说我们进程处理信号的原则是串行的处理同类型的信号,不允许递归。

那么,刚才这段代码这里:
在这里插入图片描述
当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中。
让上面的也屏蔽3号信号试一下。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里退出的原因是什么呢?
因为是同时屏蔽2,3信号,第一次发送的也是2号信号,在处理2号信号的时候会同时屏蔽2号和3号信号,所以3号不会被立刻递达,因为是先发的2号信号,3号信号先不会处理,处理完前面两个2号信号之后才会解除对2号和3号的屏蔽,因为3号默认动作是退出,所以3号递达程序也就退出了。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

可重入函数

在这里插入图片描述
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
这就会导致一个结果,node2就会数据丢失。

1.一般来说,mina执行流和信号捕捉执行流是两个执行流。
2.如果在main中和handler中,该函数被重复进入,出问题,insert函数就是不可重入函数。
3.如果在main中和handler中,该函数被重复进入,没出问题,insert函数就是可重入函数。

上面的例子,insert就是不可重入函数。

其实大部分函数都是不可重入的,这是一个特性。
如果一个函数符合以下条件之一则是不可重入的:

调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile

该关键字在C当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

int quit = 0;
void handler(int signo)
{
    printf("信号捕捉成功->\n");
    printf("quit:%d\n",quit);
    quit = 1;
    printf("quit变化之后->%d\n",quit);
}
int main()
{
    signal(2, handler);
    while(!quit);
    printf("正常退出\n");
    return 0;
}

在这里插入图片描述
在gcc编译器有个优化的选项是O3,再来看一下优化之后的效果:
在这里插入图片描述
这里进程并没有正常退出,这是为什么呢?
这里和优化是有关系的:
在这里插入图片描述
在循环这里,CPU从内存当中拿数据进行分析,但是并没有写回去。
上面说过,mian执行流和信号捕捉执行流是两个执行流,在没有进行优化的时候,捕捉到信号执行信号的动作就到了捕捉信号的执行流,将quit变成1之后返回到了main的执行流。然后CPU做出处理判断循环条件为假就跳出了循环。

那么优化之后,因为quit在main执行流没有被改变,所以编译器就认为quit没必要进行后续的判断,所以就将quit的值放进了编译器的内存里面,也就是说它的值已经无法被用户去改变了。所以这里判断的是CPU中寄存器最开始储存的那个值,就算信号捕捉执行流去改变,但是也不会影响CPU中寄存器的值。
在这里插入图片描述
那么这个时候怎么办呢?又想优化又不想出现这种情况,这个时候就需要加volatile关键字了。
在这里插入图片描述
在这里插入图片描述
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。

SIGCHLD信号

用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>
using namespace std;

void handler(int sig)
{
    cout << "捕捉到信号" << endl;
    //以下是伪代码
    /*while(1)
    {
        pid_t ret = waitpid(-1, NULL, WNOHANG);//这里不能阻塞,万一只有一部分子进程退出就不好办了,这就是阻塞式调用了
        if(ret == 0) break;
    }*/
}

int main()
{
    signal(SIGCHLD, handler);
    cout << "父进程:" << getpid() << endl;
    pid_t id = fork();
    if(id == 0)
    {
        cout << "子进程:" << getpid() << "父进程:" << getppid() <<endl;
        sleep(5);
        exit(1);
    }
    while(true) sleep(1);

    return 0;
}

在这里插入图片描述
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
在这里插入图片描述

在这里插入图片描述
这里子进程退出也没留下任何痕迹。
还有一个细节:
在这里插入图片描述
明明对于17号信号处理就是”忽略“嘛?
但其实我们默认设置和手动设置的是不一样的。
因为OS会识别,如果是手动设置的,就会修改未来创建子进程的时候的退出的属性等等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/503701.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaWeb ( 八 ) 过滤器与监听器

2.6.过滤器 Filter Filter过滤器能够对匹配的请求到达目标之前或返回响应之后增加一些处理代码 常用来做 全局转码 ,session有效性判断 2.6.1.过滤器声明 在 web.xml 中声明Filter的匹配过滤特征及对应的类路径 , 3.0版本后可以在类上使用 WebFilter 注解来声明 filter-cla…

JVM 类加载子系统

内存结构概述 粗略图: 详细图&#xff1a; 类加载器与类的加载过程 类加载子系统负责从文件系统或者网络中加载 Class 文件&#xff0c; class 文件在文件开头有特定的文件标识ClassLoader 只负责 class 文件的加载,至于它是否可以运行, 则由 Execution Engine 决定加载的类信…

AI读心重磅突破登Nature!大脑信号1秒被看穿,还能预测未来画面

夕小瑶科技说 分享 来源 | 新智元 最近&#xff0c;来自洛桑联邦理工学院的研究团队提出了一种全新的方法&#xff0c;可以用AI从大脑信号中提取视频画面。论文已登Nature&#xff0c;却遭网友疯狂「打假」。 现在&#xff0c;AI不仅会读脑&#xff0c;还会预测下一个画面了&a…

实验十九、利用运算电路解方程

一、题目 研究利用运算电路解方程。已知一元二次方程为 2 X 2 X − 6 0 2X^2X-60 2X2X−60&#xff0c;试求其解。 二、仿真电路 按方程式搭建电路&#xff0c;如图1所示。该电路为加减运算电路&#xff0c;由于同相和反相端电阻参数对称&#xff0c;故省略掉了同相输入端…

2.docker—数据卷操作

文章目录 1、配置数据卷2、数据卷容器3、小结 1、配置数据卷 挂载 # 创建启动容器时&#xff0c;使用 –v 参数 设置数据卷 sudo docker run ... –v 宿主机目录(文件):容器内目录(文件) ...sudo docker run -it --namec1 -v /home/sjj/data:/root/data_container centos:7 /…

【C++进阶之路】类和对象(中)

文章目录 前言六大默认成员函数 一.构造函数性质默认构造函数构造函数(需要传参) 二.析构函数性质默认析构函数练习 三.拷贝构造函数基本性质&#xff1a;形参必须是引用默认拷贝构造浅拷贝深拷贝自定义类型 四.赋值运算符重载函数基本特征全局的运算符重载函数局部的运算符重载…

深入学习 Kotlin 枚举的进阶用法:简洁又高效~

翻译自&#xff1a;https://towardsdev.com/mastering-enums-in-kotlin-a-guide-to-simplify-your-code-130b5934cb16 Kotlin 作为现代的、强大的编程语言&#xff0c;可以给开发者提供诸多特性和工具&#xff0c;得以帮助我们编写更加高效、更具可读性的代码。 其中一个重要的…

开源工具系列7:Kube-bench

导语 Kube-Bench 是一个基于Go开发的应用程序&#xff0c;属于 Kubernete 的安全检测的工具。它可以帮助研究人员对部署的 Kubernete 进行安全检测。 Kube-Bench 是什么 从本质上来说&#xff0c;Kube-Bench 是一个基于Go开发的应用程序&#xff0c;属于 Kubernete 的安全检…

免费版的mp3格式转换器有哪些?这三款软件帮你实现!

在娱乐文化越来越丰富的今天&#xff0c;人们越来越追求音乐、视频等娱乐方式&#xff0c;其中音乐作为一种能够治愈心灵的艺术形式备受欢迎。但要欣赏一首美妙的音乐&#xff0c;就需要我们自己去制作、编辑并转换其格式&#xff0c;以适应各种软件如MP3、MP4等格式。 方法一…

在 Python 中将 Tqdm 与 Asyncio 结合使用

动动发财的小手&#xff0c;点个赞吧&#xff01; 简介 困扰 在 Python 中使用并发编程来提高效率对于数据科学家来说并不罕见。在后台观察各种子进程或并发线程以保持我的计算或 IO 绑定任务的顺序总是令人满意的。 但是还有一点困扰我的是&#xff0c;当我在后台并发处理成百…

数据结构篇五:队列

文章目录 前言1.队列1.1 队列的概念及结构1.2 队列的实现 2. 各功能的解析及实现2.1 队列的创建2.2 初始化队列2.3 队尾入队列2.4 队头出队列2.5 获取队头元素2.6 获取队尾元素2.7 队列中有效元素个数2.8 检查队列是否为空2.9 销毁队列 3.代码实现3.1 Queue.h3.2 Queue.c3.3 te…

JavaWeb ( 七 ) JSTL Tag标签

2.5.JSTL标签与EL表达式 2.5.1.EL表达式 EL表达式 : Expression Language 目的&#xff1a;为了使JSP写起来更加简单 格式&#xff1a;${expression} EL 提供“.“和“[ ]“两种运算符来存取数据。${user.name}, ${user[“name”] }支持算术操作符, 关系操作符, 逻辑操作符…

Python:Python进阶:内存管理机制

Python内存管理机制 1. 堆2. 栈3. 引用4. Python中可变对象和不可变对象有个问题&#xff1a;你可以好好思考下总结 Python内存管理程序是用 C/C写的&#xff0c;这里我们以 CPython解释器为例说明。 在Python 中 所有数据类型 包括&#xff1a;int dict str都是一个对象&#…

层次分析法及找工作问题实战

学习知识要实时简单回顾&#xff0c;我把学习的层次分析法简单梳理一下&#xff0c;方便入门与复习。 AHP 层次分析法&#xff08;Analytic Hierarchy Process&#xff0c;简称 AHP&#xff09;是对一些较为复杂、较为模糊的问题作出决策的简易方法&#xff0c;它特别适用于那…

C++类和对象上

专栏&#xff1a;C/C 个人主页&#xff1a;HaiFan. 专栏简介&#xff1a;本章为大家带来C类和对象相关内容。 类和对象 前言面向过程和面向对象类的引入类的定义对于类中成员的命名建议 类的访问限定符及封装访问限定符封装 类的作用域类的实例化如何计算类对象的大小this指针t…

Web自动化测试——XAPTH高级定位

XAPTH高级定位 一、xpath 基本概念二、xpath 使用场景三、xpath 相对定位的优点四、xpath 定位的调试方法五、xpath 基础语法&#xff08;包含关系&#xff09;六、xpath 顺序关系&#xff08;索引&#xff09;七、xpath 高级用法1、[last()]: 选取最后一个2、[属性名属性值 an…

ESP32设备驱动-PCF8575IO扩展器驱动

PCF8575IO扩展器驱动 文章目录 PCF8575IO扩展器驱动1、PCF8575介绍2、硬件准备3、软件准备4、驱动实现1、PCF8575介绍 PCF8575用于两线双向总线 (I2C) 的 16 位 I/O 扩展器专为 2.5-V 至 5.5-V VCC 操作而设计。 PCF8575 器件通过 I2C 接口 [串行时钟 (SCL)、串行数据 (SDA)]…

flask教程8:模板

文章目录 一、模板与自定义过滤器1 模板2 过滤器转义过滤器讲解 3自定义过滤器 二、表单1表单2表单扩展 三、创建表单模型类与模板使用3.1 表单模型类 四 、使用表单接受并检验参数五、模板宏的使用六 、宏定义在外部的使用七 &#xff1a;模板继承与包含继承包含include 八 、…

PVE 安装 windows10

pve 安装教程大家可以参考视频&#xff1a;pve 安装 pve 安装 Windows10 视频教程&#xff1a;pve 安装Windows10 在安装好 pve 后我们就可以进行虚拟机的安装了。当然我们可以自行决定是否有必要进行 win10 的安装。 准备工作 1. 下载 win10 镜像文件&#xff1a;https://…

数据结构与算法基础(王卓)(35):交换排序之快排【第二阶段:标准答案、初步发现问题】

目录 第二阶段&#xff1a;一分为二 整个快排算法的程序运行大框架&#xff1a; 做出的改动&#xff08;和原来程序的区别&#xff09;&#xff1a; Project 1: PPT标准答案&#xff1a; Project 1小问题&#xff1a; Project 1还存在着一个巨大的问题&#xff1a; 具体问…