> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。> 目标:理解进程信号。
> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!
> 专栏选自:Linux初阶
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
🌟前言
上期我们学习了进程间通信,内容很多,干货满满,当然也是有难度的我们知道进程之间可以相互通信,就像七八十年代,我们远在他乡,思念家人,那时我们只能寄来的信了解家里的情况。我们可以用信来传达,那Linux是采用哪种媒介来传达信息的呢???这里不得不题Linux中的信号量了,那这个信号量到底是个啥呢???
⭐主体
学习【Linux】进程信号咱们按照下面的图解:
🌙 生活信号
首先,从物理和技术的角度来看,生活信号可以包括光信号、声信号、电信号等。例如,手机通讯、互联网通讯、音乐播放器和语音识别技术等都涉及到信号的传输和处理。在手机通讯中,信号通过无线电波进行传输,而互联网通讯则通过数字信号进行信息交换。音乐播放器通过解码信号将音乐以声音的形式播放出来,而语音识别技术则通过处理语音信号将其转换成计算机可识别的数字信号。
其次,从日常生活的角度来看,生活信号可以包括身体健康信号、情绪信号、社交信号、身体语言信号和经济信号等。例如,头痛、发烧、疲劳等可能是身体机能异常或疾病的信号;快乐、悲伤、焦虑等情绪状态则可以帮助我们了解自己的情绪状态和需求;微笑、眼神交流、姿势等非言语方式的沟通信号可以传达我们的情感和意图;手势、肢体动作、面部表情等身体语言信号可以揭示我们的情感和态度;物价上涨、收入增长、股市波动等经济信号则可以反映经济状况和趋势。
此外,交通信号也是生活中常见的信号系统之一,如红绿灯、交通标志等,它们在道路交通中起着引导和安全作用。
总之,生活信号无处不在,它们可以来自不同的方面,包括物理和技术层面以及日常生活层面。这些信号在我们的生活中起着重要的作用,帮助我们进行信息传递、情感交流、健康监测和经济分析等。因此,我们需要正确理解和应对这些信号,以便更好地生活和工作。
🌙 进程信号
进程信号是Linux中用于进程间通信和控制的一种机制。当一个进程需要发送一个信号给另一个进程时,可以调用kill系统调用或向指定进程发送信号。进程信号可以被视为一种软件中断,通知进程发生了某个事件,并打断进程当前的操作去处理这个事件。
信号是给进程发的,比如我们之前使用过的指令:kill -9 pid
进程本身是被程序员编写的属性和逻辑的组合,由程序员编码完成的;当进程收到信号的时候,进程可能正在执行更重要的代码,所以信号不一定被处理;进程本身必须要对于信号的保存能力;进程在处理信号的时候一般有三种动作:默认、自定义、忽略,处理信号也可被称为信号被捕捉。
如果信号是发给进程的,而进程是要保存的,那么应该保存在哪里?task_struct结构里,如何保存?保存是否收到了指定的信号,信号:用比特位的位置代表信号的编号,比特位的内容代表是否收到该信号,0表示没有,1表示有。
🌙 查看信号kill -l与信号解释man 7 signal
用kill -l命令可以察看系统定义的信号列表 :
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到:
man 7 signal可以查看信号详细信息的命令
🌙 信号的产生
💫 按键产生
2号信号产生:ctrl+c
2号信号作用:-->终止进程
所以当我们ctrl+c的时候该进程直接进入结束状态
3号信号产生:ctrl+\
3号信号作用:-->终止进程
所以当我们ctrl+\的时候该进程直接进入终止进程。
采用kill -3 +pid查询3号信号:
总结:
键盘是硬件,通过组合键按下给OS识别,OS将组合键解释成信号,向目标进程发信号,目标进程在合适的时候处理这个信号,对于2号和3号信号处理动作默认为终止进程。
💫 系统调用
概念:
除了键盘向前台进程发送信号之外,前台进程会影响shell,linux规定跟shell交互的时候只允许有一个前台进程,默认情况下bash也是一个进程。而实际上当我们自己运行进程时,我们的进程就变成前台进程了,而bash自动被切到后台。
使用:
kill:可以向任意进程发送任意信号
NAME
kill - send signal to a process
SYNOPSIS
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
RETURN VALUE
On success (at least one signal was sent), zero is returned. On error, -1 is returned, and errno is set appropriately.
总结:
发送信号的能力是OS的,但是有这个能力并不一定有使用这个能力的权力,一般情况下用户决定OS向目标进程发信号。所以OS有这个能力,那么对外提供能力只能通过系统调用的接口的方式来让用户向目标进程发送信号。
1.raise
作用:
给自己发送任意信号
库中讲解:
NAME
raise - send a signal to the caller
SYNOPSIS
#include <signal.h>
int raise(int sig);
RETURN VALUE
raise() returns 0 on success, and nonzero for failure.
举个栗子:
#include <iostream>
#include <unistd.h> // 包含 sleep 头文件
#include <signal.h> // 包含 raise 头文件
using namespace std;
int main(int argc,char*argv[])
{
//raise()给自己发送任意信号
int cnt = 0;
while(cnt<=10)
{
printf("cnt:%d\n",cnt++);
sleep(1);
if(cnt>=5)
{
raise(3);//kill(getpid(),signo);
}
}
}
2.abort
作用:
- abort——进程给自己发6号信号
- abort:终止进程的方式,给自己发送指定的信号SIGABRT
库中讲解:
NAME
abort - cause abnormal process termination
#include <stdlib.h>
void abort(void);
举个栗子:
#include <iostream>
#include <unistd.h> // 包含 sleep 头文件
#include <signal.h> // 包含 raise 头文件
#include <stdlib.h> // 包含 abort 头文件
using namespace std;
int main(int argc,char*argv[])
{
int cnt = 0;
while(cnt<=10)
{
printf("cnt:%d,pid:%d\n",cnt++,getpid());
sleep(1);
if(cnt>=5)
{
abort();//kill(getpid(),SIGABRT)
}
}
}
💫 硬件异常产生信号
除零发送8号信号
信号产生,不一定非得用户显示的发送,有些情况下信号会在OS内部自动产生。
举个栗子:
#include <iostream>
#include <unistd.h> // 包含 sleep 头文件
#include <signal.h> // 包含 raise 头文件
#include <stdlib.h> // 包含 abort 头文件
using namespace std;
int main(int argc,char*argv[])
{
//3.产生信号的方式:硬件异常产生信号
//信号产生,不一定非得用户显示的发送
while(true)
{
cout<<"我在运行中..."<<endl;
sleep(1);
int a = 10;
a/=0;
}
}
分析:
CPU内有很多寄存器eax,edx等,执行int a=10,a/=0;CPU内除了数据保存,还得保证运算有没有问题,所以还有状态寄存器,状态寄存器衡量这次的运算结果,10/0.相当于10乘以无穷大,结果无穷大,引起状态寄存器溢出标记位由0变成1,CPU发生了运算异常,OS得知CPU发生运算异常,就要识别异常:状态寄存器的标记位置为1,由当前进程导致的,在向目标进程发送信号,最后就终止进程了。
我们可以看到上面的结果:收到信号不一会引起进程的退出
**收到信号不一定会引起进程退出!**进程没有退出,则还有可能还会被调度,CPU内部的寄存器只有一份,但是寄存器中的内容属于当前进程的上下文,一旦出现异常我们没有能力去修正这个问题,所以当进程被切换的时候,就有无数次状态寄存器被保存和恢复的过程,所以每一次恢复的时候就让OS识别到了CPU内部的状态寄存器中的溢出标志位是1。
野指针发送11号信号
野指针引发11信号
举个栗子:
#include <iostream>
#include <unistd.h> // 包含 sleep 头文件
#include <signal.h> // 包含 raise 头文件
#include <stdlib.h> // 包含 abort 头文件
using namespace std;
int main(int argc,char*argv[])
{
while(true)
{
cout<<"我在运行中..."<<endl;
sleep(1);
int*p = nullptr;
*p=10;
}
}
分析:
OS会给当前进程发送11号信号,11号信号代表非法的内存引用(man 7 signal).OS又怎么知道野指针:野指针的时候也会引起虚拟地址到物理内存之间转化时对应的MMU报错,进而OS识别到报错,转换成信号。
💫 软件条件
管道——13号信号SIGPIPE
比如我们之前所说的管道,如果读端关闭,写端一直在写,写的数据没有读就没有意义了,OS不允许这样子,会终止这个进程,向写进程发送13号信号SIGPIPE。管道跟OS发信号的原因是因为读端关闭软件条件触发的。
定时器——4号信号SIGALRM
定时器软件条件:alarm():设定闹钟,调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
库中讲解:
NAME
alarm - set an alarm clock for delivery of a signal
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
举个栗子:
#include <iostream>
#include <unistd.h> // 包含 sleep 头文件
#include <signal.h> // 包含 raise 头文件
#include <stdlib.h> // 包含 abort 头文件
using namespace std;
int main(int argc,char*argv[])
{
//软件条件
alarm(1);
int cnt = 0;
while(true)
{
cout<<"cnt:"<<cnt++<<endl;
}
}
这份代码的意义在于可以统计1S左右,我们的计算机能够将数据累计多少次。实际上这种方法是比较慢的,为什么?打印时是要进行输出的,输出是外设,外设IO较慢。如果没有打印:
#include <iostream>
#include <unistd.h> // 包含 sleep 头文件
#include <signal.h> // 包含 raise 头文件
#include <stdlib.h> // 包含 abort 头文件
using namespace std;
int cnt = 0;
void catchSig(int signo)
{
cout<<"获取到一个信号,信号编号是:"<<cnt<<endl;
}
int main(int argc,char*argv[])
{
//软件条件
signal(SIGALRM,catchSig);
alarm(1);
while(true)
{
cnt++;
}
}
总结:
“闹钟”其实就是用软件实现的,任意一个进程都可以通过alarm系统调用在内核中设置闹钟,那么OS内可能会存在很多的闹钟,OS则需要管理闹钟:先描述,再组织,所以OS内部设置闹钟的时候,要为闹钟创建特定的数据结构对象。
内核管理闹钟比如最大堆、最小堆:比如100个闹钟可以把100个闹钟的when建小堆,最小的就在堆顶,只要堆顶的没有超时那其余的自然没有超时,所以只需要检查堆顶即可,就可以管理好闹钟。
🌙 捕捉信号的方法
signal作用:
通过signum方法设置回调函数,设置某一信号的对应动作
库中讲解:
#include <signal.h>
typedef void (*sighandler_t)(int);//函数指针
sighandler_t signal(int signum, sighandler_t handler);
举个栗子:
#include <iostream>
#include <unistd.h> // 包含 sleep 头文件
#include <signal.h> // 包含 raise 头文件
#include <stdlib.h> // 包含 abort 头文件
using namespace std;
void handler(int signal)
{
cout<<"进程捕捉到了一个信号,信号编号是:"<<signal<<endl;
}
int main()
{
//signal函数的调用,并不是handler的调用
//这仅仅是设置了对2号信号的捕捉方法,并不代表该方法被调用了
//一般这个方法不会执行,除非收到对应的信号
signal(2,handler);
while(true)
{
cout<<"我是一个进程:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
总结:
ctrl+c的时候并没有终止进程,这是我们把默认动作设置成自定义动作,想让其终止:exit(0),或者直接请上大杀器:kill -9 +pid
sigaction作用:
sigaction的作用域signal一模一样,对特定信号设置特定的回调方法。
库中讲解:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//act:结构体对象;oldact:输出型参数,获取特定信号老的处理方法
struct sigaction {
void (*sa_handler)(int);//回调方法
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;//信号集
int sa_flags;
void (*sa_restorer)(void);
};
RETURN VALUE
sigaction() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error.
一个进程在运行时,未来会收到大量同类型的信号,如果收到同类型的信号,当前正在处理某个信号信号时,会发生什么?OS会不会允许频繁进行信号提交?
#include <iostream>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
using namespace std;
void Count(int cnt)
{
while(cnt)
{
printf("cnt:%2d\r",cnt);
fflush(stdout);
cnt--;
// sleep(1);
}
printf("\n");
}
void handler(int signo)
{
cout<<"get a signo:"<<signo<<"正在处理中..."<<endl;
Count(20);
}
int main()
{
struct sigaction act,oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
//SIGINT——2号信号
sigaction(SIGINT,&act,&oact);
while(true) sleep(1);
return 0;
}
总结:
当我们进行正在递达某一个信号期间,同类型信号无法递达——当前信号正在被捕捉,系统会自动将当前信号加入到进程的信号屏蔽字,在block表中自动将2号信号屏蔽。而当信号完成捕捉动作,系统又会自动解除对该信号的屏蔽。一般一个信号被解除屏蔽的时候,会自动进行递达当前屏蔽信号,如果该信号已经被pending的话,没有就不做任何动作。进程处理信号的原则是串行的处理同类的信号,不允许递归式处理
小细节:屏蔽2号的同时还想屏蔽3号,只需要加上:
sigemptyset(&act.sa_mask);//当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
sigaddset(&act.sa_mask,3);
🌙 核心转储
问题抛出:
数组越界不一定会导致程序崩溃,实际数组编译器在编译代码时在栈上开辟多大空间与编译器强相关,数组大小是10个元素在栈帧结构上分配的字节数可能很大,数组越界可能还是在有效的栈区中,所以没有报错,OS在识别越界可能识别不出来。
举个栗子:
#include <iostream>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
using namespace std;
int main()
{
//核心转储
while(true)
{
int a[10];
//a[100]=10;//没报错
a[10000] = 10;
}
}
分析:
Term是正常结束,OS不会做额外的工作,Core代表OS初了终止的工作,还有其他工作。
在云服务器上,默认如果进程是core退出的,我们暂时看不到明显的现象,想看到现象,我们需要打开ulimit -a:查看系统给当前用户设置各种资源上限:
core file size设置成了0,这是云服务默认关闭了core file选项,想看到现象:ulimit -c
此时我们重新运行./mysignal:
总结:
输出报错多了core dumped:core代表核心,dumped:转储,核心转储,转储到:在当前目录下以core命名,后面跟了数字:引起core问题的进程的pid。核心转储是当进程出现异常的时候,我们将进程在对应的时刻,在内存中的有效数据转储到磁盘中。
形成核心转储的意义:
一旦进程出现崩溃的情况,我们更想知道为什么会崩溃,在哪里崩溃,所以OS为了方便调试,会在进程崩溃的上下文数据全部dump到磁盘当中,用来支持调试。
如何支持:gdb
这种直接快速进行调试的方式就叫事后调试,在gdb中上下稳重直接core-file core.xxxx。因为是核心转储,在进程终止时,只会检测core方式终止的进程,以core退出的是可以被核心转储的,后续可以快速定位问题。以Term终止的,一般是正常下的终止进程
🌙 信号的保存——位图
相关概念:
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择**阻塞 (Block )**某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
内核中的表示:
在进程内部要保存信号周边的信息,有3种数据结构与之是强相关的,第一个是pending表,pending表就是位图。如何理解:进程可能在任何时候收到OS给它发送的信号,该信号可能暂时不被处理,所以需要暂时被保存,进程为了保存信号采用位图来保存,这个位图就是pending位图,对应的信号被置于pending位图的信号就是该信号处于未决状态。
所以OS向进程发信号就是向目标进程的peding位图设置比特位,从0到1就是当前进程收到该信号,所以发信号应该是写信号,PCB属于OS内核结构,只有OS有权力修改pending位图,所以发送信号的载体只能是OS。
除了pending位图之外,还存在block位图:block位图比特位的位置代表信号标号,比特位的内容代表是否阻塞了该信号
此外,还有一个:typedef void(*handler_t)(int signo),handler_t handler[32]={0},这个就是函数指针数组,这个数组在内核中有指针指向它,这个数组称为当前进程所匹配的信号递达的所有方法,数组是有下标的,数组的位置(下标)代表信号的编号,数组下标对应的内容表示对应信号的处理方法。
我们之前所谈到的信号接口signal(signo,handler)的本质就是在做拿到信号在对应的数组找到对应的位置,然后将用户层设置的handler函数的地址填充进对应下标处,未来信号产生时候,修改比特位,并且该比特位没有被阻塞,OS立马拿到信号根据信号位置得到信号的编号,进而访问数组得到方法。
因为是内核数组结构,所以OS可以对应使用对应的系统接口来对数据结构任意访问。
结论:如果一个信号没有产生,并不妨碍它可以先被阻塞。进程能够识别信号是因为程序员在设置体系的时候在内核中为每个进程设置好了这3种结构能够识别信号。
信号集——sigset_t:
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
信号集操作函数:
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印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 *set, int signo);
sigprocmask :读取或更改进程的信号屏蔽字(阻塞信号集)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
sigpending :
#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
🌙 信号的捕捉
概念:
前面说过,信号产生的时候,信号可能不会立即处理,会在合适的时候处理。合适的时候就是从内核态返回用户态的时候进程处理,这也说明了曾经一定是先进入了内核态,最典型的就是系统调用与进程切换。
💫 内核态与用户态
概念:
用户代码和内核代码,平时我们自己写的代码是属于用户态的代码,但是用户态难免会访问OS自身的资源(getpid,waitpid…),硬件资源(比如printf,write,red…),用户自己写的代码为了访问资源必须直接或间接访问OS提供的接口,必须通过系统调用来完成访问。系统调用是OS提供的接口,而普通用户不能以用户态的身份执行系统调用,必须让自己的身份变成内核态。
实际执行系统调用的“人”是“进程“,但是身份其实是内核。从用户态调内核态需要身份的切换,还要调OS内部的代码,所以一般系统调用比较费时间一些。我们应该尽量避免频繁调用系统调用。
一个进程在执行时必须把上下文信息投递到CPU中,CPU中有大量的寄存器,寄存器可分为可见寄存器(eax,ebx…),不可见寄存器(状态寄存器…),凡是和当前进程强相关的,是上下文数据。
- 寄存器中还有非常多的寄存器在进程中有特定作用,寄存器可以指向进程PCB,也可以保存当前用户级的页表,指向页表起始地址。
- 寄存器中还有CR3寄存器:表征当前进程的运行级别:0表示内核态,3表示用户态,这就能够辨别是用户态还是内核态了。
如何理解我是一个进程怎么跑到OS中执行方法呢?
以前所说的进程地址空间0-3G是用户级页表,通过用户级页表映射到不同的物理空间处,而除了用户级页表之外,还有内核级页表,OS为了维护从虚拟到物理之间的OS级别的代码所构成的内核级映射表,开机时OS加载到内存中,OS在物理内存中只会存在一份,因为OS只有一份,所以OS的代码和数据在内存中只有独一份,当前进程从3-4GB映射的时候将当前内核的代码和数据映射到我们所对应的当前进程的3-4G,此时使用内核级页表就行了,所以内核级页表只有一份就可以了。所以每个进程都可以自己特定的区域内以内核级页表的方式访问OS的代码和数据。
3G-4G是OS内部的映射,所以进程建立映射的时候不仅仅把用户的代码和数据和进程产生关联,每一个进程都要通过用户级页表和OS产生关联,而每一个进程都有自己的地址空间,其中用户空间独占,而内核空间是被映射到了每一个进程的3-4G空间,每一个进程都可以通过页表映射到OS,而且每个进程看到的OS都是一样的,所以进程要访问OS的接口,其实只需要在自己的地址空间上进行跳转就可以了。
每一个进程都有3-4GB,都会共享一个内核级页表,无论进程如何切换,都不会更改任何的3-4GB。用户通过什么能够执行访问内核的接口或者数据呢?OS读取CPU中的CR3寄存器,读取运行状态,当是0内核态时才能去进行访问,所以系统调用接口起始的位置会帮我们把用户态变成内核态,从3号状态改成0号状态。所以系统调用的前半段是在用户态跑的,OS是如何通过系统调用把用户态变成内核态的:中断汇编指令int 80就是陷入内核,简单理解把状态由用户态改成内核态。调用结束时在切回来。
图解:
总结:
无论是用户态还是内核态,一定是当前进程正在运行,无非就是当前执行级别是用户态还是内核态,页表是用户级页表还是内核级页表,包括访问的资源。
💫 信号捕捉过程
概念:
通过系统调用,陷入内核,从用户态进入内核态,按理来说也会直接从内核态进入用户态,但是并不是直接返回用户态,陷入内核比较费时间,进去之后OS会做其他工作,所以OS会在进程的上下文中搜索,拿到task_struct找到进程,查3张表,先查block表:block为0说明没被阻塞,继续看pending,pending为0继续下一个…
图解:
🌙 可重入函数
一般而言,我们认为main执行流和信号捕捉执行流是两个执行流!
- 如果在main中和在handler中,该函数被重复进入,此时出问题,则该函数(比如insert)称为不可重入函数
- 如果在main中和在handler中,该函数被重复进入,此时不出问题,则该函数(比如insert)称为可重入函数
而我们目前大部分情况下用的接口,全部都是不可重入的,重入不重入是特性。
总结:
main函数调用insert,向链表head插入Node1,insert只做了第一步,然后就被中断(或者因为信号原因执行信号捕捉),此时进程挂起,然后唤醒在次回到用户态检查有信号待处理,于是切换到sighandler方法,sighandler也调用了insert函数,要把Node2头插到链表里,Node2的next结点指向下一个结点位置,下一步就是head指向Node2,完成Node2的头插,信号捕捉完之后就成功把Node2插入,接下来回到main执行流,对Node1完成插入的第二步动作,此时把head指向Node1,最后只有Node1真正插入到链表之中,而Node2结点找不到了,发生内存泄漏,出现问题。
不可重入函数:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库 函数。标准I/O库的汗多实现都以不可重入的方式使用全局数据结构。
🌙 关键字volatile
对代码进行优化后(-03),通过信号自定义方法handler修改全局q,但是程序不会退出。
举个栗子(优化前):
#include <stdio.h>
#include <signal.h>
int quit = 0;
void handler(int signo)
{
printf("%d 号号信号,正在被捕捉\n",signo);
printf("quit:%d",quit);
quit = 1;
printf("->%d\n",quit);
}
int main()
{
signal(2,handler);
while(!quit);
printf("注意,我是正常退出的\n");
return 0;
}
举个栗子(优化后):
#include <stdio.h>
#include <signal.h>
int quit = 0;
void handler(int signo)
{
printf("%d 号号信号,正在被捕捉\n",signo);
printf("quit:%d",quit);
quit = 1;
printf("->%d\n",quit);
}
int main()
{
signal(2,handler);
while(!quit);
printf("注意,我是正常退出的\n");
return 0;
}
给quit加volatile关键字,quit通过内存读取而不是寄存器,保持变量quit的内存可见性!
🌙 SIGCHLD信号
子进程退出时,会向父进程发送17号信号SIGCHLD的。
举个栗子:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void handler(int signo)
{
printf("pid:%d, %d 号信号,正在被捕捉!\n",getpid(),signo);
}
int main()
{
signal(SIGCHLD,handler);//17号信号
printf("我是父进程:%d,ppid:%d\n",getpid(),getppid());
pid_t id = fork();
if(id==0)
{
printf("我是子进程:%d,ppid:%d,我要退出了\n",getpid(),getppid());
exit(1);
}
while(1)
sleep(1);
return 0;
}
分析:
实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。
signal(SIGCHLD,SIG_IGN);
sigaction(SIGCHLD,act,oldact);
注意:虽然SIGCHLD的默认动作就是忽略,但是与手动设置表现的不一样,默认是收到信号就进行处理,该等还得等,而如果我们手动设置了SIG_IGN,子进程退出时发送给父进程的信号会被父进程忽略,但是子进程会被OS回收,这是有所区别的。含义不一样。
🌟结束语
今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。