Linux信号机制:从闹钟到进程终止的全面解析
Linux进程信号
信号快速认识
信号 VS 信号量 = 老婆 : 老婆饼 --->没有任何关系!
1、信号:闹钟、红绿灯、上课铃声、狼烟、电话铃声、肚子叫、敲门声、脸色不好.......
2、什么叫做信号:(事件通知?)中断我们人正在做的事情,是一种事件的异步通知机制
信号是一种给进程发送的,用来进行事件异步通知的机制!
张三上厕所,我们自习一会,等张三回来再讲课 --- 同步
继续上课,上课 张三上厕所 --- 异步
异步的特点:两件事情在同一时间,同时发生,互不干扰!
信号的产生,相比于进程的运行,是异步的!
人在人行横道上走,与红绿灯倒计时这两个事情是没有关系的!红绿灯颜色变化,通知到了人,人就做相应的动作! 二者唯一的交集就是通知的时候!
信号是发给进程的!
基本结论
1、信号处理,在信号没有产生的时候,早就知道号该如何处理了!
边塞士兵在狼烟升起之前,是知道升起狼烟意味着什么!
人遇到红绿灯之前,是知道遇到不同红绿灯要做出不同的对应!
2、信号的处理,不是立即处理,而是可以延迟处理!合适的时候,进行信号处理!
3、人能识别信号,是提前被“教育”过的,进程也是如此,OS程序员设计的进程,进程早已经内置了对于信号的识别和处理方式
4、信号源非常多 ---> 给进程产生信号的信号源也非常多!
产生信号
产生信号的方式非常多:
键盘产生信号
ctrl + c 是给目标进程发送信号的,相当一部分信号的处理动作,就是让自己终止!
信号都有哪些 --- ctrl + c 就是给进程发送信号,发哪个信号要知道!
kill -l 查看信号!
在计算机当中,信号其实是整型的数字!可以根据数字向进程发信号!而数字的可读性比较弱,一般会在OS内设置宏!
这里有62个信号!其中34~64是实时信号,不需要了解,而1~31是普通信号,需要好好了解!一些信号可以延迟处理,这类可以延迟处理的信号叫普通信号!
而实时信号是一旦产生就需要立即处理!
ctrl + c通过键盘向目标进程发信号,发送的就是2号信号!
如何证明?
进程收到信号,处理信号
1、默认处理动作
2、自定义信号处理动作
3、忽略处理!进程收到信号之后,合适的时候,处理信号的动作有三种
而进程这里,默认处理动作就是终止进程
所以收到了2号信号2)SIGINT,进程会默认终止我想看到信号处理的过程!
我们尝试更改进程的默认信号处理动作!进行需要调用系统调用函数,来更改进行
signal
第一个参数 signum就是上面的信号,可以输入数字或宏定义
第二个参数handler是函数指针!上面typedef void (*sighandler_t)(int)!这个int是传入第一个参数的信号!
这个函数方法就是通过信号和函数方法,进程收到当前信号,不再做默认动作,而是做你提供的函数方法动作!进程只用调用一次signal,不需要死循环调用!一旦收到信号,要求进程转而做sighandler函数方法,调用完后继续回到死循环!当收到2号信号,进程就会直接调用handlerSig函数,将收到的这个SIGINT信号转递给这个函数!这个函数指针的int参数是为了收集信号!
现在进程ctrl + c不会终止退出,而是打印函数语句!因为我们把之前的终止语句改成了打印语句!以上处理信号的操作就是自定义信号处理!也叫做自定义捕捉!
信号处理的过程整体称为信号捕捉/信号处理!
默认处理和忽略处理都是信号捕捉,只有自定义信号处理叫做自定义捕捉!这里退出还可以使用
ctrl + \发送3号信号
所以现在可以确定键盘输入ctrl + c就是向进程发送2号信号!自定义动作可以通过signal系统调用实现!
查看普通信号默认动作
要如何确定其他普通信号的默认动作?
使用指令
man 7 signal
这里找到我们的2号信号SIGINT,在Action这列看到
是Term(termination),就是停止的动作!Comment说明是键盘终止的!
Core也是退出,只是和Term不一样,Ign是忽略(ignore),Stop是暂停,Cont是继续(continue)
什么是目标进程?
前台和后台进程!(网络时会讲解精力或守护进程)./XXX --- 前台进程(键盘产生的信号,只能发给前台进程)./XXX & --- 后台进程(后台的进程无法收到键盘产生的信号!ctrl + c 不处理)
命令行shell进程 --- 前台
谈谈前后台的问题
登录Linux系统,首先会创建bash进程!输出命令行,然后scanf等待输入
当要调用程序./testsig,就会创建子进程,子进程是前台进程!
后台进程无法从标准输入中获取内容!(标准输入就是键盘)
前台进程能从标准输入中获取内容!
无论是哪个进程,都可以向标准输出打印!为什么?前台进程可以去标准输入获取,而后台进程不能去标准输入获取?
因为键盘只有一个,输入数据一定是给确定的一个进程的!
前台进程必须只有一个,因为键盘只有一个,只能处理键盘一次的输入信息给一个进程,而显示屏不一样,前后台进程都可以输出,并不影响!
所以前台进程必须只有一个!后台进程可以有多个
前台进程的本质,就是要从键盘获取数据的!
哪个进程要获取数据就放在前台,哪个进程不获取数据就放在后台
班上同学之间可以随便讲话,但是你要讲话给别人的时候,就需要确定一个人,来接收你信息!./testsig运行的时候,进程就是前台进程,前台进程只能由一个,所以bash进程自动变为后台进程,此时我们输入ls,echo,这些指令就不会传给bash了,而是为当前的前台进程,而前台进程并没有处理指令,所以是没有反应的!
党当./testing &此时进程是一个后台进程,进程可以向显示器输出,而此时的前台进程是bash进程没变,依旧可以在输入数据给bash!这也是为什么有命令的显示!bash接收到信息,进行处理了!
所以可以解释键盘产生的信号只能传给前台进程,因为键盘组合键也是键盘输入!
我们在某些进程中,会fork创建子进程,这样就会有父子进程,子进程退出,或者二者正常运行的时候ctrl + c也是可以退出,是没有问题的!但是父进程先退出,子进程就是孤儿进程,会被1进程领养,此时ctrl + c不能杀这个子进程!
父子进程运行,还是以父进程为代表在前台,父进程收到ctrl + c的信号就父子进程一起退出!
如果父进程先退出,子进程被1号进程领养,孤儿进程会自动被OS提到后台,使用ctrl + c就无法处理了,当时我们是使用kill命令才退出的!
如何进行前后台操作?
补充一部分命,前后台移动!
查看所有的后台任务指令:jobs
我们调用一个进程,以后台进程的形式!
此时我们调用jobs就可以看到后台任务了!
我们 bash指令输入输出和后台进程输出,都是输出到显示器上的!后台进程在输出,bash进程也在输出,此时显示器上的信息是混合在一起的!
此时显示器就是一个共享资源,而这样的现象就是数据不一致问题!
此时显示器文件是共享资源,以后被保护为临界资源,打印就不会互相干扰了!
此时根据jobs获取后台进程的任务号,使用指令fg 任务号就可以把特定的后台进程设置为前台进程了!我们可以对进程信号操作!(foreground)
而前台进程如何设置为后台进程?
调用一个前台进程,使用ctrl + z,前台进程会暂停,而对于前台进程来说,前台进程需要一直从标准输入获取数据,进程都暂停了,不能获取数据,OS自动把前台进程转换成后台进程,所以ctrl + z做进程切换到后台
如何证明?我们直接jobs查看即可!
如何让这个暂停的后台进程运行起来?使用指令bg 任务号!(background)
这就是前后台进程!
fg:把后台进程提到前台
bg:暂停的后台进程恢复运行
ctrl + z : 将前台进程切换到后台
综上,上面ctrl+c对目标进程发信号,这个目标信号就是前台进程!ctrl + c产生的信号发送给前天进程!
什么叫做给进程发送信号?
(第一次理解)
上面的结论:信号的处理,不是立即处理,而是可以等一会处理,合适的时候,进程信号的处理!
信号可以延迟处理,前提是处理信号的人必须把处理信号这个事情记录下来!记录的目的是希望进程完成当前任务后,在合适的时间处理信号!
接下来就要解决记录在哪里和如何记录的问题!
我们只考虑1~31的信号,不考虑实时信号
信号记录在哪里如何记录?
信号是发送给进程的!所以在进程task_struct中定义一个unsigned int sigs通过整数来记录信号!
如果发送多个信号呢?那么就把这个整数当作一个位图结构!
整数有32个比特位!
规定:
比特位的位置是信号编号
比特位的内容是是否收到假如传入的是2号信号,就在比特位位图中的2号位置上,将0置1即可,进程就知道收到了一个2号信号!
所以,信号记录在哪里?信号记录在进程task_struct中,如何记录?在PCB内维护一个整型变量来记录信号,通过整数的比特位充当位图,位图内比特位为1说明有信号,为0说明没有信号!
发送信号的本质
所以发送信号,本质是什么?
向目标进程写信号
修改位图!
这些都需要pid和信号的编号所以ctrl + c的时候,前台进程只有一个,进程是确定的,根据快捷键对应的信号,去到指定的进程PCB中,修改位图!
以及kill -9 PID 这样写的目的!-9是提供信号,PID是找到进程是PCB!
每个进程都有自己的task_struct !是属于OS的数据结构对象,修改位图,本质:修改内核的数据!
只有OS自己有资格修改自己的内核数据!
不管信号怎么产生,发送信号,在底层,必须让给OS发送!
所以OS需要提供发送信号的系统调用!
命令kill就是这类系统调用,kill就是C语言写的程序,用这个系统调用接口来完成对目标进程发信号!
来看看kill的系统调用!是OS发送信号的系统调用接口之一!
信号就是一个数字,发信号是让OS给目标进程发信号!
顺序怎么办?1、不需要确认,2、发送信号全权交给OS来做的!信号的顺序是OS先识别到哪个信号源来确定的!
信号和通信有什么区别
通信IPC主要是进程间通信的!数据是用户到用户的!
信号机制,本质是OS与进程的关系,信号本质是人和进程通过OS来通信的!
狭义上说,二者不太一样!
广义上说,信号可以算是通信的广义范畴!
调用系统命令向进程发信号
kill -signum PID
这个已经经常使用了!
系统调用产生信号
自己实现一个kill命令,完成对进程发送信号!


