【Linux】-对于信号章节补充的知识点,以及多线程知识的汇总

在这里插入图片描述
💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!


前言

这篇一开始讲解的对于信号处理的的图解补充,在信号那节博主还要补充三个小的知识点,因为这里面有一个小知识点和多线程会有点关系,所以拿出来放在一起去讲,对于线程部分讲到很多硬件的知识,和我们当初学习进程一样,也是一块比较难啃的骨头,所以博主选择另写一篇博客,给大家吧前期知识补充后,后面才可以更好的学习线程。话不多说,我们一起来看正文


一、信号处理的流程图

对于信号处理,博主在给大家看一幅比较好的图解:
在这里插入图片描述

转自:信号执行流程,这位大佬画的图解非常到位,大家先通过我的上一篇博客,把信号的整个过程都看一遍,看这幅的前提还有一个是要了解用户态和内核态,这幅图完美的展示了信号处理的流程,以及一些注解,希望大家可以明白。不懂的再在评论区发出你的问题。

二、3个小知识点

我们在信号部分还有三个小知识点要补充,一个是信号引起的多执行流,二是volatile关键字的理解,三是父子进程之间的信号

2.2.1可重入函数

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

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重(Reentrant) 函数

上面的案例造成的结果就是有可能内存泄漏,但是目前我们学到大部分都是不可重入函数,因为有多个执行流才导致这样的问题,让一个函数在计算的时候只有一个执行流执行不就行了,这个大家到时候等博主讲解到多线程的时候就知道了,因为大家对于执行流的概念还不是特别的清楚。学到线程大家就知道了


如果一个函数符合以下条件之一则是不可重入的:
(1) 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
(2) 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

2.2.2volatile

这个关键字在我们c语言就出现过,但是几乎没有使用过,这个关键字的作用保持内存的可见性,我将通过例子给大家介绍,来看代码:

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
int flag=0;
void myhandler(int sig)
{
   
    cout<<"flag:1->0"<<"signal:"<<sig<<endl;
    flag=1;
}
int main() {
   
    signal(2,myhandler);
    while(!flag)
    {
   }
    return 0;
}

在这里插入图片描述

这个程序就是通过自定义捕捉方法将全局变量修改导致循环终止,上面的结果符合我们的预期

大家还记得我们在学习右值引用的时候,就说过编译器会进行优化,那我们的gcc/g++也是编译器,所以它也会对一些操作进行优化,我们的gcc/g++不带任何选项的时候只会默认优化,它是有优化等级的,我们通过man g++来查看文档在这里插入图片描述
我们来看看默认的优化和最高级优化运行的效果:
在这里插入图片描述

通过O0和O1我们发现g++的默认优化等级是O0,我们看到一个奇怪的现象,我们使用优化程度较高的等级后,程序居然不退出了,而我们的flag确实变成了1,循环终止,进程退出,那为什么结果和我们看到的不一样呢?我们的CPU会做两种计算,一种是算术运算,一种是逻辑运算,我们的主函数里面只有一个flag检测,编译器没有发现这个执行流中没有操作可以修改这个变量,博主特地写的是!flag这是逻辑运算,就要意味检测真假要加载到CPU里面,数据会放在寄存器上面,造成内存不可见,当我们修改flag,其实是在内存层面修改了,而cpu的寄存器里面的数据还没有变,编译器没有检测到执行流修改这个变量,就导致我们每次检测都在寄存器上面去数据,因为寄存器上面的数据一直没有变过,所以程序就一直不退出

那我们怎么是的内存可见性呢,此时就需要使用volatile:
在这里插入图片描述

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

2.2.3SIGCHLD

父进程回收子进程退出的退出信息会遇到下面的问题,进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。

就好比上一节说的os是怎么知道键盘上面有数据的,不是通过轮询去时不时去检查的,而是硬件中断。信号就是模拟硬件中断的,所以我们的子进程如果提前退出了,就会给父进程发送一个信号,告诉父进程我已经终止了,这样就解决上面说到问题,解决了父进程阻塞等待和轮询带来的损耗,接下来我们一起来看看再子进程提前退出后,父进程是不是收到对应的SIGCHLD的信号,来看案例:

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

void myhandler(int signo)
{
   
    cout<<"i get a signal"<<signo<<endl;//这里只是打印,没有干其他的事
}
int main()
{
   

    signal(SIGCHLD,myhandler);
    pid_t pid;
    pid = fork();
    if(pid < 0)
    {
   
        cout<<"fork error"<<endl;
        return -1;
    }
    else if(pid == 0)
    {
   
        int cnt=0;
        while(1)
        {
   
            cout<<"i am a chld:"<<getpid()<<endl;
            if(cnt==5)
            {
   
                break;
            }
            sleep(1);
            cnt++;
        }
        cout<<" chld quit!!!"<<endl;
        exit(1);
    }
	while (true)
    {
   
        cout << "I am father process: " << getpid() << endl;
        sleep(1);
    }
    waitpid(pid,NULL,0);
    sleep(3);
    return 0;
}

在这里插入图片描述

通过结果来看,我们的父进程确实收到了子进程发过来的信号。

  1. 一个子进程
    我们的自定义捕捉函数刚才只做了打印,那我们直接将等待函数放到这个自定义捕捉函数里面不就行了
void myhandler(int signo)
{
   
    sleep(3);//三秒后再等待
    cout<<"i get a signal"<<signo<<endl;
    waitpid(-1,NULL,0);//可以等待任意进程
}

在这里插入图片描述

  1. 多个子进程
    我们看到了上面的结果,也是可以了,如果同时有10个子进程同时退出呢?退出一半呢,此时上面的代码也实现不了,我们可以循环的办法去实现10个进程同时退出的场景:
#include<iostream>
#include<sys/wait.h>
#include<unistd.h>
using namespace std;

void myhandler(int signo)
{
   
    //sleep(3);//三秒后再等待
    cout<<"i get a signal"<<signo<<endl;//这里只是打印,没有干其他的事
    pid_t rid;
    while((rid = waitpid(-1,nullptr,0))>0)
    {
   
        cout << "I am proccess: " << getpid() << " catch a signo: " << signo << "child process quit: " << rid << endl;
        //sleep(1);
    }
}
int main()
{
   

    signal(SIGCHLD,myhandler);
   for (int i = 0; i < 10; i++)
    {
   
        pid_t id = fork();
        if (id == 0)
        {
   
            while (true)
            {
   
                cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
                sleep(10);
                break;
            }
            cout << "child quit!!!" << endl;
            exit(0);
        }
        
    }
    // father
    while (true)
    {
   
        cout << "I am father process: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
退出一半呢?这时候就会出现自定义捕捉里面的循环是阻塞等待,一直退出不了自定义捕捉函数,所以使用非阻塞等待的方式:WNOHANG

void myhandler(int signo)
{
   
    //sleep(3);//三秒后再等待
    cout<<"i get a signal"<<signo<<endl;//这里只是打印,没有干其他的事
    pid_t rid;
    while((rid = waitpid(-1,nullptr,WNOHANG))>0)
    {
   
        cout << "I am proccess: " << getpid() << " catch a signo: " << signo << "child process quit: " << rid << endl;
        //sleep(1);
    }
}

在这里插入图片描述

每个退出的子进程退出的时候都会给父进程发送一个信号,因为不会阻塞等待了。

三、线程的概念

博主将通过一张图解带大家了解线程的概念

在这里插入图片描述

3.1线程的优缺点

(1)优点

  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘柚!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值