Linux进程信号

一、信号的概念

信号是进程之间通知的一种方式,属于软中断(进程收到信号并不是立即处理)。
例子:信号灯,红灯和绿灯,红灯禁止通行,绿灯可以通行,但是并不是所有人都会遵守信号灯。

二、信号的种类

使用kill -l 指令可以罗列信号的种类
在这里插入图片描述
总共62个信号:
1~31非实时信号,非可靠信号,信号可能会丢失
34~64实时信号,可靠信号,信号不会丢失

三、信号的产生

硬件产生

  • Crtl +C 2号信号 SIGINT即 interrupt中断信号

  • Crtl +Z 20号信号 SIGTSTP
    在这里插入图片描述

  • Crtl +| 3号信号 SIGQUIT

软件产生

发送信号的函数

kill 函数
可以给指定进程发送信号
#include<signal.h>
raise 函数
哪个进程调用发送给哪个进程,该函数的实现调用了kill函数
在这里插入图片描述

扩展:崩溃程序收到的信号

  1. 解引用空指针
int main()
{
	int *lp=NULL;
    *lp=10;
    return 0;                                                                                                                                                                            
}

release版本gdb:
在这里插入图片描述
debug版本gdb:
在这里插入图片描述
注:使用gdb调试时需要使用debug版本

  1. 内存访问越界
 int arr[5]={0};
 for(int i=0;i<10000;i++)
 {
 	printf("%d ",arr[i]);
 }                                                                                                                                                                               

在这里插入图片描述
内存越界后,操作系统并不会立即报错,但是如果越界访问的内存已经分配给其它进程, 操作系统便会给该进程发送一个11号信号SIGSEGV(segmentation violation,段错误),令其终止

  1. 除0
int a=10;
int b=0;
int c=a/b;                                                                                                                                                                         

在这里插入图片描述
除0操作后,操作系统发出了8号信号(SIGFPE,算术运算异常)

  1. double free
char*p =(char*)malloc(1024);
strcpy(p,"free test");
printf("%s\n",p);
free(p);
free(p);//重复释放                                                                                                                                                                           

在这里插入图片描述
在这里插入图片描述
接收到6号信号(Abnormal termination,异常终止)

四、信号的处理方式

操作系统对信号的处理方式

  • 默认处理方式,SIG_DFL,操作系统当中已经定义信号的处理方式了
    eg:
    2号信号 终止进程额
    11号信号 终止进程,并且产生核心转储文件

  • 忽略处理方式
    进程收到忽略处理方式的信号后,不进行处理
    eg:
    僵尸进程产生:子进程先于父进程退出,子进程退出的时候会给父进程发送SIGCHLD信号,父进程接收到这个信号,忽略处理,导致父进程并没有回收子进程的状态信息,子进程变成了僵尸进程

  • 自定义处理方式
    程序员可以改变信号的处理方式,定义一个函数,当进程收到该信号的时候,调用程序员自己写的函数

五、信号的注册

一个进程收到一个信号,这个过程称之为注册,信号的注册和注销并不是一个过程,是两个独立的过程
注册:操作系统内核向进程注册(发送)信号
注销:进程处理信号

sig位图是什么

  • 查看Linux内核源码中task_struct结构体的定义
    路径:linux-3.10.0-957.el7/include/linux/sched.h
    在这里插入图片描述
    task_struct结构体中有一个sigpending结构体对象pending,与信号注册有关

  • 查看sigpending的定义
    路径:linux-3.10.0-957.el7/include/linux/signal.h
    在这里插入图片描述
    sigpending结构体中有两个成员变量,双向链表list,和sigset_t结构体对象

  • 查看sigset结构体的定义
    路径:linux-3.10.0-957.el7/include/uapi/asm-generic/signal.h
    在这里插入图片描述
    sig是一个数组,但是造作系统当中并没有将其当作数组使用,而是当作位图使用,即只用到了sig这个数组的第一个元素,将这个元素的64个比特位当作位图使用,注册信号时将该信号对应的比特位置为1,表示进程收到了该信号。注:Linux操作系统中long是8个字节64个比特位,而在Windows中是4个字节 。

总结:进程的task_struct结构体中有一个struct sigpending类对象 pending,struct sigpending 结构体中有一个sigset_t类对象 signal,sigset_t结构体中有一个unsigned long类型的数组signal(使用数组是为了方便扩展,后续有更多的信号),这个数组的第一个元素的64个比特位作为位图,表示62种信号,当进程收到一个信号时,就将该信号对应的比特位置为1。

sigqueue队列

信号注册的时候除了修改信号对应的位图为1,还要添加sigqueue节点到sigqueue队列,sigqueue队列在操作系统内核中的本质是一个双向链表,满足先进先出的特性。
添加sigqueue节点时,实时信号和非实时信号有区别:

  • 非实时信号
    • 第一次注册,修改sig位图(0–>1),添加sigqueue节点
    • 第二次注册相同值的信号,修改sig位图(1–>1),不添加sigqueue节点
  • 实时信号
    • 第一次注册,修改sig位图(0–>1),添加sigqueue节点
    • 第二次注册相同值的信号,修改sig位图(1–>1),添加sigqueue节点

所以实时信号和非实时信号的区别就是第二次注册相同值的信号时,是否会添加sigqueue节点到sigqueue队列。

六、信号的注销

信号的注销与注册要做的事情一样,修改sig位图为0,将sigqueue节点进行出队操作,需要注意的就是,对于可靠信号,对sigqueue节点进行出队操作后,需要判断sigqueue队列中是否还有相同信号的sigqueue节点,如果没有再将sig位图的比特位修改为0,表示该信号已经全部注销。

七、信号的自定义处理方式

signal函数

概念

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

signum:要进行自定义处理的信号值
handler:一个函数指针,即自定义的处理方式
这里的handler指向的是一个回调函数,当进程收到对应的信号时,操作系统会调用自定义的handler函数,即被回调的函数sigcallback的参数signum是操作系统传递给sigcallback函数的,操作系统向该进程注册一个信号时,会将该信号的信号值传递给自定义的程序。

测试代码
在这里插入图片描述
运行结果:
在这里插入图片描述

sigaction函数

概念

#include <signal.h>

struct sigaction {
    void     (*sa_handler)(int);//函数指针,保存信号的处理方式
    void     (*sa_sigaction)(int, siginfo_t *, void *);//也是保存信号处理方式的函数指针,与sa_flags配合使用
    sigset_t   sa_mask;//进程在调用函数指针指向的函数处理信号时,如果收到其它信号,先将收到的信号保存在sa_mask当中,之后再放到sig位图当中
    int        sa_flags;//当sa_flags值为SA_SIGINFC的时候,信号的处理方式为sa_sigaction
    void     (*sa_restorer)(void);//保留字段
};


int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

signum:信号值
act:要更改为什么处理方式(入参)
oldact:旧的处理方式(出参)
测试代码
在这里插入图片描述
运行结果:
在这里插入图片描述

实现原理

从内核源码的角度分析

  • task_struct结构体中包含一个struct sighand_struct结构体指针sighand,(task_struct结构体位于sched.h头文件中)
    在这里插入图片描述
  • 查看sighand_struct结构体的定义,sighand_struct结构体中包含一个k_sigaction结构体数组action[_NSIG],_NSIG在Linux中一般定义为64,action数组的每一个元素都保存着一个信号的处理方式
    在这里插入图片描述
  • 查看k_sigaction结构体的定义,该结构体中有一个sigaction结构体对象sa
    在这里插入图片描述
  • 查看sigaction结构体的定义
    在这里插入图片描述
    sigaction函数修改的就是sigaction结构体的内容,而signal函数只能修改sigaction结构体中的sa_handler函数指针

总结:进程的task_struct结构体中包含一个sighand_struct结构体指针sighand,这个指针指向的sighand_sturct结构体对象中包含一个k_sigaction结构体数组action[_NISG],而k_sigaction结构体中包含一个sigaction结构体对象sa,sigaction函数就是通过修改进程的sigaction结构体来实现信号的自定义处理的。

八、信号的捕捉流程

Linux中,捕捉信号和自定义信号处理函数是指同一件事情,捕捉信号意味着使用signal()或sigaction()等函数,将进程中的某个信号与一个用户定义的信号处理函数绑定。当进程接收到该信号时,内核将自动调用用户定义的信号处理函数来处理该信号

信号的处理时机

当进程从内核态切换回用户态的时候,会调用do_signal函数,检查当前进程是否有需要处理的信号
常见的进入到内核的方式:

  • 调用系统函数
  • 内存访问越界,访问空指针
  • 调用库函数

不同处理信号方式的区别

对于默认处理方式和忽略处理方式,直接在内核中进行处理
对于自定义处理方式,则需要调用程序员自己定义的函数进行处理,具体步骤如下:

  • 从内核态切换回用户态时,会调用do_signal函数,检查是否有要处理的信号
  • 如果有自定义了信号的处理方式的信号需要处理,则切换到用户态,执行自定义函数(用户空间)
  • 执行完毕后,调用sigreturn()函数回到操作系统内核(内核空间)
  • 再次调用do_signal()函数,因为在执行自定义函数期间,进程可能会收到其它信号(内核空间)
  • 调用sys_sigreturn函数回到用户空间,继续执行代码

九、信号的阻塞

基本概念

信号阻塞的含义是,当进程的某一个信号被阻塞时,如果进程收到该信号会暂不处理,等到信号不阻塞了之后再处理
信号的注册和阻塞, 信号的阻塞并不会影响信号的注册

函数接口

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数
how:想要sigpromask函数实现的功能

  • SIG_BLOCK:设置某个信号为阻塞状态
  • SIG_UNBLOCK:设置某个信号为非阻塞状态
  • SIG_SETMASK:用第二个参数set替换旧的阻塞位图

set:新设置的阻塞位图
oldset:旧的阻塞位图

实现原理

