💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者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;
}
通过结果来看,我们的父进程确实收到了子进程发过来的信号。
- 一个子进程
我们的自定义捕捉函数刚才只做了打印,那我们直接将等待函数放到这个自定义捕捉函数里面不就行了
void myhandler(int signo)
{
sleep(3);//三秒后再等待
cout<<"i get a signal"<<signo<<endl;
waitpid(-1,NULL,0);//可以等待任意进程
}
- 多个子进程
我们看到了上面的结果,也是可以了,如果同时有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)优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现<