linux 进程信号
1.信号概念
生活中的信号:红绿灯,上下课铃声,闹钟
红绿灯出现:是信号产生
认识到红绿灯:信号识别
看到之后放入大脑:信号保存
根据红绿灯做出操作(自定义,忽略,默认):信号处理
进制中的信号:
信号产生:
信号识别:程序员进行编写的属性和逻辑的集合
信号保存:信号来了之后,先保存
信号处理(信号捕捉):默认动作,忽略动作,默认动作。
kill -l 查看信号
1-31:普通信号
34-64:实时信号
信号的默认操作
2.信号产生
产生方式:
1.由用户键盘输入例如(kill -信号编号 进程id)
2. 代码编写错误后或者硬件异常产生,由os发送。这里得信号表示:不同得信号代表不同事件发生后得原因。
硬件异常产生信号:CPU内部存在一个状态寄存器,它会将内存中值通过运算器判断是否为正常的操作,如果不是正常的操作,状态寄存器的某个对应错误原因的位图由0变1,此时进程一直运行,这个cpu会查询到这个问题,然后os一直会识别到cpu中的异常,os把这个信号转化为操作。如果这个操作不是退出进程,这个寄存器属于cpu,用户无法更改,且它属于进程上下文,当进程被切换时,就有无数次状态寄存器被保存和恢复的过程,每一次都会被os识别到异常,然后进行信号处理。
3.系统调用函数(kill(pid,信号编号)给别人的进程发送,raise(信号编号)给自己的进程发送,abort()给自己发特定信号)
4.软件产生
定时器软件:函数alarm(时间),时间之后终止进程,信号为14.作用:统计进程的循环执行多少次。与信号替换做结合,在时间之后执行别的操作。(tip:输出输出会拖慢进程速度)
核心转储:
设置了数组大小,但是访问越界了,可能不会进行出错,因为os会给这块代码多开辟栈空间。如果对数组访问超过栈空间就会报错。
core:终止之后会做其他操作,不只是错误描述。在云服务器上看不见core的错误操作。
上面的core file size为0 说明云服务器关闭了core file
如果想看到core file,就用ulimit -c 数字 给这个选项设置数量。开启以后就会形成一个输入大小的数据块
核心转储的意义:os形成这个core file ,可以用gdb对可执行文件进行调试,查找它有什么类型的错误,并且在原代码那一行。
term:正常结束
3.信号保存
进程的task_struct(PCB)中的位图,用32位表示信号的编号。
信号发送:将位图中对应的位置由0置1,操作由os进行。os需要给用户提供系统调用,这样用户才能发送信号。
1.信号的保存过程的概念
实际处理信号的动作称为递达
信号从产生到抵达之间的状态,称为未决
阻塞就是一直保持在未决的状态,就是不让它到达需要处理的状态,等阻塞接触后便可以进入递达,便可以处理。
忽略是递达后的一种处理动作
2.信号在内核中的表示
task_struct 有一个apending位图(a.比特位的位置 b. 比特位所表示的内容)
将比特位由0变1就是收到了来自os发送的信号。
block这个位图的比特位由0变1表示阻塞了对应的信号。可以在没发出信号,就可以对这个信号进行阻塞。因此,在执行信号时,如果在pending中就是递达了,可以对信号进行处理。如果在block中就不处理。其中handler是一个函数指针数组,下标是信号的编号,通过下标找到的函数是对信号的处理方法当信号来的时候现在block中判断,如果为0,再在pending进行判断,如果为1,就去handler中寻找处理方法。
4.信号处理
信号默认处理
signal(信号名或数字,函数指针):更改信号的操作也就是自定义操作,这个是读信号的捕捉。
当执行信号时就是对信号的自定义操作
5.信号捕捉
用户处于用户态
用户态要访问的资源:1.os的资源(getpid)2.硬件资源(printf)
用户态访问资源时需要进行系统调用(对硬件和os访问),但是用户态不能直接进行访问,需要从用户态转为内核态,这时才能对系统资源进行访问,此时本身是用户态,但身份是内核态。
用户态转内核态(系统调用,进程切换):
须知:CPU中存有寄存器(可见寄存器,不可见寄存器),寄存器中有指向线程的PCB,以及指向进程的页表起始地址,有一个CR3表征当前进程的运行级别(0:内核态,3:用户态)。
进程可以执行os资源的原因是:进程有1GB的虚拟空间是对应一个不同其它空间的页表,因为os分配了两个页表(用户级页表,内核级页表),内核级页表,将虚拟地址映射到物理内存的os的资源,通过用户级的空间代码直接跳转访问内核空间(系统调用接口起始位置是用户态可以访问的,目的为了更改CPU中的CR3的权限),然后通过内核级页表访问对应os的物理内存,然后访问磁盘中的系统资源。
其中内核级地址空间,每个进程都是一样的,都指向同一块空间,而用户空间是每个进程独占的。
内核态转为用户态:
内核态不能直接访问用户区代码,技术上可以访问,但是os不允许这样,因为如果内核态可以直接执行用户态代码的话,就没有权限限制,用户态有恶意代码,os不知道用户态的代码的好坏,会直接被执行。这个信号捕捉的过程就是,执行代码,需要进入系统时,由用户态转为内核态,去进行信号检测,接收,存储,辨别工作后,进行处理信号,如果为自定义处理,就会由内核态跳转到用户态去执行用户区中的信号处理代码,处理完后返回到内核态,然后内核态接收到返回值,就由内核态返回到用户态最初的代码执行区,期间进行了四次身份转换。
6.信号在代码中的表示
6.1 sigset_t
os为了处理阻塞(信号屏蔽字)和递达的位图结构,设计出来这个sigset_t信号集,这是一个位图结构。每一个信号都有一个bit位来表示,0表示不是这个状态,1表示处于这个状态。
6.2 基本信号集操作函数
sigemptyset(sigset_t *set):初始化指向的set的位图的所有为置为0,表示这个信号集不包含有效信号。
sigfillset(sigset_t *set):初始化set指向的信号集,使所有信号对应的信号全部置为1.
sigaddset(sigset_t *set, int signo):向这个set信号集中signo的位置置为1.
sigdelset(sigset_t *set,int signo):将set中signo所在的位置置为0.
sigismember(const sigset_t *set,int signo):检查signo是否在这个信号集set中。
sigpromask(int how,const sigset_t *set,sigset_t *oset)
set是新的信号屏蔽集合,oset是老的信号屏蔽集合为了以后返回到之前的信号屏蔽集。
作用:修改block信号集的位图
选项:SIG_BLOCK(添加信号屏蔽字) SIG_UNBLOCK(批量解除阻塞的信号) SIG_SETMASK(重置信号屏蔽字,改为另一个位图)
sigpending(sigset_t *set)
作用:获取pending的位图
sigaction(int signum,const strcut sigaction *act,struct sigaction *oldact)(捕捉信号)
act:输入型参数
oldact:输出型参数
第一个是一个函数指针(执行方法)。
sa_mask比较重要,为一个信号位图集,需要进行初始化。
如果函数集捕捉了信号,如果正在处理这个信号,这个信号就不会抵达了。如果后面多发了这个信号会被屏蔽,当处理完第一个信号后,系统会取消对这个信号的屏蔽,然后之前多发的信号,只会再抵达多发中的一次。
sa_mask的作用:当我们处理某种信号的时候,这个位图会自动对某种信号屏蔽,如果在处理2号信号,又想屏蔽其他的信号,就可以进行设置sa_mask.
7.可重入函数
在执行主执行流时,其中的insert函数正在执行node1,但是还没执行完就收到信号,这个信号的处理方法也是调用insert函数,此时就会插入node2,这样node1和node2的next都指向一个next,当node2的insert执行完,node2的头节点就是head,此时返回到刚刚node1没执行完的位置,将node中的指针由node2换成了node1。这样就构成无法索引到node2,这个insert就是不可重入函数。
结论:
1.main的执行流与信号捕捉执行流是两个执行流
2.如果main和handler,该函数被两个执行流重复进入,出问题就是不可重入函数,反之就是可重入函数。
8.volatile
因为main和信号捕捉是两个执行流,所以在main执行时,收信号捕捉,就会去处理信号捕捉的执行流,main也会执行自己的,如果一个全局变量,两个执行流都会进行调用,在信号捕捉执行流中更改了全局变量,但不会影响到main执行流中对于这个全局变量的判断。原因是:编译器如果进行优化后,会有以下操作,原本cpu的操作是:从内存取指令,在cpu内部分析指令,然后执行指令。当cpu从物理内存收到这个全局变量后,main对它不更改,而是判断的话,就会直接在cpu内部分析指令了,不会去内存取指令,而是会将这个全局变量一直放在cpu中的判断寄存器中,下一次调用时不会去内存中寻找,而是直接从寄存器中调用这个全局变量进行判断。而此时信号捕捉执行流对这个全局变量进行更改后,是将值放到内存中,但cpu再mian执行流中不会再去内存中调用,因此这个全局变量在两个执行流中,会起到不同值得效果。
volatile保持内存可见性,修饰的全局变量,会使cpu不管怎样,每次执行代码都会去内存中调用这个全局变量,而不是因为自己执行流没有更改,就直接在寄存器中调用。