Linux信号

发布于:2025-07-07 ⋅ 阅读:(20) ⋅ 点赞:(0)

引入

我们平常执行的代码都称为前台进程,当我们启动这个进程,bash就无法接受我们输入的其他指令,ctrl+c就可以杀掉这个进程,如果我们想启动后台进程,可以在允许一个程序的时候加上&,Linux中,一个终端配一个bash,每一个登录只允许一个进程是前台进程,多个进程是后台进程
在这里插入图片描述
但我们也会发现ctrl+c杀不了这个进程了,当我们输入一个指令的时候,当前命令变成前台进程,bash变成后台进程,所以我们写的ctrl+c是发送给bash的,因为此时bash是前台进程,后台进程是接收不到的,在回显到屏幕上我们会发现消息是混乱的,但实际上bash收到的指令并没有受到干扰,还是可以执行的
ctrl+c输入给进程默认是给目标进程发送2号信号

kill -l#可以查看信号内容
#1-31号信号称为普通信号
#34-64称为实时信号

在Linux中,信号就是数字,旁边的字母是宏,代表信号的名字
在这里插入图片描述

查看

在这里插入图片描述
第一个参数表示信号的数值,第二个参数表示自定义动作,所以这个函数被用于修改特定信号的处理动作

在这里插入图片描述
在这里插入图片描述
只需要设置一次,在进程的整个生命周期都有效
那么ctrl+c是怎么变成信号的呢:操作系统在开机的时候会被载入到内存中,并且定期检查键盘数据,键盘是文件,0号文件对应的是标准输入文件,每一个文件都有对应的缓冲区,操作系统把键盘上的数据放在操作系统的缓冲区,然后送到用户层的缓冲区,但现在的操作系统并不需要采用轮询的方式收集外设的数据,因为这样对于操作系统的负担太重,所以现在的计算机采用硬件中断的方式,每一个外设都有自己的中断号,外设准备好后把中断号传输到cpu,由cpu对于中断号进行解释,操作系统就立马识别到中断号,操作系统前面有一段空间,存放中断向量表,里面存储着各种外设中断的处理方法的地址,然后往操作系统中寻找处理方法,再把外设里的数据拷入操作系统的缓冲区,而我们学习的信号,实际上是对进程模拟的硬件中断,所以当我们按ctrl+c的时候,并没有直接把ctrl+c传入操作系统,而是把ctrl+c转化为2号信号再传入操作系统,这样进程才能收到2号信号,其他信号也是类似的
在这里插入图片描述
当我们进程需要磁盘上的数据拷贝完成的时候,拷贝前磁盘也会给cpu发送中断信号,这样操作系统就知道了,等数据拷贝完后也会给cpu发送中断,这样操作系统就知道了,就唤醒进程继续执行
关于回显:键盘是一个文件,显示器也是一个文件,文件都有自己的struct file和缓冲区,当我们在使用键盘的时候,就会把数据写入键盘的缓冲区里,当我们需要回显的时候,会把键盘缓冲区的数据给显示器的缓冲区拷贝一份,这样就完成了回显
信号的产生和我们进程的执行是异步的,属于软中断

信号的产生

信号无论是下面哪一种方法产生,最终都是操作系统发送给进程的

键盘组合键

信号可以通过键盘组合键产生,ctrl+c发送二号信号,ctrl+\发送3号信号,ctrl+z发送19号信号,让进程暂停
几乎所有的信号都可以被我们捕捉的,但我们如果修改19和9号信号的时候,却没有打印我们的自定义方法,说明不是所有的信号都是可以被signal捕捉的

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


void hander(int sign)
{
    cout<<"change sign success:"<<sign<<endl;
}

