【Linux】进程信号深度解析

信号的基础认识

基础概念

在linux平台下信号是给进程发送的,用来进行事件异步通知机制。

**异步通知:**所谓的异步通知就是信号的产生相对于进程的运行是互不干扰的,只有进程接收到信号之后才会根据信号去做相应的工作。

理解前提

1.进程对信号的处理:进程在信号没有产生的时候,早就知道信号该如何进行处理了。

2.进程对信号的处理,并不是立即处理,而是等合适的时候进行处理。

3.os程序员在设计进程时,进程早已经内置了对信号的识别和处理。

4.产生信号源非常多,给进程产生信号的信号源也非常多。

信号类别

在linux下可以通过kill -l 来查看所有的信号类别,其中前1-34个信号我们称之为基础信号,后边的我们称之为实时信号(我们本章主要介绍基础信号)。

另外这些信号都是宏定义出来的,前边的编号等价于后边的信号名称。

信号处理的常见三种动作

在linux中,进程对传来的信号主要有三种处理动作:忽略处理,默认处理,自定义捕捉处理

**忽略处理(SIG_IGN):**进程收到信号后不做任何处理动作。

**默认处理(SIG_DFL):**内核预定义处理动作。

**自定义捕捉:**通过用户实现方法,根据用户要求进行处理信号。

其他处理动作可以通过man 7 signal 查看。

常见三种处理动作验证

CTRl+C:默认终止进程,在signal表中对应2号(SIGINT)信号,以该信号作为实验。

系统调用函数signal:signal函数是linux中的一种系统调用,它用来对信号进行处理。

忽略处理(SIG_IGN)

cpp 复制代码
int main(){
    signal(SIGINT,SIG_IGN);//对CTRL+C 执行忽视处理
    while(true){
        cout<<"hello signal"<<endl;
        sleep(1);
    }
    return 0;
}
//运行结果  
/*root@VM-16-14-ubuntu:/lvpanhao/113/test/Signal# ./signal
hello signal
hello signal
hello signal
^Chello signal 执行ctrl + c,进程忽视该动作
hello signal
......*/

默认处理(SIG_DFL)

cpp 复制代码
int main(){
    signal(SIGINT,SIG_DFL);//对CTRL+C 执行忽视处理
    while(true){
        cout<<"hello signal"<<endl;
        sleep(1);
    }
    return 0;
}
//执行结果
/*root@VM-16-14-ubuntu:/lvpanhao/113/test/Signal# ./signal
hello signal
hello signal
hello signal
^C            #在linux中ctrl +c 默认动作是终止进程
*/

自定义捕捉

cpp 复制代码
void handler(int sig){
    cout<<"my number is:"<<sig<<endl;
}
//自定义捕捉函数
int main(){
    signal(SIGINT,handler);//对CTRL+C 执行忽视处理
    while(true){
        cout<<"hello signal"<<endl;
        sleep(1);
    }
    return 0;
}
//运行结果
/*
./signal 
hello signal
hello signal
^Cmy number is:2
hello signal
hello signal
hello signal
^Cmy number is:2  //ctrl+c 默认捕捉我们自定义方法
hello signal
hello signal
*/

信号的产生

通过键盘产生信号

常见键盘信号:(ctrl+c/ctrl+z/ctrl+/........)

前台进程&后台进程

**前台进程(./xx):**在shell命令行运行的进程,只有一个,可以从键盘获取输入数据。

**后台进程(./xx &):**不在shell命令行显示运行的进程,可以存在多个,不可以从键盘获取输入数据。

对于键盘产生的信号只能发送给目标前台进程

原因:对于键盘产生的信号,只想让唯一的一个进程进行接受。输入设备键盘只有一份,所以输入的数据要给一个确定的进程,这个进程就是前端进程。

典型场景案例:

正常的shell,我们输入ls/pwd可以执行并由bash输出结果。这是因为我们把shell认为前台进程。

当我们执行一端程序时,在输入ls/pwd之类的命令,os不会再有回显。这也就说明当我们执行其他程序时,shell被自动设置为后台进程。也就是说前台进程有且只有一个,键盘产生的数据只能发送给前台进程。

前后台进程相关操作

jobs:查看后台进程

fg num :把后台进程转换为前台进程 num为任务号

ctrl+z:前台进程切换为后台 在后台进行停止

