Linux进程信号【信号产生】

news2024/11/25 14:30:43

✨个人主页: 北 海
🎉所属专栏: Linux学习之旅
🎃操作环境: CentOS 7.6 阿里云远程服务器

成就一亿技术人


文章目录

  • 🌇前言
  • 🏙️正文
    • 1、进程信号基本概念
      • 1.1、什么是信号?
      • 1.2、信号的作用
      • 1.3、信号的基本认知
    • ===== 信号产生的方式 =====
    • 2、键盘键入
      • 2.1、ctrl+c 终止前台进程
        • 2.1.1、signal 注册执行动作
      • 2.2、硬件中断
    • 3、系统调用
      • 3.1、kill 函数
      • 3.2、模拟实现 myKill
      • 3.3、raise 函数
      • 3.4、abort 函数
    • 4、软件条件
      • 4.1、alarm 设置闹钟
      • 4.2、测试算力
    • 5、硬件异常
      • 5.1、除 0 导致异常
      • 5.2、状态寄存器
      • 5.3、野指针导致异常
    • 6、核心转储
      • 6.1、核心转储的概念
      • 6.2、打开与关闭核心转储
      • 6.3、核心转储的作用
  • 🌆总结


🌇前言

Linux 中,进程具有独立性,进程在运行后可能 “放飞自我”,这是不利于管理的,于是需要一种约定俗成的方式来控制进程的运行,这就是 进程信号,本文将会从什么是进程信号开篇,讲述各种进程信号的产生方式及作用

不同的信号指示灯代表着不同的执行动作

图示


🏙️正文

1、进程信号基本概念

1.1、什么是信号?

信号 是信息传递的承载方式,一种信号往往代表着一种执行动作,比如:

  • 鸡叫 => 天快亮了
  • 闹钟 => 起床、完成任务
  • 红绿灯 => 红灯停,绿灯行
  • ……

信号

当然这些都是生活中的 信号,当产生这些 信号 时,我们会立马想到对应的 动作 ,这是因为 我们认识并能处理这些信号

我们能进行处理是因为受过教育,学习了执行动作,但对进程来说,它可没有接受过九年义务教育,也不知道什么时候该干什么事

于是程序员们给操作系统植入了一批 指令一个指令表示一种特殊动作,而这些指令就是 信号(进程信号)

通过 kill -l 查看当前系统中的信号集合表

kill -l

信号
这些就是当前系统中的 进程信号,一共 62 个,其中 1~31 号信号为 普通信号(学习目标),用于 分时操作系统;剩下的 34~64 号信号为 实时信号,用于 实时操作系统

  • 分时操作系统:根据时间片实行公平调度,适用于个人电脑
  • 实时操作系统:高响应,适合任务较少、需要快速处理的平台,比如汽车车机、火箭发射控制台

实时操作系统

图示

普通信号只保存它有无产生,实时信号可以保持很长时间

因为我们的系统属于 分时操作系统,所以只需要研究 1~31 号信号即可,当然也不是全部研究,部分信号只做了解即可

1.2、信号的作用

早在 《Linux进程学习【进程状态】》 我们就已经使用过 信号 了,比如:

  • kill -9 pid 终止进程运行
  • kill -19 pid 暂停进程运行
  • kill -18 pid 恢复进程运行

就连常用的 ctrl+cctrl+d 热键本质上也是 信号

这么多信号,其对应功能是什么呢?

  • 可以通过 man 7 signal 进行查询
man 7 signal

图示

简单总结一下,1~31 号信号对应的功能如下(表格内容引用自 2021dragon Linux中的31个普通信号)

信号编号信号名功能
1SIGHUP如果终端接口检测到一个连接断开,则会将此信号发送给与该终端相关的控制进程,该信号的默认处理动作是终止进程
2SIGINT当用户按组合键(一般采用 Ctrl + C )时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程,该信号的默认处理动作是终止进程
3SIGQUIT当用户按组合键(一般采用 Ctrl + \ )时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程,该信号不仅终止前台进程组,同时会产生一个 core 文件
4SIGILL此信号表示进程已执行一条非法指令,该信号的默认处理动作是终止进程,同时产生一个 core 文件
5SIGTRAP该信号由断点指令或其他 trap 指令产生,该信号的默认处理动作是终止进程,同时会产生一个 core 文件
6SIGABRT调用 abort 函数是产生此信号,进程异常终止,同时会产生一个 core 文件
7SIGBUS当出现某些类型的内存故障时,常常产生该信号,,该信号的默认处理动作是终止进程,同时产生一个 core 文件
8SIGFPE此信号表示一个算术运算异常,比如除0、浮点溢出等,该信号的默认处理动作是终止进程,同时产生一个 core 文件
9SIGKILL该信号不能被捕捉或忽略,它向系统管理员提供了一种可以杀死任一进程的可靠方法
10SIGUSR1这是一个用户定义的信号,即程序员可以在程序中定义并使用该信号,该信号的默认处理动作是终止进程
11SIGSEGV指示进程进行了一次无效的内存访问(比如访问了一个未初始化的指针),该信号的默认处理动作是终止进程并产生一个 core 文件
12SIGUSR2这是另一个用户定义的信号,与 SIGUSR1 相似,该信号的默认处理动作是终止进程
13SIGPIPE如果在管道的读进程已终止时对管道进行写入操作,则会收到此信号,该信号的默认处理动作是终止进程
14SIGALRM当用 alarm 函数设置的定时器超时时产生此信号,或由 setitimer 函数设置的间隔时间已经超时时也产生会此信号
15SIGTERM该信号是由应用程序捕获的,使用该信号让程序有机会在退出之前做好清理工作。与 SIGKILL 信号不同的是,该信号可以被捕捉或忽略,通常用来表示程序正常退出
16SIGSTKFLT该信号指示协处理器上的堆栈故障(未使用),该信号的默认处理动作是终止进程
17SIGCHLD在一个进程终止或停止时,SIGCHLD 信号被发送给其父进程。按系统默认,将忽略此信号。如果父进程希望被告知其子进程的这种状态改变,则应捕捉此信号。信号捕捉函数中通常要调用一种 wait 函数以取得子进程 PID 及其终止状态
18SIGCONT可以通过发送该信号让一个停止的进程继续运行
19SIGSTOP这时一个作业控制信号,该信号用于停止一个进程,类似于交互停止信号( SIGTSTP ),但是该信号不能被捕捉或忽略
20SIGTSTP交互停止信号,当用户按组合键(一般采用 Ctrl+Z )时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程
21SIGTTIN后台进程读终端控制台时,由终端驱动程序产生此信号并发送给该后台进程,该信号的默认处理动作是暂停进程
22SIGTTOU后台进程向终端控制台输出数据,由终端驱动程序产生此信号并发送给该后台进程,该信号的默认处理动作是暂停进程
23SIGURG套接字上有紧急数据时,向当前正在运行的进程发出此信号,报告有紧急数据到达,该信号的默认处理动作是忽略
24SIGXCPU进程执行时间超过了分配给该进程的 CPU 时间,系统产生该信号并发送给该进程,该信号的默认处理动作是终止进程,同时会产生一个 core 文件
25SIGXFSZ如果进程写文件时超过了文件的最大长度设置,则会收到该信号,该信号的默认处理动作是终止进程,同时会产生一个 core 文件
26SIGVTALRM虚拟时钟超时时产生该信号,与 SIGALRM 信号类似,但是该信号只计算该进程占用 CPU 的使用时间,该信号的默认处理动作是终止进程
27SIGPROF该信号类似与 SIGVTALRM,它不仅包括该进程占用 CPU 的时间还包括执行系统调用的时间,该信号的默认处理动作是终止进程
28SIGWINCH当窗口大小发生变化时,内核会将该信号发送至前台进程组,该信号的默认处理动作是忽略
29SIGIO此信号指示一个异步 I/O 事件,该信号的默认处理动作是终止进程
30SIGPWR电源故障,该信号的默认处理动作是终止进程
31SIGSYS该信号指示一个无效的系统调用,该信号的默认处理动作是终止进程,同时会产生一个 core 文件

注意: 其中的 9 号 和 19 号信号是非常特殊的,不能修改其默认动作

1.3、信号的基本认知

进程信号由 信号编号 + 执行动作 构成,一个信号对应一种动作,对于进程来说,动作无非就这几种:终止进程、暂停进程、恢复进程3 个信号就够用了啊,为什么要搞这么多信号?

  • 创造信号的目的不只是控制进程,还要便于管理进程,进程的终止原因有很多种,如果一概而论的话,对于问题分析是非常不友好的,所以才会将信号细分化,搞出这么多信号,目的就是为了方便定位、分析、解决问题
  • 并且 普通信号 就 31 个,这就是意味着所有普通信号都可以存储在一个 int 中,表示是否收到该信号(信号的保存)

所以信号被细化了,不同的信号对应不同的执行动作,虽然大部分最终都是终止进程

进程的执行动作是可修改的,默认为系统预设的 默认动作

  1. 默认动作
  2. 忽略
  3. 自定义动作

所以我们可以 更改信号的执行动作(后面会专门讲信号处理相关内容)

信号有这么多个,并且多个进程可以同时产生多个信号,操作系统为了管理,先描述、再组织,在 PCB 中增加了 信号相关的数据结构:signal_struct,在这个结构体中,必然存在一个 位图结构 uint32_t signals 存储 1~31 号信号的有无信息

//信号结构体源码(部分)
struct signal_struct {
	atomic_t		sigcnt;
	atomic_t		live;
	int			nr_threads;

	wait_queue_head_t	wait_chldexit;	/* for wait4() */

	/* current thread group signal load-balancing target: */
	struct task_struct	*curr_target;

	/* shared signal handling: */
	struct sigpending	shared_pending;

	/* thread group exit support */
	int			group_exit_code;
	/* overloaded:
	 * - notify group_exit_task when ->count is equal to notify_count
	 * - everyone except group_exit_task is stopped during signal delivery
	 *   of fatal signals, group_exit_task processes the signal.
	 */
	int			notify_count;
	struct task_struct	*group_exit_task;

	/* thread group stop support, overloads group_exit_code too */
	int			group_stop_count;
	unsigned int		flags; /* see SIGNAL_* flags below */

	/*
	 * PR_SET_CHILD_SUBREAPER marks a process, like a service
	 * manager, to re-parent orphan (double-forking) child processes
	 * to this process instead of 'init'. The service manager is
	 * able to receive SIGCHLD signals and is able to investigate
	 * the process until it calls wait(). All children of this
	 * process will inherit a flag if they should look for a
	 * child_subreaper process at exit.
	 */
	unsigned int		is_child_subreaper:1;
	unsigned int		has_child_subreaper:1;

	//……
};

下面对 进程信号 做一波概念性的总结

1.信号是执行的动作的信息载体,程序员在设计进程的时候,早就已经设计了其对信号的识别能力
2.信号对于进程来说是异步的,随时可能产生,如果信号产生时,进程在处理优先级更高的事情,那么信号就不能被立即处理,此时进程需要保存信号,后续再处理
3.进程可以将 多个信号 或 还未处理 的信号存储在 signal_struct 这个结构体中,具体信号编号,存储在 uint32_t signals 这个位图结构中
4.所谓的 “发送” 信号,其实就是写入信号,修改进程中位图结构中对应的比特位,由 0 置为 1,表示该信号产生了
5.signal_struct 属于内核数据结构,只能由 操作系统 进行同一修改,无论信号是如何产生的,最终都需要借助 操作系统 进行发送
6.信号并不是立即处理的,它会在合适的时间段进行统一处理

所以 进程信号 可以分为三步:信号产生 =》 信号保存 =》 信号处理

图示

本文讲解的就是 信号产生 部分相关知识,下面正式开始学习 信号产生


===== 信号产生的方式 =====

2、键盘键入

信号产生(发送)的第一种方式:键盘键入

通俗来说就是命令行操作

2.1、ctrl+c 终止前台进程

系统卡死遇到过吧?程序死循环遇到过吧?这些都是比较常见的问题,当发生这些问题时,我们可以通过 键盘键入 ctrl + c 发出 2 号信号终止前台进程的运行

下面是一段死循环代码:

#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
    while(true)
    {
        cout << "我是一个进程,我正在运行…… PID: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

运行程序后,会一直循环打印,此时如果想要终止进程,可以直接按 ctrl + c 发出 2 号信号,终止前台进程

结果

此时发出了一个 2 号信号 SIGINT 终止了该进程的运行

如何证明呢?如何证明按 ctrl + c 发出的是 2 号信号呢?

证明自有方法,前面说过,一个信号配有一个执行动作,并且执行动作是可以修改的,需要用到 signal 函数(属于 信号处理 部分的内容,这里需要提前用一下)

ctrl + c 终止的是当前正在运行的前台进程,如果在程序运行时加上 & 表示让其后台运行,此时会发现无法终止进程

结果

像这种后台进程 ctrl + c 是无法终止的,可以通过 kill -9 PID 发出 9 信号终止它

2.1.1、signal 注册执行动作

signal 函数可以用来 修改信号的执行动作,也叫注册自定义执行动作

图示
signal 调用成功返回上一个执行方法的值(其实就是下标,后面介绍),失败则返回 SIG_ERR,并设置错误码

返回值可以不用关注,重点在于 signal 的参数

  • 参数1 待操作信号的编号
  • 参数2 待注册的新方法

参数1 就是信号编号,为 int,单纯地传递 信号名也是可以的,因为信号名其实就是信号编号的宏定义

图示

参数2 是一个函数指针,意味着需要传递一个 参数为 int,返回值为空的函数对象

  • 参数 int 是执行动作的信号编号
void handler(int)	//其中的函数名可以自定义

显然,signal 函数是一个 回调函数,当信号发出时,会去调用相应的函数,也就是执行相应的动作

我们先对 2 号信号注册新动作,在尝试按下 ctrl + c,看看它发出的究竟是不是 2 号信号

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int signo)
{
    cout << "当前 " << signo << " 号信号正在尝试执行相应的动作" << endl;
}