通过系统调用,实现信号的产生!
其他系统调用产生信号
案例
如果对每个普通信号都做自定义处理,那么程序应该不能退出了呀!
我们发现
现在这个进程刀枪不入,信号没法让进程退出!
我们尝试可以知道,除了9号信号,其他信号都无法处理!
OS内为了防止有进程恶意进行全信号自定义处理! OS规定大部分信号都可以自定义处理,只有SIGKILL一个信号是禁止被自定义捕捉的!
所以9号信号不被自定义捕捉!
还有没有其他系统调用可以用来发送信号?
raise

raise函数可与把恰当进程发送指导的信号(自己给自己发信号)

一个小案例,每秒给自己发送一个信号,发送到第九个的时候退出!也是呼应上面知识!
接着测试后面的信号,到第十九个信号的时候,程序停止了,查看得知第十九个信号是SIGSTOP是一个暂停信号,这个信号也无法做自定义处理!
(普通信号中,不能做自定义处理的信号只有9和19信号!)
这样就可以向自己发信号!想给自己发任意信号,就可以使用raise,想给其他进程发任意信号就使用kill!
abort

abort函数使当前进程接收到信号而异常终止!
abort给自己发送指定的信号!
用案例测试,可以指导abort是给自己发送6号信号SIGABRT,但是上面测试中,6号信号并没有退出,可以做自定义处理呀!
这就是abort的特点,当abort发送,进程必须处理!目的就是用来终止进程的!abort调用的时候,就不做捕捉了!恢复默认处理!
硬件异常产生信号
问题:在编程的时候,程序有时候会崩掉!
char * msg = "helloworld";
*msg = 'H';
语言语法可以知道,不能修改常量字符串!之后学习虚拟地址空间,也可以知道常量字符串会通过页表加载到内存,页表权限为只读同样不能修改!OS不允许修改,同样是写失败!
但是语法错误,并不是进程退出的理由!OS不让写,OS就把进程杀了,那么OS是如何杀掉进程的?
修改常量字符串,除0,野指针,都会使程序崩溃!
OS是如何通过异常杀死进程的?
除零操作:

程序退出,原因是收到了8号信号,8号信号SIGFPE,是浮点数错误信号!a/=0;这个除零操作导致进程直接崩溃!进程被人发送8号信号,进而导致进程退出!
野指针操作:

野指针操作导致程序退出,原因是收到11号信号,11号信号为SIGSEGV,是段错误!
在C/C++编程下程序崩溃,大多情况是这两个错误导致进程崩溃,
进程崩溃的原因是一旦犯了这两个错误,就会给进程发送8和11号信号!导致程序退出!
异常如何产生信号?
现在清楚了异常是第4个了解的产生信号的方法!现在想要知道,异常是如何产生信号的?
信号是存储在进程中的,所以发信号永远都是OS!
程序犯错了!OS识别出来进程犯错了!OS根据犯错的类型向目标进程发送信号,和用户没关系!
OS如何知道进程异常出错的?
现在需要知道,OS是如何知道进程犯错的?
除零计算本质是在CPU上进行的!而CPU有各种寄存器!
CPU内存在状态寄存器EFLAGS
这个寄存器内部有若干个比特位,其中某个比特位是用来记录是否溢出!以及其他异常情况,都在这个比特位中记录!
CPU是硬件,而OS是软硬件的管理者!CPU记录当前进程的上下文,进程出现异常!CPU记录异常,OS通过CPU获得异常,OS找到CPU处理的进程以此向异常进程发送信号终止!CPU处理进程计算,因为进程计算出错,从而导致硬件错误!OS知道异常并发信号停止进程!这也是崩溃的原因!
野指针异常的原理相似!
野指针地址是0,0地址不能页表映射,此时要做的是向0地址赋值为100,CPU内寄存器中,有寄存器是存储进程运行的虚拟内存,此时就做到对野指针赋值的一行,CPU中CR3是存储页表获得的物理地址,CPU根据MMU寄存器去页表做虚拟地址到物理地址的转化!此时发现,0地址不能转化,并且还发现要对0地址赋值,此时CPU的MMU寄存器硬件报错!
硬件报错,OS就可以获取到CPU的错误信号,以此向异常进程发送11号信号!
这也解释了C/C++程序崩溃的原因!归根结底是出现了硬件错误!出现错误,OS就发送信号向异常进程,进程因此被杀掉,我们看到进程就是崩溃了!
C++的try...catch编译好后,就是信号捕捉的过程!
OS如何知道硬件出错,这个话题之后聊!
由软件条件产生信号
进程(w) == 管道 == 进程(r)
之前管道讲解,读端关闭,写端写入无意义,写端崩溃,OS会发送SIGPIPE杀掉写端进程!
这个SIGPIPE就是软件条件不足导致的!
管道是软件产生的!此时管道写条件不满足!此时软件条件不满足,进程退出!
OS不做任何浪费时间和空间的事情!
软件条件alarm

alarm系统调用是为当前进程设置闹钟!
闹钟时间到了,会给当前进程delivery一个信号!
调用alarm(5)设置了一个闹钟,闹钟时间到了,会要求OS给当前进程发送信号5
调用alarm(0)是取消设置的闹钟!
第一次调用alarm(5)调用成功闹钟响了的返回值为0!表示剩余时间为0!
第一次调用了alarm(5),3s之后!再次调用alarm(10),返回值为2
解决上面问题
调用alarm(5),会等待时间超时,超时返回值为0,表示剩余时间为0!
第一次调用alarm(5),3s后调用alarm(10),是重置闹钟!alarm(10)返回2,表示上一个闹钟还剩2s!
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发送SIGALRM信号,该信号的默认处理动作是终止当前进程!
alarm的返回值永远是上一次闹钟的剩余时间,不是本次设置的值!(不懂问豆包)


通过案例来查看alarm产生的是什么信号,实验可以知道,alarm产生的是14号信号,SIGALRM信号!
alarm默认处理的动作是输出alarm clock之后退出程序!
我们可以看到,这里打印的效率并不高!while死循环开始的1s内,打印了将近6w次的输出,这对于CPU来说,效率并不高!为什么?
做cout的时候,本质是IO,向显示器文件写入!xshell -> ./xxx -> 云服务器 -> 网络 -> 我们看到,
这个过程的次数并没有这么多!
alarm以信号驱动进程运行!
我们提高一下效率!

这里设定闹钟,循环只做计数!1s计数结束,发送信号,做自定义处理,打印cnt数量!

数量变为5亿多次!
所以,IO的效率是6w级别,没有IO的效率是5亿级别,有IO和没有IO是数量级别的差距!差了1w倍!
通过alarm实现唤醒功能!主程序什么都不做,一直暂停!一旦有信号就唤醒运行!
以闹钟为驱动力,每隔1s来做一次动作!
通过设置闹钟,1s后向进程发送信号,进程就执行一种任务!以此往复!

这样是只设置了一次闹钟!之后不会再有闹钟了!这个是一次性闹钟!
如果需要每隔1秒就执行设置一次闹钟!那就在闹钟信号自定义处理的结尾在设置一次闹钟即可,上图注释位置!
这样就实现了!

这个接口会暂停等待一个信号!只有等待到信号,信号处理完毕,才会返回,其余都会暂停等待!循环做pause就可以实现主函数的休眠状态!
这样做的意义:通过OS设置闹钟,进程主函数做休眠,通过闹钟发送信号唤醒执行某种任务!
接下里就是设置一些任务,然后每隔1s做这些任务
C++11的知识点:using func = std::function<void()>;
using区别名,function包装器