bg num (任务号) :唤醒后台进程

调用系统命令产生信号

通过执行kill命令向目标进程发送信号,比如:kill -9 id 向目标进程发送终止信号。

使用函数产生信号

kill:向目标进程发信号

int kill(pid_t pid, int sig);

pid:进程pid

sig:要发送的信号

模拟实现kill:kill -nu id

cpp 复制代码
//模式实现kill,kill -9 id
int main(int argc,char* argv[]){
    if(argc<3){
        cout<<"kill -sig -id"<<endl;
        exit(-1);
    }
    kill(stoi(argv[1]+1),stoi(argv[2]));

    return 0;
}

raise:给自己发信号

raise(int sig);

**sig:**要发送的进程号

案例:给自己发送2号信号

cpp 复制代码
void handler(int sig){
    cout<<"my number is:"<<sig<<endl;
    exit(-1);
}
//自定义捕捉函数
int main(){
    signal(2,handler);//对CTRL+C 执行忽视处理
    while(true){
        sleep(2);
        raise(2);
    }
    return 0;
}

abort:给当前进程发送异常信号而终止

cpp 复制代码
void handler(int sig){
    cout<<"my number is:"<<sig<<endl;
}
//自定义捕捉函数
int main(){
    signal(SIGABRT,handler);//对CTRL+C 执行忽视处理
    while(true){
        sleep(2);
        abort();
    }
    return 0;
}

硬件异常产生信号

硬件异常是指硬件以某种方式被检测到并通知内核,然后内核向当前进程发送适当的信号。

常见两种硬件异常情况:
进程执行了除0的指令:CPU的运算单元会产生异常, os将这个异常解释为SIGFPE信号(8)发送给进
程。
进程访问了非法内存地址:MMU(页表转化)会产生异常,os将这个异常解释为SIGSEGV信号(11)发送给进程。
结论: 我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。
由上我们可以知道:硬件产生异常首先会把信号发送给os,然后os识别信号异常内容,发送给进程。

除0问题

cpp 复制代码
void handler(int sig)
{
    cout << "my number is:" << sig << endl;
}
// 自定义捕捉函数
int main()
{
    for (int i = 0; i < 32; i++)
    {
        signal(i, handler); // 对CTRL+C 执行忽视处理
    }
    sleep(2);
    int a = 10;
    a /= 0;
    return 0;
}
//运行结果
/*
my number is:8
my number is:8
my number is:8
my number is:8
my number is:8
my number is:8
my number is:8
my number is:8
my number is:8
.....

*/

问题:除0错误后为什么一直捕捉到该信号?
CPU运算异常后,实际上 OS 会检查应用程序的异常情况,其实在CPU中有⼀些控制和状态
寄存器,主要用于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解
为⼀个位图,对应着⼀些状态标记位、溢出标记位。OS 会检测是否存在异常状态,有异常存在就会调用对应的异常处理方法。
除零异常后,我们并没有做清理内存关闭进程打开的文件切换进程等操作,所以CPU中还保留该进程上下文数据以及寄存器内容,除零异常会⼀直存在cpu中,就有了我们看到的⼀直发出异常信号的现象。

Core Dump

进程退出位结构示意图

图解:

进程退出的信息可以用一个整数的低16位进行标识。

其中低16位的次八位(8-15)代表进程正常退出状态,0-7标识进程的退出信号。

规则:

进程正常退出返回正常的退出信号(8-15),0-7位为0;

进程异常退出,8-15位无意义,0-6 表示终止进程的信号编号(1-32),位 7 是 "core dump 标志"。1表示产生core dump,0表示无。

用waitpid()验证上诉三个状态

cpp 复制代码
int main(){
pid_t pid=fork();
    if(pid==0){
        //子进程
        int a=10;
        a/=0; //程序异常
        exit(2);//退出码无意义
    }
    int status=0;
    waitpid(pid,&status,0);
    cout<<"signal:"<<(status)<<"exit code:"<<(status>>8)<<"core dump:"<<((status>>7)&1)<<endl;
}
//运行结果 #结果证明程序异常退出,退出状态无效,只会收到对应的信号。
/*./signal
signal:8 exit code:0 core dump:0*/

core dump理解

当⼀个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。