int main()
{

    //给 2 号信号注册新方法
    signal(2, handler);

    while(true)
    {
        cout << "我是一个进程,我正在运行…… PID: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

结果

当我们修改 2 号信号的执行动作后,再次按下 ctrl + c 尝试终止前台进程,结果失败了!执行动作变成了我们注册的新动作

这足以证明 ctrl + c 就是在给前台进程发出 2 号信号,ctrl + c 失效后,可以通过 ctrl + \ 终止进程,发出的是 3 号信号(3 号信号在发出后,会生成 核心转储 文件)

普通信号只有 31 个,如果把所有普通信号的执行动作都改了,会发生什么呢?难道会得到一个有着 金刚不坏 之身的进程吗?

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int signo)
{
    cout << "当前 " << signo << " 号信号正在尝试执行相应的动作" << endl;
}

int main()
{

    //给所有普通信号注册新方法

    for(int i = 1; i < 32; i++)
        signal(i, handler);

    while(true)
    {
        cout << "我是一个进程,我正在运行…… PID: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

图示

大部分信号的执行动作都被修改了,9 号信号没有,因为 9 号信号是 SIGKILL,专门用于杀死进程,只要是进程,他都能干掉

19 号信号 SIGSTOP 也无法修改执行动作,所以前面说过,9SIGKILL19SIGSTOP 信号是很特殊的,经过特殊设计,不能修改其执行动作!

2.2、硬件中断

当我们从键盘按下 ctrl + c 时,发生了这些事:CPU 获取到键盘 “按下” 的信号,调用键盘相应的 “方法” ,从键盘中读取数据,读取数据后解析,然后发出 3 号信号

其中 CPU 捕获键盘 “按下” 信号的操作称为 硬件中断

CPU 中有很多的针脚,不同的硬件对应着不同的针脚,每一个针脚都有自己的编号,硬件与针脚一对一相连,并通过 中断控制器(比如 8259)进行控制,当我们按下键盘后

  • 中断控制器首先给 CPU 发送信息,包括键盘对应的针脚号
  • 然后 CPU 将获取到的针脚号(中断号)写入 寄存器 中
  • 最后根据 寄存器 里的 中断号,去 中断向量表 中查表,找到对应硬件的方法,执行它的读取方法就行了

这样 CPU 就知道是 键盘 发出的信号,然后就会去调用 键盘 的执行方法,通过键盘的读取方法,读取到 ctrl + c 这个信息,转化后,就是 2 号信号,执行终止前台进程的动作

键盘被按下 和 键盘哪些位置被按下 是不一样的

  • 首先键盘先按下,CPU 确定对应的读取方法
  • 其次才是通过 读取方法 从键盘中读取数据

注:键盘读取方法如何进行读取,这是驱动的事,我们不用关心

图示

硬件中断 的流程与 进程信号 的流程雷同,同样是 先检测到信号,然后再去执行相应的动作,不过此时发送的是 中断信号,执行的是 调用相应方法罢了

信号 与 动作 的设计方式很实用,操作系统只需要关注是否有信号发出,发出后去中断向量表中调用相应的方法即可,不用管硬件是什么样、如何变化,做到了 操作系统 与 硬件 间的解耦


3、系统调用

除了可以通过 键盘键入 发送信号外,还可以通过直接调用 系统接口 发送信号,毕竟 bash 也是一个进程,本质上就是在进行程序替换而已

3.1、kill 函数

信号的发送主要是通过 kill 函数进行发送

图示
返回值:成功返回 0,失败返回 -1 并设置错误码

参数1:待操作进程的 PID

参数2:待发送的信号

下面来简单用一下(程序运行 5 秒后,自己把自己杀死)

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

int main()
{

    int n = 1;
    while (true)
    {

        cout << "我是一个进程,已经运行了 " << n << " 秒 PID: " << getpid() << endl;
        sleep(1);
        n++;

        if (n > 5)
            kill(getpid(), SIGKILL);
    }

    return 0;
}

图示

kill 函数当然也可以发送其他信号,这里就不一一展示了,其实命令行中的 kill 命令就是对 kill 函数的封装,kill -信号编号 -PID 其中的参数2、3不正是 kill 函数所需要的参数吗?所以我们可以尝试自己搞一个 myKill 命令

3.2、模拟实现 myKill

这里就直接利用 命令行参数 简单实现了

#include <iostream>
#include <string>
#include <signal.h>

using namespace std;

void Usage(string proc)
{
    // 打印使用信息
    cout << "\tUsage: \n\t";
    cout << proc << " 信号编号 目标进程" << endl;
    exit(2);
}

int main(int argc, char *argv[])
{
    // 参数个数要严格限制
    if (argc != 3)
    {
        Usage(argv[0]);
    }

    //获取两个参数
    int signo = atoi(argv[1]);
    int pid = atoi(argv[2]);

    //执行信号发送
    kill(pid, signo);

    return 0;
}

下面随便跑一个进程,然后用自己写的 myKill 命令给进程发信号

图示

我们可以把这个程序改造下,改成进程替换的方式,让后将自己写的命令进行安装,就能像 kill 一样直接使用了

3.3、raise 函数

发送信号的还有一个 raise 函数,这个函数比较奇怪,只能 自己给自己发信号

结果
返回值:成功返回 0,失败返回 非0

就只有一个参数:待发送的信号

可以这样理解:raise 是对 kill 函数的封装,每次传递的都是自己的 PID

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

int main()
{
    int n = 1;
    while (true)
    {

        cout << "我是一个进程,已经运行了 " << n << " 秒 PID: " << getpid() << endl;
        sleep(1);
        n++;

        if (n > 5)
            raise(SIGKILL); //自己杀死自己    
    }
	return 0;
}

结果

3.4、abort 函数

abortC 语言提供的一个函数,它的作用是 给自己发送 6SIGABRT 信号

图示

没有返回值,也没有参数

值得一提的是,abort 函数即使在修改执行动作后,最后仍然会发送 6 号信号

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int signo)
{
    cout << "收到了 " << signo << " 号信号,已执行新动作" << endl;
}

int main()
{
    signal(6, handler);
    // signal(SIGABRT, handler);    //这种写法也是可以的

    int n = 1;
    while (true)
    {

        cout << "我是一个进程,已经运行了 " << n << " 秒 PID: " << getpid() << endl;
        sleep(1);
        n++;

        if (n > 5)
            abort();
    }
    return 0;
}

结果

即使执行了我们新注册的方法,abort 最后仍然会发出 6 号信号终止进程

同样是终止进程,C语言 还提供了一个更好用的函数:exit(),所以 abort 用的比较少,了解即可

总的来说,系统调用中举例的这三个函数关系是:kill 包含 raiseraise 包含 abort,作用范围是在逐渐缩小的

图示


4、软件条件

信号产生(发送)的第三种方式:软件条件

其实这种方式我们之前就接触过了:管道读写时,如果读端关闭,那么操作系统会发送信号终止写端,这个就是 软件条件 引发的信号发送,发出的是 13SIGPIPE 信号

4.1、alarm 设置闹钟

系统为我们提供了 闹钟(报警):alarm,这个 闹钟 可不是用来起床的,而是用来 定时

图示

返回值:如果上一个闹钟还有剩余时间,则返回剩余时间,否则返回 0
参数:想要设定的时间,单位是秒

当时间到达闹钟中的预设时间时,闹钟会响,并且发送 14SIGALRM 信号

比如这样:

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

int main()
{

    alarm(5);   //设定一个五秒后的闹钟

    int n = 1;
    while (true)
    {

        cout << "我是一个进程,已经运行了 " << n << " 秒 PID: " << getpid() << endl;
        sleep(1);
        n++;
    }
    return 0;
}

图示

我们也可以更改 14SIGALRM 信号的执行动作,让闹钟不断响起(自举)

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int signo)
{
    cout << "收到了 " << signo << " 号信号,已执行新动作" << endl;
    int n = alarm(10);
    cout << "上一个闹钟剩余时间: " << n << endl;
}

int main()
{
    signal(SIGALRM, handler);
    alarm(10);   //设定一个十秒后的闹钟

    while(true)
    {
         cout << "我是一个进程,我正在运行…… PID: " << getpid() << endl;
         sleep(1);
    };

    return 0;
}

结果

系统中不止一个闹钟,所以 OS 需要 先描述,再组织,将这些闹钟管理起来

可以借助闹钟,简单测试一下当前服务器的算力

4.2、测试算力

如何简单粗暴的测试算力? 设个 1 秒后响起的闹钟,看看程序能将一个值累加至多少

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

int main()
{
    alarm(1);   //设定一个一秒后的闹钟

    int n = 0;
    while(true)
    {
        cout << n++ << endl;
    };

    return 0;
}

结果

这个云服务这么拉吗?只能累加几万次

其实不是,主要是因为当前程序涉及了 IO,这是非常耗时间的,可以取消 IO 并修改 SIGALRM 的执行动作为打印变量,看看能累加多少次

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

int n = 0;

void handler(int signo)
{
    cout << n << endl;
    exit(1;
}

int main()
{
    signal(SIGALRM, handler);
    alarm(1);   //设定一个一秒后的闹钟

    while(true)
        n++;

    return 0;
}

结果

可以看到,取消 IO 后,累加了 5 亿多次,近 10000 倍的差距

通过这个简单的小程序证明了一件事:IO 是非常慢的,能不 IO 就不 IO

注:因为当前是云服务器,存在 网络延迟 的影响,所以实际差异更大

注意: 闹钟是一次性的,只能响一次


5、硬件异常

最后一种产生(发送)信号的方式是:硬件异常

所谓 硬件异常 其实就是我们在写程序最常遇到的各种报错,比如 除 0、野指针

5.1、除 0 导致异常

先来看一段简单的错误代码

#include <iostream>
using namespace std;

int main()
{
    int n = 10;
    n /= 0;

    return 0;
}

显然是会报错的是,毕竟 0 不能作为常数

结果

根据报错信息,可以推测出此时发送的是 8SIGFPE 信号(浮点异常)

让我们通过 signal 更改 8 号信号的执行动作,尝试逆天改命,让 除 0 合法?

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int signo)
{
    cout << "虽然除 0 了,但我不终止进程" << endl;
}

int main()
{
    signal(SIGFPE, handler);
    int n = 10;
    n /= 0;

    return 0;
}

图示

结果:一直在死循环似的发送信号,明明只发生了一次 除 0 行为

想要明白背后的原理,需要先认识一下 状态寄存器

5.2、状态寄存器

CPU 中,存在很多 寄存器,其中大部分主要用来存储数据信息,用于运算,除此之外,还存在一种特殊的 寄存器 =》 状态寄存器,这个 寄存器 专门用来检测当前进程是否出现错误行为,如果有,就会把 状态寄存器(位图结构)中对应的比特位置 1,意味着出现了 异常

当操作系统检测到 状态寄存器 出现异常时,会根据其中的值,向出现异常的进程 轮询式 的发送信号,目的就是让进程退出

比如上面的 除 0 代码,发生异常后,CPU状态寄存器 修改,变成 异常状态,操作系统检测到 异常 后会向进程发送 8 号信号,即使我们修改了 8 号信号的执行动作,但 因为状态寄存器仍然处于异常状态,所以操作系统才会不断发送 8 号信号,所以才会死循环式的打印

图示

能让 状态寄存器 变为 异常 的都不是小问题,需要立即终止进程,然后寻找、解决问题

毕竟如果让 除 0 变为合法,那最终的结果是多少呢?所以操作系统才会不断发送信号,目的就是 终止进程的运行

5.3、野指针导致异常

除了 除 0 异常外,还有一个 臭名昭著 的异常:野指针问题

比如:

#include <iostream>
using namespace std;

int main()
{
    int* ptr = nullptr;
    *ptr = 10;

    return 0;
}

结果
Segmentation fault 段错误 这是每个 C/C++ 程序猿都会遇到的问题,因为太容易触发了,出现段错误问题时,操作系统会发送 11SIGSEGV 信号终止进程,可以通过修改执行动作验证,这里不再演示

那么 野指针 问题是如何引发的呢?

借用一下 共享内存 中的图~

图示

野指针问题主要分为两类:

  1. 指向不该指向的空间
  2. 权限不匹配,比如只读的区域,偏要去写

共识:在执行 *ptr = 10 这句代码时,首先会进行 虚拟地址 -> 真实(物理)地址 之间的转换

指向不该指向的空间:这很好理解,就是页表没有将 这块虚拟地址空间 与 真实(物理)地址空间 建立映射关系,此时进行访问时 MMU 识别到异常,于是 MMU 直接报错,操作系统识别到 MMU 异常后,向对应的进程发出终止信号

C语言中对于越界 读 的检查不够严格,属于抽查行为,因此野指针越界读还不一定报错,但越界写是一定会报错的

权限不匹配:页表中除了保存映射关系外,还会保存该区域的权限情况,比如 是否命中 / RW 等权限,当发生操作与权限不匹配时,比如 nullptr 只允许读取,并不允许其他行为,此时解引用就会触发 MMU 异常,操作系统识别到后,同样会对对应的进程发出终止信号

页表中的属性

  • 是否命中
  • RW 权限
  • UK 权限(不必关心)

图示

注:MMU 是内存管理单元,主要负责 虚拟地址 与 物理地址 间的转换工作,同时还会识别各种异常行为

一旦引发硬件层面的问题,操作系统会直接发信号,立即终止进程

到目前为止,我们学习了很多信号,分别对应着不同的情况,其中有些信号还反映了异常信息,所以将信号进行细分,还是很有必要的


6、核心转储

Linux 中提供了一种系统级别的能力,当一个进程在出现异常的时候,OS 可以将该进程在异常的时候,核心代码部分进行 核心转储,将内存中进程的相关数据,全部 dump 到磁盘中,一般会在当前进程的运行目录下,形成 core.pid 这样的二进制文件(核心转储 文件)

6.1、核心转储的概念

对于某些信号来说,当终止进程后,需要进行 core dump,产生核心转储文件

比如:3号 SIGQUIT4号 SIGILL5号 SIGTRAP6号 SIGABRT7号 SIGBUS8号 SIGFPE11号 SIGSEGV24号 SIGXCPU25号 SIGXFSZ31号 SIGSYS 都是可以产生核心转储文件的

不同信号的动作(Action

  • Trem -> 单纯终止进程
  • Core -> 先发生核心转储,生成核心转储文件(前提是此功能已打开),再终止进程

但在前面的学习中,我们用过 36811 号信号,都没有发现 核心转储 文件啊

难道是我们的环境有问题吗?

确实,当前环境确实有问题,因为它是 云服务器,而 云服务器 中默认是关闭核心转储功能的

6.2、打开与关闭核心转储

通过指令 ulimit -a 查看当前系统中的资源限制情况

ulimit -a

图示

可以看到,当前系统中的核心转储文件大小为 0,即不生成核心转储文件

通过指令手动设置核心转储文件大小

ulimit -c 1024

图示

现在可以生成核心转储文件了

就拿之前的 野指针 代码测试,因为它发送的是 11 号信号,会产生 core dump 文件

图示

核心转储文件是很大的,而有很多信号都会产生核心转储文件,所以云服务器一般默认是关闭的

云服务器上是可以部署服务的,一般程序发生错误后,会立即重启
如果打开了核心转储,一旦程序 不断挂掉、又不断重启,那么必然会产生大量的核心转储文件,当文件足够多时,磁盘被挤满,导致系统 IO 异常,最终会导致整个服务器挂掉的
还有一个重要问题是 core 文件中可能包含用户密码等敏感信息,不安全

关闭核心转储很简单,设置为 0 就好了

ulimit -c 0

6.3、核心转储的作用

如此大的核心转储文件有什么用呢?

答案是 调试

没错,核心转储文件可以调试,并且直接从出错的地方开始调试

这种调试方式叫做 事后调试

调试方法:

  1. gcc / g++ 编译时加上 -g 生成可调试文件
  2. 运行程序,生成 core-dump 文件
  3. gdb 程序 进入调试模式
  4. core-file core.file 利用核心转储文件,快速定位至出错的地方

图示

之前在 进程创建、控制、等待 中,我们谈到了 当进程异常退出时(被信号终止),不再设置退出码,而是设置 core dump 位 及 终止信号

也就是说,父进程可以借此判断子进程是否产生了 核心转储 文件

图示


🌆总结

以上就是本次关于 Linux进程信号【信号产生】的全部内容了,作为进程信号系列的开篇之作,包含了很多内容,首先是对信号的产生、保存、处理相关概念进行了学习,然后针对信号产生,阐述了四种不同的方式,最后学习了核心转储的相关概念,掌握了一种特殊的调试方式


星辰大海

相关文章推荐

Linux进程间通信【消息队列、信号量】

Linux进程间通信【共享内存】

Linux进程间通信【命名管道】

Linux进程间通信【匿名管道】

Linux基础IO【软硬链接与动静态库】

Linux基础IO【深入理解文件系统】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/684705.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

十四、go语言的指针

一、指针 1.1 指针的概念 指针是存储另一个变量的内存地址的变量。 我们都知道&#xff0c;变量是一种使用方便的占位符&#xff0c;用于引用计算机内存地址。 一个指针变量可以指向任何一个值的内存地址它指向那个值的内存地址。 在上面的图中&#xff0c;变量b的值为156&…

怎么输入文字生成绘画图?分享一份文字生成绘画教程

你是否曾经幻想过&#xff0c;将文字化为绘画&#xff0c;将思维转化为图像&#xff0c;让你的想象力得到更好的释放与表达&#xff1f;现在&#xff0c;这一切都变得可能了&#xff01;通过一些绘画软件&#xff0c;你可以轻松生成各种风格的绘画图。只需要一个创意的想法和一…

Selenium教程__POM架构(17)

POM是Page Object Model的简称&#xff0c;它是一种设计思想&#xff0c;意思是&#xff0c;把每一个页面&#xff0c;当做一个对象&#xff0c;页面的元素和元素之间操作方法就是页面对象的属性和行为。 POM一般使用三层架构&#xff0c;分别为&#xff1a;基础封装层、页面对…

SkyWalking--traceId的作用

原文网址&#xff1a;SkyWalking--traceId的作用_IT利刃出鞘的博客-CSDN博客 简介 本文介绍SkyWalking中traceId的作用。 traceId是什么 SkyWalking的一个核心功能就是&#xff1a;链路追踪。链路追踪就是跟踪一个请求的所有链路&#xff0c;而这个链路都是通过一个id来串起…

数据结构--算法空间复杂度

数据结构–算法空间复杂度 只需关注存储空间大小与问题规模相关的变量 计算规则与算法的时间复杂度类似 eg&#xff1a; S ( n ) O ( n 2 ) O ( n ) O ( 1 ) O ( n 2 ) S(n) O(n^2)O(n)O(1) O(n^2) S(n)O(n2)O(n)O(1)O(n2) 知识点回顾 & 重要考点

java list集合数据去重方式

1.概述 最近又是一轮代码review , 发现了一些实现去重的代码&#xff0c;在使用 list.contain … 我沉思&#xff0c;是不是其实很多初学者也存在这种去重使用问题&#xff1f; 所以我选择把这个事情整出来&#xff0c;分享一下。 2.contain 去重 首先是造出一个 List 模拟…

[CKA]考试之七层负载均衡Ingress

由于最新的CKA考试改版&#xff0c;不允许存储书签&#xff0c;本博客致力怎么一步步从官网把答案找到&#xff0c;如何修改把题做对&#xff0c;下面开始我们的 CKA之旅 题目为&#xff1a; Task 如下创建一个新的nginx Ingress资源&#xff1a; 名称: pong Namespace: i…

掌握这些容易被忽略的Vue细节,轻松排查问题,省时省力!

v-bind 绑定的值是 null 或者 undefined v-bind 如果绑定的值是 null 或者 undefined&#xff0c;那么该 attribute 将会从渲染的元素上移除。 当attribute 为布尔型时&#xff0c; 行为略有不同。 <button :disabled"isButtonDisabled">Button</button&…

CSS之平面转换

简介 作用&#xff1a;为元素添加动态效果&#xff0c;一般与过渡配合使用 概念&#xff1a;改变盒子在平面内的形态&#xff08;位移、旋转、缩放、倾斜&#xff09; 平面转换也叫 2D 转换&#xff0c;属性是 transform 平移 transform: translate(X轴移动距离, Y轴移动距…

@Valid接口参数校验怎么做,详细教程

接口参数校验教程 一、在字段上可以使用这个注解来设置校验 Null&#xff1a;被注释的元素必须为null NotNull&#xff1a;被注释的元素不能为null AssertTrue&#xff1a;该字段只能为true AssertFalse&#xff1a;该字段的值只能为false Min("value","messa…

机器学习:监督学习

目前&#xff0c;在机器学习系统中&#xff0c;监督学习&#xff08;Supervised Learning&#xff09;占主导地位。由于监督学习的任务定义明确&#xff0c;例如识别垃圾邮件或预测降水&#xff0c;因此它比无监督学习具有更多潜在用例&#xff1b;而与强化学习相比&#xff0c…

剑指offer(C++)-JZ51:数组中的逆序对(算法-排序)

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 题目描述&#xff1a; 在数组中的两个数字&#xff0c;如果前面一个数字大于后面的数字&#xff0c;则这两个数字组成一个逆序对…

Java IO 学习总结(四)BufferedReader 缓冲字符流

Java IO 学习总结&#xff08;一&#xff09;输入流/输出流 Java IO 学习总结&#xff08;二&#xff09;File 类 Java IO 学习总结&#xff08;三&#xff09;BufferedInputStream Java IO 学习总结&#xff08;四&#xff09;BufferedReader 缓冲字符流 前言&#xff1a; 学…

Kibana介绍安装

目录 Kibana入门配置和安装启动数据探索Metricbeat仪表盘Nginx指标仪表盘【Metricbeat】Nginx日志仪表盘查看集群数据 Kibana入门 Kibana 是一款开源的数据分析和可视化平台&#xff0c;它是 Elastic Stack 成员之一&#xff0c;设计用于和 Elasticsearch 协作。可以使用 Kiban…

2023上半年软考系统分析师科目一整理-03

2023上半年软考系统分析师科目一整理-03 1. 嵌入式 1. 嵌入式 嵌入式系统已被广泛应用到各行各业。嵌入式系统是一个内置于设备中&#xff0c;对设备的各种传感器进行管理与控制的系统。通常&#xff0c;根据系统对时间的敏感程度可将嵌入式系统划分为( A )两种&#xff0c;而…

netwox构建IP协议数据包【网络工程】(保姆级图文)

目录 netwox构建IP协议数据包1) 不指定选项&#xff0c;直接运行该模块。执行命令如下&#xff1a;2) 指定源 IP 地址为 192.168.43.95&#xff0c;目标 IP 地址为 192.168.43.97。执行命令如下&#xff1a;3) 通过抓包&#xff0c;验证构造的 IP 数据包。捕获到的数据包如图所…

管理类联考——英语——趣味篇——不择手段——b开头单词

第一部分 核心词汇趣讲 Unit 2 boom n./v.&#xff08;发出&#xff09;隆隆声&#xff1b;激增&#xff0c;繁荣 loom想象成&#xff1a;一百(100)米(m)外有个妹妹(m也可以想象成妹妹)&#xff0c;你能看得清她吗?→&#xff08;模糊之物)耸现。 boom&#xff1a;六百个妹…

低代码可视化拖拽编辑器实现方案

一、前言 随着业务不断发展&#xff0c;低代码、无代码平台越来越常见&#xff0c;它降低开发门槛、快速响应业务需求、提升开发效率。零开发经验的业务人员通过可视化拖拽等方式&#xff0c;即可快速搭建各种应用。本文主要是讲解低代码可视化拖拽平台前端展示层面的实现逻辑…

@Async使用什么线程池?

文章目录 前言一、前言1、ThreadPoolTaskExecutor2、SimpleAsyncTaskExecutor3、测试代码 二、各种情况模拟1、未配置线程池2、配置异步线程池3、配置1个或多个非异步线程池4、同时配置异步和非异步线程池 三、总结 前言 本文的目的&#xff0c;主要是看到网上各种说辞&#x…

JVM-类加载与运行区详细分析(一)

目录 一、为什么会有类加载机制 二、类加载机制原理是什么 1、什么是类加载器&#xff1a;宏观 2、类加载器工作原理 1、装载 2、链接 3、初始化 3、何为装载的机制&#xff1a;微观 4、上面既然我们已经知道了啥是双亲委派了&#xff0c;那么怎么去破坏呢&#xff1f;…