🌟hello,各位读者大大们你们好呀🌟
🍭🍭系列专栏:【Linux初阶】
✒️✒️本篇内容:Linux信号的基本概念(生活信号、技术信号、信号生命周期、信号的保存位置和发送本质),信号的产生(四种方式、一个系统调用接口)
🚢🚢作者简介:计算机海洋的新进船长一枚,请多多指教( •̀֊•́ ) ̖́-
文章目录
- ☀️一、Linux信号的基本概念
- 🌻1.生活中的信号
- 🌻2.技术应用角度的信号
- 🌻3.信号的生命周期
- 🌻4.用kill -l命令可以察看系统定义的信号列表
- 🌻5.信号的保存位置 & 信号发送的本质
- ⚡️(1)信号的保存位置
- ⚡️(2)信号发送的本质
- ☀️二、信号的产生
- 🌻1.按键产生信号
- 🌻2.系统调用接口 - signal(信号捕捉+方法实现)
- 🌻3.系统调用向进程发送信号
- ⚡️(1)kill系统调用
- ⚡️(2)raise & abort 调用
- 🌻4.硬件异常产生信号
- ⚡️(1)除0问题
- ⚡️(2)野指针问题
- 🌻5.由软件条件产生信号
- ⚡️(1)闹钟简介
- ⚡️(2)闹钟管理
- ☀️三、知识归纳
- ☀️四、信号的核心转储
- 结语
☀️一、Linux信号的基本概念
🌻1.生活中的信号
- 我们生活中等快递小哥取快递、红绿灯、闹钟、手机消息等,我们都可以将他们理解为一种信号。
- 接收快递的前置条件是你知道怎么处理快递,也就是你能“识别快递”。
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
- 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”。
- 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2.执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)。
- 快递到来的整个过程,对你来讲是
异步
的,即信号可能随时产生,但是信号到来时我么不一定要立即处理,你可能做着更重要的事。
🌻2.技术应用角度的信号
我们可以将上面的概念迁移到我们的计算机信号学习中:
首先这里要补充一个共识,信号是给进程发的。
- 进程如何识别信号?
认识+动作
。 - 为什么进程有能力认识信号?进程是程序员编码完成的,具有逻辑和属性的集合,也就是说进程有对应的逻辑和属性来认识和辨别信号。
- 当收到信号之后,进程可能在执行其他更重要的代码,所以
信号不一定被立即处理
。 - 为了以后能在合适的时间处理信号,
进程本身具有对于信号的保存能力
。 - 进程在处理信号的时候,通常有三种动作:
默认、自定义、忽略
,此时我们称信号被捕捉
。
🌻3.信号的生命周期
- 信号的生命周期分为 4个阶段:预备、信号产生、信号保存、信号处理。
🌻4.用kill -l命令可以察看系统定义的信号列表
使用指令 kill -l
,我们可以发现系统为我们提供的信号有 64种,其中 [1, 31]我们称为普通信号,[32, 64]为实时信号,我们主要学习的是普通信号。
🌻5.信号的保存位置 & 信号发送的本质
⚡️(1)信号的保存位置
我们知道,信号是要发送给进程的,而进程需要保存信号,那么信号被保存在哪里呢?信号被保存在进程的 task_struct
中。
在进程的 task_struct中保存有 unsigned int signal
这样一个数据,它代表一个整数,或者说信号的位图
。我们知道 unsigned int类型的整数由 32个比特位组成,而我们的普通信号 [1, 31]只有31个,也就是说我们可以使用比特位的位置代表信号编号。
我们还可以用比特位的内容,代表是否收到对应的信号,0为没有,1为有。
⚡️(2)信号发送的本质
- 通过对信号保存位置的学习,我们不难理解,其实信号发送的本质就是:
修改进程内部PCB中的信号位图
。 - 进程PCB是内核维护的结构对象,也就是说,我们以后无论学习多少种信号发送方式,本质上都是通过 OS给目标进程发送信号。
- 为了满足上层用户的需求,操作系统一定会给我们提供发送信号处理信号的系统调用接口。
- kill 命令的底层一定调用了对应的系统调用。
☀️二、信号的产生
🌻1.按键产生信号
- 用户输入命令,在Shell下启动一个前台进程。用户按下
Ctrl+C
,这个按键输入会被OS获取,解释成信号,发送给目标前台进程。前台进程因为收到信号,进而引起进程退出。 - 其中,
Ctrl+C
会被 OS解释成 2号信号(SIGINT),进程收到该信号后的默认动作为终止进程。
[root@localhost code_test]$ cat sig.c
#include <stdio.h>
int main()
{
while(1){
printf("I am a process, I am waiting signal!\n");
sleep(1);
}
}
[root@localhost code_test]$ ./sig
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C
[root@localhost code_test]$
总结:我们的按键可以被OS解释为信号(如:Ctrl+C会被 OS解释成 2号信号)。
———— 我是一条知识分割线 ————
🌻2.系统调用接口 - signal(信号捕捉+方法实现)
- 当我们想捕捉一个信号,自定义实现相应的动作,我们可以使用 signal系统调用接口。
- 我们可以用
man signal
指令查看对应的手册。 - 使用 signal 系统调用接口,当收到某个信号(signum),会自动帮我们调用一个回调函数(handler),该回调函数返回值是 void,参数是 int。
-
【注意】
signal函数仅仅是设置了对信号的捕捉方法,需要进程收到对应的信号,才会调用函数。
- 代码运行起来之后,如果我们给对应的进程发送 2号信号(Ctrl+C/kill -2 id),它会自动执行对应的 handler函数。
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
std::cout << "进程捕捉到了一个信号,信号编号是: " << signo << std::endl;
// exit(0);
}
int main()
{
// 这里是signal函数的调用,并不是handler的调用
/// 仅仅是设置了对2号信号的捕捉方法,并不代表该方法被调用了
// 一般这个方法不会执行,除非收到对应的信号!
signal(2, handler);
while(true)
{
std::cout << "我是一个进程: " << getpid() << std::endl;
sleep(1);
}
}
总结:我们可以用 signal系统调用接口捕捉信号,并在捕捉到对应信号让进程执行特定的方法代码。
注意:对于某些恶意进程,我们可以用
kill -9 pid
管理员信号将对应进程杀死。9号信号无法被捕捉,因此9号信号可以杀掉所有异常进程。
———— 我是一条知识分割线 ————
🌻3.系统调用向进程发送信号
⚡️(1)kill系统调用
kill()
可以向任意进程发送任意信号。- kill系统调用函数,手册信息如下,返回值:成功为1,失败为-1。
我们平时使用的 kill命令,实际上底层使用了 kill系统调用函数。
- makefile文件
.PHONY:all
all:mysignal mytest
mytest:mytest.cc
g++ -o $@ $^ -std=c++11
mysignal:mysignal.cc
g++ -o $@ $^ -std=c++11 -g
clean:
rm -f mysignal mytest
- mysignal.cc
#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
static void Usage(const std::string& proc)
{
std::cout << "\nUsage: " << proc << " pid signo\n"
<< std::endl;
}
// ./myprocess pid signo
int main(int argc, char* argv[])
{
// 2. 系统调用向目标进程发送信号
//kill系统调用函数
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
pid_t pid = atoi(argv[1]);
int signo = atoi(argv[2]);
int n = kill(pid, signo);
if (n != 0)
{
perror("kill");
}
}
- mytest.cc
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
//我写了一个将来会一直运行的程序,用来进行后续的测试
int main()
{
while(true)
{
std::cout << "我是一个正在运行的进程,pid: " << getpid() << std::endl;
sleep(1);
}
}
- 运行结果
⚡️(2)raise & abort 调用
- raise & abort 实际上是在 kill系统调用的基础上做了相应的封装。
raise()
给自己 发送 任意信号【= kill(getpid(), 任意信号)】。abort()
给自己 发送 指定的信号SIGABRT(6号信号), 【= kill(getpid(), SIGABRT)】。
关于信号处理的行为的理解:有很多的情况,进程收到大部分的信号,默认处理动作都是终止进程。
信号的意义:信号的不同,代表不同的事件,但是对事件发生之后的处理动作可以一样!
int main(int argc, char* argv[])
{
// kill()可以想任意进程发送任意信号
// raise() 给自己 发送 任意信号kill(getpid(), 任意信号)
// abort() 给自己 发送 指定的信号SIGABRT, kill(getpid(), SIGABRT)
// 关于信号处理的行为的理解:有很多的情况,进程收到大部分的信号,默认处理动作都是终止进程
// 信号的意义:信号的不同,代表不同的事件,但是对事件发生之后的处理动作可以一样!
int cnt = 0;
while(cnt <= 10)
{
printf("cnt: %d, pid: %d\n", cnt++, getpid());
sleep(1);
if(cnt >= 5) abort(); // kill(getpid(), signo)
// if(cnt >= 5) raise(9); // kill(getpid(), signo)
}
}
总结:我们可以使用kill系统调用,raise & abort 调用向进程发送信号。
🌻4.硬件异常产生信号
当我们运行代码时,如果操作系统识别到了异常,会向进程发送了特定的信号,使进程终止。
下面我举两个例子帮助大家理解:
⚡️(1)除0问题
CPU内部存在一个状态寄存器
,以下述代码为例,当CPU在运行过程中,发现运算结果是没有意义的时候,会将状态寄存器的标志位从0置1,而后操作系统发现后,将8号信号发送给对应的进程,进程获取到8号信号之后就自行终止了。
int a = 10;
a /= 0;
至此,我们就知道了,当我们获取到 8号信号(SIGFPE)时,代码中存在 除0错误。
⚡️(2)野指针问题
当代码出现野指针并运行的时候,会导致虚拟地址到物理地址转化过程中的一个名为 MMU硬件
的报错,进而被操作系统识别到报错,而后操作系统会向进程发送 11号信号,使进程终止。
[root@localhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
signal(SIGSEGV, handler);
sleep(1);
int* p = NULL;
*p = 100;
while (1);
return 0;
}
[root@localhost code_test]$ ./sig
catch a sig : 11
catch a sig : 11
catch a sig : 11
至此,我们就知道了,当我们获取到 11号信号(SIGSEGV)时,代码中存在 段错误。
总结:虽然我们知道我们收到信号之后操作系统的大部分处理方式为终止进程,但是我们仍旧可以根据收到信号的不同来判断进程报错的原因是什么,比如收到 8号信号为除 0错误、11号信号为 段错误。
我们可以通过下述指令查看不同信号产生原因
和 处理办法
man 7 signal
———— 我是一条知识分割线 ————
🌻5.由软件条件产生信号
SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了,即读端关闭但是写端仍旧不断写入,OS会产生 SIGPIP(13号)信号。本节主要介绍alarm函数
和SIGALRM信号
。
⚡️(1)闹钟简介
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
代码示例(统计1S左右,我们的计算机能够将数据累计多少次!):
int cnt = 0;
void catchSig(int signo)
{
std::cout << "获取到一个信号,信号编号是: " << std::endl;
// exit(1);
//alarm(1); //可以重复设定闹钟
}
int main()
{
// 4. 软件条件 -- "闹钟"其实就是用软件实现的
// IO其实很慢
//统计1S左右,我们的计算机能够将数据累计多少次!
signal(SIGALRM, catchSig);
alarm(1);
while (true)
{
cnt++;
}
}
总结:1.如果代码需要 IO,将会减慢计算机的运行速度;2.闹钟可用于在特定时间后向进程发送特定信号(SIGALRM信号),闹钟使用的是alarm函数。
⚡️(2)闹钟管理
总结:OS会定时检测堆结构堆顶的闹钟,如果闹钟超时,会向该闹钟对应的进程发送信号,然后检测下一个闹钟。
☀️三、知识归纳
- 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者。
- 信号的处理是否是立即处理的?在合适的时候
- 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?信号被保存在进程的
task_struct
中。 - 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?能,OS设计者已经将对应的处理方法设定好了。
- 如何理解OS向进程发送信号?本质:修改进程的PCB位图。
☀️四、信号的核心转储
核心转储:当进程出现异常时,在对应时间内,将进程的有效数据转储到磁盘中。
在上面的文章中我们提到,我们可以通过指令查看信号处理方法:
man 7 signal
其中,Action中的 Term代表进程是正常结束的,也就是说 OS不会给我们做额外的工作。而 Core
代表进程终止,也就是说除了终止进程,OS还要做额外的工作(核心转储)。
当我们在云服务器上操作时,Core所做的额外工作我们不能明显看到。因为云服务器默认关闭了核心转储(core file选项)。
———— 我是一条知识分割线 ————
云服务器可以通过指令查看是否开启核心转储 和 打开核心转储。
ulimit -a //查看
ulimit -c 1024 //打开核心转储
核心转储发生后,会自动生成一个带有原 pid后缀的文件。
为什么要有核心转储?支持调试(迅速找到错误原因和位置)。
支持方法:gdb调试 + core-file XXX。
Term和Core差别总结:只有 Core终止的进程支持核心转储,调试查找错误。Term则是正常杀死进程,不支持额外操作。
结语
🌹🌹 【Linux初阶】信号入门 | 信号基本概念+信号产生+核心转储 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