目录
信号的概念
种类
信号的产生
硬件产生方式
软件产生方式
根据信号值判断代码出错的原因
信号的处理方式
信号的注册
概念:
内核中信号注册位图以及sigqueue队列的理解:
注册
信号的注销
信号的自定义处理方式
信号阻塞
信号的捕捉流程
其他扩展内容:
信号只是告诉我们有这样一个信号,但是具体这个信号如何处理,什么时候处理是由进程决定的,所以是软中断。
可以通过kill -l命令来查看所有信号。
注意:总共有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函数
pid:进程号,给哪个进程发信号,就填写哪个进程的进程号。
sig:要发送的信号值。
2、raise函数
谁调用给谁发信号,该函数在实现过程中是调用kill函数的。
sig:要发送的信号值
在之前gdb调试的时候,有一种情况就是对崩溃后产生的coredump文件进行调试,进而确定程序崩溃原因。产生coredump文件的方式以及限制因素:
前提:能够生成coredump文件,ulimit -a查看corefilesize的大小。
如果是0:通过ulimit -c unlimited命令将其改为无限制。
unlimited权限只是说在操作系统层面,不会限制你产生coredump文件。但是注意:硬盘空间大小也是决定能否产生coredump文件的一个因素,coredump文件大小必须小于磁盘空间大小才能产生。
1、解引用空指针、野指针(垂悬指针)
2、除0
3、越界访问
4、double free
1、通过man 7 signal查看操作系统对信号的处理方式。
2、默认处理方式
SIG_DFL
在操作系统当中已经定义好信号的处理方式
3、忽略处理方式
SIG_ING
联想到僵尸进程的产生原因:
子进程先于父进程退出,子进程在推出的时候会给父进程发送SIGCHID信号,而父进程接收到这个信号后,是忽略处理的,从而导致父进程没有回收子进程的退出状态信息,从而子进程就变成了僵尸进程。
4、自定义处理方式
程序员可以更改信号的处理方式,定义一个函数,当进程收到该信号的时候,调用程序员自己写的函数。
一个进程收到一个信号,这个过程称之为信号的注册。信号的注册和注销时两个独立的过程。
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表示没有接受到该信号。
如图:
既然数组的一个元素就可以搞定,为何又大费周章,给一个数组呢?
原因是为了后续可能会扩展的信号提供空间。
位图更改为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函数
signum :信号值
handler:等改为哪一个处理函数,是一个函数指针,接受一个函数地址
handler指向的函数是一个回调函数,再调用 signal函数的时候,给第二个参数传递函数地址的时候并没有调用传递的函数,而是在等进程收到了某个信号之后,才会调刚才注册的函数。
注意:9号信号是不能被程序员自定义处理的。
2、sigaction
signum:信号量
act:将信号的处理方式改为act指针指向的内容
oldact:原来信号的处理方式,输出型参数,用户只需要提供空间,后续可以通过oldact查看该信号原来的操作。
后两个参数都是一个结构体指针:
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):保留字段
信号的注册是信号的注册,信号阻塞时阻塞。信号的阻塞并不会影响信号的注册。进程收到这个信号之后,由于阻塞,暂时不处理该信号。
内核代码:
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。
可以通过kill -9将进程杀死。
注意:9号和19号信号不能被阻塞。
信号的处理时机:
从内核态切换到用户态的时候,会调用do_signal函数处理信号,该函数会判断是否有信号并作出相应操作:
有信号:判断当前信号是否被阻塞;阻塞的话暂时不处理;未阻塞,处理信号
处理信号时,不同的处理方式:
默认/忽略处理:直接在内核就处理结束
自定义处理:
1、执行用户在用户空间自定义的处理函数
2、调用sigruturn()函数再次回到操作系统内核
3、再次调用do_signal函数处理信号
4、调用sys_sigreturn函数回到用户空间,继续执行代码
如图所示:
调用系统调用函数;内存访问越界,访问空指针;调用库函数
1、之前学习的进程等待,父进程在等待子进程退出,回收它的状态信息的时候,有两种方式,分别是:
wait接口:阻塞等待
waitpid接口:非阻塞等待,搭配循环使用
这两种方式父进程在等待子进程期间都是无法执行其他代码的,导致父进程的效率低下,我们可以使用信号的方式来解决这个问题:
我们都知道,父进程回收子进程退出信息收到的是SIGCHILD信号,因此我们可以将该信号的处理方式自定义一下,然后在接收到该信号的时候,再转去执行自定义处理方式里面的wait函数来回收子进程的退出信息。这样父进程再等待子进程退出的同时能够去执行其他代码。
2、volatile关键字
作用:保证内存的可见性。每次CPU要计算的数据都是从内存中获取,拒绝编译时优化的方案(从寄存器获取)
gcc/g++的编译选项:-00 -01 -02 -03,优化级别依次增大
加上volatile关键字后,每次都会从内存中读取数据。