Linux信号-信号概念/信号的产生及处理方式
程序员文章站
2022-07-12 10:29:27
...
信号
一.信号的基本概念
在生活中,我们会听到上下课铃,你会对应的知道该到上课或者下课的时间了;在早上,闹钟响了,你知道是时候起床了;在马路上,看见红灯你知道不能过马路等等。其实这些都是信号,你可以根据你接收到的信号不同而做出不同的反应。
同样的,在操作系统中,也有相应的信号机制。比如在Linux下,我们有很多不同的信号,我们可以用kill -l查看系统定义的信号列表:
可以看到,一共有62种信号。其中,1-31号为普通信号,34-64为实时信号。
我们也可以看见,每个信号都有一个编号和一个宏定义名称。这些宏定义都可以在头文件signal.h中找到。
二.信号的产生方式
1.键盘产生信号-----前台进程
用户在终端按下某些键时,终端驱动程序会发送信号给前台进程。比如ctrl+c会产生2号SIGINT信号;ctrl+\会产生3号SIGQUIT信号;ctrl+z会产生20号SIGTSTP信号(该信号会使前台进程停止)。
这里简单介绍一下前台进程和后台进程的区分:
同时可以看到,上图是由键盘用ctrl+c向进程发送了信号让其终止。我们编写一个程序检测一下ctrl+\是否发送的是3号SIGQUIT信号,测试代码如下:
#include <stdio.h>
int main()
{
printf("This is YoungMay\n");
while(1);
return 0;
}
将程序编译并运行,在它死循环时,我们从键盘输入ctrl+\,可以看到进程退出且core dump。
Core Dump
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump,也叫做核心转储。
进程异常终止通常是因为有bug,比如非法访问内存导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。
一个进程能产生多大的core文件取决于进程的Resourse Limit(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包括用户密码等敏感信息,不安全,尤其是针对线上服务器。但在开发调试阶段可以用ulimit命令改变这个限制,让其允许产生core文件。
首先我们用ulimit命令改变shell进程的Resourse Limit,允许产生core文件最大位1024K
然后运行可执行程序,从而产生core文件:
接着我们用core文件调试查看程序core dump原因:
可以看到程序时因为收到了3号信号从而core dump了。
2. 硬件异常产生信号
硬件异常产生信号,这些条件由硬件检测到并通知内核,然后由内核向当前进程发送适当的信号。例如,当前进程执行了除以0的指令,CPU运算单元产生了异常,内核将这个异常解释为将8号SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为将11号SIGSEGV信号发送为进程。
(1)除0异常:
#include <stdio.h>
int main()
{
int a = 5 ;
int b = 0 ;
printf("a/b = %d\n",a/b);
return 0;
}
程序运行结果如下:
可以看到进程依旧core dump了,我们可以用上面同样的方法查看进程core dump原因。由理论知识可知,进程是收到了8号信号。(这里不再演示)
(2)访问非法内存
#include <stdio.h>
int main()
{
int *p;
*p = 4;
return 0;
}
程序运行结果:
它core dump的原因应该就是进程收到了11号信号。
3. 命令/系统调用接口产生信号
(1)通过命令向进程发送信号:
一个终端下:
两个终端:
可以看到,我们向该进程发送9号信号,该信号默认的处理动作是终止进程。所以我们可以看到该进程被kill而退出。
(2)通过系统调用接口产生信号
1)kill函数
我们可以调用Kill函数向进程发送信号,kill函数原型如下:
参数:pid为进程的pid,你要向哪个进程发送信号,就写哪个进程的pid;sig就是你要发送的信号的编号。
返回值:成功返回0,失败返回-1。
编写测试代码如下:
首先编写一个程序test1.c,让它打印出自己的pid之后,就死循环:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("pid: %d\n",getpid());
while(1);
return 0;
}
再写一个程序test.c,实现利用命令行参数向另一个进程发送信号:#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char* argv[])//利用命令行参数向另一进程发送信号
{
if(argc != 3)//命令格式输入错误
{
printf("可执行文件+pid+signo\n");
return -1;
}
int ret = kill(atoi(argv[1]),atoi(argv[2]));//调用kill向进程发信号
if(ret == -1)
{
perror("kill");
return -2;
}
else
{
printf("send %d to %d succeed\n",atoi(argv[2]),atoi(argv[1]));
}
return 0;
}
我们编译两个程序并运行: 可以看到,我们在一个终端运行test1,test1打印出它的pid后就一直死循环;我们在另一个终端下运行test,并利用命令行参数输入test1的pid以及要发送的9号信号。最后看到test1进程被killed。
程序中用到了atoi函数:
函数原型:
函数功能:用于将一个字符串转换为一个int型
返回值:无法转换时返回0;成功时返回转换的int整数
说明:如果传入的参数字符串的第一个字符就不能识别为int型时,函数停止读入该字符串
2)raise函数
函数原型:
函数功能:向调用它的进程发送信号
返回值:成功返回0,失败返回非0
测试代码如下:
#include <stdio.h>
#include <signal.h>//raise
#include <stdlib.h>//atoi
#include <unistd.h>//sleep
int main(int argc, char* argv[])
{
if(argc != 2)
{
printf("可执行程序+signo\n");
return -1;
}
int i = 10;
while(i--)
{
printf("This is YoungMay\n");
sleep(1);
if(i == 5)
{
int ret = raise(atoi(argv[1]));
if(ret != 0)//调用成功返回0,否则返回非0
{
perror("raise");
return -2;
}
else
{
printf("send %d succeed\n",atoi(argv[1]));
}
}
}
return 0;
}
运行结果如下:
其中,程序为打印发送信号成功,是因为9号信号直接终止了进程,未让进程进行到该句。
3)abort函数
abort函数原型为:
函数功能:用于给自己发送6号SIGABRT信号
返回值:无返回值。因为就像exit函数一样,abort函数总是会成功,所以无返回值。
编写测试代码如下:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("I am YoungMay\n");
abort();//给自己发信号
printf("He is fafa\n");
return 0;
}
运行结果如下:
4. 软件条件产生信号
之前在进程间通信的管道中有提过:当写端一直在写,但是读端不读还关闭。此时操作系统就会向管道发送SIGPIPE信号终止它。这个信号就是由软件条件产生的信号。在这里,我们在介绍一个由alarm函数触发的信号SIGALRM。
(1)alarm函数
1)函数原型:
2)函数功能:设定一个闹钟,告诉内核在seconds秒后向当前进程发送SIGALRM信号,该信号的默认处理动作是终止当前进程。
3)参数:为0 表示取消以前设定的闹钟,其他无符号整型数表示多少秒之后闹钟“响”
4)返回值:0或是以前设定的闹钟时间剩下的秒数
(2)编写代码测试闹钟向进程发送信号:
//alarm函数会给进程发送SIGALRM信号
#include <stdio.h>
#include <unistd.h>
int main()
{
unsigned int ret = alarm(2);//两秒之后响的闹钟
printf("This is YoungMay\n");
sleep(3);
printf("That is fafa\n");
return 0;
}
运行结果:
我们可以看到,进程打印完一条消息后,在等待3秒时,被2秒后“响”的闹钟终止了进程。
(3)编写代码测试取消闹钟:
//alarm函数会给进程发送SIGALRM信号
#include <stdio.h>
#include <unistd.h>
int main()
{
unsigned int ret = alarm(2);//两秒之后响的闹钟
printf("This is YoungMay\n");
sleep(1);
ret = alarm(0);
printf("ret :%d\n",ret);//取消闹钟
printf("That is fafa\n");
return 0;
}
运行结果:
我们可以看到,取消闹钟之后,alarm函数的返回值是剩余的时间。
三. 信号的常见处理方式
1. 忽略此信号
2. 执行该信号的默认处理动作,一般默认处理动作为终止进程
3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号。
上一篇: Java高级特性——反射
下一篇: dfs