int main()
{
    //signal(SIGINT,hander);
    signal(19,hander);
    while(1)
    {
        cout<<"hello world\n"<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

kill命令

kill -signo pid

kill系统调用

kill

给其他进程发送一个信号
在这里插入图片描述

#include<iostream>
#include<unistd.h>
#include<string>
#include<signal.h>
#include<sys/types.h>

using namespace std;

void Usage(string str)
{
    cout<<"Usage\n\t"<<str<<"signo pid\n\n";
}

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int signo=stoi(argv[1]);
    int pid=stoi(argv[2]);
    int n=kill(pid,signo);
    if(n==-1)
    {
        perror("kill fail\n");
        exit(2);
    }
    return 0;
}

在这里插入图片描述

raise

给调用者发送一个信号,底层是用kill函数实现的
在这里插入图片描述
这个接口在满足条件的时候会给自身进程发送一个对应的信号

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


void hander(int sign)
{
    cout<<"change sign success:"<<sign<<endl;
}

int main()
{
    signal(SIGINT,hander);
    //signal(19,hander);
    int cnt=0;
    while(cnt++)
    {
        cout<<"hello world\n"<<"pid="<<getpid()<<endl;
        sleep(1);
        if(cnt%2==0)
            raise(2);
    }
    return 0;
}

在这里插入图片描述
可以看到每隔2秒都会给自己发一次2号信号

abort

给自己发送一个6号信号,代表终止这个进程,底层是用kill函数实现的
在这里插入图片描述

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
using namespace std;


void hander(int sign)
{
    cout<<"change sign success:"<<sign<<endl;
}

int main()
{
    signal(SIGINT,hander);
    abort();
    //signal(19,hander);
    int cnt=0;
    while(++cnt)
    {
        cout<<"hello world\n"<<"pid="<<getpid()<<endl;
        sleep(1);
        if(cnt%2==0)
            raise(2);
    }
    return 0;
}

在这里插入图片描述

可以发现后续代码是没有执行的
如果我们把6号信号捕捉后,用signal方法执行我们的自定义方法,却发现最后不仅我们的自定义方法执行了,6号信号原本的方法也执行了
在这里插入图片描述
但如果我们是开另外一个窗口,运行这个程序,输入kill -6 pid,却发现这个程序并没有终止

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
using namespace std;


void hander(int sign)
{
    cout<<"change sign success:"<<sign<<endl;
}

int main()
{
    //signal(SIGINT,hander);
    signal(6,hander);
    //abort();
    //signal(19,hander);
    int cnt=0;
    while(1)
    {
        cout<<"hello world\n"<<"pid="<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
所以我们可以知道abort()这个函数里面应该还有对应的终止的动作

异常

比如说除0错误,也就是八号信号,下面信号修改了后,进程一直没有退出
在这里插入图片描述

在这里插入图片描述
段错误,是11号信号
在这里插入图片描述
如果我们捕捉了11号信号,最后也会出现上面除零错误一样的现象,进程不退出,所以进程收到异常信号之后,并不一定会退出,出现异常的是进程,而不是操作系统,当操作系统发现cpu出异常了,就会让进程自己崩溃
在这里插入图片描述
页表的地址转化其实是通过内存管理单元(MMU)管理的,当发生野指针的时候,MMU找不到对应的映射的物理地址,转化失败,MMU就会把转化失败的虚拟地址放在cpu的寄存器中,继而被操作系统识别到
比如说溢出或者越界这样的问题,实际上会被放在cpu中不同的寄存器,这样cpu就可以认出不同类型的异常,如果进程出异常也不退出,当前进程在异常的时候也会一直被调度,每次在调度的时候,操作系统就会发现问题,上下文就被挂起,但由于没有退出,再次被调度的时候,操作系统再次发现问题,再次调度这个进程的上下文,再次挂起,循环往复,所以就会一直打印我们自己的方法。而我们无法在进程运行的时候解决这个问题,所以信号是用来给用户指出进程被杀死的原因,而不是让用户修改错误,上层用户层的try_catch其实底层和信号是有关系的

软件条件

异常不一定只由硬件产生,比如说13号信号,当管道的读端关闭,写端也不会继续写,操作系统会把写端的进程杀掉,亦或者我们用read打开一个不存在文件指针表内的文件,也会出错

alarm

一旦闹钟响了,那么这个函数就会给当前进程发送一个14号信号,返回值的意思就是如果我们提前给进程发14号信号的话,就会返回还剩下的时间,但闹钟只会响一次,响过以后就不会再响了
在这里插入图片描述

#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
    //定一个5s的闹钟
    size_t n=alarm(5);
    while(1)
    {
        cout<<"wait-------------"<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
如果我们想要做一个定时任务的话,隔一段时间就会执行一次

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
using namespace std;

void work()
{
    cout<<"change sign success\n"<<endl;
}

void hander(int signo)
{
    work();
    alarm(5);
    sleep(1);
}



int main()
{
    signal(14,hander);
    //定一个5s的闹钟
    size_t n=alarm(5);
    while(1)
    {
        cout<<"wait-----------------end"<<endl;

        sleep(1);
    }
    return 0;
}

在这里插入图片描述
上图的代码可以定时,其实是因为第一次执行signal代码之后,执行了一个5秒的alarm,就执行了第一次的hander代码,设置了一个五秒的闹钟,当闹钟响的时候,由于signal信号对应的动作已经被修改,所以闹钟就会继续调用hander方法,再次设置一个alarm闹钟,循环往复
如果在一次闹钟等待时间内再次调用了alarm函数设置了新的闹钟,那么之前设置的秒数将会被新的闹钟时间所取代,alarm的返回值的意思是,当我们设置闹钟的时候,可能上一次还有一个闹钟,还没有被执行,这个返回值其实就是上一个闹钟的剩余时间
由于这样,操作系统里就存在大量的闹钟,所以每一个闹钟其实就是一个结构体,通过链表管理起来,但如果我们想先处理那些距离结束时间短的闹钟,就可以把这些闹钟用优先级队列/堆管理起来,这样每一次就不用遍历每一个闹钟,就可以找到最急的闹钟

作用

在这里插入图片描述
这里面的core dump只有一位,如果是0则为正常终止,如果是1则为异常,当我们给子进程发布2号信号,8号信号等,core dump都是0在这里插入图片描述
云服务器上无法演示,因为云服务器上的core功能被关闭的,如果我们想打开这个功能,就可以设置指令

ulimit -c 10240

在这里插入图片描述
在这里插入图片描述
当我们把core file size大小设置为10240后,再运行当前代码,给子进程传递8号信号后,coredump就变成1,而且还形成了一个文件core.707,这里的707指的是子进程的pid,也就是说,打开这个功能的时候,就会发生核心转储,一旦进程出现异常,也就是运行时错误,操作系统就会把进程消息转储到当前目录下,这个文件里面包括了我们出错的那一段原始代码
在这里插入图片描述
上述功能可以通过gdb实现,这就是事后调试

core-file core.pid

在这里插入图片描述

core表示异常终止,coredump为1,term表示正常中止,coredump为0,cont表示继续,stop表示停止进程
在这里插入图片描述

信号的保存

把信号处理的过程叫做信号递达,信号从产生到递达之间的状态称为信号未决,信号产生后就会把pending数组对应的位置设置为1,有一个函数指针数组,对应的位置指向一个个的方法,进程可以选择阻塞(block)这个信号,即使这个信号没有产生,我们也可以通过block数组阻塞这个信号,阻塞和忽略不一样
在这里插入图片描述
比如说我们想阻塞2号信号

#include<iostream>
#include<signal.h>
#include<unistd.h>

using namespace std;

int main()
{
    //忽略2号信号,SIG_IGN表示忽略
    signal(2,SIG_IGN);
    while(1)
    {
        cout<<"ignore signal 2"<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

会发现ctrl+c无法终止进程

普通信号总共有31个,是通过位图管理的,本质上就是操作系统去修改对应进程的PCB里的信号位图,这样,进程就不必马上处理信号,而是可以保存,直到更重要的事情做完,再去处理这个信号

sigset_t

这个字段必须与下面的接口一起使用,直接打印这个数据类型是没有任何意义的,用于设置传入的set的值
在这里插入图片描述

sigprocmask

这个函数可以用来修改block数组的内容,被称为信号屏蔽字
在这里插入图片描述
在这里插入图片描述
SIG_BLOCK表示添加信号屏蔽,set包含了我们当前希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCK表示减少信号屏蔽,set里包含了我们当前希望去掉信号屏蔽字的信号,相当于mask=mask&~set
SIG_SETMASK表示把当前信号屏蔽设置为set,相当于mask=set
如果设置成功返回0,出错返回-1
oset表示的是一个输出型参数,它会把我们还没修改block表的时候的mask保存下来,然后传出来

sigpending

下面这个函数用于把pending表里的数据带出来,返回值为0是成功,返回值是1为失败,返回值为其他则为错误码
在这里插入图片描述

signal

这个函数用于修改hander表对应的方法的地址,换成我们自定义的方法
在这里插入图片描述

实现屏蔽2号信号

#include<signal.h>
#include<iostream>
#include<unistd.h>

using namespace std;


void print(const sigset_t& out)
{
    for(int i =31;i>0;i--)
    {
        if(sigismember(&out,i))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
}

int main()
{
    sigset_t set,oset;
    sigemptyset(&set);
    sigemptyset(&oset);
    sigaddset(&set,2);

    sigprocmask(SIG_BLOCK,&set,&oset);
    sigset_t out;
    while(1)
    {
        int n=sigpending(&out);
        if(n<0)
        {
            cout<<"sigpengding fail"<<endl;
            continue;
        }
        cout<<endl;
        print(out);
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
可以看到当我们按下ctrl+c发送2号信号后,pending数组里的2号位置变为1,而且一直没有被处理
如果我们解除屏蔽,并且输入ctrl+c,在屏蔽解除前,2号信号并不会被触发,而屏蔽解除后,2号信号立刻被触发

#include<signal.h>
#include<iostream>
#include<unistd.h>

using namespace std;


void print(const sigset_t& out)
{
    for(int i =31;i>0;i--)
    {
        if(sigismember(&out,i))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout<<endl;
}

int main()
{
    sigset_t set,oset;
    sigemptyset(&set);
    sigemptyset(&oset);
    sigaddset(&set,2);

    sigprocmask(SIG_BLOCK,&set,&oset);
    sigset_t out;
    int cnt=0;
    while(1)
    {
        int n=sigpending(&out);
        if(n<0)
        {
            cout<<"sigpengding fail"<<endl;
            continue;
        }
        print(out);
        sleep(1);
        cnt++;
        //解除屏蔽
        if(cnt==10)
        {  
            cout<<"解除屏蔽"<<endl;
            sigprocmask(SIG_UNBLOCK,&set,&oset);
        }
    }
    return 0;
}

在这里插入图片描述
但我们无法屏蔽所有信号,就像9号和19号这样的信号

信号的捕捉

信号处理时机

当进程从内核态返回到用户态的时候,会进行对信号的检测和处理,当我们调用系统调用的时候,操作系统会自动做身份切换,以前我们研究的时候,都是在3G的用户空间,每一个进程都有自己的进程地址空间,有多少个进程就有多少个用户级页表,但内核级页表只有一份,每一个进程的3-4GB中的内容全部都是一样的
在这里插入图片描述

操作系统的本质上就是基于时钟中断的死循环,时钟中断是计算机里的一个时钟芯片执行的,每隔纳秒级别就给操作系统发送一个时钟中断,每次操作系统接收到时钟中断后,都会中断去运行一下进程的调度,如果当前进程时间片没到,操作系统就返回,如果当前进程的时间片到了,操作系统就把这个进程从cpu上剥离,然后再重新选一个进程放上cpu运行
内核态和用户态的切换主要是靠cpu上的ecs寄存器,ecs上的低两位是用来标记用户态还是内存态的,00表示内核态,11表示用户态,当ecs上的低2位处于内核态的时候,才能访问进程地址空间上的3-4G的位置

如果我们想执行自己的自定义信号方法,应该是在用户态下的,因为如果是内核态,是可以做一些非法操作的,而且signal函数其实是可以把对应的信号对应的方法转化为我们自己的方法,如果我们在这个修改的函数里调用一些比如说获取密码等操作,在内核态下的权限是足够的,所以可以执行
在这里插入图片描述

信号捕捉方法

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

sigaction

sigaction可以捕获特定信号,第一个参数是信号的编号,第二个参数和第三个参数要传入一个和类名一样的结构体指针,第一个参数是输入型参数,把用户要设置的自定义方法输入进去,第二个参数是输出型参数,输出修改这个信号方法前的那个方法,返回值是0表示修改成功,是-1表示修改失败,其他的是退出码
在这里插入图片描述
在这里插入图片描述

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstring>
#include<stdlib.h>
using namespace std;

void hander(int signo)
{
    cout<<"exchange process success,signo is "<<signo<<endl;
}

int main()
{
    struct sigaction set,oset;
    memset(&set,0,sizeof(set));
    memset(&oset,0,sizeof(oset));
    set.sa_handler=hander;

    sigaction(2,&set,&oset);

    while(1)
    {
        cout<<"i am a process"<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
信号的pending表由1->0的是发生在执行对应动作之前

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstring>
#include<stdlib.h>
using namespace std;

void print()
{
    sigset_t set;
    sigemptyset(&set);
    sigpending(&set);
    for(int i=31;i>0;i--)
    {
        if(sigismember(&set,i))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout<<endl;
}

void hander(int signo)
{
    print();
    cout<<"exchange process success,signo is "<<signo<<endl;
}

int main()
{
    struct sigaction set,oset;
    memset(&set,0,sizeof(set));
    memset(&oset,0,sizeof(oset));
    set.sa_handler=hander;

    sigaction(2,&set,&oset);

    while(1)
    {
        cout<<"i am a process"<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
可以看到在我们发送信号之后,pending表是全0的,说明信号触发后在执行相应动作之前已经把pending表由1置0了,在处理一个信号的时候,就会把当前信号加入信号屏蔽字里,当某一个信号被捕捉期间,这个信号是无法再次被递达的,只有等当前处理做完的时候,才能再次进行处理,而其他信号是没有被屏蔽的

补充

可重入函数

概念

在执行第一次头插的时候,插入进行到一半,就收到捕捉信号,信号捕捉的时候又要执行一次头插,说明一个函数被重复执行了,handler函数被main和handler执行流重复执行,这下面导致了一个因为节点丢失导致内存泄漏的问题,如果一个函数在重入的情况下出错了,这种函数称为不可重入函数,否则就叫可重入函数
在这里插入图片描述
当这个函数满足:
1.调用了malloc和free,因为malloc也是用全局链表来管理堆的
2.调用了标准I/O函数,因为I/O函数的很多实现也是通过不可重入的全局数据结构
其中一点的时候,这个函数就是不可重入的

volitile

编译器可能会把变量优化,下面是g++编译器优化的不同级别

#include<signal.h>
#include<iostream>
using namespace std;

int flag=0;

void handler(int signo)
{
    cout<<"change success:"<<signo<<endl;
    flag=1;
}
int main()
{
    //flag变量可能会被优化到cpu寄存器里
    signal(2,handler); 
    while(!flag);
    cout<<"exit success"<<endl;
    return 0;
}

在这里插入图片描述
当我们是-O0优化的时候,按ctrl+c是可以正常结束的,但当我们是-O1优化的时候,按ctrl+c就无法正常退出了
在这里插入图片描述

因为main和handler是两个执行流,在没有优化之前每一次都要从内存里读取flag放到cpu的寄存器中,所以并没有意料之外的结果,由于不需要每次读取,所以导致效率提高,但优化之后,由于main执行流并不知道在handler执行流中修改了flag的值,以为我们对flag没有任何修改,于是就直接从寄存器里用,所以handler的方法并没有修改寄存器里的flag,而是只修改了内存中的flag,所以我们发送2号信号是退出不了的,如果我们不想让它过度优化,可以给这个变量加上volitile关键字

#include<signal.h>
#include<iostream>
using namespace std;

//int flag=0;
volatile int flag=0;

void handler(int signo)
{
    cout<<"change success:"<<signo<<endl;
    flag=1;
}
int main()
{
    //flag变量可能会被优化到cpu寄存器里
    signal(2,handler); 
    while(!flag);
    cout<<"exit success"<<endl;
    return 0;
}

在这里插入图片描述
这样,就没有优化flag,在我们发送2号信号的时候程序正常执行退出

SIGCHLD

子进程退出的时候是需要父进程参与的,它会向父进程发送SIGCHLD信号,也就是17号信号,父进程就执行17号信号对应的动作,这样才能让父进程获得子进程的退出状态,释放子进程的僵尸状态,最后一定是父进程最后退出

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

void handler(int signo)
{
    cout<<"child exit code is "<<signo<<endl;
}

int main()
{
    signal(17,handler);
    pid_t id=fork();
    if(id==0)
    {
        cout<<"i am child process ,pid is "<<getpid()<<endl;
        sleep(3);
    }

    else
    {
        while(1)
        {
            cout<<"i am parent process ,pid is "<<getpid()<<endl;
            sleep(1);
        }
    }
    return 0;
}

在这里插入图片描述

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

void handler(int signo)
{
    int ret=waitpid(-1,nullptr,0);
    cout<<"child exit code is "<<signo<<"ret="<<ret<<endl;
}

int main()
{
    signal(17,handler);
    pid_t id=fork();
    if(id==0)
    {
        cout<<"i am child process ,pid is "<<getpid()<<endl;
        sleep(3);
    }

    else
    {
        while(1)
        {
            cout<<"i am parent process ,pid is "<<getpid()<<endl;
            sleep(1);
        }
    }
    return 0;
}

我们可以把wait放在handler里,但这样就会有一个问题,如果我们有多个子进程,那如果我们同时退出,所有的子进程同时给父进程发送17号信号调用对应的handler方法,那么我们最多也就执行一次,那剩下的子进程怎么办呢,所以我们可以把waitpid放在一个循环中,只要waitpid到一个子进程的pid,就会释放,但这里还是有一个问题,就是在等待的时候,如果我们只有一部分的子进程退出,那我们就会卡在循环的判断条件里,收到信号也无法继续执行并返回,所以这里必须要非阻塞等待

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

void handler(int signo)
{
    int ret=0;
    while((ret=waitpid(-1,nullptr,WNOHANG))>0)
    {
        cout<<"child exit code is "<<signo<<"ret="<<ret<<endl;
    }
}

int main()
{
    signal(17,handler);
    for(int i=1;i<=10;i++)
    {
        pid_t id=fork();
        if(id==0)
        {
            cout<<"i am child process "<<i<<" pid is "<<getpid()<<endl;
            exit(0);
        }

        else
        {
            cout<<"i am parent process ,pid is "<<getpid()<<endl;
        }
    }
    sleep(50);
    return 0;
}

在这里插入图片描述

SIG_IGN

父进程可以调用sigaction把子进程的17号信号设置为SIG_IGN,这样子进程在退出的时候会自动被释放,也不会产生僵尸进程,也不会通知父进程

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

void handler(int signo)
{
    int ret=0;
    while((ret=waitpid(-1,nullptr,WNOHANG))>0)
    {
        cout<<"child exit code is "<<signo<<",ret="<<ret<<endl;
    }
}

int main()
{
    //signal(17,handler);
    signal(17,SIG_IGN);
    for(int i=1;i<=10;i++)
    {
        pid_t id=fork();
        if(id==0)
        {
            cout<<"i am child process "<<i<<" pid is "<<getpid()<<endl;
            sleep(5);
            exit(0);
        }
    }
    while(1)
    {
        cout<<"i am parent process ,pid is "<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

可以看到最后子进程最后也不会变成僵尸进程然后再被父进程回收
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到