目录
1. 信号的概念
信号是一个软件中断,相当于是一个口头的约束,对你的限制力比较低,举个例子来说就是当你买的快递到了需要你去取的时候,而你正在打游戏,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取。在这个过程中,你获得了一个取快递的信号,但是你并没有选择立即执行,而是选择了在合适的时候去取,这就是软件中断。
2. 信号的种类
-
目前Linux的信号数量为62个,分为两种类型:
① 非实时信号(非可靠信号),对应信号量为
1~31
,它的特点是有可能信号会发送丢失
② 实时信号(可靠信号),对应信号量为34~64
,它的特点是信号不会发送丢失 -
kill -l
:可以罗列出具体的信号值。 -
man 7 singal
:罗列所有的信号的具体信息。信号的具体信息:
信号的动作:
如果某一个信号的处理动作是core
,那么它默认是需要完成终止进程+产生coredump文件。产生coredump文件依赖于ulimit -a
中对应的core file size
,并将其设置为unlimited
。
3. 信号的产生
3.1 硬件产生
ctrl + c
:使当前运行的进程中断,产生一个值为2号的信号量,是一个SIGINT
。ctrl + z
:使当前运行的进程停止,产生一个值为20号的信号量,是一个SIGTSTP
。ctrl + |
:使当前运行的进程退出,产生一个值为3号的信号量,是一个SIGQUIT
。
3.2 软件产生
- kill 命令
kill [PID]
:终止一个进程。
kill -[num] [PID]
:给进程号为PID的进程发送一个信号值为num的信号。 - kill 函数(包含在
#include<signal.h>
中,是一个系统调用函数)int kill(pid_t pid, int sig);
功能是:给pid进程发送sig的信号。 - raise函数(包含在
#include<signal.h>
中,是一个库函数)int raise(int sig)
功能:谁调用给谁发送signal信号。
4. 信号的注册
- 一个进程接收到一个信号,这个过程就被称之为信号的注册。
- 信号的注册和信号的注销并不是一个过程,是两个独立的过程。
- 信号的注册分为两种情况:可靠信号的注册和非可靠信号的注册。
在说可靠信号的注册和非可靠信号注册之前,我们先来看看信号在操作系统内核中到底是怎样存储的。
4.1 信号注册在内核中的存储表示
① 首先我们查看源码中的
struct task_struct
结构体(PCB)中关于信号的处理程序即signal handlers
,我们本节就看其中的一个变量struct sigpending pending
。
② 然后我们转到该结构体的内部定义对其进行查看
发现有一个sigset_t
类型的变量,这个sigset_t
类型应该是被typedef出来的。③ 我们使用
grep
在内核源码中对sigset_t
进行搜索,发现其包含在signal.h文件中。
④ 在signal.h
头文件中对sigset_t
进行查看
说明sigset_t类型是一个结构体,它包含了一个无符号长整型的数组。
至此,我们可以清楚的知道信号注册时在内核中到底是如何存储的。
总结一下就是:
- 在操作系统内核的
task_struct
结构体内部有一个变量struct sigpending pending
;- 内核定义的结构体
struct sigpending
当中有两个变量:一个是内核定义的双向链表,一个是sigset_t signal。- 内核定义的类型
sigset_t
是一个结构体,而该结构体内部有一个变量,该变量为一个数组,是一个无符号长整形的数组。
因此:
- 信号的注册本质上是在使用sig数组,但是并不是按照数组类型的方式来使用,而是按照位图(比特位)的方式在使用,即是将整个数组看做是一个位图,若某一个信号被注册,则某个信号的对应数组中的比特位就会被置为1。一般在信号注册时候,称之为操作sig位图。
- sig数组的比特位数远远是大于62的,那些剩下的比特位会被置位保留位。
- 操作系统内核对于注册的时候,还有一个sigqueue队列,信号的注册逻辑位,将信号对于的sig位图中的比特位置为1,并在sigqueue队列中添加一个sigqueue节点。因此,我们可以通过注册同一信号两次,来区分可靠信号和非可靠信号的注册逻辑。
4.2 非可靠信号的注册
第一次注册信号:
- 更改信号对应在sig位图中的比特位,将其从0变为1。
- 在sigqueue队列中添加一个sigqueue节点。
第二次注册同样的信号:
- 更改信号对应在sig位图中的比特位,将其从1变为1。
- 对于第二次的信号,不添加sigqueue节点到sigqueue队列中。
总结:
如果有多次同一的非可靠信号来注册,对于非可靠信号而言,只会添加一次sigqueue节点,换而言之,就是只注册了一次。
4.3 可靠信号的注册
第一次注册信号:
- 更改信号对应在sig位图中的比特位,将其从0变为1。
- 在sigqueue队列中添加一个sigqueue节点。
第二次注册同样的信号:
- 更改信号对应在sig位图中的比特位,将其从1变为1。
- 在sigqueue队列中添加sigqueue节点。
总结:
如果有多次同一可靠信号来注册,那么会添加多次sigqueue节点,换言之,就是注册了多次
5. 信号的注销
5.1 非可靠信号的注销
- 将信号对应到sig位图当中的比特位置为0。
- 将对应的非可靠信号的sigqueue节点进行出队操作。
5.2 可靠信号的注销
- 先将可靠信号对应的sigqueue节点进行出队操作
- 再判断sigqueue队列中是否有同类的可靠信号的sigqueue节点
① 若有:则不会将sig位图中对应的比特位置为0.
② 若没有:则将信号对应到sig位图当中的比特位置为0。
6.信号的处理方式
6.1 默认处理方式
默认处理方式:已经在操作系统内核当中定义好了。
对应的是宏:SIG_DFL
,注意是宏,而不是信号。
6.2 忽略处理方式
忽略处理方式:操作系统定义进程收到某一个信号之后,忽略掉。
对应的宏是:SIG_IGN
。
问题:为什么子进程先于父进程退出的时候,子进程就会变成僵尸进程?
解答:子进程先于父进程退出,子进程就会向父进程发送一个SIGCHLD信号。而父进程并不会做任何事情,导致子进程资源没有进程可以回收,导致僵尸进程的产生。
6.3 自定义处理方式
自定义处理方式:程序员可以定义某个信号的处理方式。
6.3.1 signal函数
sighandler_t signal(int signum, sighandler_t handler)
参数:
- signum:待要更改的信号的值
- handler:是一个函数指针,接收的是一个函数地址,并且该函数没有返回值,但有一个int类型的参数。
sighandler_t
对应的类型是:typedef void (*sighandler_t)(int);
返回值:
也就是说 signal 的返回值是指向之前的信号处理程序的指针,之前的信号处理程序,也就是在执行signal(signo,func)之前,对信号signo的信号处理程序。我们一般不需要来接收它,操作系统会自动进行相应的操作。
举个例子:
我们需要编写一个文件,实现当我们给定一个2号信号(即ctrl + c)的时候,让它去执行我们自己定义好的一个sigCallBack函数,该函数中用printf函数打印一句话,“It’s test to processing the SIGINT signal”。
代码实现:
>#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigCallBack(int sig)
{
printf("It's test to processing the SIGINT signal\n");
}
int main()
{
printf("test start!\n");
signal(2,sigCallBack);
while(1)
{
sleep(1);
}
return 0;
}
那么,当该代码运行的时候,在终端按下ctrl + c
,就会回调去调用sigCallBack函数去执行相应的内容。
运行结果
自定义signum这个信号的处理方式,定义为handler这个函数指针保存的函数地址对应的函数。换句话来说就是当进程收到signum这个信号的时候就会调用handler中保存的函数。
用图来理解就是:
注意:是从内核处调用的回调函数,不是从main函数处调用
小结:
signal函数向内核注册了一个信号的处理函数,调用signal函数的时候,并没有调用注册的函数(注册的函数在进程收到信号之后才调用),这种方法称为回调。
6.3.2 sigaction函数
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
参数:
- signum:待要自定义处理的信号。
- act:要将信号处理方式更改为act。
- oldact:原来的处理方式。
struct sigaction结构体定义
结构体参数解释:
void (*sa_handler)(int)
:默认的信号处理函数保存的函数指针void (*sa_handler)(int ,siginfo_t * ,void *)
:函数指针,但要配合sa_flags一起使用,当sa_flags当中的值为SA_SIGINFO的时候,信号处理是按照sigaction结构体
当中保存的函数地址来处理的(默认的),而不是传递进来的那个函数指针。sigset_t sa_mask
:注意上面讲过sigset_t是一个位图,那么sa_mask所实现的是,当进程在处理某一个信号的时候,有可能还会收到其他的信号,此时其他的信号就暂时存在sa_mask当中。int sa_flags
:指定了一组修改信号行为的标志。当其为SA_SIGINFO的标志的时候,就按照默认的信号处理函数处理,当为0的时候,就按照我们传递进来的信号处理函数处理。void (*sa_restorer)(void)
:sa_restorer元素已经过时了,不应该被使用。它作为一个保留字段保留了下来。
返回值:
若是修改成功则返回0,失败返回-1。
举个例子:
还是上面signal函数中所实现的例子,只不过这次讲signal函数修改为sigaction函数。
代码实现:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigCallBack(int sig)
{
printf("It's test to processing the SIGINT signal\n");
}
int main()
{
printf("test start!\n");
struct sigaction sa;
sa.sa_handler = sigCallBack;
//将位图中的比特位情况全部设置为0
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(2,&sa,NULL);
while(1)
{
sleep(1);
}
return 0;
}
需要注意的是,该代码中有一个sigemptyset
函数,是将对应的位图全部设置为0。
运行结果:
6.4 从内核角度去分析信号的处理
① 首先,我们还是从struct task_struct结构体开始看起,我们可以找到一个
struct signal_struct* sighand1
的变量。
② 查看struct sighand_struct
结构体的定义。
在该结构体的内部有一个struct k_sigaction action[_NSIG]
的变量。注意他不是一个指针,而是一个数组,因此它会直接展开在sighand_struct结构体的内部。
③ 查看struct k_sigaction
结构体的定义。
该结构体内部就是我们使用sigaction
函数中所提到的struct sigaction
结构体。
④我们来看看struct sigaction
结构体的定义。
注意这里的__sighandler_t
是void (*sa_handler)(int)
typedef出来的。
signal函数和sigaction函数区别
- signal函数的内部也是在调用sigaction函数的,它修改的是一个
__sighandler_t
的函数指针。- 而sigaction函数是直接对
struct sigaction
结构体进行相应的修改。
画图解释如下: