信号是一个软件中断,相当于是一个口头的约束,对你的限制力比较低,举个例子来说就是当你买的快递到了需要你去取的时候,而你正在打游戏,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取。在这个过程中,你获得了一个取快递的信号,但是你并没有选择立即执行,而是选择了在合适的时候去取,这就是软件中断。
目前Linux的信号数量为62个,分为两种类型:
① 非实时信号(非可靠信号),对应信号量为
1~31
,它的特点是有可能信号会发送丢失
② 实时信号(可靠信号),对应信号量为34~64
,它的特点是信号不会发送丢失
kill -l
:可以罗列出具体的信号值。
man 7 singal
:罗列所有的信号的具体信息。
信号的具体信息:
![]()
信号的动作:
如果某一个信号的处理动作是core
,那么它默认是需要完成终止进程+产生coredump文件。产生coredump文件依赖于ulimit -a
中对应的core file size
,并将其设置为unlimited
。
ctrl + c
:使当前运行的进程中断,产生一个值为2号的信号量,是一个SIGINT
。ctrl + z
:使当前运行的进程停止,产生一个值为20号的信号量,是一个SIGTSTP
。ctrl + |
:使当前运行的进程退出,产生一个值为3号的信号量,是一个SIGQUIT
。
kill [PID]
:终止一个进程。
kill -[num] [PID]
:给进程号为PID的进程发送一个信号值为num的信号。
#include
中,是一个系统调用函数)
int kill(pid_t pid, int sig);
功能是:给pid进程发送sig的信号。
#include
中,是一个库函数)
int raise(int sig)
功能:谁调用给谁发送signal信号。
在说可靠信号的注册和非可靠信号注册之前,我们先来看看信号在操作系统内核中到底是怎样存储的。
① 首先我们查看源码中的
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
是一个结构体,而该结构体内部有一个变量,该变量为一个数组,是一个无符号长整形的数组。
因此:
第一次注册信号:
第二次注册同样的信号:
总结:
如果有多次同一的非可靠信号来注册,对于非可靠信号而言,只会添加一次sigqueue节点,换而言之,就是只注册了一次。
第一次注册信号:
第二次注册同样的信号:
总结:
如果有多次同一可靠信号来注册,那么会添加多次sigqueue节点,换言之,就是注册了多次
① 若有:则不会将sig位图中对应的比特位置为0.
② 若没有:则将信号对应到sig位图当中的比特位置为0。
默认处理方式:已经在操作系统内核当中定义好了。
对应的是宏:SIG_DFL
,注意是宏,而不是信号。
忽略处理方式:操作系统定义进程收到某一个信号之后,忽略掉。
对应的宏是:SIG_IGN
。
问题:为什么子进程先于父进程退出的时候,子进程就会变成僵尸进程?
解答:子进程先于父进程退出,子进程就会向父进程发送一个SIGCHLD信号。而父进程并不会做任何事情,导致子进程资源没有进程可以回收,导致僵尸进程的产生。
自定义处理方式:程序员可以定义某个信号的处理方式。
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
#include
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中保存的函数。
小结:
signal函数向内核注册了一个信号的处理函数,调用signal函数的时候,并没有调用注册的函数(注册的函数在进程收到信号之后才调用),这种方法称为回调。
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
#include
#include
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。
运行结果:
① 首先,我们还是从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
结构体进行相应的修改。
画图解释如下: