[linux] 详解linux进程信号

Posted 哦哦呵呵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[linux] 详解linux进程信号相关的知识,希望对你有一定的参考价值。

1. 信号概念

  信号就是程序要执行某件事情之前发送的信号,通知进程要做什么事情,是一个软件中断,信号是进程之间事件异步通知的一种方式。
  下面用一个具体的例子解释信号。在日常生活中有非常多种的信号,比如说在过马路时,红灯不通行,绿灯通行。这就是一个发送给行人与车辆的信号。
  在linux种,写了一个程序,但是这个程序因为某种原因,没有办法正常结束了,可以通过 ctrl+c 强制的结束掉这个进程。在这个过程中,使用ctrl+c产生了一个硬件的中断,给进程发送了一个结束的信号,进程接收到这个信号并且立即处理这个信号,结束进程。

2. 信号的种类

在linux中,共有62种信号可以通过 kill -l命令进行查看.
在这里插入图片描述
1. 非可靠信号(非实时信号)
在信号分类中,1-31号信号被称为非可靠信号,即受到上述信号后,进程不会立即处理该信号,信号有可能会丢失

2. 可靠信号(实时信号)
34-64号信号被称为可靠信号,信号绝对不会丢失

3. 信号的处理流程

在这里插入图片描述

  • 在信号处理信号时,当处理完某些功能,进入内核态后,处理完异常之后,一定会调用do_signal函数,在函数中会检测当前有没有要处理的信号,如果有则判断是否执行默认处理,还是自定义的处理方式,处理完成后才会返回用户态,如果没有则直接返回用户态。
  • 执行流从内核态切换到用户态之前一定会调用do_signal函数处理信号
  • 从用户态切换到内核态时,是调用了系统调用函数,或者进程异常

4. 信号的产生方式

4.1 软件产生

1. 命令产生
kill命令: 可以通过kill -num的方式向进程中发送指定的信号.
在这里插入图片描述
在这里插入图片描述

2. 函数产生

int kill(pid_t pid, int sig);
// 参数: pid要发送信号的进程pid
//      sig发送信号的序号
/
int raise(int sig);
// 作用:向自身发送信号,内部封装了kill函数
/
unsigned int alarm(unsigned int seconds);
// 作用:参数传递一个秒数,在事件到达之后发送一个SIGALRM信号

代码测试

#include <signal.h>    
#include <stdio.h>    
#include <unistd.h>    
    
int main()    
{    
    //kill(getpid(), 2);                                                                     
    raise(2);              
                           
    // 如果没有打印这一句 说明上述语句没有发送成功    
    printf("hello");       
                           
    return 0;              
}

在这里插入图片描述

4.2 硬件产生

ctrl+c: 发送的为2号信号即SIGINT
ctrl+z: 发送的为3号信号即SIGQUIT
ctrl+|: 发送的为20号信号即SIGSTP
在这里插入图片描述

5. 信号的注册

  信号的注册是通过改变signal位图中的比特位进行信号的注册。

在这里插入图片描述

linux中信号的种类共有62个,但用于存储信号的变量只有2个long的长度,共128个字节,如果一个变量存储一个是远远不够的,所以采用 位图 的方式进行存储信号,数组中对应的每一位存储一个信号,有信号则置为1,无则保持为0。并且数组的每一位没有全部使用,含有预留位置。

并且signalpending结构体中含有一个sigqueue队列,信号接收到进行注册时,需要在sigqueue队列中添加结点

  • 非可靠信号的注册
    第一次: 将信号对应的比特位置为1,在sigqueue队列中添加sigqueue结点
    第二次: 只会将信号对应的比特位从1改为1,不会添加sigqueue结点
  • 可靠信号的注册
    第一次: 将信号对应的比特位置为1,并且在sigqueue队列中添加sigqueue结点
    第二次: 将信号对应的比特位从1改为1,并且还会在sigqueue队列中添加sigqueue结点

6. 信号的注销

1. 非可靠信号的注销
  将信号对应在signal位图中的比特位置为0,并且将sigqueue结点从sigqueue队列当中进行出队操作。

2. 可靠信号的注销
  1.将可靠信号对应的sigqueue节点从sigqueue队列中进行出队操作。
  2.判断sigqueue队列当中是否还有当前可靠信号的sigqueue节点。如果没有,则将signal位图当中可靠信号对应的比特位置为0。如果有,则不会将signal位图当中的可靠信号对应的比特位置为0。

7. 信号的处理方式

7.1 信号的处理方式及基本概念

默认处理方式:SIGDFL,在操作系统内核有对该信号的默认处理动作。
忽略处理方式:SIGCHILD,子进程在退出时会给父进程发送一个SIGCHILD信号,但是父进程不关心这个信号
自定义处理方式:更改信号的处理方式。

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

7.2 信号自定义处理方式

1. 接口函数

函数一

sighandler_t signal(int signum, sighandler_t handler);
// 作用:更改信号的处理方式
// 参数: signum 待自定义处理函数的信号
//      handler: 函数指针,接收一个函数地址,该函数的返回值是void
//               参数为int,当进程受到某个信号时,就会触发调用该函数,
//               并将该信号的值,传递给回调函数
#include <stdio.h>                                                                           
#include <unistd.h>    
#include <signal.h>    
    
void sigcb(int signo)    
{    
    printf("signo: %d\\n", signo);    
}    
                      
int main()                                                  
{          
	// 当收到一下三种信号时,会调用回调函数输出进程号                                                         
    signal(SIGINT, sigcb);    
    signal(SIGTSTP, sigcb);                          
    signal(SIGQUIT, sigcb);             
                             
    while(1)                                
    {    
        sleep(1);                                                      
    }                                                                
                                                              
    return 0;    
} 

