Linux:信号详解

        信号,这在生活中是非常常见的,比如说红绿灯、手机铃声等。所谓信号就是在人或事情感受到这个元素产生以后会做出相应的处理动作,这就是信号。而在我们Linux下,什么是信号呢?

信号的基本概念:

        1.定义:信号更多的是通知事件的发生。信号产生之后第一时间也不是直接处理而是先存储下来。

                       信号实际是一个软中断。【软中断是一种需要内核为正在运行的进程去做一些事情(通常为I/O)的请求。】

        2.流程:信号的产生--->信号的注册--->信号的阻塞(不处理)--->信号的注销--->信号的处理

        3.分类:①不可靠信号(非实时信号):1~31

                       ②可靠信号(实时信号):34~64

                        Linux下有62种信号,使用kill -l 命令查看

                      

 


信号的产生:

        在Linux终端下,我们常常在键盘上输入ctrl+c来终止某个进程(这个按键只能用于终止前台进程,如果运行进程之前加上&那么这个进程就会到后台运行,此时ctrl+c就没效果了)。实际上,在用户按下Ctrl-C的时候,由操作系统向进程发送SIGINT信号,然后该进程对这个信号进行了响应,进而进程终止。其实我们在谈到进程状态的时候,其中谈到kill -9这个操作,这个操作实质也是一种信号。

 信号的产生一般有三种:

   1.通过硬件中断产生:ctrl+c

   2.程序异常产生: SIGFPE 、SIGSEGV   ...

   3..软件条件产生: ① kill :     int kill(pid_t pid ,int sig):向指定的进程发送指定的信号。成功返回0,失败返回-1

                                    ② raise:   int raise(int sig):向自身发送信号。成功返回0,失败返回-1

                                    ③ abort:  void abort(void):向自身发送SIGABRT信号。这个信号如同exit函数一样,永远成功没有返回                                                                                               值。它可以使当前进程接收到异常信号而终止

                                    ④ alarm: unsigned int alarm(unsigned int seconds):

                                                        设置一个定时器,在n秒之后向进程发送SIGALRM信号,取消上一个定时器,并且返回上一个                                                             定时器剩余时间。也就是说调用alarm相当于给进程设定了一个闹钟,闹钟时间到达的时候,便                                                           会发送信号,其信号默认动作是终止当前进程。

  • 核心转储(core dump):保存当前程序运行的数据以及调用栈信息,用于错误原因定位调试。

                           功能:如果程序运行出现错误,可以直接通过core文件来进行gdb调试(因为有些错误可能是偶然发生的)

                           打开方式:ulimit -c 1024

                           core dump默认关闭:原因:隐私安全和资源占用

       

      举个例子:

                           

        这个例子中我们看到出现了段错误,在我们进程遇到问题,会收到操作系统发送的信号,需要终止进程时,可以选择把进程的用户空间的数据全部保存到磁盘上,文件名通常是core开头的,后面加上进程的pid,这就叫做Core Dump。 
进程异常终止,常常是因为有Bug的存在,如野指针非法访问内存导致的段错误,事后我们可以用调试器来检查core文件,找到进程错误的原因。 
        一个进程允许产生多大的core文件取决于进程Recource Limit(这个信息保存在PCB当中),在Linux操作系统下是默认不允许产生core文件的,因为core文件中可能包含着用户的密码等敏感信息,是不安全的。不过为了我们的实践,我们可以利用指令ulimit来改变这个限制,允许让其产生core文件:

         

        此时在修改了ulimit之后,运行刚才的段错误文件,这时候产生了一个core文件。我们可以看看这个core文件多大:

        

        可以看到这个core文件是相当的大,如果系统将它加载出来是非常费时费力的,因此系统默认将其关闭。 
        如果一旦出现段错误,然后产生core文件后,我们就可以用gdb test core.2885来进行调试,最终找到错误。

 

 

 

信号的注册:

      给一个进程发送信号,就是修改这个进程pcb中关于信号的pending位图,将相应的信号位 置为1.

     

 

信号的阻塞:

先要了解两个概念:

  • 信号的递达:信号的处理。
  • 信号未决:这是一种状态,信号从注册成功到信号递达之间的一种状态

        信号的阻塞是指暂时不处理信号(阻止信号的递达),并不是不接收信号。

        产生信号后,操作系统会在进程的PCB中修改一个名叫信号位图上的内容,信号位图可以理解成一个拥有31个比特位的位图,每一个信号都对应一个比特位,所以是31个比特位,而操作系统发信号给进程,实质上是经过某些调用改变信号位图上对应比特位由0改为1,这样信号就发送了。而这个时候信号处于未决状态(从产生到修改位图上的对应比特位),当进程收到信号时,会在合适的时间进程处理,实际上进程去执行信号的动作称为信号的递达。而进程可以选择阻塞某一个信号,当信号被进程阻塞时,操作系统向进程发送信号,此时信号只会未决而永远不会递达,直到解除阻塞。 
        注:阻塞的时候信号还没有递达,而忽略是信号递达后的一个处理方式。
        要阻塞一个信号,就是修改pcb中关于信号的block位图,将相应的信号位置1,这个位图就像一个备注,说明这个信号暂时不去处理。

      从上图可以看到,从上至下分别是一号信号到31号信号,此时二号信号已经未决,所以它的二号比特位由0变为1,意味着操作系统已经向该进程发送了信号,而进程对二号信号进行进行了阻塞,那么这个时候二号信号是永远不能递达的,因为受到了进程的阻塞,除非解除阻塞,才能够递达,如果一直无法递达,那么未决表上二号比特位永远都是1。 另外,常规信号在递达之前如果产生多次同一种信号,那么只记一次;如果是实时信号,那么将这些多次产生的实时信号进行保存,保存到一个链式队列内。

  • sigset_t  信号集

        从我们上面的图来看,无论是未决表还是阻塞表,它们只有一个比特位来判断是否未决或阻塞,即0或1,并不记录该信号产生了多少次,阻塞标志也是这样的。这个时候引入一个新的数据类型叫做sigget_t,这个数据类型称作信号集,这个类型可以表示每个信号的有效或者是无效的状态,

       sigset_t信号集中,每个信号只用一个比特位来表示,0与1代表它的信号是否未决或阻塞,我们对它们的操作只能运用信号集操作函数来操作。而无法直接对其内部的元素进行访问操作。

       pending结构体中这个sigset_t位图是未决信号集,里面放的是注册了但是还没处理的信号。

       block结构体中这个sigset_t位图是阻塞信号集,里面放的是被阻塞了的信号。

 

信号集操作函数:

   #include <signal.h>

  •        int sigemptyset(sigset_t *set)    //清空一个信号集合
  •        int sigfillset(sigset_t *set)          //将所有的信号都添加到set集合中
  •        int sigaddset(sigset_t *set, int signum)     //添加指定的单个信号到set集合中
  •        int sigdelset(sigset_t *set, int signum)      //从集合中移除一个指定的信号
  •        int sigismembert(const sigset_t *set, int signum)      //判断一个信号是否在一个集合中

 

 

sigprocmask函数:阻塞信号/解除阻塞

          头文件:#include <signal.h>

      函数原型:int sigprocmask(int how, sigset_t *set,sigset_t *oldset)

              作用:读取或者更改进程信号集内的屏蔽字(阻塞信号集) 

              参数:  how:选择对信号集的操作动作

                                       SIG_BLOCK           阻塞集合中的信号

                                       SIG_UNBLOCK      对集合中的信号解除阻塞

                                       SIG_SETMASK       将集合中的信号设置到阻塞集合中

                             set:要阻塞/解除阻塞的集合

                        oldset:保存原先阻塞集合中的信号

          返回值:如果成功则为0,如果出错返回-1 

     

有两个信号是不会被阻塞的:SIGKILL 、 SIGSTOP

 

