概念
1.概念:进程信号就是一种事件的通知机制,给进程通知发生了什么事情,然后进程要放下当前干的事情去处理这个事情。
2.查看进程信号: ①:指令:kill -l //查看linux中的信号种类--62种 如下图: 其中,每一个信号都有自己的编号和宏定义,这些宏定义可以在signal.h中找到。 如上,其中对于前31个信号,它是linux下自己定义的,后面的信号是后来增加的,增加的信号比自己定义的信号更好一点,具体表现在以下几个方面:
- 前1~34个信号被称为非实时信号(非可靠信号)。
- 后面的信号被成为实时信号(可靠信号)。
- 非实时信号可能会丢失,实时信号不会丢失(具体在下面会讲到)。
对于上面出现的信号,我们都可用如下指令来查看其具体功能:
man 7 signal
生命周期
对于信号的生命周期一般分为如下几步:产生、注册、注销、处理、堵塞。
1.信号的产生:
对于信号的产生分为两种,一种为软件产生,一种为硬件产生:
①:软件产生: 概念:是通过函数的调用来产生信号,让进程去接收,然后去执行。 对于软件产生一般有以下几种:
kill -signum pid; :kill命令 其中是给pid(进程id)这个进程发送一个能让其进程退出的信号(这也是kill信号能杀死进程的原理)int kill(pid_t pid,int sig); :给指定的信号发送一个指定的信号(其中pid为进程的id,sig为我们要发送的信号)成功返回0,失败返回-1int raise(int sig); :给自己发送指定的信号,成功返回0,失败返回-1void abort(void); :给自己发送SIGABRT信号,其中abort调用的时候一定会成功,所以没有返回值unsigned int alarm(unisgned int s); :表示的是s秒后给自己发送SIGALRM信号。pause() :休眠接口signal(int sig,func) :将sig号进程改为func函数去处理,就是改变信号的处理方式
其中这些函数的头文件为:#include<signal.h>
②:硬件产生: 是通过键盘来进行操作,也是我们经常会使用到的:
- ctrl+c:发送的是2号信号。
- ctrl+\:发送的是3号信号。
- ctrl+z:发送的是20号信号。
2.信号的注册: 对于产生的信号,我们需要通过对信号进行注册,进程才知道自己接收到了一个信号需要去处理,主要目的就是在进程中做标记,这就要用到pcb中的一个未决信号集合(是一个位图的结构),用于标记进程信号。 且看如下图: 其中sigpending是在pcb中,它是一个位图的结构,那么里面的数据要么是1要么是0,所以我们在操作的时候,对于传进来信号了,那么对应的位置就会变为1,这样程序就知道我们传进来的几号信号,然后对应的去处理。 对于进程而言,如果在同一时间接收到许多的进程信号,那么他会根据信号的次序来进行一一处理,但是对于可靠信号而言,进程会优先处理,并且,可靠信号和非可靠信号在信号在接收多个信号的时候会有不同的操作,对于传入的信号,它们还有一个对应的信号队列(sigqueue)链表,其中每增加一个非可靠信号,都会在上面增加一个对应的节点(不管是相同的还是不相同的都可以),而对于非可靠信号,他对目前已经加入的相同的信号只能在链表中添加一个,所以面对同时接收的同一信号,他会丢失这样的多余的信号,只留相同信号的一个。 所以:
- 可靠信号:有信号传入,给对应的位图变为1,代表有这个信号传入,并且在链表上去添加这个信号(不管此时位图是1还是0都去添加)。
- 非可靠信号:有信号传入,给对应的位图变为1,代表有这个信号传入,然后在链表上去添加这个信号,但是此时如果有相同的信号加入时,他发现位图此时的这个信号的位置为1,那么就直接丢弃了,不会在链表上再继续加入。
3.信号的注销:
①:概念:删除待处理的未决信号的信息。 ②:其中:一个是位图(pending)中的,一个是链表上的。 ③: 位图:主要是查看传入了什么信号。 链表:主要是查看传入了多少信号。 ④:注销: 注销方式对可靠信号和非可靠信号的注销一共有两种不同的方式:
- 对于可靠信号:他通过位图知道传入了那些可靠信号,然后在链表上去进行删除,删除完后再查看链表上还有没有相同的可靠信号,如果有,那么删除,如果没了,那么将位图的相应的位置置为0。
- 对于不可靠信号:因为链表上相同的节点只有一个,那么对于其注销,是通过位图知道有什么不可靠信号,然后在链表上将其删除,然后对应位图置为0。
4.信号的处理: 就是给进程传来了信号,信号是怎么处理的? 其实是在进程中有运行信号的的处理函数(每个信号对应的处理函数是不同的)
①:其中信号的处理方式一般有三种,如下:
- 默认的处理方式:系统中针对每个信号定义好的处理方式。
- 忽略处理方式:收到了信号处理方式是忽略。
- 自定义处理方式:用户自己定义好的信号处理函数然后对传来的信号进行替换。
其中对于自定义处理方式会用到一个函数:
sighandler_t signal(int signum,sighandler_t handler);
其中:
- signum:要修改处理方式的信号的值。
- handler:一个函数的指针,一般情况下写自己写的函数。
其中如果设置成SIG_DFL则是-默认,SIG_IGN则是-忽略。
②:自定义函数的捕捉流程: 因为对于信号的处理,他是在内核态完成的。 所谓内核态:就是当程序通过系统调用访问内核空间的过程成为内核态。(因为一个进程无法访问内核,只能通过系统调用接口去访问内核) 如图:
这就是对自定义处理信号的捕捉流程。 其中注意的是:收到信号后,程序进入到内核态,会将在内核态期间收到的所有信号处理完才返回用户态。
③:内核态和用户态的判断方法:
- 如果当前运行的程序是自己写的程序或者是库函数,那么就是运行态。
- 如果当前是系统调用,或者中断/异常的处理就是内核态。
5.信号的阻塞:
①:概念:信号的阻塞,就是对接收到的信号进行阻塞,不去处理这个信号,等到这个信号被解除阻塞的时候,我们再去处理这个信号。
②:阻塞的原理: 对于信号的阻塞,其实在pcb中还是有一个阻塞的信号集合,他会配合这未决信号集合进行相应的操作,然后再标记出传进来的信号是阻塞的还是不阻塞的,具体看下图:
如上图,未决信号表(pending)上显示传来了2信号和3信号,但是在阻塞信号集合(block)上发现2号信号是阻塞的,所以在函数指针中(handler)中会标记一个0,意味着这个信号传来的时候,我们先不执行,等到我们接触阻塞信号的时候(也就是block中对应信号的标记解除时),然后handler中的标记也标记为可以处理了,然后去处理这个信号。
③:函数的接口:
int sigprocmask(int how,sigset_t* new,sigset_t* old);
功能:对调用该函数的进程的阻塞信号集合进行操作。 其中:
- how有下面三种写法:
SIG_BLOCK : 意思为 block |= new ,向block集合中添加new信号。 SIG_UNBLOCK:意思为 block &= new,从block集合中去除new信号。 SIG_SETMASK:意思为 block = new,将new设置为block。 - new:要添加/移除阻塞的信号集合。
- old:用于接收修改前的block信号。(一般情况下是接触堵塞的时候使用)
④:阻塞一个进程流程:
- 修改信号的处理方式。(signal(sig,handler))
- 阻塞指定的信号。(sigprocmask(SIG_BLOCK,new,old))
- 等待用户按下回车。(getchar())
- 接触阻塞。(sigprocmask(SIG_UNBLOCK,new,old))
其中:还需要以下函数:
- sigemptyset(sigset_t *set) //清空set中的指令集合。
- sigaddset(sigset_t *set,int sig) //将信号sig添加到set集合中。
- sigfillset(sigset_t* set) //将所有信号添加到set集合中。
- sigdelset(sigset_t* set,int sig) //将set中的sig信号移除。
- sigismember(sigset_t* set,int sig) //判断sig信号是否在set集合中。
然后进行以下操作: 先看代码:
然后我们发现,我们阻塞的2信号和40信号,然后我们开始进行操作: 首先我们在该进程中运行这个程序,然后打开另一个进程,去给其发送信号:
其中我们发送了3个2号信号和3个40信号,然后返回进程按回车查看: 最终得到的是这个结果,也验证了我们对非可靠信号和可靠信号的注册顺序以及是否有丢失情况。(其中,程序中的回车是为了保证连续的操作)
注意:在进程中有两个进程是不能被阻塞的,一个是:SIGKILL信号—9号信号,一个是SIGSTOP信号—19号信号。这两个信号的作用都是停止和杀死进程,这是必要的,如果我们将这两个信号还能进行修改,那么如果我们将所有所有信号都改了,那么这个进程就无法被系统操作了,就有可能出现这个进程,我们永远也无法让其消失。
特殊的: ①:一个进程kill杀不死的进程有:僵尸进程,信号被修改了处理方式,信号被堵塞。 ②:对于sleep这个函数,他会让进程进入一个可中断休眠态,此时任意一个信号都能打断这个休眠态,并且打断之后,进程会从该位置往下运行,此时不管当时sleep时间够没够,都会直接向下运行。
信号的应用
通过学习这节的内容,我们了解到,我们对于进程的操作,比如杀死一个进程,子进程的退出,以及异常的发生等等,这一切的一切全部都可以归结于进程接收到了信号,然后做出的相应的回答。
下面具体有两个应用: 1.僵尸进程的处理。 我们知道,僵尸进程我们一般的处理方式就是让父进程退出,而对于僵尸进程的产生,是因为子进程的退出时,父进程没有关心到子进程已经退出了。
本质:子进程退出的时候给父进程发送了一个信号-SIGCHLD,但是这个信号是默认操作方式是忽略,所以是父进程接收了这个信号,但是什么都没有做。
所以对于上述的方法,我们应该对上述的情况,我们此时就可以使用下面这个办法进行解决,就是在父进程中,将这个SIGCHLD信号的处理方式变成我们处理的方式:
sigcb()
{
while(waitpid(-1,NULL,WNOHANG)>0);
}
其中,sigcb是我们对这个SIGCHLD信号—17号信号传进来的一个自定义操作,而当我们父进程接收到这个信号的时候,我们就可以使用我们的自定义操作,直接让子进程的退出时,被释放,防止其成为僵尸进程。而对其进行while循环,意思是防止有多个子进程同时传入,而17号信号是不可靠信号,会出现丢失的情况。
2.在对管道的操作中,当读端关闭的时候,写端会直接触发异常,此时这个信号是----SIGPIPE。
关键字:volatile
1.作用:用于修饰遍历,保持变量的内存可见性。防止编译器过度优化cpu处理数据时,出现的数据不从内存中加载的情况。有了这个关键字,不管cpu如何优化,这个数据被使用的时候,cpu总是要从内存中去加载。
如图: 优化时,但没有添加volatile: 当a用了之后,他就会加入到cpu中,此时为了保证效率。 而当加了volatile时,则就变成下面: 每次用完a后,a就不会留在cpu中,就相当于图上的吧a还回去了(其实没有,只是在cpu中没有保存这个数据),然后等到第二次来拿的时候,还是按原来的步骤,从内存中去取a。
2.对于上面的情况,我们用下面的程序进行运行,会出现不同的结果: 此时,我们没有优化运行的时候,我们会得到下面这个情况,我会给其发送2号进程,然后while循环结束,运行printf中的内容: 而我们此时将代码优化: 优化的方法是,在gcc -g后面添加-O1或者-O2…或者最大-O5,最大五级优化,则运行如下: 此时我们再给2号信号也不会终止循环,这是因为其被加入到cup中了,然后往后的用a是直接拿cpu中最开始保存的a的值是1,即便是后面在内存中将a改为0了,但是在cpu中还是1,所以这就体现出了volatile这个关键字的重要性了,如下: 这个运行结果是给a加了volatile关键字的运行结果。
函数的可重入与不可重入
函数的重入:就是一个函数可以在不同的执行流程中同时进入执行。 如下程序: 我们可以发现,当我们main函数运行到test函数的sleep(3)这个位置的时候,这个时候会睡眠3秒,而在这3秒期间进程可能会接收到一个2号信号,而此时如果接收到2号信号了,我们开始的想法是,如果接收到信号和不接受到信号时,我们的两个printf都输出的是4,但是,在这期间接收到2号信号的时候,此时就会先在此期间先去处理信号,然后开始时a加了一次,此时又通过信号的的出现,又运行test函数,此时a又被加了一次,而因为开始main函数运行的test函数时,只吧a加了,b还没加,那么此时等信号处理完,自定义信号处理函数中的的printf中的值就是3(2+1),然后此时返回main主控流程,此时主控流程中运行到test函数的sleep上,然后往下直接运行b++,所以此时main函数中的printf打印的大小就是4。 来看运行结果: 所以这样就会出现可重入函数和不可重入函数:
①:可重入函数:一个函数就算可以重入,也不会造成预期之外的结果。 ②:不可重入函数:一个函数重入后,会造成预期之外的结果。
而它俩的判断方法是:一个函数内部如果对全局数据进行了不受保护的非原子操作,则这个函数就是不可重入函数。
|