进程异常终止通常是因为有Bug,比如除0错误,事后可以用调试器检查core文件以查清错误原因,这叫做 Post-mortem Debug (事后调试)。简单来说os把异常退出的信息保存在了core文件中,我们可以直接去调试该文件可以快速找出错误信息(核心转储的作用)。

⼀个进程允许 产⽣多大的 core 文件取决于进程的 Resource Limit (这个信息保存 在PCB
中)。默认是不允许产生 core 文件的, 因为 core 文件中可能包含用户密码等敏感信息,不安全。查看相关core信息:ulimit -a //更改core文件大小:ulimit -c size.

core dump相关验证

(以上诉代码为例)

ulimit -c 1024:先设置一下core大小(因为默认为0)

gdb可以直接运行该文件(可以直接找出错误地方)

Core VS Term

进程的异常推出信号有core和term两种状态,可以通过man 7 signal查看**。**

核心区别:core模式会在当前路径下形成一个core文件,进程因异常退出的时候,进程在内存中的核心数据会从内存拷贝到磁盘(核心转储),然后在退出。而term模式下,进程遇到异常直接退出。

由软件条件产生信号

管道破裂(SIGPIPE)

当进程向一个读端已关闭的管道 写入数据时,内核会向该进程发送 SIGPIPE 信号,使进程终止

该方式是基于软件条件产生的信号方法之一。

alarm(SIGALRM)
调用 alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 信号,该信号的默认处理动作是终止当前进程

该方式也是基于软件条件产生的信号方法之一。

软件条件----闹钟是软件设置,条件是闹钟超时,触发信号。

unsigned int alarm(unsigned int seconds);
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

不同进程有不同的alarm,对alarm的管理方式"先描述后组织"(小根堆结构的组织,按时间片轮转的方式进行的)

理解软件条件

在os中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简单理解就是,软件条件是因操作系统内部或外部软件操作而触发的信号产生。

总结

所有的信号产生最后都要经过os调用管理,实际上信号的产生只需要修改一张表的位图(pending表)即可,后续在信号的保存会讲。

信号的保存

相关概念

1.实际上执行信号的处理动作称为信号递达。(信号已经达到,进行处理 )

2.信号从产生到递达之间的状态,称为信号未决。(显示在位图中,未来得急处理)

3.进程可以选择阻塞(屏蔽信号)某个信号(无法达到递达状态)。(可以未决但不能抵达)

4.被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作。

注意:阻塞和忽略时不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

信号在内核中的示意图

三张表都是以位图的形式存在的

**block表:**表示信号是否阻塞(信号阻塞表)

比特位的位置:几号信号。

比特位的内容:是否阻塞,1阻塞,0不阻塞。

**pending表:**表示保存收到信号的位图(信号未决表)

比特位的位置:表示收到几号信号。

比特位的内容:是否收到该信号。

是否能被递达:1.看block是否阻塞。2.看是否收到哦这个信号。

**handler表:**自定义函数指针(信号递达之后怎样去处理)。

自定义函数处理方法。

上图传来信号举例:

SIGHUP传来:未阻塞,等信号未决递达后处理默认动作。

SIGINT传来:被阻塞,处于未决状态,不能递达,解除阻塞后可递达。

SIGQUIT:一旦产生就阻塞,处理动作时用户自定义方法。

进程解除对某信号的阻塞之前 这种信号产生了多次,在linux系统中os对于这种信号处理方式时,常规信号递达前只处理一次,而实时信号可以产生多次存放在维护的一个队列中。

回答一个问题

至此我们可以回答之前的一个问题。

对于信号的处理,进程并不是立刻对信号进行处理的而是先把他记录下来等到合适的时候处理。

记录在哪?

信号发送给目标进程,目标进程在自己的pcb中维护一张pending表,发送来的信号如果不被立刻执行,那就会暂时存放在目标进程pcb中的pending表中。

如何记?

该pending表是一张位图结构,通过改变位图即可完成记录。

发送信号的本质是什么?

发送信号的本质我们可以看作是修改目标进程维护的一张位图,当然这一操作对于用户来讲都是没权限的,os必须给我们提供一些系统调用接口(kill)之类的。然后os帮我们去修改即可。

信号集操作函数

sigset_t类型

对于pending表和block表每个信号只有一个bit的标志,非0即1,并不记录该信号产⽣了多少次。因此,未决标志和阻塞标志可以用相同的数据类型sigset_t来存储。