信号的注销:

       就是从pending集合中将即将要处理的信号相应位置为0 (从pcb的pending集合中移除)  

       注册:  

                 非可靠信号:就是讲相应pending位图置1,然后添加一个sigqueue结构到链表中,之后如果有相同信号到来,就不做任何操作。意味着后来的信号在前一个信号未处理之前不会重复注册,代表丢了。

                 可靠信号:就是不管有没有注册都要置1,并且添加节点到链表中,所以不会丢信号。

      注销:

                 非可靠信号:删除链表节点,相应位图置0

                 可靠信号:删除节点,判断是否还有相同信号节点,如果没有位图置0,如果有就不置0

 

信号的处理:

    处理时有三种操作:

  •              默认操作:按照操作系统中对信号事件的既定处理方式。
  •              忽略操作:直接将信号丢掉。
  •              自定义处理:用户自己定义事件的处理方式。

 

     handler表就是进程在递达时所能够执行的动作,包含:默认操作、忽略操作、自定义处理。

 

 信号的捕捉流程:

          主要是针对信号的自定义处理方式,处理自定义动作的这个过程就叫做信号的捕捉。

          实现函数有:signal、sigaction

          信号并不是立即处理的,而是选择一个合适的时机去处理,合适的时机就是当前程序从内核态切换到用户态的时候。

          信号的捕捉流程是当我们发起系统调用/程序异常/中断当前程序从用户态运行切换到内核态,去处理这些事情;处理完毕后,要从内核返回用户态,但是在返回之前会看一下是否有信号需要被处理,如果有就处理信号(切换到用户态执行信号的自定义处理方式),处理完毕之后再次返回内核态,判断如果没有信号要处理了就调用sys_sigreturn返回用户态(我们程序之前运行的位置)。

 

  •     面试题:程序如何从用户态切换到内核态?

                     答:1.发起系统调用(read,write...)   2.程序异常   3.程序中断

 信号捕捉函数:

  • signal
sighandler_t signal(int signum, sighandler_t handler);
//这个函数第一个参数为要捕捉的信号,第二个参数为要执行的默认动作,这是一个函数指针

 代码实现:

                      

        这里我们对二号信号进行了捕捉,二号信号为按键输入Ctrl+C便可产生。发现这时候并不执行二号信号应该有的动作终止进程,而改为了执行自定义动作,这就完成了一次信号捕捉。但是,并不是所有信号都能够捕捉,其中kill -9九号信号是无法捕捉的。

  • sigaction
#include <signal.h>

int sigaction(int signo, const struct sigaction* act, struct sigaction* ocat);

// signum:信号编号
// act:如果act非空,那么根据act的内容作为该信号新的处理动作,即自定义动作
// ocat:如果oact非空,则通过oact来传出该信号原来的处理动作。act与oact都指向sigaction结构体


struct sigaction
{
  void (*sa_handler)(int); //处理函数

  void (*sa_sigaction)(int,siginfo_t *,void *);  //处理函数

  sigset_t sa_mask; //在处理信号的时候可以通过这个mask暂时阻塞一些信号,处理完毕后会还原
  
  int sa_flags; //决定了我们使用哪个回调接口,并且还有一些其它的选项信息

  void (*sa_restorer)(void);
}

当某个信号处理函数被调用的时候,内核将自动将当前信号加入进程的信号屏蔽字当中,当信号处理函数调用结束返回时,自动恢复到原来的信号屏蔽字。这是为了防止在处理某个信号的时候,如果这种信号再次产生,那么它就会被阻塞到当前调用结束。

  • 僵尸进程的避免     

        僵尸进程是子进程先于父进程退出后,操作系统会通知父进程说子进程已挂,让父进程收尸,但是父进程不予理睬,此时子进程就会成为僵尸进程。

        操作系统如何通知父进程说子进程退出呢?会通过信号:SIGCHLD -17号信号

        在不了解信号之前,我们避免产生僵尸进程的处理方法就是让父进程一直等待子进程的退出,但是这样会浪费父进程的资源。在学习了信号之后,我们可以通过自定义信号:SIGCHLD的处理方式,相当于提前告诉进程,当接收到这个信号的时候使用waitpid,这样就不用让父进程一直等了,节约资源。

 

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值