1. 什么是信号?
信号是一种异步事件通知机制,用于在操作系统内核、进程、或进程自身之间传递一个简单消息,通知进程某个特定事件已经发生。
您可以把它想象成现实生活中的中断 或警报。比如,你正在工作,突然手机响了(一个信号),你需要决定是接听(处理)、忽略还是直接挂断(默认操作)。
在现实生活中,信号通常是红绿灯,上下课铃声,烽火狼烟等 信号产生后,我们要接收信号,我们在接收到这些信号之前就知道接收到这些信号后要做什么,而视这些信号的紧急程度我们接收到信号后可以选择立即处理或稍后处理,最后,当我们处理时要怎么处理信号?由此,我们经历了信号产生到处理的流程,可以得出几个结论
基本结论:
• 进程怎么能识别信号呢?进程识别信号是内置的,进程、信号·都是程序员写的。进程内部已经内置了对于信号的识别和处理机制
• 信号产生之后,你知道怎么处理吗?知道。如果信号没有产生,你知道怎么处理信号吗?
知道。所以,信号的处理方法,在信号产生之前,已经准备好了。
• 处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?合适的时候。在信号处理之前,我们会先保存信号
• 进程处理的三种方式:a.默认动作 b.忽略信号 c.自定义捕捉, 后续都叫做信号捕捉。
2.查看信号

编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

我们可以看到,大部分信号的默认行为都是终止进程
3. 信号的常见处理行为
当一个信号发送给一个进程时,该进程会对该信号采取以下三种行动之一:
默认动作:每个信号都有一个系统预定义的默认行为。大多数信号的默认动作是终止进程,有些还会产生核心转储文件(core dump)。
忽略信号 :进程可以显式地告诉内核,它希望忽略某个信号。但有两个信号不能被忽略或捕获:SIGKILL 和 SIGSTOP。这是为了给系统管理员一个最终控制进程的手段。
捕获信号:进程可以预先注册一个信号处理函数。当信号发生时,内核会中断进程的正常执行流程,转而执行这个注册好的函数(称为信号处理器)。执行完毕后,进程再从中断点继续执行(除非在处理器中退出)。
signal函数

handler:函数指针,表示更改信号的处理动作,当收到对应的信号,就回调执行handler方法
返回值
成功:返回之前的信号处理函数
失败:返回 SIG_ERR
测试
Ctrl+C 的本质是向前台进程发送SIGINT 即2 号信号,我们证明一下

cpp
#include<iostream>
#include<signal.h>
using namespace std;
void handler(int signumber)
{
cout<<"我是信号:"<<signumber<<endl;
}
int main()
{
//默认处理
signal(SIGINT,SIG_DFL);
while(true)
{
sleep(1);
printf("I am waiting signal\n");
}
return 0;
}

cpp
...
//忽略该信号
signal(SIGINT,SIG_IGN);
...

cpp
...
//自定义函数
signal(SIGINT,handler);
...

4 信号的产生方式
4.1 通过终端按键产生信号
4.1.1 基本操作
• Ctrl+C (SIGINT) 已经验证过,这里不再重复
• Ctrl+\(SIGQUIT)可以发送终止信号并生成core dump文件,用于事后调试(后面详谈)
4.1.2 理解OS如何得知键盘有数据

4.1.3初步理解信号起源
📌 注意:
• 信号其实是从纯软件角度,模拟硬件中断的行为
• 只不过硬件中断是发给CPU,而信号是发给进程
• 两者有相似性,但是层级不同,这点我们后面的感觉会更加明显
4.2 调用系统命令向进程发信号
cpp
#include<iostream>
using namespace std;
int main()
{
while(true)
{
sleep(1);
printf("I am waiting signal,my pid:%d\n",getpid());
}
return 0;
}
如kill命令

4.3 使用函数产生信号
kill
kill 命令是调用 kill 函数实现的。kill 函数可以给一个指定的进程发送指定的信号。


样例:实现自己的kill 命令
cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
//输入标准
// mykill -signumber pid
int main(int argc, char *argv[])
{
if(argc != 3)
{
//输入不符合标准
std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;
return 1;
}
int number = std::stoi(argv[1]+1); // 去掉-
pid_t pid = std::stoi(argv[2]);
int n = kill(pid, number);
return n;
}

raise
raise 函数可以给当前进程发送指定的信号(自己给自己发信号)。


cpp
#include <iostream>
#include <signal.h>
using namespace std;
void handler(int signumber)
{
// 整个代码就只有这一处打印
cout << "获取了一个信号: " << signumber << endl;
}
// mykill -signumber pid
int main()
{
signal(2, handler); // 先对2号信号进行捕捉
// 每隔1S,自己给自己发送2号信号
while (true)
{
sleep(1);
raise(2);
}
}