sigset_t称为信号集:这个类型可以表示每个信号的"有效"或"无效"状态。

阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞。

阻塞信号集也叫做当前进程的信号屏蔽字**。**

未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。

相关操作函数

#include <signal.h>

int sigemptyset(sigset_t *set);(清空,初始化)

int sigfillset(sigset_t *set); (设置有效)

int sigaddset(sigset_t *set, int signo);(增加对应位置的信号)

int sigdelset(sigset_t *set, int signo); (删除对应位置的信号)

int sigismember(const sigset_t *&pening, int signo);(判断信号是否在集合中)

//在使用时一定要先使用sigemptyset/sigfillset函数对信号集(sigset_t)进行初始化,确定信号集的状态。

Sigprocmask

在内存中读取或更改进程的信号屏蔽字。

用法:

int sigprocmask(int how,const sigset_t *sset,sigset_t *oset);

how:如何进行修改信号屏蔽字

SIG_SETMASK:全部更新block表。

SIG_UNBLOCK:去掉阻塞

set:更改进程的信号屏蔽字

oset(输出型参数):读取进程当前信号屏蔽字,没有被更改的信号集。
如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset⾥,然后根据set和how参数更改信号屏蔽字。

Sigpending

int sigpending(sigset_t *set);

//读取当前进程的未决信号集,通过set参数传出

//这里不需要去专门设置pending表,只需要读取即可,因为信号的设置通过信号产生的几种方法即可完成设置。

整合demo

该代码只是针对上诉接口进行的一个练习(主要是能够直观的看到信号被阻塞递达的过程)。

