本章我们将讲解Linux信号这部分的内容,本章将介绍信号的产生,发送,信号的捕捉,屏蔽等操作,将对信号进行一些列系统的了解与学习。目标已经确定,接下来就要搬好小板凳,准备开讲了…
对于进程来讲,即便是信号还没有产生,我们进程已经具有识别和处理这个信号的能力了。
使用kill -l
命令罗列出来的内容叫做信号,我们可以看到目前Linux系统下64种不同的类型:
信号左侧的数字和右侧的名称是一回事,其实都是宏,大写的字母是宏名称,宏的值就是左侧对应的编号。
这二者的差别是:早期有实时操作系统,我们现在用的是分时操作系统。
基于时间片轮转,基于优先级抢占的调度算法。
有很多情况会产生信号:
信号发送的本质:
信号都是由操作系统向系统写入的:
崩溃现象就是底层代码引起了硬件的问题,进而被操作系统识别,然后操作系统将硬件问题识别成信号,然后向进程发送,然后终止进程。
何为异步:
以点外卖为例,当外卖到了时,你可能正在忙着做其他事情,外卖员给你发了条取餐消息,但是你并不能立即去取。
此时我们知道自己的外卖到了(知道收到了信号),等手上的活忙完了再去取(过一会再去处理信号)。
同步和异步:
信号可能在任何时候都能产生,可能是用户产生,也可能是操作系统产生的,这个产生对进程来讲是异步的。
因为信号产生是异步的:
处理信号的三种行为:
信号的处理,也叫做信号的的捕捉,递达处理动作。
必须记住这个信号有没有,是什么信号:
只有操作系统有这个权利,能直接修改这个
task_struct
内的数据位图!
OS
是进程的管理者,进程的所有的属性的获取和设置,只能由OS
来!!
无论信号怎么产生,最终一定只能是OS
帮我们进行信号的设置的!
Ctrl + C
的本质是向前台进程发送信号。
我们死循环打印Hello World,在一直死循环期间,我们输入命令ls
并不会列出该目录下的文件名。
myproce跑起来之后,再输入其他指令是没用的,因为这个进程占用了前面bash所对应的终端。当前bash没法做命令行响应,此时这种进程叫做前台进程。
将进程放到后台:
26733是进程编号。
后台进程,可以执行命令行指令,但是用Ctrl + C终止不了了。
fg 1
就将该进程提至前台了,再次Ctrl + C
就可以了。
补充:
任务管理:
如果没有"+“和”-"符号显示在作业列表中,则表示当前没有前台或后台运行的作业。作业列表可能是空的,也就是没有任何正在运行的作业。这通常发生在你没有在前台执行命令或
将任何作业放到后台时
。
bg指令:
要将一个正在前台运行的作业切换到后台运行,可以按下"Ctrl + Z",这会将该作业暂停,并返回到命令行界面。然后,可以使用"bg"
命令将作业放到后台继续运行,此时作业会继续执行,但不会再占用终端。
Ctrl + C
是向前台发送二号信号。
代码演示:
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}
int main()
{
// 给该信号设置了回调捕捉,自定义动作
// 这里不是调用handler方法,这里只是设置了一个回调,让SIGINT产生的时候,该方法才会被调用。
// 如果不产生SIGINT,该方法不会被调用!
// 当二号信号产生的时候,才调用后面的方法。
signal(SIGINT, handler);
signal(3, handler);
sleep(3);
cout << "进程已经设置完了" << endl;
sleep(3);
while (true)
{
cout << "我是一个正在运行中的进程: " << getpid() << endl;
sleep(1);
}
return 0;
}
注意:
SIGINT
产生的时候,该方法才会被调用。函数指针,回调函数:
Ctrl + C
本质是给前台产生了2号信号,发送给目标进程,其中目标进程默认对2号信号的处理,是终止自己。3号信号
。kill -9 + 进程ID
信号可以杀掉进程,那我们能否将kill -9 + 进程ID
的信号捕捉了呢?kill -9 + 进程ID
该信号并不能被捕捉,这是为了防止一些恶意进程杀不掉的情况。D状态
的进程。进程状态复习-传送门SIGKILL (信号编号为9)
:用于立即终止一个进程。无论进程是否希望接收该信号,都无法阻止或忽略它。SIGSTOP (信号编号为19或17)
:用于暂停一个进程的执行。与SIGKILL
类似,无法被捕捉或忽略。SIGCONT (信号编号为18或19)
:用于继续一个被暂停的进程的执行。与前两个信号不同,SIGCONT
是可以被捕捉的,但在默认情况下,它会立即恢复进程的执行。这些不可捕捉信号通常由操作系统或其他系统级实体发送,用于管理进程的状态和行为。在正常情况下,用户进程无法阻止或修改这些信号的执行。
kill
不仅是命令而且也是系统调用接口:
不能杀掉不是自己的进程:
有了上述接口,再加上我们之前学的main函数的几个参数
,我们可以手搓一个kill
指令:
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
static void Usage(const string& proc)
{
cout << "Usage:\n\t" << proc << "signo pid" << endl;
}
// 自己实现一个kill命令
// mykill 9 1234
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
if (kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == -1)
{
cerr << "kill: " << strerror(errno) << endl;
exit(2);
}
return 0;
}
根据进程名字杀掉某个进程:killall + 进程名
。
killall
命令默认会发送SIGTERM
(信号编号为15)信号给目标进程。SIGKILL
(信号编号为9)。killall
命令要小心,确保只终止你想要终止的进程,以免造成意外的影响。kill是给任意进程发任意信号,raise是给自己发任意信号:
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}
int main()
{
// 这里没有调用对应的handler方法,仅仅是注册
signal(2, handler);
while (true)
{
// 每次循环都给自己发送2号信号
sleep(1);
raise(2);
}
// 每隔1秒都会收到一个2号信号
return 0;
}
向自己发送6号SIGABRT
信号:
#include
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}
int main()
{
// 这里没有调用对应的handler方法,仅仅是注册
signal(6, handler);
while (true)
{
sleep(1);
abort();// exit(), ahort();
}
return 0;
}
终止进程:
abort()
是即使捕捉了,但是依然会退出进程。硬件是在推着操作系统做一系列动作:
#include
#include
#include
#include
using namespace std;
int cnt = 0;
void handler(int signo)
{
cout << "我是一个进程,刚刚获取了一个信号: " << signo << "cnt: " << cnt << endl;
exit(1);
}
// 信号闹钟
int main()
{
// 未来一秒钟之后会超时
signal(SIGALRM, handler);
alarm(1);
// 如果没有自定义操作,默认alarm会自定义终止,会收到SIGALRM信号
// 统计该进程一秒钟cnt++多少次
while (1)
{
cnt++;
// cout << "hello: " << cnt++ << endl;
}
return 0;
}
相比于CPU独立做计算,IO
非常慢。
SIGALRM
信号是用于告知进程某个定时器已经超时的信号。它通常由内核或通过使用alarm
函数设置的定时器触发。当定时器超时时,内核向进程发送SIGALRM
信号,进程可以选择捕获和处理该信号,或者使用默认操作(即终止进程)。SIGALRM
信号的处理方式(通过信号处理函数或信号处理器),那么SIGALRM
信号将以默认操作的方式处理,即终止进程。这意味着如果定时器超时并且进程没有捕获该信号,进程会被终止。在Linux中越界访问都叫段错误。
所谓的崩溃,本质是什么呢?
进程崩溃是因为收到了异常信号,那么为什么会收到异常信号呢?
C++ try catch:
MMU是内存管理单元(Memory Management Unit)的简称。
实操注意:
因为没有解决这个问题,这个异常一直都在,所以操作系统一直给进程发信号,所以刷屏了。
Core Dump
会把进程在运行中,对应的异常上下文数据,core dump到磁盘上,方便调试。
发上云服务器是设置成0的,禁止发生core dump
(一般是关掉的),但是可以打开。
8号信号本身就要产生core
文件的,然后指令发现多了一个文件,里面是乱码。
当一个进程异常退出时,收到了某些信号,系统为了便于用户调试,会告诉用户触发core dump
机制,core dump
机制叫做核心转储
。
代码演示:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
pid_t id = fork();
if (id == 0)
{
// 子进程
int *p = nullptr;
*p = 1000; // 野指针问题
exit(1);
}
// 父进程
int status = 0;
// nullptr, NULL, 0, '\0' == 0;
waitpid(id, &status, 0); // nullptr
// core dump表明当前进程在退出时,是否发生core dump
printf("exitcode: %d, signo: %d, core dump flag: %d\n",
(status >> 8) & 0xFF, status & 0x7F, (status >> 7) & 0x1);
return 0;
}
核心转储(Core Dump)是指在程序运行过程中发生了严重错误导致程序崩溃时,系统将程序内存的完整快照保存到一个核心转储文件中。
- 这个文件包含了程序崩溃时的内存状态、寄存器的内容以及其他相关的调试信息。
- 核心转储文件主要用于程序崩溃分析和调试目的。通过分析核心转储文件,开发人员可以了解程序崩溃时的内存状态,定位错误的原因,并进行问题排查和修复。
- 核心转储文件通常具有可读性较低的二进制格式,需要使用调试工具或分析器来解析和分析。
- 在许多操作系统上,默认情况下,当程序崩溃时会自动生成核心转储文件。开发人员也可以在程序中通过设置相应的参数或使用调试工具来控制核心转储的生成及其行为。
- 需要注意的是,由于核心转储文件可能会包含敏感信息,如内存中的数据,因此在进行排查和分析时需要遵守相应的隐私保护规定,并确保核心转储文件的安全性。
生成的Core Dump
文件很大:
Core Dump一般配台gdb使用:
每次执行出错都会产生core dump文件,这种调试策略叫做事后调试。
云服务器关掉的原因:
因为如果大型程序,一旦代码有问题,会自动重启程序,如果一重启就挂掉,就会产生core文件,所以一直重启就会产生大量的core文件,就很大概率将磁盘空间被打满,此时就会危及到操作系统正常工作了。