【Linux】进程信号

目录

信号的概念

种类

信号的产生

硬件产生方式

软件产生方式 

根据信号值判断代码出错的原因

信号的处理方式 

信号的注册

概念:

内核中信号注册位图以及sigqueue队列的理解:

注册

信号的注销 

信号的自定义处理方式 

信号阻塞 

信号的捕捉流程

其他扩展内容:


信号的概念

信号只是告诉我们有这样一个信号,但是具体这个信号如何处理,什么时候处理是由进程决定的,所以是软中断。

种类

可以通过kill -l命令来查看所有信号。

【Linux】进程信号_第1张图片

注意:总共有62个信号,没有32和33号信号。

其中1-31号信号被称之为非实时信号,也叫非可靠信号,它在使用的过程中信号可能会丢失。34-64号信号被称为实时信号,也叫可靠信号,它在使用过程中信号不会丢失。

信号的产生

硬件产生方式

1、Ctrl + c

产生的是2号信号SIGINT,是一个中断信号。

2、Ctrl + z

产生的是20号信号SIGTSTP,是一个暂停信号。

3、Ctrl + \

产生的是3号信号SIGQUIT,是一个暂停信号。

4、kill命令向进程发送信号

通过kill -[信号值] [pid]向进程发送信号。

软件产生方式 

1、kill函数

【Linux】进程信号_第2张图片

pid:进程号,给哪个进程发信号,就填写哪个进程的进程号。

sig:要发送的信号值。 

【Linux】进程信号_第3张图片  

2、raise函数

【Linux】进程信号_第4张图片

 谁调用给谁发信号,该函数在实现过程中是调用kill函数的。

sig:要发送的信号值

【Linux】进程信号_第5张图片

【Linux】进程信号_第6张图片

根据信号值判断代码出错的原因

在之前gdb调试的时候,有一种情况就是对崩溃后产生的coredump文件进行调试,进而确定程序崩溃原因。产生coredump文件的方式以及限制因素:

前提:能够生成coredump文件,ulimit -a查看corefilesize的大小。

如果是0:通过ulimit -c unlimited命令将其改为无限制。

unlimited权限只是说在操作系统层面,不会限制你产生coredump文件。但是注意:硬盘空间大小也是决定能否产生coredump文件的一个因素,coredump文件大小必须小于磁盘空间大小才能产生。 

1、解引用空指针、野指针(垂悬指针)

【Linux】进程信号_第7张图片

 2、除0

【Linux】进程信号_第8张图片

3、越界访问

【Linux】进程信号_第9张图片

4、double free

【Linux】进程信号_第10张图片

信号的处理方式 

1、通过man 7 signal查看操作系统对信号的处理方式。

【Linux】进程信号_第11张图片

2、默认处理方式 

SIG_DFL

在操作系统当中已经定义好信号的处理方式 

 3、忽略处理方式

SIG_ING

联想到僵尸进程的产生原因:

子进程先于父进程退出,子进程在推出的时候会给父进程发送SIGCHID信号,而父进程接收到这个信号后,是忽略处理的,从而导致父进程没有回收子进程的退出状态信息,从而子进程就变成了僵尸进程。

 4、自定义处理方式

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

信号的注册

概念:

一个进程收到一个信号,这个过程称之为信号的注册。信号的注册和注销时两个独立的过程。

内核中信号注册位图以及sigqueue队列的理解:

task_struct 结构体内部有一个结构体变量struct sigpending pending,struct sigpending结构体有两个成员变量,一个是 struct list_head  list(双向链表),另一个是sigset_t signal (数组)

具体研究以下这个数组,sigset_t本质上是一个结构体,它的内部成员变量是一个数组:unsignal long sig[_NSIG_WORDS],对于这个数组,操作系统并没有将它当作数组使用,而是把它看作是位图;在Linux64位平台下,long占8个字节,也就是64个bit位,而目前信号的数量只有62个,所以每个bit位表示一个信号是足够的。 

该数组中的每一个元素有64个比特位,对于现有的62个信号,规定每一个信号对应数组中某个比特位,其中比特位位1表示接收到了信号,0表示没有接受到该信号。

如图:

【Linux】进程信号_第12张图片

 既然数组的一个元素就可以搞定,为何又大费周章,给一个数组呢?

原因是为了后续可能会扩展的信号提供空间。

注册

位图更改为1,添加sigqueue节点到sigqueue队列。

信号在注册的时候,会将信号对应的bit位由0改为1,表示当前进程收到了该信号。

在sigqueue队列中添加一个sigqueue节点。队列在操作系统当中本质上是一个双向链表(具有先进先出的特点)

非实时信号注册:

第一次注册:修改sig位图(0-->1);修改sigqueue队列

第二次注册:修改sig位图(1-->1);不会添加sigqueue节点

 实时信号注册:

第一次注册:修改sig位图,修改sigqueue队列

第二次注册:修改sig位图,添加sigqueue节点到sigqueue队列

信号的注销 

非可靠信号:

1、将信号对应的sig位图当中的bit位置为0

2、将对应信号的sigqueue节点进行出队操作

可靠信号 :