实现了以上功能!
可以让一个进程处于暂停状态!由外部的信号来驱动,每隔1s完成对应的任务!
这就是操作系统!
OS的本质就如同这个进程!OS从开机就一直在死循环!OS也是进程,被调度的!每隔一段时间,传递类似信号的机制(硬件中断) ,让OS执行对应的方法!
而OS调用的时间间隔更短,会高速的调用设置闹钟并发送信号,执行对应的任务!
以此就可以在OS内设置对应的进程管理,内存管理等操作!进程设计为链表,创建进程,实际上是向链表插入进程结构体结点!以此对进程进行管理!
这其实就是OS的操作原理!
OS原理了解即可,重在理解,以信号推动进程运行的思路理解即可!
之后学习完OS的运行原理,在谈谈代码!
快速理解闹钟
OS能够不断运行,离不开信号的外部刺激!这个刺激是时间中断(之后再讲)
OS中可以有很多闹钟,状态不定,OS需要对闹钟进行管理,再次提到,先描述再组织!闹钟在OS中本质是结构体对象,设置闹钟本质是创建闹钟结构体对象!
struct timer_list{
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_t_base_s *base;
};
所有的闹钟通过list_head来连接!
闹钟设置了自己的过期时间expires,根据时间戳来设置(设置闹钟5s,当前时间戳为1000,expires就是1005)void (*function)(unsigned long);这个是闹钟要执行的方法!
接下来是对闹钟的理解:
通过以固定时间循环调用闹钟,我们设置一个变量,每次调用都++,通过这个变量的大小可以确定,可以知道从开机到现在经过了多长时间!换言之,OS内部是可以记录时间的!
而对于进程PCB内部的时间片,我们设置10s,在信号处理调度处,每次--,时间片为0切换进程,这样进程就是被提供了10s的时间运行!时间片本质就是一个计数器!
OS运行,OS软件是必须需要外部刺激,设置时间片这一个操作,外部刺激只要是固定的,那么设置时间片不就是设置一个计数器,通过外部刺激调用,就减去对应的时间!
数据结构堆,设置一个最小堆,这个最小堆的大小判断,由闹钟的超时时间来定!这个最小堆堆顶这个闹钟,一定是最短超时的闹钟!
一个最小堆闹钟数据,堆顶的闹钟超时时间为1005,此时时间为1000,这5s内都没有闹钟超时,但是时间一旦为1006,OS就把最小堆堆顶出堆,最小堆会自动跳转找到新的堆顶,拿出结点,同时调用超时闹钟的处理方法!这个方法就是向目标进程发送SIGALRM信号!
这样就可以形成经过一段时间后向进程发送的功能了!
(堆结构其实就是闹钟底层时间的方式!)
闹钟一系列操作都是软件操作,闹钟超时之后向目标进程发送信号的方式,叫做软件条件!软件:闹钟是软件设计的,条件: 指的是超时!
无论是管道还是闹钟,都是OS内部管理的,软件条件一触发就向目标进程发信号!所以软件条件就是产生信号的方法之一!
总结
信号产生的方式,最终都是OS来进行信号发送,修改比特位!
保存信号
信号的产生已经了解,但是进程接收到信号,并不会立即处理,要到合适的时间进行处理!所以接下来要讲解信号的保存!
目前了解的是进程PCB内由一个变量为位图来保存信号!
问题是进程如何处理信号,是否有其他信息要保存!
-
实际执行信号的处理动作称为信号递达(
Delivery)信号递达有默认、自定义、忽略三种方式!
-
信号从产生到递达之间的状态,称为信号未决(
Pending)。信号在位图中,还没来得及处理!
-
进程可以选择阻塞(Block)某个信号
-
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作
阻塞[屏蔽]信号
例子老师上课布置作业(发信号),把作业要求记录到小本子(记录到位图,信号未决),课上完后,回到宿舍,打开小本子,获取作业要求,开始做作业!(信号递达)
讨厌这个老师上课,正常上课,记录作业,因为讨厌老师,就是不写这个作业,此时作业未决了,但是绝对不会递达!这个过程就是作业被阻塞了!
直到有一天,不再讨厌这个老师了,我回去把之前记录的作业(未决的作业),全部递达,完成作业,也就是解除对作业的阻塞!1~31号信号,都是一位老师,进程可以选择讨厌某个信号,不递达,对该信号进行阻塞,阻塞的信号,依旧可以被收到,依旧会被保持到位图,就是不递达信号,直到接触阻塞!
这就是把信号进程阻塞或屏蔽!
- 注意,阻塞忽略是不同的,只要信号被阻塞就不会递达,而忽略是递达之后可选的一种处理动作!
阻塞和忽略是前后关系!解除阻塞了才能忽略
信号的三张表
进程与普通信号相关的有三张表!在进程PCB中!
unsigned int pending,是保存收到信号的位图!发送信号到进程,OS修改的位图表就是pending表!也叫做未决表
比特位的位置:表示第几个信号
比特位的内容:是否收到信号!
unsigned int block,是保存阻塞信号的位图!
比特位的位置:表示第几个信号
比特位的内容:是否阻塞信号
0000 1111 pending
0000 0100 block
收到了1~4的信号,但是在递达的时候,只有3号信号不能递达!
信号是否能递达,通过pending & (~block)的结果即可判断!0000 1011
handler
学习signal系统调用的时候,看到sighandler_t是函数指针!所以handler其实是一个sighandler_t handler[31]的函数指针数组!
数组下标:表示信号编号
数组内容:表示信号处理方法的函数指针内部
SIG_DFL是默认处理方式SIG_IGN是忽略处理方式
所以signal注册修改信号处理方法,本质是去修改handler表!
handler是存储信号处理的方法!
handler pending block 共同支撑进程识别信号!
信号在没有产生之前,就知道要如何处理了!这句号的缘由由此处解释!每个进程的PCB内设置好对应的处理动作!
细节
SIG_DFL和SIG_IGN


使用SIG_IGN可以对信号进行忽略
也可以在自定义处理函数内将信号处理为默认模式!



查看信号信息应该将三个表横着看!
block和pending之间没有直接的约束关系!(老师布置作业,和你写不写作业没有关系!)
验证信号保存的话题
Linux提供信号操作:
1、一定是围绕着这三张表展开的!
signal这个系统调用就已经学习过了,是处理handler表的系统调用!

这里看到信号保存的内核信息!block和pending都可理解!只是handler不太一样!handler确实是数组,只是对元素进行了包装!在内部依旧可以找到sighandler_t的指针变量!
sigset_t

阻塞表和未决表都可以用0/1来表示是否成立!所以都可以用同一个数据类型来表示,sigset_t信号集!我们可以认为就是一个整数作为位图,但实际上是一个结构体!
block表,0/1就是表示是否被阻塞,pending表0/1表示是否被未决!
而阻塞信号更多叫做为进程的信号屏蔽字!(Signal Mask)
这个不久和之前权限部分,umask很像吗?
umask内三位bite位,为1的表示权限默认为0!
之后介绍接口都是sigset_t!
转到定义,可以知道是一个结构体!
位图设计
为了方便讲解,来谈谈,位图这么设计的!之后网络还需要使用!
位图结构体:
struct bits{
int bitmap[10];//位图比特总数为32 * 10 = 320
}
要看第39位比特位的内容方法:
int index = 39/32 = 1 ------- bitmap[1]
int pos = 39%32 = 7 -------- bitmap[1]第7个比特位
操作接口
而这里sigset_t位图不能是我们自己去置1操作吧?
现在又对应的接口来提供位图操作!
sigemptyset()清空位图sigfillset()将位图全部置1sigaddset()将对应位置内容置1sigdelset()将对应位置内容置0sigismember()判断指定位置内容是否为1,即判断信号是否在位图!
sigprocmask
调用这个函数,就是去获取,设置、更新自己的block表!
参数1是表示想要做什么操作!
用上面的三个参数来表示要做的参数!SIG_SETMASK根据传入在第二个参数,用户自己设置的set,将进程PCB的block表整体给更新!SIG_BLOCK根据传入的参数,根据set参数内为1的信号,新增到进程PCB的block中!SIG_UNBLOCK将第二个参数set的内容取反,与进程PCB内的block内的比特位以此按位与操作,做解除屏蔽的方法!
第三个参数是什么呢?
我们修改block位图!会有修改错误,反悔的时候!所以需要记录原先的位图数据!
第二个参数是输入型参数,第三个参数是输出型参数!
sigpending

sigpending是用于获取pending信号集,调用接口的参数是输出型参数!
传入一个sigset_t指针,sigpending会在内部拷贝一份pending信号集到对应的地址!
怎么pending位图只提供查找,不能修改呀?
其实修改pending位图我们早早讲过了
上面学习信号产生的方法,这些产生的信号最终都是去修改进程的pending位图呀!
整合dome
整合dome!实现屏蔽2号信号,设置进程block表默认不对任何进程进行屏蔽!只对2号信号屏蔽!
获取pending信号集,输出观察变化!