cpp 复制代码
void Print(sigset_t &pending)
{
    cout << "我是进程:" << getpid() << " pending表:";
    for (int i = 31; i >= 1; i--)
    {
        if (sigismember(&pending, i))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}
void handler(int sig)
{
    cout << "获取到了信号:" << sig << endl;
}
int main()
{
    // 屏蔽2号信号,不断获取pending表看一下情况
    // 对2号信号捕捉 因为递达后对2号信号的默认处理动作是终止所以设置一个自定义捕捉方法
    signal(2, handler);
    // 1.设置block表
    sigset_t block, oldblock; // 设置block表
    // 初始化block,oldblock
    sigemptyset(&block);
    sigemptyset(&oldblock);
    // 设置2号信号到阻塞block中
    sigaddset(&block, 2);
    // 把这个表设置到内存中
    sigprocmask(SIG_SETMASK, &block, &oldblock);
    int cnt = 0;
    while (true)
    {
        // 2.设置pending表
        sigset_t pending;
        // 清空
        // 获取pending表内容
        int n = sigpending(&pending);
        if (cnt == 10)
        {
            sigprocmask(SIG_SETMASK, &oldblock, nullptr);
            cout << "解除屏蔽,信号递达" << endl;
        }
        Print(pending);
        sleep(1);
        cnt++;
    }
    return 0;
}

信号的处理

信号捕捉的全流程

**信号捕捉:**如果信号的处理动作是用户自定义的函数,在信号递达的时候调用这个自定义的函数,这就称之为信号捕捉。

**先来回答一个问题:**如果我们写的程序没有调用任何的系统调用,那么该程序会进入内核吗?

答案是肯定的,我们所有的程序都是被cpu调度的,cpu中时间片切换也需要内核中调度器完成的,自然我们的程序会进入内核。

上图简单解释:整个信号捕捉流程

在用户层用户自定义了sighandler信号捕捉函数,main函数执行流,在执行到某处时发生中断或异常时会从用户态陷入内核态,在内核中处理完该异常返回用户态的时候,会检查是否有信号的传递,如果有信号,并且信号的处理方法时自定义,那么这时执行流会从内核转到用户执行自定义捕捉方法,然后自动执行特殊的系统调用 sigreturn进入内核状态处理其他信号,如果这个时候没有其他信号递达那就再次回到主函数运行的的地方继续运行。

**简单过程速记:**整个过程会有四次身份变化(内核-->用户,用户-->内核)。

**特此说明:**当从内核返回用户时必须要切换身份,从内核-->用户,因为如果用户层实现的方法是一些非法操作,而内核权限很大,那会造成不必要的麻烦。

操作系统是怎么运行的

硬件中断

**硬件中断:**由外部硬件设备触发的,中断系统运行流程,叫做硬件中断。

问题引入:os是如何识别到硬件设备准备好了的?

由根据冯诺依曼图可知数据层面,外部硬件设备是和cpu不想链接的,但是在控制信号层面cpu是可以和外部设备进行连接的。

在整个os系统中,外部设备通过中断控制器与cpu相交互(如下图)。当某个硬件设备准备好之后会向中段控制器发送中断号,然后中断控制器通知cpu有对应硬件设备就绪,接着cpu就在中断控制器中获取中断控制号,由不同的中断控制号对应不同的外部设备os自然也就知道了是哪个外部设备准备好了。cpu获得了中断号之后会根据提前加载的中断向量表进行一一查询,找到对应的中断方法进行处理。

中断向量表:中断向量表是os启动时加载的一个函数指针数组,存放着对应中断服务程序 的入口地址,CPU 响应中断时,会根据中断号,去中断向量表里找到对应位置的地址,执行对应的方法。

**结论:**硬件设备是否就绪并不是通过cpu获得的,而是由硬件设备自动通知cpu的。所以所谓的操作系统也是基于硬件中断实现的一种软件(这是为什么我们计算机要加电的原因)。

时钟中断

**时钟中断:**os自动定期执行相应的调度方法。

问题引入:上边讲的硬件中断可以触发cpu去执行相对应得方法,但如果没有硬件的中断那os就一直阻塞等待,啥也不做吗?

其实就算没有任何硬件设备的触发,cpu也不会阻塞等待,那是因为其实在cpu内部集成了一种时钟中断控制器。他会定期每隔一段时间执行一次,触发cpu去执行对应的时钟中断方法,其实这也是进程的调度,时间片结束原理。这种集成在cpu内部的时钟源也叫cpu的主频。

软中断

**软中断:**没有外部硬件驱动,由cpu内部软件触发的中断叫软中断。

为了让操作系统支持进行系统调用,cpu设计了对应的汇编指令(int/syscall),可以让cpu内部主动触发中断逻辑。

**注意:**硬件中断是硬件中断发送给cpu信号,cpu被动去做相应的处理方法,而软中断是cpu主动产生中断信号去处理相应方法。

系统调用原理是什么?

系统当中存在一张系统调用表(表里边函数指针数组,存放所有系统调用,每一个系统调用会有一个唯一下标,下标叫系统调用号。)

对于我们平常使用的系统调用函数其实也是在用户层进行的一个封装,里边封装了相关系统的系统调用号,和int/systemctl函数,当用户去调用这个函数是,首先会让cpu int 0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查系统调用表,执行对应的方法。

缺页中断/野指针/除0错误

这些错误全部都会被转换成为CPU内部的软中断,然后走中断处理流程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来 处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等等。

缺页中断:如果系统调 用发生缺页中断,会发送缺页中断号,操作系统识别到了缺页中断号之后,会建设物理和虚拟地址的映射填充页表。

除0错误:如果系统发生除0错误会照成cpu硬件出错,然后发信号给cpu,cpu根据特定的信号处理特定的方法。

用户态和内核态

**用户态:**以用户身份,只能访问自己的(0~3GB)(cpu的执行权限为3)

**内核态:**以内核的身份,运行你通系统调用的方式,访问(3-4GB);(cpu的执行权限为0)

在系统中,用户或则内核自己通过cpu中的cs段寄存器存储特定的值(cpl)来区分当前系统处于用户态还是内核态。(0:用户态,3内核态)。

进程的虚拟地址空间分为,用户区(0~3GB)通过用户页表进行映射物理内存。和内核区(3~4Gb),也可以通过内核级页表去映射对应的物理地址。实际上内核区映射的物理地址是整个的操作系统对应的内容。

一个用户一个用户页表,多个用户内核区可以映射同一个物理内核,内核页表系统只有一份所以说无论os调用了多少进程,每个进程3-4GB区域总能看到同一份内核,我们总能找到os。这也就是为什么其中一个进程出异常就可以调用os中的一些方法。

可重入函数(内存泄漏)

(简单了解,线程那里会讲)

如上图:两个执行流(main /handler)同时执行insert,被两个以上的执行流重复进入了。如果程序报错,则证明该函数不是可重入函数,如果程序没有报错则证明函数是可重入函数。

内存泄漏点:

像上图main函数调用insert进行节点插入,在第二步的时候,handler函数也要执行insert函数,这时handler函数执行插入,插入完之后紧着这区执行main函数剩下的代码,也就是head->node1;这时node2节点就是内存泄漏点。
如果一个函数符合以下条件之一则是不可重入的:

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

volatile关键字

volatile作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
问题引入:cpu如何对变量进行修改的。

如上图所示:一般cpu对一个变量进行修改通常要3步

1.加载变量到内存中的寄存器。

2.在寄存器中计算该变量。

3.把计算后的变量写回物理内存中。

案例分析:

cpp 复制代码
int flag=0;
void handler(int sig){
    cout<<"更改全局变量,"<<flag<<"->1"<<endl;
    flag=1;
}
int main(){
    signal(2,handler);
    while(!flag);
    cout<<"process quit normal!!!"<<endl;
    return 0;
}
//运行结果
正常编译
/*^C更改全局变量,0->1
process quit normal!!!*/
优化-O1编译
/*^C更改全局变量,0->1
^C更改全局变量,1->1
^C更改全局变量,1->1
^C更改全局变量,1->1
......*/

正常编译情况执行了ctrl+c,程序捕捉到了2号信号修改了全局变量flag由0->1,主程序循环不成立,程序退出。

-O1编译情况下,执行ctrl+c程序捕捉到了2号信号,虽然修改了全局变量也程序也不会退出了。这是因为while 循环检查的 flag,并不是内存中最新的 flag,这就存在了数据二异性的问题。 while 检测的 flag 其实已经因为优化(这里的优化是之物理内存的值只会加载一次到cpu中,不会二次加载),被放在了CPU寄存器当中。很明显这时就需要 volatile。

cpp 复制代码
volatile int flag=0;

^C更改全局变量,0->1
process quit normal!!!
//正常退出

SIGCHLD信号

SIGCHLD是第17号信号:子进程退出后会给父进程发送SIGCHLD信号。

多进程的时候有些子进程退出,可以通过waitpid进行循环回收,但没有退出的子进程,waitpid会阻塞等待,父进程啥也做不了。通过设置参数(WNOHANG)非阻塞轮询等待,可以解决这个问题。但直接用SIGCHLD,处理动作为SIG_IGN,也可以直接解决。

代码验证:

cpp 复制代码
//验证子进程退出确实会向父进程发送一共SIGCHLD信号
void Say(int sig){
    cout<<"child say to father:"<<sig<<endl; 
    sleep(1);
}
int main(){
    //子进程退出后会给父进程发信号
    signal(SIGCHLD,Say);//say自定义捕捉//确定子进程退出后确定给父进程发信号
    //signal(SIGCHLD,SIG_IGN);//也可以直接回收子进程。
    //创建子进程
    pid_t id=fork();
    if(id==0){
        //子进程
        cout<<"i am child,i will exit"<<endl;
        sleep(3);
        exit(-1);
    }
    //父进程
    //回收子进程
    waitpid(id,nullptr,0);//WNOHANG 非阻塞轮询 多进程不会被阻塞(防止部分子进程退出,部分没退出)。
    cout<<"i am father"<<endl;
    return 0;
}
相关推荐
10000hours2 小时前
【Vim】vim常用命令:查找&编辑&可视区块
linux·编辑器·vim
chenyuhao20242 小时前
Linux网络编程:HTTP协议
linux·服务器·网络·c++·后端·http·https
广东大榕树信息科技有限公司3 小时前
动环监控如何有效提升机房环境管理的可靠性与响应速度?
运维·网络·物联网·国产动环监控系统·动环监控系统
txzz88883 小时前
CentOS-Stream-10 搭建NTP服务器(一)
linux·服务器·centos·ntp服务
冉佳驹4 小时前
Linux ——— 虚拟地址、页表、物理地址与 waitpid 和进程管理中的核心概念和技术
linux·waitpid·进程程序替换·exit·地址空间·非阻塞轮询·exec系列
先跑起来再说4 小时前
Go 语言的 Mutex 底层实现详解:状态位、CAS、自旋、饥饿模式与信号量
服务器·后端·golang
最后一个bug4 小时前
CPU的MMU中有TLB还需要TTW的快速查找~
linux·服务器·系统架构
zdd567895 小时前
行存表与列存表简述
运维·postgresql
小杨同学495 小时前
Linux 从入门到实战:常用指令与 C 语言开发全指南
linux