1、将对应信号的sigqueue节点进行出队操作

2、判断sigqueue队列当中是否还有相同信号的sigueue节点;如果有,比特位不作修改;没有,对应比特位置为0 

信号的自定义处理方式 

让程序员自己定义某一个信号的处理方式,当进程收到该信号后就会执行程序员自定义的处理方式。

1、signal函数 

【Linux】进程信号_第13张图片

signum :信号值

handler:等改为哪一个处理函数,是一个函数指针,接受一个函数地址

handler指向的函数是一个回调函数,再调用 signal函数的时候,给第二个参数传递函数地址的时候并没有调用传递的函数,而是在等进程收到了某个信号之后,才会调刚才注册的函数。 

注意:9号信号是不能被程序员自定义处理的。

 【Linux】进程信号_第14张图片

【Linux】进程信号_第15张图片

2、sigaction

【Linux】进程信号_第16张图片

signum:信号量

act:将信号的处理方式改为act指针指向的内容

oldact:原来信号的处理方式,输出型参数,用户只需要提供空间,后续可以通过oldact查看该信号原来的操作。 

后两个参数都是一个结构体指针:

【Linux】进程信号_第17张图片

void (*sa_handler)(int):保存信号默认处理方式的函数指针

void (*sa_sigaction)(int, siginfo_t*, void*):也是一个保存信号的处理方式的函数指针,但是  没有使用;使用的时候需要配合sa_flags一起使用;当sa_flags的值为SA_SIGINFC的时候,信号按照sa_sigaction保存的函数地址进行处理。

sigset_t  sa_mask: 是一个sig位图,用来暂存进行在执行该信号的处理过程中收到的其他信号,后续会放到进程的信号的位图当中。

int sa_flags:我们一般情况下只会使用1和3

void  (*sa_restorer)(void):保留字段 

【Linux】进程信号_第18张图片

【Linux】进程信号_第19张图片 结合内核理解:

【Linux】进程信号_第20张图片

信号阻塞 

信号的注册是信号的注册,信号阻塞时阻塞。信号的阻塞并不会影响信号的注册。进程收到这个信号之后,由于阻塞,暂时不处理该信号。

内核代码:

struct task_struct{
    .........
    sigset_t blocked;
    .........
}

 sigset_t blocked是一个位图,当要阻塞一个信号的时候,将该信号对应的比特位设置为1即可。

接口:

how:想让sigprocmask做什么

        SIG_BLOCK:设置某个给信号为阻塞状态

        SIG_UNBLOCK:设置某个信号为非阻塞状态

        SIG_SETMASK:用第二个参数set来替换原来的阻塞位图

set:新设置的阻塞位图

oldset:原来老的阻塞位图 

函数原理解析:当how为SIG_BLOCK时,函数会根据set,计算新的阻塞位图,计算方式是:block(old) | set;当how为SIG_UNBLOCK时,函数会根据set,计算新的阻塞位图,计算方式是:block(new) = block(old) & (-set);当how为SIG_SETMASK时,函数会根据set,计算新的阻塞位图,计算方式是:block(new) = set。

【Linux】进程信号_第21张图片

可以通过kill -9将进程杀死。

注意:9号和19号信号不能被阻塞。 

信号的捕捉流程

信号的处理时机:

从内核态切换到用户态的时候,会调用do_signal函数处理信号,该函数会判断是否有信号并作出相应操作:

有信号:判断当前信号是否被阻塞;阻塞的话暂时不处理;未阻塞,处理信号

处理信号时,不同的处理方式:

默认/忽略处理:直接在内核就处理结束

自定义处理:

        1、执行用户在用户空间自定义的处理函数

        2、调用sigruturn()函数再次回到操作系统内核

        3、再次调用do_signal函数处理信号

        4、调用sys_sigreturn函数回到用户空间,继续执行代码

如图所示: 

 

【Linux】进程信号_第22张图片 常见的进入系统内核的方式:

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

其他扩展内容:

1、之前学习的进程等待,父进程在等待子进程退出,回收它的状态信息的时候,有两种方式,分别是:

wait接口:阻塞等待

waitpid接口:非阻塞等待,搭配循环使用

这两种方式父进程在等待子进程期间都是无法执行其他代码的,导致父进程的效率低下,我们可以使用信号的方式来解决这个问题:

我们都知道,父进程回收子进程退出信息收到的是SIGCHILD信号,因此我们可以将该信号的处理方式自定义一下,然后在接收到该信号的时候,再转去执行自定义处理方式里面的wait函数来回收子进程的退出信息。这样父进程再等待子进程退出的同时能够去执行其他代码。 

【Linux】进程信号_第23张图片 

【Linux】进程信号_第24张图片 

【Linux】进程信号_第25张图片 

2、volatile关键字

作用:保证内存的可见性。每次CPU要计算的数据都是从内存中获取,拒绝编译时优化的方案(从寄存器获取)

gcc/g++的编译选项:-00 -01 -02 -03,优化级别依次增大 

加上volatile关键字后,每次都会从内存中读取数据。 

你可能感兴趣的:(Linux,linux,运维)