如果对所有信号的进行阻塞,那么不是无法杀进程?
9号信号,不可被捕捉,不可被阻塞!
观察pending从0置1
要观察到这个,需要提前发送信号,并且该信号为阻塞状态,之后将信号解除阻塞状态,对信号做自定义处理,即可看到由1到0


pending处理时机
pending信号位,将信号从1置为0,是在递达之前操作,还是递达完成之后操作?
我们在自定义处理函数内不再次获取进程的pending表,输出出来,如果结果是0100 0000... 说明pending表是做完递达动作才进行,若为0,就表示是在递达之前就完成了操作!
实验结果可以知道是
进程在执行handler方法之前,对应信号的pending已经被清理了!
信号保存的次数问题
Linux中,常规信号在递达之前产生多次只计一次,
而实时信号在递达之前产生多次可以依次放在一个队列里面,目前不讨论实时信号!
普通信号的信号丢失并没有什么影响,严格意义上,不算丢失!普通信号本质是做通知机制!
Core 和 Term的区别(硬件异常结尾问题)
我们知道信号的默认动作,大部分都是退出,但是了解signal的action可以发现,默认动作退出操作有两个!
Core和Term有什么区别呢?
Core是核心的意思,会在当前路径下,形成一个文件,进程异常退出的时候,进程在内存中的核心数据,从内存拷贝到磁盘,形成一个文件——核心
——支持debug!
完成以上操作进程会退出
而Term是异常直接退出,不会做任何转储功能!
二者区别就是Core会多做一个操作!
但是这个转储文件从来没见过呀!
云服务器上,core dump 功能是被禁止掉的!
为什么要被禁掉!一般大公司的服务器用来跑自己的项目!而项目出现了问题往往是直接重启并产生异常报错文件,而这个给异常没有解决一直做重启和异常文件产生,这样不断的展示文件,内存要炸!
所有core dump功能往往在实验测试阶段启用,生产阶段不会启动!如何查看?如何打开?
ulimit -a可以查看用户层的设置!打开功能使用
ulimit -c XXXX(大小),这个打开也是临时的,完全打开要改配置文件!
这样就打开了,可以使用的文件大小为40960
我们接下来调用一个异常程序看看!
这次异常退出有了不同!还产生了core文件!
我的core文件被Ubuntu存放到上图的目录下了!
为什么要执行核心转储?
主要是为了支持debug!
当代码量大了,通过core-file指令,通过core文件,快速找到异常点!
如果之bug一直找不到,可以直接开启core dump ,直接程序运行崩溃,生成core文件,gdb,core-file core,直接帮助我们定位到出错行!——事后调试!
进程退出部分的知识点回顾!

捕捉信号

信号保存,在合适的时候处理,我们到现在还没有讲,什么时候是合适是时间!
合适的时候?是什么时候
信号递达的方式有三种,都是如何处理的

用户模式中调用系统调用等方法进入内核完成某个任务,完成后,会检查是否有可以递达的信号(do_signal),有可以递达的信号,并且是自定义处理方法的,就从内核态回到用户态,调用对应的方法,在方法结尾会有对应的系统调用回到内核,返回得用户主控制流程被中断的位置,继续运行!
上面就是信号处理的流程!并且是自定义处理方法的信号处理过程!那信号处理是默认和忽略的要如何处理?
忽略处理动作:在内核中do_signal中,发现有信号未决,但是handler方法为
SIG_IGN,此时OS内核将pending表中的对应1置为0,返回用户态跳转的位置!默认处理动作:识别到信号,要处理的信号是默认处理,而大部分默认处理是退出,我们就需要退出杀死进,此时就在内核态,刚刚好可以直接释放进程地址空间,释放PCB
而默认动作是暂停!那么直接在内核,找到进程PCB,将内核状态设置为s,链入等待队列中等待,唤醒直接再从之前暂停的地方运行!
可以看到,默认和忽略操作,比自定义捕捉简单!
重谈捕捉过程
我们在执行自定义方法(用户写的) ->OS是否要做身份切换?
以用户身份在调用用户态的自定义方法!
自定义方法中有可能调用非法操作,以用户身份调用,非法操作没有权限去调用,而用内核身份,权限大,非法操作可以通过内核执行,反而会出错!会增加新的bug!
所以需要切换身份为用户身份执行自定义方法!
解释:执行完handler函数后,如何实现调用系统调用回内核?
可用栈帧的知识点来理解这个!
调用函数,要开辟栈帧,处理创建函数的代码和形参的胡数据,还会创建一个返回地址,记录原来调用接口的地址!当函数运行完释放栈帧,就会调用pop 返回地址,以此实现返回操作!
这里就是将返回地址强转为系统调用sigreturn函数,handler函数调用完,就会调用sigreturn函数回内核!
这个信号捕捉的过程,经历了4身份切换!
可以通过这记忆!
结论:进程不会对信号进行立即处理!会等待到跳转到内核态处理任务结束返回的时候,做信号检测处理!识别到信号即可跳转到用户态调用处理方法!返回内核在返回用户原来的位置!
进程凭什么进入内核?
进程说到底还是进程,进程被CPU调度,是并发的!每个进程在CPU上的时间只是暂时的,都会从CPU上剥离下来!这个过程的强制的!OS强制介入,最后强制进入内核态了!
所以进程调度过程中,进程是会频繁的进入内核态的!只要被调度,进程就会执行一段自己的代码,再执行一段内核的代码!这是OS设计的,这个些操作的理解,要学习接下来的学习!
内核态和用户态 ✨✨✨重点
这部分学习,会学到(重谈地址空间,OS如何运行,硬件中断,缺陷....)
硬件中断知识点相对重要!
OS是怎么运行的?

