🌟hello,各位读者大大们你们好呀🌟
🍭🍭系列专栏:【Linux初阶】
✒️✒️本篇内容:Linux信号的基本概念(生活信号、技术信号、信号生命周期、信号的保存位置和发送本质),信号的产生(四种方式、一个系统调用接口)
🚢🚢作者简介:计算机海洋的新进船长一枚,请多多指教( •̀֊•́ ) ̖́-
异步
的,即信号可能随时产生,但是信号到来时我么不一定要立即处理,你可能做着更重要的事。我们可以将上面的概念迁移到我们的计算机信号学习中:
首先这里要补充一个共识,信号是给进程发的。
认识+动作
。信号不一定被立即处理
。进程本身具有对于信号的保存能力
。默认、自定义、忽略
,此时我们称信号被捕捉
。使用指令 kill -l
,我们可以发现系统为我们提供的信号有 64种,其中 [1, 31]我们称为普通信号,[32, 64]为实时信号,我们主要学习的是普通信号。
我们知道,信号是要发送给进程的,而进程需要保存信号,那么信号被保存在哪里呢?信号被保存在进程的 task_struct
中。
在进程的 task_struct中保存有 unsigned int signal
这样一个数据,它代表一个整数,或者说信号的位图
。我们知道 unsigned int类型的整数由 32个比特位组成,而我们的普通信号 [1, 31]只有31个,也就是说我们可以使用比特位的位置代表信号编号。
我们还可以用比特位的内容,代表是否收到对应的信号,0为没有,1为有。
修改进程内部PCB中的信号位图
。Ctrl+C
,这个按键输入会被OS获取,解释成信号,发送给目标前台进程。前台进程因为收到信号,进而引起进程退出。Ctrl+C
会被 OS解释成 2号信号(SIGINT),进程收到该信号后的默认动作为终止进程。[root@localhost code_test]$ cat sig.c
#include
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号信号)。
———— 我是一条知识分割线 ————
man signal
指令查看对应的手册。【注意】
signal函数仅仅是设置了对信号的捕捉方法,需要进程收到对应的信号,才会调用函数。#include
#include
#include
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号信号可以杀掉所有异常进程。
———— 我是一条知识分割线 ————
kill()
可以向任意进程发送任意信号。我们平时使用的 kill命令,实际上底层使用了 kill系统调用函数。
.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
#include
#include
#include
#include
#include
#include
#include
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");
}
}
#include
#include
#include
//我写了一个将来会一直运行的程序,用来进行后续的测试
int main()
{
while(true)
{
std::cout << "我是一个正在运行的进程,pid: " << getpid() << std::endl;
sleep(1);
}
}
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 调用向进程发送信号。
当我们运行代码时,如果操作系统识别到了异常,会向进程发送了特定的信号,使进程终止。
下面我举两个例子帮助大家理解:
CPU内部存在一个状态寄存器
,以下述代码为例,当CPU在运行过程中,发现运算结果是没有意义的时候,会将状态寄存器的标志位从0置1,而后操作系统发现后,将8号信号发送给对应的进程,进程获取到8号信号之后就自行终止了。
int a = 10;
a /= 0;
至此,我们就知道了,当我们获取到 8号信号(SIGFPE)时,代码中存在 除0错误。
当代码出现野指针并运行的时候,会导致虚拟地址到物理地址转化过程中的一个名为 MMU硬件
的报错,进而被操作系统识别到报错,而后操作系统会向进程发送 11号信号,使进程终止。
[root@localhost code_test]$ cat sig.c
#include
#include
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
———— 我是一条知识分割线 ————
SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了,即读端关闭但是写端仍旧不断写入,OS会产生 SIGPIP(13号)信号。本节主要介绍alarm函数
和SIGALRM信号
。
#include
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函数。
总结:OS会定时检测堆结构堆顶的闹钟,如果闹钟超时,会向该闹钟对应的进程发送信号,然后检测下一个闹钟。
task_struct
中。核心转储:当进程出现异常时,在对应时间内,将进程的有效数据转储到磁盘中。
在上面的文章中我们提到,我们可以通过指令查看信号处理方法:
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的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