目录
[|. Basic Concepts of Signals](#|. Basic Concepts of Signals)
[||.The type of signals](#||.The type of signals)
[Linux 常见信号速查表](#Linux 常见信号速查表)
[|||.Signal processing(处理)](#|||.Signal processing(处理))
[IV. What is the use of signals](#IV. What is the use of signals)
注:版权声明
本文章内容均来自本人的个人笔记为个人学习总结,参考自B站课程:码农论坛《C++环境高级编程》。由于当时方便记笔记,笔记中部分图片来源于原课程视频截图,版权归原作者"码农论坛"及相关权利人所有。对于linux系统,文章中我用的ubuntu,up主用的centos,但原理是相同的,不影响技术学习。
根据《中华人民共和国著作权法》第二十四条规定,本笔记引用上述内容系为个人学习、研究之目的,属于"合理使用"范畴,不影响原作品的正常使用,亦不损害原著作权人的合法权益。
本笔记无任何商业用途,仅供个人学习交流。感谢原up主的课程分享!
|. Basic Concepts of Signals
信号(signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但是,不能给进程传递任何数据。信号产生的原因有很多,在Shell中,可以用kill和killall命令发送信号:
1.kill -信号的类型 进程编号
2.killall -信号的类型 进程名
先看代码:

运行程序后,执行一下命令:

在linux中信号缺省值是编号为15的信号,所以前两个效果一样,都是"已终止"
后两个:

使用kill杀死进程的方法:

运行test程序,尝试用步骤1的方法一杀死进程:

结果:

方法二:使用ps命令查看进程,在ps列表中找到./test进程编号即可!!!


ok!
注意,易混淆------注意:进程编号不是c++里的this_thread::get_id()!!!这是线程id不是进程编号!!!
||.The type of signals
Linux 常见信号速查表
| 信号名 | 信号值 | 默认动作(缺省动作) | 发出信号的原因 |
|---|---|---|---|
| SIGHUP | 1 | 终止进程 | 终端挂起或控制进程终止 |
| **SIGINT | 2 | 终止进程 | 键盘中断 (Ctrl + C) |
| SIGQUIT | 3 | 终止进程+内核转储 | 键盘退出键 (Ctrl + ) |
| SIGILL | 4 | 终止进程+内核转储 | 非法指令 |
| SIGABRT | 6 | 终止进程+内核转储 | 由 abort() 函数发出的退出指令 |
| SIGFPE | 8 | 终止进程+内核转储 | 浮点运算异常 (如除以0) |
| **SIGKILL | 9 | 终止进程 | 强制杀死程序 (kill -9) |
| **SIGSEGV | 11 | 终止进程+内核转储 | 无效内存引用 (如操作空指针、数组越界) |
| SIGPIPE | 13 | 终止进程 | 管道破裂 (向无读取端的管道写数据) |
| **SIGALRM | 14 | 终止进程 | 由 alarm() 函数设置的闹钟超时 |
| **SIGTERM | 15 | 终止进程 | 终止信号 (kill 或 killall 默认发送) |
| SIGUSR1 | 10 | 终止进程 | 用户自定义信号 1 |
| SIGUSR2 | 12 | 终止进程 | 用户自定义信号 2 |
| **SIGCHLD | 17 | 忽略信号 | 子进程状态改变 (如结束、停止) |
| SIGCONT | 18 | 继续进程 | 让一个被停止的进程继续执行 |
| SIGSTOP | 19 | 停止进程 | 停止进程 (不能被捕获或忽略) |
| SIGTSTP | 20 | 停止进程 | 终端停止键 (Ctrl + Z) |
| SIGTTIN | 21 | 停止进程 | 后台进程尝试从控制终端读取 |
| SIGTTOU | 22 | 停止进程 | 后台进程尝试向控制终端写入 |
| 其它信号 | <=64 | 终止进程 | 系统自定义信号 |
默认动作补充说明 (对应表格中的描述):
-
终止进程: 进程直接退出。
-
终止进程+内核转储: 进程退出,并生成 core dump 文件用于事后调试。
-
停止进程: 进程暂停执行,可以被 SIGCONT 信号恢复。
-
忽略信号: 信号被丢弃,对进程无影响。
-
特殊标志 : 这些信号 ( SIGKILL(9), SIGSEGV(11),SIGSTOP(19)) 自带 "不能被捕获" 和 "不能被忽略" 的特性,这是系统强制规则。
可以先不看信号的含义和用途,打**的是需要重点关注的!!!
第一列是已经定义好的宏,第二列是信号编号
结合下面的代码学习这一块!
信号值和信号名可以互相替换:

替换成:

解释一下几个重要的信号:
1.信号2:就是ctrl+c
2.信号9:
信号9的默认处理方式是进程被终止而且不能被捕获,不能被忽略。
先看一个示例代码,后续的操作都是根据这个代码来的:
cpp
#include<iostream>
#include<string>
#include<cstdio>
#include<cstring>
#include<sys/stat.h>
#include<unistd.h>
#include<errno.h>
#include<thread>
#include<signal.h>
using namespace std;
void func(int signum)
{
cout<<"接收到信号: "<<signum<<endl;
}
int main(int argc,char *argv[])
{
//使用signal函数来获取信号
signal(1,func);
signal(15,func);//当signal接收到了1,15的信号之后,就会运行func函数!!!
while(true)
{
cout<<"程序执行了一次任务"<<endl;
sleep(1);
}
}
改动上面的部分代码,试图捕获9的信号:

编译运行,然后发送9的信号:

结果:

根本没有调用信号处理函数func!而是程序直接被杀死了。
所以这行代码是无效的!
尝试忽略9的信号:

编译运行,然后先发送2的信号:

程序没有任何反应:

表示2被成功忽略了
但是我发送9的信号:

服务程序直接被杀掉了

说明9是不能被忽略的!这行代码依然无效
3.信号14:
SIGALRM 14 终止进程 由闹钟alarm()函数发出的信号。
alarm() 函数定义在 <unistd.h>
同样程序也可以设置闹钟:

设置闹钟,参数为5,表示5s后向本程序发送信号14
编译运行:

5s后,闹钟响了。
使用闹钟的目的,是为了执行某些任务,比如以下代码;
cpp
#include<iostream>
#include<string>
#include<cstdio>
#include<cstring>
#include<sys/stat.h>
#include<unistd.h>
#include<errno.h>
#include<thread>
#include<signal.h>
using namespace std;
void func(int signum)
{
cout<<"接收到信号: "<<signum<<endl;
}
//增加函数func1,用于执行定时任务
void func1(int sig)
{
cout<<"接收到信号:"<<sig<<",闹钟响了!"<<endl;
alarm(5);//关键操作------ 重新设置一个 5 秒后的闹钟。如果没有这行,程序只会在启动后 5 秒触发一次 14 号信号;加了这行,每次触发闹钟后会 "续期",实现每 5 秒触发一次的循环闹钟。
}
int main(int argc,char *argv[])
{
//使用signal函数来获取信号
signal(1,func);
signal(15,func);//当signal接收到了1,15的信号之后,就会运行func函数!!!
alarm(5);//闹钟,5s后会向本程序发送信号14,注意:alarm() 是 "一次性" 的 ------ 如果不在 func1 中 //重新调用 alarm(5),只会触发一次 14 号信号。
//它是程序第一次触发 14 号闹钟信号的源头,没有这行代码,你的 func1
//永远不会被调用,闹钟逻辑就完全失效了。
//设置闹钟的处理信号
signal(14,func1);
while(true)
{
cout<<"程序执行了一次任务"<<endl;
sleep(1);
}
}
运行:

详细解释:
One:主函数中 alarm(5) 的核心作用
简单说:主函数里的 alarm(5) 是 "首次定时",负责触发第一次 14 号信号;而 func1 里的 alarm(5) 是 "续期定时",负责让闹钟每隔 5 秒循环触发。
拆解执行逻辑(没有主函数 alarm(5) 的情况):
如果删掉主函数的 alarm(5),程序运行后:
只会注册 14 号信号的处理函数 func1,但永远不会有操作系统给进程发 14 号信号;
func1 从头到尾不会被调用,"闹钟响了" 的提示永远不会出现;
程序只会一直打印 "程序执行了一次任务",定时功能完全消失。
Two:为什么会循环调用闹钟?
因为当我第一次在主函数调用alarm之后,向程序发送信号14,接受信号,调用func1,但是在func1又调用了alarm,又发送了一次信号14,程序接受信号再次调用func1,如此循环!!!
假如我删去主函数的alarm,转变为手动发送信号14:

编译运行,刚开始没有调用任何闹钟:

切换另一个窗口,发送信号14:

程序就会成功调用信号14!

4.信号15:就是缺省的信号
5.信号11:
|-----------------|------------|------|-----------------------------------|
| SIGSEGV | 11 | 终止进程 | 无效的内存引用(数组越界、操作空指针和野指针等)。 |
就是在写程序的时候如果出现了内存泄漏等问题,系统就会发出信号11,终止进程并进行内核映像转储(core dump)。且不能被捕获,不能忽略!
|||.Signal processing(处理)
进程对信号的处理方法有三种:
1)对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程。
2)设置信号的处理函数,收到信号后,由该函数来处理。
3)忽略某个信号,对该信号不做任何处理,就像未发生过一样。
signal()函数可以设置程序对信号的处理方式。
函数声明:
sighandler_t signal(int signum, sighandler_t handler);
参数signum表示信号的编号(信号的值)。
参数handler表示信号的处理方式,有三种情况:
1)SIG_DFL:恢复参数signum信号的处理方法为默认行为。
2)一个自定义的处理信号的函数,函数的形参是信号的编号。
3)SIG_IGN:忽略参数signum所指的信号。
解释处理1:也有不终止进程的
比如killall -17 test 执行程序不会被终止
解释处理2:
函数signal
代码(就是第一个,重温一下):
cpp
#include<iostream>
#include<string>
#include<cstdio>
#include<cstring>
#include<sys/stat.h>
#include<unistd.h>
#include<errno.h>
#include<thread>
#include<signal.h>
using namespace std;
void func(int signum)
{
cout<<"接收到信号: "<<signum<<endl;
}
int main(int argc,char *argv[])
{
//使用signal函数来获取信号
signal(1,func);
signal(15,func);//当signal接收到了1,15的信号之后,就会运行func函数!!!
while(true)
{
cout<<"程序执行了一次任务"<<endl;
sleep(1);
}
}
简单测试这个代码:


当输入其他除了15,1的信号时系统就会正常执行信号!
这个形参signum可以改为其他的形参!!!
在这个函数中,参数是怎么传递的???
传递方式:
这个参数不是你手动传递的,而是操作系统在触发信号处理函数时自动传递的,整个过程可以拆解为 3 步:
步骤 1:你注册信号处理规则
signal(15, func) 本质是告诉操作系统:
"如果我的程序收到 15 号信号,请调用 func 函数,并且把「15」这个信号编号作为参数传给 func。"
步骤 2:操作系统捕获信号
当你用 kill -15 PID 给程序发信号时,Linux 内核会识别到这个信号,并找到对应的进程(你的 test 程序)。
步骤 3:操作系统调用处理函数并传参
内核会暂停你程序的正常执行(比如暂停 while 循环),主动调用 func 函数,并把「信号编号」(比如 15)作为实参传给 func 的形参(不管你形参叫 signum 还是 s)。
解释处理3:
在代码中新增一个语句:

编译之后执行:

进程仍在进行!
这几个signal的本质:

解释:SIG_DFL函数
在func函数新增一条语句:

转换另一个窗口:
第一次执行信号1:

程序:

第二次执行信号1:

程序:

解释原因:

第一次发送信号1时,程序调用了func函数,把1的信号恢复成了默认的处理行为!
所以再次执行1时,就是系统的默认操作了!
IV. What is the use of signals
后台运行的服务程序,不能直接杀掉。
为什么呢?因为直接杀掉等于让它"猝死"------程序正在做的事还没收尾,文件可能没保存、网络连接可能没关闭,容易出问题。
更稳妥的做法是:给它发一个信号。
信号是什么?你可以把它理解成"给程序的一个暗号"。程序收到暗号后,会主动执行一段清理代码------保存数据、关闭连接、释放资源------然后再自己退出。这样就是"有计划地离开"。
另外,如果你想知道这个程序还在不在运行,可以发一个值为 0 的信号。它不会真的做什么,只会告诉你:我还活着。
看代码演示:
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void EXIT(int sig)
{
cout << "收到了信号:" << sig << endl;
cout << "正在释放资源,程序将退出......\n";
// 以下是释放资源的代码。
//在实际开发中,有很多资源需要释放,比如保存数据,关闭文件,断开与数据库的连接,断开与网络的连接等等
cout << "程序退出。\n";
exit(0); // 进程退出。
}
int main(int argc,char *argv[])
{
// 忽略全部的信号,防止程序被信号异常中止。
for (int ii=1;ii<=64;ii++) signal(ii,SIG_IGN);
// 如果收到2和15的信号(Ctrl+c和kill、killall),本程序将主动退出。
signal(2,EXIT); signal(15,EXIT);
while (true)
{
cout << "执行了一次任务。\n";
sleep(1);
}
}
编译运行,并按下ctrl+c
