Linux信号-信号概念/信号的产生及处理方式
信号
一.信号的基本概念
每天听到下课铃声时会促使你意识到该去上课或放学的时间;当闹钟响起时你会明白自己该起床;在马路上注意到红灯就会知道不能穿过马路等等;事实上这些都是信号它们传达着特定的信息供你做出相应的反应
类似的,在操作系统的层次上也存在专门的信号处理机制。例如,在Linux操作系统中有多样的信号可供使用,并且我们可以利用命令 kill -l 来查看系统所定义的所有可用信号。

可以看到,一共有62种信号。其中,1-31号为普通信号,34-64为实时信号。
通过查看文档资料,我们可以了解到每一个信号都包含编号以及对应的宏定义名称。这些宏定义都位于头文件 signal.h中。
二.信号的产生方式
1.键盘产生信号----- 前台进程
操作者在终端按动特定键时, 终端驱动程序将响应并传递控制指令至后台进程. 比如, 操作者按下 ctrl+c 会出现编号为 2 的 SIGINT 事件; 按下 ctrl+回车会出现编号为 3 的 SIGQUIT 事件; 按下 ctrl+z 则会出现编号为 20 的 SIGTSTP 事件(该动作将导致后台进程暂时休眠).
这里简单介绍一下前台进程和后台进程的区分:


通过键盘上的快捷键Ctrl+C向进程发送了一个中断信号使其终止 同时可以看出 上图展示了这一操作过程 我们开发了一个检测工具 来验证是否发送了特定的3号SIGQUIT中断码 并附上了相应的测试代码
#include <stdio.h>
int main()
{
printf("This is YoungMay\n");
while(1);
return 0;
}
代码解读
将程序进行编译后执行,在它陷入死循环时(即出现无法继续执行的情况),通过键盘输入Ctrl+Break(通常表示按住Ctrl键同时按下Break键),可以看到进程退出且核心文件出现。

**
**
Core Dump
当一个进程发生异常终止时
进程异常通常是由软件缺陷引发的。例如,在某些情况下程序可能会尝试访问未分配内存区域而导致段错误。在事件发生后可以通过调试工具来分析核心文件内容以确定问题所在。即为Post-mortem Debugging(事后故障排查)。
生成核心文件的大小主要由进程的资源限制决定(这些数值记录于PCB字段中)。通常情况下不会生成核心文件(因为它们可能包含用户的敏感信息如密码),这样操作存在风险。但在开发调试阶段使用ulimit命令可以修改这一限制
通过ulimit命令设置shell进程的Resourse Limit参数为最多1024K

然后运行可执行程序,从而产生core文件:

接着我们用core文件调试查看程序core dump原因:

可以看到程序时因为收到了3号信号从而core dump了。
2. 硬件异常产生信号
硬件触发了特定的异常信号,这些条件会被硬件捕获并通知内核.随后会向相关进程发送相应的处理指示.例如,当一个进程中运行除零操作时,运算单元会触发一个FPU异常,内核则将其解释为此处应激性处理请求并生成相应的中断信号.再比如,如果一个进程中尝试访问了一个未分配的内存区域,MMU会检测到这一无效访问事件导致系统状态不正常,并将此情况视为非法操作请求并相应地触发相应的错误处理机制.
(1)除0异常:
代码解读
#include <stdio.h>
int main()
{
int a = 5 ;
int b = 0 ;
printf("a/b = %d\n",a/b);
return 0;
}
代码解读
程序运行结果如下:

通过细致观察和分析发现核心抛出事件的发生情况。
我们可以通过类似的方法来探索其他异常现象的原因。
根据相关理论知识可知, 进程接收了信号ID 8.
(2)访问非法内存
#include <stdio.h>
int main()
{
int *p;
*p = 4;
return 0;
}
代码解读
程序运行结果:

它core dump的原因应该就是进程收到了11号信号。
3. 命令/系统调用接口产生信号
(1)通过命令向进程发送信号:
一个终端下:

两个终端:

通过查看相关数据流程图后可知,在该进程中执行如下操作:向该进程发送9号信号,并由其默认的操作程序终结当前任务。由此可见,在此过程中相关子进程将被终止。
(2)通过系统调用接口产生信号
1)kill函数
我们可以调用Kill函数向进程发送信号,kill函数原型如下:

参数说明:其中pid表示发送信号给指定进程的唯一标识符;sig则用于指示要发送信号的目标进程对应的唯一标识符。具体来说:当用户需要向某个特定进程发送控制台提示时,请直接给出该进程对应的pids值;而若要发送其他类型的操作指令,则应提供相应的sigs值以供系统执行处理。
返回值:成功返回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操作。具体来说,在这个操作中使用了特定命令使得该程序能够打印出其进程ID(PID)。随后在另一个终端中执行了命令test时,通过传递命令行参数指定target进程的PID以及 intended signal为9号信号(SIGKILL)。经过一段时间后发现目标进程已无法响应并最终被kill信号所终止
程序中用到了 atoi函数:
函数原型:

函数功能:用于将一个字符串转换为一个int型
返回值:无法转换时返回0;成功时返回转换的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 信号;该信号的默认处理方式为终止该进程。
参数:当值设为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秒期间(轻)响起了闹钟,并终止了进程。
(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. 执行该信号的默认处理动作,一般默认处理动作为终止进程
实现一种信号处理机制,并被要求在处理该信号时切换到用户态执行相应的处理函数以实现这一目标的方式被称为捕获一个信号。
