linux--进程信号

信号引入

一般来说,进程间的通信(进程间传输数据)根据通信内容可以划分为两种:低级通信和高级通信。

  • 低级通信只能传递状态和整数值(进程间控制信息的交换)
    由于进程的互斥和同步,需要在进程间交换一定的信息,所以也归为进程通信。
    1.功能:只能传递状态和整数值(控制信息)。
    2.特点:传送信息量小,效率低,每次通信传递的信息量固定,若传递较多信息则需要进行多次通信。
    3.编程复杂:用户直接实现通信的细节,容易出错。
  • 高级通信提高信号通信的效率,传递大量数据,减轻程序编制的复杂度。(进程间大批量数据的交换)
    分为三种方式:共享内存模式、消息传递模式、共享文件(管道)模式。
    之前的文章中有介绍部分内容-----> 《进程间通信:管道与共享内存》

在计算机科学中,信号是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。所以说,信号就属于进程通信的低级通信,主要用于进程之间事件异步的通知方式。

信号基本概念

信号(signal),又称为软中断信号。用来通知进程发生了异步事件。键盘中断或者一个错误条件(比如进程试图访问它的虚拟内存中不存在的位置等)都有可能产生一个信号。Shell也使用信号向它的子进程发送作业控制信号。
进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。

注意:信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。

信号的分类

发出信号的原因很多,这里按发出信号的原因简单分类,以了解各种信号:
(1) 与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。
(2) 与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误。
(3) 与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而系统资源又已经耗尽。
(4) 与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。
(5) 在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号。 (6) 与终端交互相关的信号。如用户关闭一个终端,或按下break键等情况。 
(7) 跟踪进程执行的信号。

信号源

内核为进程生产信号,来响应不同的事件,这些事件就是信号源。主要的信号源如下:

异常:进程运行过程中出现异常;
其它进程:一个进程可以向另一个或一组进程发送信号;
终端中断:Ctrl-C,Ctrl-\等;
作业控制:前台、后台进程的管理;
分配额:CPU超时或文件大小突破限制;
通知:通知进程某事件发生,如I/O就绪等;
报警:计时器到期。

用kill -l 命令可以查看系统定义的的信号列表
系统中有一组定义好的信号,它们可以由内核产生,也可以由系统中其它有权限的进程产生。可以使用kill命令列出系统中的信号集。
在 Linux 中,信号的种类和数目与硬件平台有关。内核用一个字的位图代表所有的信号,每个信号占一位,因此一个字的位数就是系统可以支持的最多信号种类数。在64位操作系统中一个字为无符号long类型,占64位,所以此系统最多支持64个信号。
使用 kill -l 可得到62个信号种类,前31个信号为不可靠信号(非实时信号),可能被丢失;后31个信号为可靠信号(实时信号),不能被丢失。
在这里插入图片描述
常见的信号

SIGHUP: 从终端上发出的结束信号;
SIGINT: 来自键盘的中断信号(Ctrl-C);
SIGQUIT:来自键盘的退出信号(Ctrl-\);
SIGFPE: 浮点异常信号(例如浮点运算溢出);
SIGKILL:该信号结束接收信号的进程;
SIGALRM:进程的定时器到期时,发送该信号;
SIGTERM:kill 命令发出的信号;
SIGCHLD:标识子进程停止或结束的信号;
SIGSTOP:来自键盘(Ctrl-Z)或调试程序的停止执行信号;

信号的产生

通过终端按键产生信号

ctrl+ c:给前台进程发送一个SIGINT, 中断当前的前台进程
ctrl+ z:SIGTSTP使进程暂停
ctrl+,SIGQUIT,使进程退出,并且产生coredump文件(在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做core的文件,也叫做内核映像转储).

调用系统函数向进程发信号

kil -[signalno] [pid] 给指定进程发送指定信号
eg:kil -9 [pid] 给该进程发送九号信号SIGKILL

kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
raise函数可以给当前进程发送指定的信号(即自己给自己发信号)。

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。

abort函数使当前进程接收到信号而异常终止。

#include <stdlib.h>
void abort(void);
//abort函数,谁调用谁退出,   对应信号SIGABRT
就像exit函数一样,abort函数总是会成功的,所以没有返回值。

软件条件产生

alarm定时器函数

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理
动作是终止当前进程。

硬件异常产生
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
如:11号信号: SIGSEGV段错误信号。可能是访问了空指针或者内存访问越界。

信号的注册与注销

注册

从信号在源码里的角度来看,在进程表(pcb)的表项中有一个软中断信号域(sig位图),该域中每一位对应一个信号,当有信号发送给进程时,对应位置为1.
在这里插入图片描述
信号的注册又根据种类的不同分为两种情况
非可靠信号: 1~31
更改sig位图当中对应的比特位为1,在sigqueue队列当中在增加对应信号所对应的节点
当多次收到同一个信号的时候,只添加一次节点, 也就是意味着,第二次收到的同样的信号被丢弃掉了。
可靠信号: 34~64
1.第一次收到该信号
更改sig位图当中对应的比特位为1 ,并且在sigqueue队列当中增加对应信号所对应的节点
2.第二次再收到同样信号
当多次收到同样一个信号的时候 ,先判断sig位图当中的比特位是否为1 ,并且在sigqueue队列当中增加对应信号所对应的节点,即不会丢弃。

注销

非可靠信号的注销
将sig位图当中对应信号的比特位置位0 ,并且将sigqueue队列当中的节点去除掉(操作系统拿着节点处理信号了)
可靠信号的注销
将待处理的信号在sigqueue队列当中对应的节点进行盘算.当前处理的对应信号的节点是否在sigqueue队列当中还有相同类型的节点
有:不改变sig位图当中的对应比特位上的1 ,换句话说,不将1置位0
没有:直接将sig位图当中的比特位置位0

信号处理

先介绍缺省操作
每一个信号都有一个缺省动作,它是当进程没有给这个信号指定处理程序时,内核对信号的处理。有5种缺省的动作:

异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做core的文件中(内核映像转储),而后终止进程。
退出(exit):不产生core文件,直接终止进程。
忽略(ignore):忽略该信号。
停止(stop):挂起该进程。
继续(continue):如果进程被挂起,则恢复进程的运行。否则,忽略信号。

信号处理的可能情况

进程可以对任何信号指定另一个动作或重载缺省动作,指定的新动作也可以是忽略信号。进程也可以暂时地阻塞一个信号。因此进程可以选择对某种信号所采取的特定操作,这些操作包括:
忽略信号:进程可忽略产生的信号,但 SIGKILL 和 SIGSTOP 信号不能被忽略,必须处理(由进程自己或由内核处理)。进程可以忽略掉系统产生的大多数信号。
阻塞信号:进程可选择阻塞某些信号,即先将到来的某些信号记录下来,等到以后(解除阻塞后)再处理它。SIGKILL信号也不能被阻塞。
由进程处理该信号(信号捕捉):进程本身可在系统中注册处理信号的处理程序地址,当发出该信号时,由注册的处理程序处理信号(就是信号捕捉)。
由内核进行缺省处理:信号由内核的缺省处理程序处理,执行该信号的缺省动作。例如,进程接收到SIGFPE(浮点异常)的缺省动作是产生core并退出。
大多数情况下,信号由内核处理

处理信号的方法

收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:
 1.第一种方法是,忽略信号,对该信号不做任何处理,就象未发生过一样。
 2.第二种方法是,对该信号的处理保留系统的默认值,如上面的缺省操作,但是对大部分的信号的缺省操作是使得进程终止。
 3.第三种是类似中断的处理程序,对于需要处理的信号,进程可以指定自定义处理函数,内核在处理该信号的时候切换到用户态去执行该函数来处理,这种当时称为做信号捕捉
 
进程通过系统调用signal来指定进程对某个信号的处理行为。

信号阻塞

先梳理下面的概念:
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意
1.阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
2.信号阻塞并不能说是信号就不能注册了。
操作系统处理信号的逻辑
当程序从用户态切换到内核态之后,处理do. signal函数的时候,发现收到某个信号, 想要处理这个信号之前,先判读block位图当中对应信号的bit为是否为1:

  • block当中对应bit位为1 :则不处理该信号, sigqueue当中对应的信号的节点还是存在
  • block当中对应bit位为0 :则处理该信号
int sigprocmask(int how, const sigset t *set, sigset t *oldset);
//更改sigset_ t位图当中的比特位的值
how:
- SIG_ BLOCK--> 设置某个信号为阻塞状态,用修改位图到达目的block(new)= block(old)| set
- SIG_ UNBLOCK-->设置某个信号为非阻塞状态block(new) = block(old) & (~set)
- SIG_ SETMASK -->设置新的阻塞的sigset _t位图block(new)= set
set:要设置的新的阻塞位图
oldset:之前程序当中阻塞的位图,出参

下图可以具体分析阻塞对信号的处理流程
在这里插入图片描述
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。
信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在 上图的例子中,

  • SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

特殊情况:如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
Linux是这样实现的:常规信号(非可靠信号)在递达之前产生多次只在sigqueue队列里放一次,意味着只会处理一次;而实时信号(可靠信号)在递达之前产生多次可以依次放在一个队列里,意味着被处理多次,即每一次都会被处理。
代码测试:

 #include<stdio.h>
  2 #include<signal.h>
  3 #include<unistd.h>
  4 void sigcallback(int signo)  //自定义的信号处理函数
  5 {
  6   printf("this signo is %d\n",signo);
  7 }
  8 int main()
  9 {
 10   signal(2,sigcallback);  //测试非可靠信号阻塞时怎么处理 
 11   signal(37,sigcallback);//测试可靠信号
 12   sigset_t set,oldset;
 13   sigemptyset(&set);//位图初始化 全为0
 14   sigemptyset(&oldset);//之前的旧位图也初始化为0
 15   sigfillset(&set);//初始化为全1
 16 
 17 
 18   sigprocmask(SIG_BLOCK,&set,&oldset);//设置为阻塞态, 由于set全为1 oldset全为0 或运算后新的位图还是全为1 ,即所有信号都是阻塞态
 19   getchar();  //从键入字符这个语句开始,实现接下来的阻塞态转化为非阻塞态                                                                                                                                 
 20   sigprocmask(SIG_UNBLOCK,&set,NULL);   //设置非阻塞态,与操作(~set)必为0,及此时所有信号被跟改为非阻塞态
 21   while(1)
 22   {
 23     sleep(1);
 24   }
 25   return 0;
 26 }

并且信号阻塞时,并不影响信号的注册。
且九号和十九号信号不可以被阻塞。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

信号捕捉(执行自定义处理函数)

操作系统默认对信号的处理
当sig位图当中收到某-一个信号的时候,意味着sig位图当中的某一个比特位被置为了1 ,作系统处理该信号该信号的时候,就会从PCB当中去寻找sighang. struct这个结构体的指针,从而找到sa_ handler, 进而操作系统内核去调用sa. handler保存的函数地址。完成信号的具体功能。

信号捕捉函数
signal :相当于改掉了sa. handler保存的函数地址,意味着,当收到了自定义信号的时候,操作系统内核会去调用sa. handler保存的新的函数地址,而这个函数是我们程序员定义的,也就达到了更改信号处理函数的目的。
在这里插入图片描述
从进程控制块pcb分析更改默认的执行函数为我们自定义信号执行函数
在这里插入图片描述

从用户态和内核态分析系统执行信号自定义处理函数的流程
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

在这里插入图片描述
在这里插入图片描述
注意:

  • 内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。
  • 如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时, 才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。
  • 处理自定义函数时,又是一个独立的执行流,和原来的执行流不是调用关系,他是在内核态被内核检测到存在自定义信号处理函数时才去调用的在用户态里定义的那个函数。并且在此期间,原有的执行流就阻塞到信号产生的地方,等待信号处理函数结束,再继续执行原有执行流,而不是回到调用信号处理函数的地方。要注意理解这一点!

重入与竞态条件

竞态条件
多个执行流访问同一个资源的情况下 ,会对程序结果产生一个二义性的结果 ,称之为竞态条件。
重入:多个执行流可以访问到同一个资源
可重入:多个执行流访问同一个资源,不会对程序的结果产生影响
不可重入:多个执行流访问同一个资源,产生一个二义性的结果
举个例子:
在这里插入图片描述
解释说明: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库的很多实现都以不可重入的方式使用全局数据结构。

SIGCHLD

之前进程控制的时候有个状态叫僵尸状态,具体情况就是:子进程退出时,会给父进程发送一个SIGCHID信号,但是操作系统内核对这个信号的默认处理动作为忽略处理,而导致父进程无法处理这个信号,父进程也就无法获取子进程的退出状态,从而子进程变为僵死状态。
现在我们学习了自定义信号处理,就可以更改这个信号的默认处理动作。避免僵尸状态的产生。

  • 更改SIGCHLD信号的处理方式,自定义一个处理函数,当子进程退出的时候,内核会去调用用户自定义的函数(回调函数),在回调函数当中去调用wait或者waitpid接口,来进程等待子进程;
  • 注意一点的是:在自定义信号处理函数的时候,不要将17号信号之外的函数定义为调用wait或者waitpid的函数,否则当进程收到一个其他信号的时候,触发内核调用用户自定义函数的时候,由于没有一个子进程退出,可能导致内核执行流卡死;
  • 子进程退出给父进程发送SIGCHLD信号,就会触发调用用户自定义的函数,在该函数当中调用wait进行等待,则就会将预防僵尸进程的产生,并且会避兔之前父进程调用wait函数阻塞,什么事情都干不了的情况。