OS启动要有不断相应外部事件的能力!
OS怎么知道键盘上是有数据的?
计算机外设这么多,OS如何知道外设是否准备好?是否完成什么工作?
外设和CPU对应的针脚进行间接的连接!
外设设备就绪的时候,就会向CPU发送硬件中断!发送这个硬件中断,CPU就知道对应的外设就就绪了!
冯·诺依曼体系结构可以知道,CPU只和内存进程IO读写!这个是数据信号而言的!
这里还有控制信号!外设会与CPU也会连接,只是不做数据上的大规模传输,而是做信号上的传输!即0/1的信号传输!也就是硬件上电信号的传输!
通过针脚CPU可以间接的和外设进行信号沟通!
信号沟通说白了就是高低电频的变化!
外设不可能直接和CPU相连!毕竟计算机外设这么多!如果都向CPU发信号,CPU不好处理!
所以外设和CPU之间有中断控制器(8259)!
每个设备都会有自己控制器!
磁盘有磁盘控制器!会处理对OS发送的处理要求!
中断控制器的入口会与外设连接,外设就绪,就会把信号传给中断控制器,中断控制器来向CPU针脚发送信号!
中断控制器的接口连接不同的外设,设备就绪中断控制器就会生成中断号,这个中断号可以认为是接口的编号!
操作设备,要把设备的要求发给设备!一般包含操作的命令、地址、内容!这些要求是要发给设备的控制器!
外设、内存这些设备内部也有寄存器!不然如何理解CPU发来的数据!CPU把物理地址发给内存的地址寄存器,以此找到对应的位置!
设备的寄存器还有多种,命令寄存器、地址寄存器、数据寄存器!
综上,寄存器的概念外设也会存在!
可以认为,生成中断号的设备就是中断控制器的寄存器!
根据针脚是否获得高电频,将对应针脚的编号输入到这个寄存器,并且通知CPU有中断消息!
CPU访问中断控制器的寄存器,获取到中断号,CPU就知道哪一个设备准备好了!
硬件话题就这么多!
接下来是软件部分!
此时CPU得知哪些设备就绪,并不能解决问题,CPU不能运行代码,CPU只知道哪些设备就绪,但并不知道要如何处理!只有软件知道如何处理!
所以OS会在其内部设置一个中断向量表(interrupt descriptor table,IDT)
简化理解,可以认为IDT是一个函数指针数组!OS内部会提前设置好对应设备的处理方式,比如键盘要如何输入,磁盘要如何读写文件,等等!
OS启动前,就已经知道如何处理外设!此时CPU获取了中断号,知道有设备就绪!CPU此时就会执行OS的代码,到中断向量表中,根据中断号索引这个数组,找到处理方法!将这个方法的入口传给CPU,CPU就可以执行中断处理方法
OS再也不用关注外设是否准备好了!而是外设准备好了,会通知OS发送中断,之后CPU自动调用处理方法!OS内的这些方法都是OS的一部分!
这里的概念不会很熟悉吗?
发中断 —— 发信号
保存中断号 —— 记录信号
中断号 —— 信号编号
处理中断 —— 处理信号,自定义捕捉!
信号是纯软件的概念
信号机制,实际上是以软件的形式来模拟硬件中断的!
硬件中断其实是信号的父亲,虽然二者实际设计没有关系,但是思想是相同的!
CPU会有自己的事情,所以要做中断操作,要保护好CPU的信息!即保护好寄存器内的数据!中断结束后再继续CPU的工作!
当没有中断到来的时候,OS在干什么?
OS什么都没做,OS只是暂停!
在Linux内核中main函数中,看到OS一直在死循环做暂停!
而在外设中,有一个设备叫做时钟源
会高频向中断控制器发送中断信号,生成中断码,CPU根据中断号其寄存器获得中断码,CPU会在中断向量表中,找到对应的处理中断方法:进程调度!
所以,OS在硬件时钟源的中断的驱动下,进行调度!
以周期性的发送信号,CPU就会周期性的做进程调度, OS工作本质是在硬件的驱动下进行调度的,OS需要电也是这个原因!
OS就是基于中断进行工作的软件!
设计发现,时钟源处于外设,要和外设竞争中断控制器!成功后传递,没有效率!所以之后直接设计时钟源集成到CPU内部,只要没有外设中断,CPU内的时钟源就会周期性的发送中断信号,让CPU去做进程调度!CPU内有了一个概念——主频![]()
电脑CPU信息中,最后面的频率就是中断触发的频率!
所以OS就是在做死循环!没有任务就是一直暂停,时钟源以固定的时间,向CPU发送中断信号,进程PCB内有设置时间片,执行调度进程,每次对当前进程做count--,减的是时钟源发送信号的周期时间,当count== 0的时候,时间片耗尽(时间片本质就是计数器!),此时再去做其他进程的调度!
离线也可以知道当前时间的原因!
计算机内部设置了一个硬件,RTC实时时钟
集成在主板芯片组,搭配一颗主板纽扣电池(CMOS 电池)独立供电。
哪怕主机完全断电、拔掉电源线,电池依然给 RTC 供电,持续计时。
常见电池:CR2032,正常可用 3~5 年。
工作原理:
RTC 内部有精准晶振,按固定频率跳动,硬件层面持续累加时间。
它会记录:年、月、日、时、分、秒、星期,独立于操作系统运行。
系统关机、断网、休眠时,计时全由 RTC 负责。所以即使离线使用计算机,开机的时候,OS就可以从RTC实时时钟获取当前时间!得到当前的时间戳!时钟源发送信号的频率是固定的,我们设置一个total变量,每次时钟源发送信号就total++,之后我们可以根据total和频率推出的时间,来计算出开机到现在的时间!
关机的时候,再将时间传给RTC
这就是离线登录可以获取时间的原因!
所以计算机OS就是依靠外部中断进行运行的!
内核代码理解
看看中断处理:进程调度在内核中的实现方式!
set_intr_gate,这是在中断向量表进程调度的内核方法,0x20是时钟源发送中断信号,timer_interrupt是要处理方法!
timer_interrupt是通过汇编编写的!
注意,C/C++是可以与汇编语言汇编的,毕竟C/C++代码编译,也要汇编为汇编语言,再编成.o重定向文件,最后链接为可执行程序!
在timer_interrupt中有一个方法call _do_timer,查看do_timer函数,内部就有对当前进程的处理了!会判断当前进程的时间片--判断,若时间片大于0,说明进程运行时间没到,中断不做处理!时间片未被耗尽,退出!
小于0,时间片耗尽,schedule**进程调度!进程调度内部做switch_to做进程替换!
硬件会高频的做中断处理,OS就会以每纳秒级别的时间去检查当前进程的时间片,这个时间期间就是CPU去执行当前进程内容!所以OS在硬件的驱动下做进程调度了!
底层原理就是这样,不论是说明OS!OS运行就是依靠外部中断!记住了!
中断向量表就是OS的一部分,启动就自动加载到内存了!通过硬件中断,OS不需要对外设进行任何周期性的检查或者轮询!现在是外设主动来找OS!
这类外部设备触发的,中断系统运行流程,叫做硬件中断!
此时OS只需要等待时钟中断,定期调度进程,OS啥都不用做,只需要做死循环,需要什么新功能,直接向中断向量表中添加新的方法即可!
OS只需要在硬件时钟的推动下,自动调度了!
软中断
有没有可能因为软件问题,也触发上面的逻辑?
有!为了让OS支持进行系统调用,CPU也设计了对应的汇编指令(int 或syscall),可以让CPU内部触发中断逻辑!
进程在调度期间执行自己的代码,当代码中存在a/0这样的错误,希望能够被处理!CPU内部的EFLAFS寄存器,识别到a/0结果造成硬件上的溢出!即出现错误,CPU内就讲这个寄存器内出错的信息规定成为一种CPU内部触发的中断!
类似于时钟中断在CPU内周期性的发送时间中断!这里CPU一旦识别到a/0这类异常,CPU就会生成一个中断号,CPU就会处理这个中断信号,到中断向量表中查询这个中断号的处理方式!以此就可以让OS做中断服务:异常处理的工作!即给目标进程发送信号!
所以OS是如何知道硬件异常呢?所以的硬件异常都会被CPU以中断号的方式告诉OS!
缺页中断
缺页中断亦是如此!访问进程的虚拟空间是已经开辟了,但是页表没有建立映射关系,CPU在做地址转换的时候,发现没有物理地址,就会产生对应的中断号,然后触发缺页中断!这说明缺页中断在中断向量表中也有自己的处理方法!即申请物理空间,构建物理和虚拟地址的映射!
以上这类由软件导致硬件异常,没有外部设备硬件设备驱动的,由CPU内软件触发的错误,称为软中断!
能软件触发中断吗?
可以的!
让CPU通过软件主动中断!——软中断
X86:
int
x86_64:syscall
在CPU的指令集中,CPU内部是有自己的指令集的!这些指令肯定是提前注册完的!
所以C/C++代码:本质就是编译为指令集+数据的形式
而触发中断需要中断编号和中断处理方法!
当我们进行系统调用的时候,具体是怎么进入OS,完成系统调用过程的,毕竟CPU只有一个!
所以的系统调用是被写到系统调用表的函数指针数组中!
将所有的系统调用的底层函数的地址放入到函数指针数组中!
从此每一个系统调用,都有一个唯一的下标!
这个下标叫做系统调用号!——内核中!
要触发中断,就需要有对应的方法,在中断向量表的0x80位置设置中断的处理方法!
void CallSystem()
{
//1、获取系统调用号n!
mov n eax;
//2、调用系统调用方法(当然,肯定要做安全检测)
sys_call_table[n]();
}
至此,用户通过int 0x80就可以执行对应的方法!
用户层面,系统调用是如何调用的?
假设系统调用open()为例子,看看open系统调用的底层实现!是用汇编写的!
move exa 5 //把数据5移动到寄存器中!
int 0x80 //调用方法!
以上两步是核心的操作!传输系统调用号到内核!!
在int 0x80中,就是int n; move n exa
内核就获取到用户传输的系统调用号!
所以int 0x80触发软中断!OS会自动查对应的方法并执行,从寄存器中获取系统调用号!索引对应的方法!以此实现系统调用!
这就软中断的全过程!
OS不提供任何系统调用接口!OS只提供 系统调用号!
open、fork 这些系统调用接口都是glibc封装提供的!
int open() { move eax 5; syscall or int 80; }
系统调用的底层实现!

OS内核,设置了一个0x80的调用,一旦CPU获取了一个0x80的中断,就会调用system_call方法!
而system_call中,最关键的就是 call[_sys_call_table+eax*4],直接调用系统调用表的第eax个函数方法!_sys_call_table是系统调用表地址!而eax是编号,*4元素大小为4字节!
以此OS就可以调用glibc提供的系统调用,而OS只需要提供系统调用号!

而在用户层,glibc内部封装的方法!最后两行,将vfork转化为系统调用号,放到eax中,将系统调用号写到寄存器中!让后调用syscall
#define SYS_ify(syscall_name) __NR_##syscall_name是一个宏定义!将系统调用的名称转换为对应的系统调用号!比如:SYS_ify(open) ---> __NR_open
这个__NR_open就是系统调用号!系统调用号是OS内核提供,内核提供系统调用入口函数,或直接使用汇编级别中断命令int or syscall来完成系统调用
系统调用的实现是通过glibc实现,而OS需要提供系统调用号,和处理系统调用中断的处理方法即可!
系统调用名称的转换,已经传系统调用号,都是glibc封装实现的
所以系统调用还与C语言有关系!即所有的语言都与C语言有关!以后也是!
所以之前讲的,缺页中断,除0异常,野指针操作,内存越界访问,这些都是转化成中断来处理解决!
所以OS就是躺在中断处理历程上的代码块
CPU内部的软中断,比如int 0x80 或者 syscall,我们叫做陷阱
CPU内部的软中断,比如除零/野指针等,我们叫做异常

我们回到虚拟地址空间,以32位为例子,4GB的虚拟地址空间,0~3GB的区域是用户区域,而3~4GB之间就是内核区,内部有sys_fork(),内部有系统调用相关方法!所以我们在代码区使用系统调用, 底层调用syscall;发生中断,暂停进程,陷入内核,CPU触发中断,暂停进程保护现场,进行调用中断处理方法,就在虚拟地址空间上,从用户区跳转到内核区,执行对于的sys_fork()!
系统调用的过程,也是在进程地址空间上进行的!
所有的函数调用,都是通过地址空间之间的跳转
OS是学科集大成者!
用户态与内核态