阻塞位图中,某个位为1时,表示该信号被阻塞,为0表示非阻塞

  • 第一个参数为SIG_BLOCK,设置阻塞时,用第二个参数set与oldset按位或,得到新的阻塞位图
    在这里插入图片描述
  • 第一个参数为SIG_UNBLOCK,设置非阻塞时,用第二个参数set取反和oldset按位与
    在这里插入图片描述
  • 第一个参数为SIG_SETMASK,用第二个参数set替代旧的阻塞位图
    在这里插入图片描述

测试代码

程序
在这里插入图片描述
运行结果
在这里插入图片描述
这里阻塞了所有的信号,所以想要终止进程,需要向进程发送9号或者19号信号,这两个信号是不能被阻塞的

阻塞实时与非实时信号

测试代码
在这里插入图片描述
向进程发送信号
在这里插入图片描述
测试结果
在这里插入图片描述
进程接收到的2号信号和40号进程都是5次,但是2号信号只处理了一次,而40号信号处理了五次,进一步验证了非可靠信号和可靠信号。这是因为可靠信号第二次注册同一信号时,会再次添加节点到sigqueue队列,而非可靠信号不会。
阻塞信号与解除阻塞的顺序关系
这里还有一个现象,我们发送信号的时候先发送的2号信号,但是解除阻塞以后先处理的时40号信号,这是因为在这个程序中,使用sigfillset函数将set位图只为了全1,内核会默认按照从小到大的顺序进行处理,即先将2号信号添加到信号掩码(signal mask)中,再添加40号信号,当进程再次调用sigprocmask()函数修改信号掩码时,内核会按照先进后出的顺序恢复信号掩码,这样就保证了解除阻塞的顺序与阻塞的顺序相反,所以看到的现象是先处理40号信号,再处理2号信号。

十一、扩展

自定义信号处理解决僵尸进程

回顾僵尸进程的概念:僵尸状态是一种特殊的状态,当子进程退出,而父进程还在运行,但父进程没有读取到子进程的返回信息,子进程就会进入z状态,成为僵尸进程
之前解决僵尸进程的办法是,父进程调用wait或者waitpid函数进行进程等待,直到子进程退出,回收子进程的退出状态信息。但是这种解决方式也有缺点,父进程调用wait函数后会进行阻塞等待,waitpid函数虽然不会阻塞等待,但是一般要搭配循环使用,这两种方式都会导致父进程无法再做其他事。
这一问题可以通过自定义信号处理方式来解决,自定义17号信号(SIGCHLD),当子进程退出时,内核会向父进程发送SIGCHLD信号,将wait()函数或者waitpid()函数写在SIGCHLD信号的自定义处理方式中,父进程便会在子进程退出时进行进程等待,避免产生僵尸进程。

测试代码

#include<stdio.h>    
#include<unistd.h>    
#include<stdlib.h>    
#include<signal.h>    
#include<sys/wait.h>    
    
void sigcallback(int signum)    
{    
    //在SIGCHLD信号的自定义处理方式中调用wait    
    printf("recv signum is %d...\n",signum);    
    int status;    
    wait(&status);    
}    
    
int main()    
{    
    pid_t pid=fork();    
    if(pid<0)    
    {    
        perror("fork");    
        return -1;    
    }    
    else if(pid==0)    
    {    
        //child    
        //子进程三秒后正常终止    
        sleep(3);    
        exit(0);    
    }    
    else    
    {    
        //father    
        //父进程中,自定义信号处理,在等待期间可以处理其他业务    
        signal(SIGCHLD,sigcallback);                                                                                                                                         
    
        while(1)    
        {    
            printf("I'm running...\n");    
            sleep(1);    
        }    
    }    
    return 0;    
} 

运行结果:
在这里插入图片描述

查看进程状态:
在这里插入图片描述

volatile关键字

作用:保证内存的可见性。每次CPU要获取数据都是从内从中获取,拒绝编译器的优化方案,即不从寄存器中获取数据。
示例
在这里插入图片描述
这个程序中,将2号信号自定义处理,并且在自定义处理函数中将全局变量g_val的值赋为0,所以只要在向该进程发送2号信号,即在键盘键入Ctrl C,循环条件不满足,进程便会终止
运行结果如图:
在这里插入图片描述
但是,如果编译的时候选择的优化等级较高,便会产生不同的结果,gcc/g++的编译选项“-O0 -O1 -O2 -O3”,一般优化级别越来越高,处理速度越快。默认
优化等级为O0,即不进行任何优化,下面将优化等级设置为O3,查看程序运行情况:
在这里插入图片描述
优化后运行结果:
在这里插入图片描述
发送2号信号没有终止进程,因为此时优化等级较高,CPU会直接中寄存器中获取数据,虽然内存中的g_val的值已经被修改为0,但是寄存器中的g_val的值仍然是1,所以循环还在继续。
volatile关键字的作用就是让CPU始终从内存获取数据,只要修改了内存中的g_val的值,就可以让进程结束,如图:
在这里插入图片描述
volatile关键字修饰后运行结果:
在这里插入图片描述

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值