#include<stdio.h>                                                                                                                              
  2 #include<unistd.h>
  3 #include<signal.h>
  4 #include<sys/wait.h>
  5 #include<sys/types.h>
  6 #include<stdlib.h>
  7 void sigcallback(int signo)
  8 {
  9   printf("signo is %d \n",signo);
 10   wait(NULL);//调用进程等待方法
 11 }
 12 int main()
 13 {
 14   signal(SIGCHLD,sigcallback);
 15   pid_t pid=fork();
 16   if(pid<0)
 17   {
 18     perror("fork");
 19     return -1;
 20   }
 21   else if(pid==0)
 22   {
 23     //child
 24     sleep(4);
 25     printf("i am child \n");
 26     _exit(0);//进程终止,一旦子进程退出,便会产生sigchld信号,此时便会另一个执行流去调用自定义信号处理函数里的wait函数,来避免僵尸状态
 27 
 28   }
 29 	//father
 30   while(1)
 31   {
 32     sleep(4);
 33     printf("i am father\n");
 34   }
 35   
 36   return 0;                                                                                                                                    
 37 }

在这里插入图片描述

volatile关键字

使变量保持内存可见性,即每次访问这个变量的时候,都是去内存中重新加载。
冯诺依曼体系结构中一个数据要被计算,都是cpu到内存中去取数据在进行计算,但是如果编译器编译时使用了编译优化后,由于常用的数据会保存在寄存器里,而cpu就会直接去寄存器里取这个值,这样可能就会出现,当这个变量的值被改变后,寄存器里的值可能还没被更新,cpu就拿去用了,最后导致程序错误的结果。
cpu–>寄存器–>缓存–>内存–>磁盘
解决办法就是使用volatile关键字,让每次访问这个变量的时候,都是去内存中重新加载最新的值。

	 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <signal.h>
  4 
  5 volatile int g_val = 1;                                                                                                                        
  6 
  7 void sigcallback(int signo)
  8 {
  9      g_val = 0;
 10      printf("change val!,signo:%d %d\n", signo, g_val);
 11 }
 12 
 13 int main()
 14 
 15 {
 16   signal(2,sigcallback);
 17   while(g_val)//程序要是不用内存的新改的值,便会在一直死循环,程序也不会退出!使用volatile关键字后,就会去内存里重新取值了,程序也就会结束
 18   {
 19     ;
 20   }
 21   printf("over!\n");
 22   return 0;
 23 }

未加volatile关键字修饰前
在这里插入图片描述
在这里插入图片描述
加了volatile关键字之后
在这里插入图片描述

进程间通信总结

进程之间的多种通信方法各自有各自的优点和缺点,因此.对于不同的应用问题,要根据问题本身的情况来选择进程间的通信方式。
几种通信方法总结:

  • 如果用户传递的信息较少.或是需要通过信号来触发某些行为. 本文提到的软中断信号机制不失为一种简捷有效的进程间通信方式(低级通信)。
  • 但若是进程间要求传递的信息量比较大或者进程间存在交换数据的要求,那就需要考虑别的通信方式了(高级通信)。
    -(1) 匿名管道简单方便.但局限于单向通信的工作方式.并且只能在创建它的进程及其子孙进程之间实现管道的共享:
    -(2)命名管道虽然可以提供给任意关系的进程使用.但是由于其长期存在于系统之中,使用不当容易出错,所以普通用户一般不建议使用。
    -(3)消息缓冲可以不再局限于父子进程.而允许任意进程通过共享消息队列来实现进程间通信.并由系统调用函数来实现消息发送和接收之间的同步.从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题.使用方便,但是信息的复制需要额外消耗CPU的时间.不适宜于信息量大或操作频繁的场合。
    -(4) 共享内存针对消息缓冲的缺点改而利用内存缓冲区直接交换信息,无须复制,快捷、信息量大是其优点。但是共享内存的通信方式是通过将共享的内存缓冲区直接附加到进程的虚拟地址空间中来实现的。因此,这些进程之间的读写操作的同步问题操作系统无法实现。必须由各进程利用其他同步工具解决。另外,由于内存实体存在于计算机系统中.所以只能由处于同一个计算机系统中的诸进程共享,也不方便网络通信。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值