abort
abort 函数使当前进程接收到信号而异常终止。


cpp
#include <iostream>
#include <signal.h>
using namespace std;
void handler(int signumber)
{
// 整个代码就只有这一处打印
std::cout << "获取了一个信号: " << signumber << std::endl;
}
int main()
{
signal(SIGABRT, handler);
while (true)
{
sleep(1);
abort();
}
}
实验可以得知,abort给自己发送的是固定6号信号,虽然捕捉了,但是还是要退出


4.4 由软件条件产生信号
SIGPIPE 是一种由软件条件产生的信号,在"管道"中已经介绍过了。
cpp
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main() {
int pipefd[2];
pipe(pipefd);
close(pipefd[0]); // 关闭读端
// 向没有读端的管道写入数据,产生 SIGPIPE 信号
write(pipefd[1], "hello", 5);
return 0;
}

本节主要介绍alarm 函数和SIGALRM 信号。

调用 alarm 函数可以设定一个闹钟,也就是告诉内核在seconds 秒之后给当前进程发SIGALRM 信号,该信号的默认处理动作是终止当前进程。
cpp
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig) {
printf("Alarm signal received!\n");
}
int main() {
signal(SIGALRM, alarm_handler);
// 设置5秒后产生 SIGALRM 信号
alarm(5);
printf("Waiting for alarm...\n");
pause(); // 等待信号
return 0;
}



如何理解软件条件
在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生。
4.5 硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
例如当前进程执行了除以0的指令, CPU的运算单元会产生异常, 内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址, MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
模拟除0
cpp
#include <iostream>
#include <signal.h>
using namespace std;
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
sleep(1);
//故意不让进程退出
}
// v1
int main()
{
signal(SIGFPE, handler); // 8) SIGFPE
sleep(1);
int a = 10;
a /= 0;
return 0;
}


我们可以看到执行结果是在不停的打印,打印结果我们可以理解,但为什么会一直打印呢?
📌 注意:
通过上面的实验,我们可能发现:
发现一直有8号信号产生被我们捕获,这是为什么呢?上面我们只提到CPU运算异常后,如何处理后续的流程,实际上 OS 会检查应用程序的异常情况,其实在CPU中有一些控制和状态寄存器,主要用于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解为一个位图,对应着一些状态标记位、溢出标记位。OS 会检测是否存在异常状态,有异常存在就会调用对应的异常处理方法。
除零异常后,我们并没有清理内存,关闭进程打开的文件,切换进程等操作,所以CPU中还保留上下文数据以及寄存器内容,除零异常会一直存在,就有了我们看到的一直发出异常信号的现象。访问非法内存其实也是如此,大家可以自行实验。
我们知道,计算都是在CPU中计算的,因此当代码执行到除0时,CPU发现异常给进程发SIGFPE信号要求操作系统终止进程,但我们捕获信号时采用的是自定义打印行为,并没有终止进程 CPU执行代码时,一定是在调度一个进程,进程一直不退出的话,那么这个进程就会一直被调度,CPU就一直会执行除0代码,从而一直发现异常给进程发信号,直到进程终止
cpp
...
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
exit(1);
}
...

模拟野指针
cpp
#include <iostream>
#include <signal.h>
using namespace std;
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
exit(1);
}
int main()
{
signal(SIGSEGV, handler);
sleep(1);
int *p = NULL;
*p = 100;
return 0;
}


5 Core 和 Term 的区别
在Linux信号处理中,Core 和 Term 是两种不同的进程终止方式,主要区别在于是否生成核心转储文件。

• 首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump(核心转储)。
• 进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug (事后调试)。
• 一个进程允许 产生多大的core 文件取决于进程的Resource Limit (这个信息保存 在PCB中)。默认是不允许产生 core 文件的, 因为core 文件中可能包含用户密码等敏感信息,不安全。
在实际开发中:
生产环境:通常禁用 Core dump 以节省磁盘空间
开发环境:启用 Core dump 便于调试
信号处理:对 Term 信号实现优雅关闭,对 Core 信号记录调试信息
到此,Linux进程信号(壹):产生信号就讲完了,怎么样,是不是感觉大脑里面多了很多新知识。
如果觉得博主讲的还可以的话,就请大家多多支持博主,收藏加关注,追更不迷路
如果觉得博主哪里讲的不到位或是有疏漏,还请大家多多指出,博主一定会加以改正
博语小屋将持续为您推出文章