Linux —— Linux进程信号 - 信号保存 和 信号处理

目录

[1. 信号保存](#1. 信号保存)

[1.1 信号相关的常见概念:](#1.1 信号相关的常见概念:)

[1.2. 理解阻塞](#1.2. 理解阻塞)

[1.3 进程是如何识别信号的??](#1.3 进程是如何识别信号的??)

[1.3.1 sigset_t](#1.3.1 sigset_t)

[1.3.2 信号集操作函数](#1.3.2 信号集操作函数)

[1.3.2.1 系统调用:sigprocmask](#1.3.2.1 系统调用:sigprocmask)

[1.3.2.2 系统调用:sigpending](#1.3.2.2 系统调用:sigpending)

[1.3.2.3 扩展理解](#1.3.2.3 扩展理解)

问题1:

问题2:

问题3:

[2. 信号处理](#2. 信号处理)

[2.1 信号处理的流程:](#2.1 信号处理的流程:)

[2.2 尝试理解 ---- 理解内核和用户态](#2.2 尝试理解 ---- 理解内核和用户态)

[2.3 操作系统是怎么运行的](#2.3 操作系统是怎么运行的)

[2.3.1 周边问题 ---- 硬件中断](#2.3.1 周边问题 ---- 硬件中断)

[2.3.2 时钟中断](#2.3.2 时钟中断)

细节1:

细节2:

细节3:

细节4:

[2.3.3 软中断](#2.3.3 软中断)

[2.3.4 系统调用的本质:](#2.3.4 系统调用的本质:)

[2.3.5 缺页中断?写时拷贝?内存碎片化处理?除零野指针错误?](#2.3.5 缺页中断?写时拷贝?内存碎片化处理?除零野指针错误?)

[2.3.6 总结:](#2.3.6 总结:)

[2.4 sigaction:](#2.4 sigaction:)

[3. 可重入函数:](#3. 可重入函数:)

[5. volatile:](#5. volatile:)

[6. SIGCHLD信号](#6. SIGCHLD信号)


C++中的异常(throw/catch)和操作系统信号(如SIGSEGV)是两套独立机制。

  • 野指针错误会被硬件捕获,操作系统向进程发送SIGSEGV信号,默认终止程序;如果注册了信号处理函数,可以执行自定义动作(如打印)。
  • 内存分配失败(new)会抛出std::bad_alloc异常,可以被catch捕捉并打印。
  • 信号与C++异常没有直接关系,但某些硬件异常(如段错误)会通过信号触发,而信号处理函数不能用throw抛出异常(极不安全)。
  • 两者间接关系:信号可以转换为异常(如将SIGSEGV转为C++异常),但一般不这么设计。

1. 信号保存

1.1 信号相关的常见概念:

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)
  • 进程可以选择 阻塞(Block)/屏蔽 某个信号。(阻塞(Block)/屏蔽相当于就是一个开关)

举一个例子,来理解前3个概念:

上课的时候,老师布置作业,当前正在上课,没有办法立即处理这个作业,将题写在小本子上,下课之后,回到宿舍中吃个饭再做作业。回到宿舍做作业称为递达这个作业;老师在课上布置作业,还没有写这个作业时,在处理这个作业之前,称为信号未决;回到宿舍,不想动了,将作业记录了下来,但是就是不写,这个状态就是屏蔽信号。作业积累了半学期了,一直没写,越来越多,你发现这样不行呀,一鼓作气就全写完了,这个过程就是对信号解除屏蔽

  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。[ 忽略这个东西是属于递达中的一种,阻塞属于阻塞的一种 ,两个概念起效果的阶段是不一样的]

1.2. 理解阻塞

1.3 进程是如何识别信号的??

// 内核结构 2.6.18
struct task_struct {
...
/* signal handlers */
struct sighand_struct * sighand ;
sigset_t blocked
struct sigpending pending ;
...
}
struct sigpending {
struct list_head list ;
sigset_t signal; //位图结构
};

struct sighand_struct {
atomic_t count;
struct k_sigaction action [_ NSIG ]; // #define _NSIG 64 1-64个信号 kill -l
spinlock_t siglock;
};

对每张表的操作无非就是增删查改,还有可能增加了一些接口:

1.3.1 sigset_t

sigset_t 称为信号集 ,sigset_t 类型对于每种信号用一个bit表示"有效" 或 "无效" 状态,至于这个类型内部如歌存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对他的内部数据做任何解释,比如printf直接打印sigset_t变量是没有意义的。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask,对应block位图)

1.3.2 信号集操作函数

#include <signal.h>

  • int sigempty (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 *set , int signo);//判定一个信号是否在集合中
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表⽰该信号集不包含任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表⽰ 该信号集的有效信号包括系 统⽀持的所有信号。
  • 注意,在使⽤sigset_ t类型的变量之前,⼀定要调 ⽤sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调⽤sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

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

**所以要设置到内核中,必须调用系统调用!!!**设置到内核中,本质就是要修改进程的pending表,block表,handler表,修改PCB内核数据结构,只有OS有权限,所以必须调用系统调用!!!

1.3.2.1 系统调用:sigprocmask
1.3.2.2 系统调用:sigpending

block表和pending表都设置为全0,将block表中的二号信号默认设为屏蔽的,不断获取pending表,打印pending表,看见的就应该是全0,此时,发送2号信号,因为2号信号不会被递达,所以pending表中有一个位图由0变为1,对应代码:

cpp 复制代码
#include <iostream>
#include <functional>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

void PrintPending(sigset_t &pending)
{
    std::cout << "[pid:"<< getpid() << "]" << "sigpending list:  ";
    // 从右到左,比特位是从低到高, 0000 0000
    for(int signo =31; signo > 0; signo--)
    {
        if(sigismember(&pending,signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << "\r\n";
}

int main()
{
    // 1.屏蔽2号信号
    // 1.1 用户层面,设置位图
    sigset_t block, oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);

    sigaddset(&block, SIGINT); // 将2号信号进行添加,这里的时候,我们有没有设置当前进程的信号屏蔽字(阻塞信号集)??------ 并没有!!!

    // 只有真正的系统调用才将信号屏蔽字设置进去
    // 1.2 设置内核的信号屏蔽字
    sigprocmask(SIG_SETMASK, &block, &oblock);

    while (true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        // 2.1 获取当前进程的pending信号集
        sigpending(&pending);

        // 2.2 不断打印所有的pending信号集中的信号
        PrintPending(pending);
        sleep(1);
    }
}

运行结果:

1.3.2.3 扩展理解

问题1:屏蔽了所有的信号呢??9号信号不可被捕捉,不可被屏蔽

问题2:如果解除对2号的屏蔽,也要看到由 1->0

问题3:2号信号被递达,pending 1->0,抵达前变化,还是抵达后变化?递达前变化恢复

问题1:
cpp 复制代码
    // 1.3 屏蔽所有的信号
    for(int i = 1;i <= 31; i++)
    {
        sigaddset(&block, i);
    }

运行结果:

从上面的结果中可以看出 9 号和 19 号信号无法被屏蔽。

问题2:
cpp 复制代码
#include <iostream>
#include <functional>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

void PrintPending(sigset_t &pending)
{
    std::cout << "[pid:" << getpid() << "]" << "sigpending list:  ";
    // 从右到左,比特位是从低到高, 0000 0000
    for (int signo = 31; signo > 0; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << "\r\n";
}

void handler(int signo)
{
    std::cout << "我获取到了:"<< signo << "信号" << std::endl;
    // 不要让进程终止
}

int main()
{
    // 0.设置2号信号的处理动作,不要让它终止
    signal(2, handler);

    // 1.屏蔽2号信号
    // 1.1 用户层面,设置位图
    sigset_t block, oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);

    sigaddset(&block, SIGINT); // 将2号信号进行添加,这里的时候,我们有没有设置当前进程的信号屏蔽字(阻塞信号集)??------ 并没有!!!

    // 只有真正的系统调用才将信号屏蔽字设置进去
    // 1.2 设置内核的信号屏蔽字

    sigprocmask(SIG_SETMASK, &block, &oblock);

    int cnt = 15;
    while (true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        // 2.1 获取当前进程的pending信号集
        sigpending(&pending);

        // 2.2 不断打印所有的pending信号集中的信号
        PrintPending(pending);
        cnt--;
        if (cnt == 0)
        {
            // 解除对2号信号的屏蔽  --- 2号信号的默认动作是终止进程
            std::cout << "解除对2号的屏蔽啦!!" << std::endl;
            // 怎么做??oblock老的屏蔽字
            sigprocmask(SIG_SETMASK, &oblock, nullptr);
        }
        sleep(1);
    }
}

运行结果:

问题3:

执行主代码是当前进程,处理信号捕捉也是当前进程去执行的,在2好信号捕捉的代码中,获取pending && 打印,若果获取到pending位图2号信号还是1,意味着要将handler方法做完才会递达;如果正在执行handler方法,打印pending表,位图已经为0了,表明执行handler之前就已经被置为0了。两个事实:1. 执行信号捕捉方法的依旧是当前进程自己;2. 如果是执行完捕捉方法之后才递达的,在正在执行handler方法,打印pending表中的位图还是1。

代码:

cpp 复制代码
#include <iostream>
#include <functional>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

void PrintPending(sigset_t &pending)
{
    std::cout << "[pid:" << getpid() << "]" << "sigpending list:  ";
    // 从右到左,比特位是从低到高, 0000 0000
    for (int signo = 31; signo > 0; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << "\r\n";
}

void handler(int signo)
{
    std::cout << "我获取到了:" << signo << "信号" << std::endl;
    // 不要让进程终止

    sigset_t pending;
    sigemptyset(&pending);
    // 2.1 获取当前进程的pending信号集
    sigpending(&pending);

    // 2.2 不断打印所有的pending信号集中的信号
    std::cout << "#######################" << std::endl;
    PrintPending(pending);
    std::cout << "#######################" << std::endl;

}

int main()
{
    // 0.设置2号信号的处理动作,不要让它终止
    signal(2, handler);

    // 1.屏蔽2号信号
    // 1.1 用户层面,设置位图
    sigset_t block, oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);

    // 1.3 屏蔽所有的信号
    // for(int i = 1;i <= 31; i++)
    // {
    //     sigaddset(&block, i);
    // }

    sigaddset(&block, SIGINT); // 将2号信号进行添加,这里的时候,我们有没有设置当前进程的信号屏蔽字(阻塞信号集)??------ 并没有!!!

    // 只有真正的系统调用才将信号屏蔽字设置进去
    // 1.2 设置内核的信号屏蔽字

    sigprocmask(SIG_SETMASK, &block, &oblock);

    int cnt = 15;
    while (true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        // 2.1 获取当前进程的pending信号集
        sigpending(&pending);

        // 2.2 不断打印所有的pending信号集中的信号
        PrintPending(pending);
        cnt--;
        if (cnt == 0)
        {
            // 解除对2号信号的屏蔽  --- 2号信号的默认动作是终止进程
            std::cout << "解除对2号的屏蔽啦!!" << std::endl;
            // 怎么做??oblock老的屏蔽字
            sigprocmask(SIG_SETMASK, &oblock, nullptr);
        }
        sleep(1);
    }
}

运行结果:

从上面的结果可以看出:2号信号被递达,pending 1->0,是抵达前变化的!!!

2. 信号处理

2.1 信号处理的流程:

上面其实就是信号捕捉的一个大致的流程,但是为什么要进入内核态?什么叫做内核态?什么又叫做用户态?只有系统调用才能进入内核态吗?比如,写一个死循环,里面没有调用任何系统调用,死循环里面一行代码都不写,这个进程好像也能处理信号,为什么没有调用系统调用也能进入内核??

2.2 尝试理解 ---- 理解内核和用户态

2.3 操作系统是怎么运行的

操作系统是怎么运行的??得先了解硬件中断、时钟中断、死循环、软中断、缺页中断

2.3.1 周边问题 ---- 硬件中断

要真正理解上面的内核和用户态,首先的谈谈其它的知识点,硬件中断

  • 中断向量表就是操作系统的⼀部分,启动就加载到内存中了。OS开机时不是要加载内核嘛,最先加载的软件就是中断
  • 通过外部硬件中断,操作系统就不需要对外设进⾏任何周期性的检测或者轮询
  • 由外部设备触发的,中断系统运行流程,叫做硬件中断

以上便是中断向量表的理论部分,下面的便是Linux0.11-1的的源代码,中断向量表实际上的实现是比较复杂的,这里就简单的看看

2.3.2 时钟中断

进程可以在OS的指挥下,被调度,被执行,那么OS自己本身也是软件,被谁指挥,被谁推动执行呢?

外部设备可以触发硬件中断,但是这个需要用户或者设备自己触发,有没有一种设备它可以以固定的时间点,固定的频率,一直向CPU出发硬件中断呢?---- 时钟中断!!!

对应的源代码的大致框架:

补充细节:

细节1:

时钟中断由CPU内部自动产生,不需要外设了,CPU自主的用中断形式来走_timer_interrupt,来进行调度了

细节2:
细节3:
细节4:

2.3.3 软中断

do_timer也是在向量表中的!!!

电脑有时候关机了,断电了,将电脑关机上一两个礼拜,之后再次开机,没有联网,电脑的时间是正确的;或者是有时候才买回来的笔记本,刚打开时间是不对的,但是一连上网之后,时间又变成正确的了。不管是台式机还是笔记本,在主板上是存在小型的纽扣电池的,将所有的设备全部都断电了,但是纽扣电池会给你的元器件做计数统计,时钟中断依旧触发,依旧做计数,当OS启动时,会从那些硬件设备中将数据读出来,也就是说,笔记本内部是包含了一个圆形的纽扣电池,在你关机时间不久的情况下,下次再开机,时间就是对的。但是,将你的电脑长时间不通电,过上一两年之后,时间就不对了,因为那个纽扣电池长时间就没电了,这就是为什么才买回来的笔记本刚开机时间就是不对的,设备的出厂日期大概率是生成到售卖的时间间隔远,也有可能是库存。

在32位的系统中,通常是int 0x80,64位调用的是syscall

2.3.4 系统调用的本质:

看一下源代码:

上面的流程就可以拿着系统调用号完成系统调用

如何证明呀??

⽽系统调⽤号,不是 glibc 提供的,是内核提供的,内核提供系统调⽤⼊⼝函数 man 2 syscall ,或者直接提供汇编级别软中断命令 int or syscall ,并提供对应的头⽂件或者开发入口,让上层语⾔的设计者使⽤系统调⽤号,完成系统调⽤过程。

所以是语言层完成将系统调用函数名转换为系统调用号,也是语言层完成将系统调用号放入寄存器中,语言层帮我们调用int 0x80,然后触发软中断,OS开始调用系统调用。**所以系统调用是基于软中断的!!!**所以我们也可以通过汇编调系统调用,是可以的!!因为C语言能!!C标准库是在用户层的!!但是目前还不行,因为还需要考虑传参和返回值之类的,所以在Linux系统中,还提供了一个系统调用:syscall

所以,C++、Java、Php、Python所有的计算机语言只要在Linux中跑,都要跟C语言有关。

确实 int 0x80 or syscall 陷入内核,让CPU执行OS的代码,但是这里还差一点,差的就是权限如何体现,安全如何保证???如何做到OS被别人访问时只能通过系统调用来访问,是如何做到这点的??

2.3.5 缺页中断?写时拷贝?内存碎片化处理?除零野指针错误?

缺⻚中断?内存碎⽚处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,然后⾛中断处理例程,完成所有处理。有的是进⾏申请内存,填充⻚表,进⾏映射的。有的是⽤来处理内存碎⽚的,有的是⽤来给⽬标进⾏发送信号,杀掉进程等等。

2.3.6 总结:

回过头来:信号处理

至此,以上便是信号处理的理论部分。

信号的捕捉也要有捕捉方法:

2.4 sigaction:

先用sigaction实现出与signal同样的效果,再来谈谈 sigset_t sa_mask;这个参数。

代码:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
    std::cout << "获取到一个信号:"<<signo << std::endl;
}

int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&(act.sa_mask));  //我们现在是没有设置到内核的
    sigaction(2, &act, &oact);    //将2号信号的捕捉方法,设置到内核中!!通过系统调用

    while(true)
    {
        std::cout << "我是一个进程:"<< getpid() << std::endl;
        sleep(1);
    }

}

sigaction 和 signal 用起来没什么区别呀??实际上之后用的最多的也是 signal,但是要理解一下 sa_mask:

当某个信号的处理函数被调⽤时,内核⾃动将当前信号加⼊进程的信号屏蔽字,(当我们正在捕捉2号信号时,2号信号是会被自动屏蔽掉的哦)当信号处理函数返回时⾃动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产⽣,那么 它会被阻塞,直到当前处理结束为⽌(也就是说,你正在处理某一个信号,这个信号就会被自动屏蔽掉,可以防止任意信号进行递归处理!!如果正在处理2好号信号,此时来了3号信号,这样也是会进行递归的,但这样最多只递归上31次,因为只有31个信号)。**如果在调⽤信号处理函数时,除了当前信号被⾃动屏蔽之外,还希望⾃动屏蔽另外⼀些信号,则⽤sa_mask字段说明这些需要额外屏蔽的信号, (如果用户想屏蔽其他信号,就用sa_mask就把其他信号加进来)**当信号处理函数返回时⾃动恢复原来的信号屏蔽字。 sa_flags字段包含⼀些选项,本篇文章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本篇文章不考虑。

如何证明呢?

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
    std::cout << "获取到一个信号:" << signo << std::endl;
    
    while (true)
    {

        sigset_t pending;
        sigemptyset(&pending);

        sigpending(&pending);

        for (int i = 31; i > 0; i--)
        {
            if (sigismember(&pending, i))
                std::cout << "1";
            else
                std::cout << "0";
        }
        std::cout << std::endl;

        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&(act.sa_mask)); // 我们现在是没有设置到内核的
    sigaction(2, &act, &oact);   // 将2号信号的捕捉方法,设置到内核中!!通过系统调用

    while (true)
    {
        std::cout << "我是一个进程:" << getpid() << std::endl;
        sleep(1);
    }
}

处了屏蔽掉2号信号,也想把3、4、5、6号给屏蔽掉:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
    std::cout << "获取到一个信号:" << signo << std::endl;
    
    while (true)
    {

        sigset_t pending;
        sigemptyset(&pending);

        sigpending(&pending);

        for (int i = 31; i > 0; i--)
        {
            if (sigismember(&pending, i))
                std::cout << "1";
            else
                std::cout << "0";
        }
        std::cout << std::endl;

        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&(act.sa_mask)); // 我们现在是没有设置到内核的
    sigaddset(&(act.sa_mask),3);
    sigaddset(&(act.sa_mask),4);
    sigaddset(&(act.sa_mask),5);
    sigaddset(&(act.sa_mask),6);
        
    sigaction(2, &act, &oact);   // 将2号信号的捕捉方法,设置到内核中!!通过系统调用

    while (true)
    {
        std::cout << "我是一个进程:" << getpid() << std::endl;
        sleep(1);
    }
}

3. 可重入函数:

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了 malloc 或 free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准 I/O库函数。标准 I/O库的很多实现都以不可重入的方式使用全局数据结构。
  • 具有全局变量,全局数据,这样的函数一般也是不可重入的
  • 大部分的函数都是不可重入的函数

可重入函数:函数里面只有局部变量,没有任何的全局变量,这种函数一般都是可重入函数。

5. volatile:

volatile 是C90标准的C语言中的32个关键字之一,称为易变关键字。

cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int flag = 0;

void handler(int signo)
{
    flag = 1;
    printf("改变flag: 0->1\n");
}

int main()
{
    signal(2,handler);
    printf("进程启动:%d\n",getpid());
    while(!flag);
    printf("进程正常结束!\n");

    return 0;
    
}

运行结果:

cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile int flag = 0;  // 保持内存可见性

void handler(int signo)
{
    flag = 1;
    printf("改变flag: 0->1\n");
}

int main()
{
    signal(2,handler);
    printf("进程启动:%d\n",getpid());
    while(!flag);
    printf("进程正常结束!\n");

    return 0;
    
}

运行结果:

6. SIGCHLD信号

进程⼀章讲过⽤wait和waitpid函数清理僵⼫进程,⽗进程可以阻塞等待⼦进程结束,也可以⾮阻 塞地查询是否有⼦进程结束等待清理(也就是轮询的⽅式)。采⽤第⼀种⽅式,⽗进程阻塞了就不 能处理⾃⼰的⼯作了;采⽤第⼆种⽅式,⽗进程在处理⾃⼰的⼯作的同时还要记得时不时地轮询⼀ 下,程序实现复杂。

其实,子进程在终⽌时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,⽗进程可以自定义SIGCHLD信号的处理函数,这样⽗进程只需专⼼处理⾃⼰的⼯作,不必关⼼⼦进程了,⼦进程 终⽌时会通知父进程,⽗进程在信号处理函数中调⽤wait清理⼦进程即可。

证明:子进程在终⽌时会给父进程发SIGCHLD信号

SIGCHLD是17号信号:

证明代码:

cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

void handler(int signo)
{
    printf("%d进程, 收到了信号: %d\n", getpid(),signo);
}

int main()
{
    signal(SIGCHLD , handler);
    pid_t id = fork();
    if(id == 0)
    {
        sleep(3);
        exit(0);
    }
    while(1)
    {
        printf("我是父进程:%d\n",getpid());
        sleep(1);
    }
}

以上的运行结果证实了:子进程在终止时会给父进程发SIGCHLD信号。

回收子进程的全新的方案:

cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

void handler(int signo)
{
    pid_t rid = waitpid(-1, NULL, 0);
    printf("%d进程, 收到了信号: %d, 回收子进程: %d\n", getpid(),signo,rid);
}

int main()
{
    signal(SIGCHLD , handler);
    pid_t id = fork();
    if(id == 0)
    {
        printf("我是子进程:%d\n", getpid());
        sleep(3);
        exit(0);
    }
    while(1)
    {
        printf("我是父进程:%d\n",getpid());
        sleep(1);
    }
}

运行结果:

上面的情况只适用于一个子进程。

如果是以下的情况的话:

场景1:如果我们创建了多个子进程,多个子进程几乎同时退出 --- while循环进行回收

cpp 复制代码
void handler(int signo)
{
    while (1)
    {
        pid_t rid = waitpid(-1, NULL, 0);
        if(rid > 0)
            printf("%d进程, 收到了信号: %d, 回收子进程: %d\n", getpid(), signo, rid);
        else if(rid < 0)
            break;
    }
}

场景2:如果我们创建了多个子进程,一部分退出,一部分不退出。比如6个子进程要退出,4个子进程不退的话,当我们回收了6个子进程之后,在回收第 7 个子进程的时候就会被阻塞。

cpp 复制代码
void handler(int signo)
{
    while (1)
    {
        pid_t rid = waitpid(-1, NULL, WNOHANG); 
        if(rid > 0)
            printf("%d进程, 收到了信号: %d, 回收子进程: %d\n", getpid(), signo, rid);
        else if(rid < 0)
            break;
        else
            break;
    }
}

事实上,由于UNIX 的历史原因,要想不产⽣僵⼫进程还有另外⼀种办法:⽗进程调 ⽤sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的⼦进程在终⽌时会⾃动清理掉,不会产⽣僵⼫进程,也不会通知⽗进程。系统默认的忽略动作和用户用sigaction函数⾃定义的忽略 通常是没有区别的,但这是⼀个特例。此⽅法对于Linux可⽤,但不保证在其它UNIX系统上都可用。

cpp 复制代码
int main()
{
    signal(SIGCHLD, SIG_IGN);
    pid_t id = fork();
    if (id == 0)
    {
        printf("我是子进程:%d\n", getpid());
        sleep(3);
        exit(0);
    }
    while (1)
    {
        printf("我是父进程:%d\n", getpid());
        sleep(1);
    }
}

需要注意的是:

1. 这种方法不通用,因为不是所有的系统都支持的

2. 有时候我们还需要子进程的退出信息,所以进程等待是少不了的

3. 子进程在终⽌时会给父进程发SIGCHLD信号,该信号的默认处理动作确实是忽略,但是这个忽略有点特殊,OS认为对该忽略做正常处理:收到17号信号什么都不做,包括不回收子进程;如果自己设置了SIG_IGN,就要求OS将来退出时收到17,主动处理掉子进程。

没有设置SIG_IGN:父进程对子进程的处理SIGCHLD,对signal函数的处理不是SIG_IGN,而是SIG_DFL(只不过SIG_DFL在内核中默认被设置成为了忽略)

手动设置SIG_IGN:回收子进程。

结束语:

掌握好block、pending和handler三张表,你就能从容应对各种信号相关场景。如果这篇文章帮你理清了思路,不妨收藏备用。

相关推荐
沐风_ZTL7 小时前
Ubuntu 22.04中OpenCode 安装与配置完整指南,及常问题解决办法
linux·ai·opencode
网络与设备以及操作系统学习使用者8 小时前
vi与vim在openEuler中的差异及应用
linux·运维·网络·学习·vim
专注VB编程开发20年8 小时前
python运行提速方案全解
java·linux·服务器
相思难忘成疾8 小时前
Ubuntu 入门:安装、网络、软件一站式教程
linux·网络·ubuntu
luoqice9 小时前
linux下安装rtsp流媒体服务器
linux·音视频
学困昇9 小时前
Linux IPC 详解:匿名管道、命名管道、共享内存与信号量
linux·运维·服务器·c语言·c++·人工智能
汽车搬砖家9 小时前
VM Fusion安装Ubuntu系统
linux
AI小小怪10 小时前
保姆级教程:Ubuntu 22.04 安装 NVIDIA GPU 驱动 + CUDA 12.6(RTX 3080 显卡)
linux·nvidia·cuda
Embedded-Xin10 小时前
ROS2进阶——消息服务质量QOS策略
linux·机器人·嵌入式