在这里插入图片描述

函数二

int sigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);
// 参数: signum 待自定义处理信号
//       act 想要将信号更改为什么处理动作,是一个结构体变量:
// 			参数1: void(*sa_handler)(int); 默认保存信号处理函数的函数指针
//		 	参数2: void(*sa_sigaction)(int, siginfo_t*,void*)
//			参数3: int sa_flags; 与上一个参数连用,一般不使用
//			参数4: sigset_t sa_mask 当正在处理信号时,会将注册的信号先放入该变量中过度,
//				当处理完该信号后,会将刚才注册的信号放入signal位图中
//			参数5: void(*sa_restorer)(void); 预留信息
//		oldact:信号之前的处理动作

#include <stdio.h>                                                                                                                                                       
#include <signal.h>    
#include <unistd.h>    
    
void sigcb(int signo)    
{    
    printf("signo:%d\\n", signo);    
}    
    
int main()    
{    
    struct sigaction act;    
    act.sa_handler = sigcb;    
    act.sa_flags = 0;    
    sigemptyset(&act.sa_mask); // 将mask中的比特位全部置为0 
    
    // 拦截SIGINT信号
    sigaction(SIGINT, &act, NULL);    
    
    while (1)    
    {    
        sleep(1);    
    }    
    
    return 0;    
} 

在这里插入图片描述

8. 信号的阻塞

  当准备处理信号时,会判断当前信号是否为阻塞,如果该信号为阻塞,则暂时不会处理该信号。

8.1 block位图

  该位图的结构与signal位图的类型相同,处理信号之前,会先查看block位图当中对应信号为的位图值是否为1,如果为1,则暂时不会去处理该信号。

8.2 接口函数设置阻塞状态

int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
// 作用:设置信号为阻塞状态
// 参数:
//	  how: 告诉sigprocmask函数以什么方式进行工作
//		  SIG_BLOCK: 设置某个信号为阻塞状态 block(new) = block(old) | set
//		  SIG_UNBLOCK: 解除信号为阻塞状态 block(new) = block(old) (~set)
//		  SIG_SETMASK: 替换原来的block位图 block(new) = set
//    set: 要修改新的位图
//    oldset: 在没有更改之前老的block位图

代码验证可靠信号与非可靠信号的阻塞

#include <signal.h>    
#include <stdio.h>    
#include <unistd.h>    
    
void signalcallback(int signo)    
{    
    printf("signo: %d\\n", signo);    
}    
    
int main()    
{
    // 更改2 40号信号的处理方式
    // 2: 非可靠信号
    // 40: 可靠信号
    signal(2, signalcallback);                                                                                                                                           
    signal(40, signalcallback);
                     
    // 阻塞所有信号       
    sigset_t set;                                  
    sigset_t oldblock;                         
    sigfillset(&set); // 将set中所有比特位置为1   
                                              
    sigprocmask(SIG_SETMASK, &set, &oldblock);
              
  	// 程序运行后,不输入内容会发生阻塞  
    getchar();                                    
                                              
    sigprocmask(SIG_SETMASK, &oldblock, NULL);
             
    return 0;
}

启动两个窗口,一个窗口进行进程阻塞时的等待,另外一个窗口发送信号。
在这里插入图片描述

在这里插入图片描述
现象总结

  • 信号的阻塞并不会干扰信号的注册
  • 可靠信号受到几次处理几次,非可靠信号收到多次只处理一次,非可靠信号可能会丢失
  • 先处理可靠信号,最后处理非可靠信号,按照队列出队的顺序进行处理
  • 4、9、19号信号不能被阻塞,保证程序退出的条件

9. volatile关键字

先看一段代码

#include <stdio.h>                                                                           
#include <unistd.h>
#include <signal.h>

int g_v = 1;

void signalcallback(int signo)
{
    g_v = 0;
    printf("signo: %d\\n", signo);
}

int main()
{
    signal(2, signalcallback);

    while(g_v)                                                                              
    {                                                                                       
                                                                                            
    }                                                                                       
                                                                                            
    return 0;                                                                               
} 

在这段代码中,我们阻塞了2号信号,在回调函数内部,修改了g_v这个全局变量的值,main函数中的while循环依赖与这个值的运行去运行,当发送二号信号之后,在回调函数内部,修改g_v之后,程序应该会停止运行

在这里插入图片描述

上图中可以看出,虽然进入了回调函数,并且修改了全局变量的值,但并没有跳出循环。
因为该程序的优化等级较高,效率较快,所以程序会从寄存器中将值取出,我们更改的值存在内存中,内存中的值发生了更改但是寄存器中的值没有发生更改,所以程序没有结束。
如果在全局变量前加上volatile关键字,就会禁止程序从寄存器中取值,保持内存的可见性。

#include <stdio.h>    
#include <unistd.h>    
#include <signal.h>    
    
// 进程程序在寄存器中取值,而是从内存中取值,保持内存可见性                                  
volatile int g_v = 1;                       
                                            
void signalcallback(int signo)              
{                                           
    g_v = 0;                                
    printf("signo: %d\\n", signo);           
}                                           
                                            
int main()                                  
{                                           
    signal(2, signalcallback);              
                                                                                            
    while(g_v)                                                                              
    {                                                                                       
                                                                                            
    }                                                                                       
                                                                                            
    return 0;                                                                               
} 

在这里插入图片描述

以上是关于[linux] 详解linux进程信号的主要内容,如果未能解决你的问题,请参考以下文章

linux进程间通信之Posix 信号量用法详解代码举例

Linux:详解进程信号(信号的捕捉流程,信号的阻塞volatile关键字)

[linux] 详解linux进程信号

[linux] 详解linux进程信号

万字详解Linux系列进程信号

万字详解Linux系列进程信号