虚拟地址空间,0~3G的用户区不用多说!程序调用用户提供的数据和代码,不会需要系统调用的!里面的数据只要获取到虚拟地址就可以直接访问
讲讲3~4G区域的内核区!
OS也是软件,一定也在内存!而虚拟地址空间有3~4GB区间的内核区需要做内核的映射,有内核页表!
内核页表也是做虚拟到物理地址的映射!
而用户页表会存在多份!而内核页表做到是内核区的虚拟地址空间,映射到OS在物理内存加载的地址!
而OS内部有设定一系列中断服务、系统调用表、异常处理方法、以及要做的死循环暂停,内部的数据结构!
使用OS会存放在内存一个区域,有且只有一份,而内核页表是做,虚拟到物理的映射,因为系统只有一份,所有内核页表也只有一份,所有进程共享!
结论:多个进程共用一份内核页表!这意味着,无论进程如何调度,我们总能找到OS!
我们调用系统调用方法,与当前进程是否调用是无关的!只要能够从用户区跳转到内核区,就可以访问到OS所有的代码和数据!
所以上节课,说到,我们在程序运行的时候突然来中断,CPU可以立即去处理中断服务方法,凭什么?
就是因为此时CPU暂停了当前进程,而CPU只需要通过PCB内的虚拟地址空间,就可以直接从用户层陷入内核层中,就可以直接访问到OS在内存中所有的内容!就是为什么能直接处理!
只要找到进程,就可以找到OS!
用户和内核的访问问题(引出概念)
用户和内核,都在同一个[0,4GB]的地址空间上了!
那么此时用户层随机的访问[3~4GB]上的地址,不就可以直接访问到OS内核吗?
这违背了OS为了保护自己,不相信用户,必须调用系统调用的方式进行访问!
所以,为了实现用户能够访问内核,以及OS的安全原则!
此时就引入了一个概念!用户态和内核态!
用户态:以用户身份,只能访问自己的
[0,3GB]
内核态:以内核身份,允许你通过系统调用的方式,访问OS[3,4GB]
在系统中,用户或OS,怎么知道当前除于内核态还是用户态?
要有识别区分用户和内核态的能力!
cs寄存器(代码段寄存器),记录当前状态!
0(00):内核态
3(11):用户态
所以状态是用硬件提供的!是CPU内提供了记录当前系统状态的寄存器!
当访问用户代码的时候,cs寄存器的代码段地址,指向的是用户区的代码段!此时地址的后两位就是11,代表用户态!
若此时访问内核区,CPU寻址发现内核区确实用户态,就要中终止这个进程,走中断处理这个进程!
所以cs寄存器内的变换就代表着状态的变化!
int 0x80 or syscall,就是让cs段寄存器指向内核区地址!并且修改权限标志位修改为0,此时就是陷入内核!为了防止通过地址随意访问内核数据,所以要求通过syscall绑定的系统调用方法,提供对应的系统调用号,来访问OS,其余都认为是非法操作!
CPU的权限寄存器来决定当前系统的状态,用户态or内核态!
所以,用户态就是CPU的权限级别为3,只允许访问虚拟地址[0~3GB]范围的数据!
内核态就是CPU的权限级别为0,只允许访问虚拟地址为[3~4GB]范围的数据!OS允许以系统调用方式,把系统调用号传给eax寄存器!切换地址空间,使用内核页表,映射地址访问OS,根据中断处理方法,查找系统调用表,CPU执行对应的系统调用!
权限位有名字,叫CPL!表示当前权限级别!
现在已经理解了内核态和用户态以及系统调用!
现在就可以来谈最开始的问题:
捕捉信号的步骤,可以完整讲解了!
当程序,因为异常、中断、系统调用,要进行内核处理,此时就从用户态转化为内核态,权限位由0变3;进入内核,解决完事物后,在返回用户态之前,做信号的判断,看内核进程PCB的pending表和block表!若有信号,则需要处理,根据handler表中的方法,若是自定义处理,此时需要执行用户态函数方法,所以又从内核态到用户态,执行自定义方法!执行完后,又会根据特殊的系统调用回到内核态!回到内核态再调用返回用户态的函数!
由此完成了对信号的全部学习!从产生 --> 保存 --> 捕捉
最初的例子,
ctrl + c,按下键盘后,键盘外设就绪,CPU收到中断码。暂停进程,进行中断处理,识别键盘数据,发现数据为ctrl + c,发现是终止前台进程中断信号,CPU内产生就继续执行中断,执行中断处理方法,向进程发送信号!就是对当前进程PCB的pending对应的信号修改为1,再之后因为中断、异常、系统调用再次进入内核,处理完返回用户态之前,做pending检查,检查到信号, 就做信号处理!
之后就从理论到实践
实践
补充——捕捉信号

sigaction是做检查并且改变信号处理动作!sigaction不仅可以处理普通信号,还可以处理实时信号!
参数:
signum为信号编号*act参数为结构体,表示如何处理信号!即设置自定义捕捉方法!*oldact保存旧动作,保留恢复前动作的操作!是输出型参数!
详谈
struct sigaction
目前只能了解第一、三行的属性!
第一行就是自定义处理方法!sigset_t是信号集,通过实践来了解!

接下来聊聊,sigaction内的信号集!
核心是,当有信号发送给进程,在对信号进行处理递达的操作,除了对pending对应的信号进行1->0操作,还对block做对应信号进行0->1操作,对已有信号进行屏蔽操作!即不允许同一个信号高频的重复递达!

对sigaction的自定义方法进行设置,将2号信号不断进行pending表输出!
还需要对sigaction中的信号集和flags属性进行操作!
将信号集和flag初始化,不要产生随机数!
随后测试可以看到,在进程处理2号信号的时候,在向进程发送2号信号,进程的pending表的2号信号处为1,但是并不会被递达,因为block表的2号信号为1!
可以了解信号集的概念了!当处理某个信号,处理屏蔽当前信号,还希望可以屏蔽另外一些信号!则用信号集来说明这些信号!
以上操作,就是把3,4信号加入到信号集,并且通过sigaction注册,对2号信号进行捕捉,就会把2,3,4都屏蔽!
可重入函数
前情:单链表的插入,通过两个指针即可完成!

上图情况!主函数做链表插入,node1->next处理完,刚刚好程序处理信号!而信号的处理方法是对node2进行插入,而node2正常插入,此时就是图3的状态!回到原理的位置,在做完链表插入,就会导致node2结点内存泄漏!
这里有main执行流和handler执行流,这两者是同一个进程的串型运行!
而insert方法,同时被两个执行流重复进入,这个情况叫做函数重入!
而函数重入导致程序出现异常,叫做不可重入函数!而没有影响的,叫做可重入函数!
将二者当作特点!并没有好坏之分!
大部分的函数,都是不可重入函数!

而到多线程并发访问的时候,函数重入就是高频操作了!
volatile(面试点!)


上面例子中,main函数中,并不会对flag进行修改!
编译器不能识别执行流的概念,识别main代码块中没有修改,认为只做检查
![]()

编译器优化级别!
但随着编译器的发展,有编译器优化级别较高的情况,编译器会将main函数,没有做处理的变量,如flag直接优化载入CPU寄存器,这样CPU不用多次访存操作!
但是放在当前例子中,就会造成一些问题!就是通过handler流将全局变量修改,只是修改内存中的变量,但是CPU不会进行访存操作,还是使用原来的是数据!程序就会一直死循环!即寄存器覆盖了进程看到变量的真实情况!内存看不见了!
所以volatile关键字就有作用了!volatile是保证内存空间可见性!CPU与内存之间可见性!
SIGCHLD信号
SIGCHLD子进程终止或暂停时给父进程发送SIGCHLD信号
既然进程退出会发送SIGCHLD信号,那么可以对这个信号做自定义处理,直接在信号处理中做waitpid!
这样就可以让父子进程异步的运行了,子进程结束,父进程直接处理即可,不需要等待!
但是这样做就会有一个问题,有多个子进程退出,这样就有可能只有一个子进程被父进程处理,其他都变成僵尸进程!
所以在自定义处理函数中做循环,一直做 waitpid检查!但是waitpid默认是阻塞的,如果有一个子进程一直不退出,就会一直等待,不能处理其他子进程
此时就可以使用WNOHANG做非阻塞等待操作,如果其他子进程都是未退出,就会返回0,就做退出,这样就可以

这要就实现了子进程等待的处理!
这就是以后进程退出的方案了!
更好的方案!
如果不做子进程的运行结果,单纯的不想产生僵尸,可以一下方法!

系统会自动将退出的子进程回收,不会出现僵尸
但是有个问题!就是SIGCHLD信号默认动作就是SIG_IGN!上面自动处理操作也是做SIG_IGN呀,为什么?
还是有区别的!
SIGCHLD的默认动作是signal(SIGCHLD,SIG_DFL)这个才是默认动作,而默认动作具体的操作是SIG_IGN!而使用signal(SIGCHLD,SIG_IGN)才是忽略!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐













所有评论(0)