Linux 进程信号深剖

本文深入探讨了Linux系统中的进程信号,包括信号的概念、产生、发送、处理方式、终端按键与信号的关系,以及核心转储的原理和调试方法。通过实例展示了如何捕获和处理特定信号,如SIGINT(Ctrl+C)和SIGTERM,并介绍了信号的阻塞、未决状态以及信号集操作。此外,还讨论了信号在内核中的表示,如何使用sigset_t、sigprocmask和sigpending等函数进行信号管理。
摘要由CSDN通过智能技术生成

传统艺能😎

小编是双非本科大二菜鸟不赘述,欢迎米娜桑来指点江山哦
在这里插入图片描述
1319365055

🎉🎉非科班转码社区诚邀您入驻🎉🎉
小伙伴们,满怀希望,所向披靡,打码一路向北
一个人的单打独斗不如一群人的砥砺前行
这是和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
直达: 社区链接点我


在这里插入图片描述

概念🤔

信号是一个生活中随处可见的概念,没什么深层隐晦的地方,一个信号包含了各种信息,比如你快递到了,你该吃饭了或者你该睡觉了等等

首先在 Linux 系统中,我们首先需要 明 白 信 号 的 产 生 是 异 步 的 \color{red} {明白信号的产生是异步的} ,比如有以下代码:

#include <stdio.h>
#include <unistd.h>

int main()
{
   
	while (1){
   
		printf("hello signal!\n");
		sleep(1);
	}
	return 0;
}

运行结果是死循环,我们要终止死循环的话通常会直接 ctrl c:

在这里插入图片描述
这里的 ctrl c 动作就是一个信号,本质上是因为输入产生了一个硬中断,ctrl c 被翻译成 2 号信号送到操作系统,操作系统再送到前台进程,然后前台进程退出。

这里所谓的 2 号信号我们可以听过 signal 函数进行捕捉,声明为:

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signum:要捕捉的信号编号  
handler:捕捉信号的处理方法,处理方法的参数是 int,返回值是 void

这里就以对 ctrl c 的捕捉为例:
在这里插入图片描述
可以看到这里系统确实收到另一个 2 号信号!

信号是进程之间事件异步通知的一种方式,属于 软 中 断 \color{red} {软中断}

ctrl c 产生的信号只能发给前台进程,在命令后面加 & 就可以放到后台运行,这样 Shell 不必等待进程结束就可以接收新命令,启动新的进程。当然 Shell 可以同时运行一个前台进程和任意多个后台进程,但是只有前台进程才能接到像 ctrl c 这种控制信号

该进程的用户空间代码执行到任何地方都可能收到 SIGINT信号(ctrl c 对应的 2 号信号)而终止,所以信号相对于进程的控制流程来说是异步的。

信号发送🤔

我们可以通过 kill -l 命令查看 Linux 中所有的信号:

在这里插入图片描述
其中1-31号信号是普通信号,34-64号信号是实时信号,普通信号和实时信号各自都有31个,每个信号都有一个编号和一个宏定义名称:

在这里插入图片描述

信号记录🤔

实际上进程接收一个信号,该信号是被记录在该进程的进程控制块当中的。我们都知道进程控制块本质上就是一个结构体变量,我们主要就是记录一个信号是否产生,因此我们可以用一个 32 位的位图来记录信号是否产生

在这里插入图片描述
其中比特位的位置代表信号的编号,而比特位的内容就代表是否收到对应信号,比如第 6 个比特位是 1 就表明收到了 6 号信号

信号产生🤔

首先我们要知道信号是如何产生的

收到信号的本质就是进程内的信号位图被修改,也就是进程的数据被修改,而只有操作系统才有资格修改进程的数据,因为操作系统是进程的管理者。也就是说,信号的产生本质上就是操作系统直接去修改目标进程的 task_struct 中的信号位图。

常见信号处理方式🤔

对于一个信号我们有三种处理方式:

  1. 执行该信号的默认处理动作
  2. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号
  3. 直接忽略信号

Linux 我们可以使用 man 指令来查看处理方式:

man 7 signal

在这里插入图片描述

终端按键产生信号🤔

最开始我们对于死循环程序输入 ctrl c 进行终止的动作就是通过终端按键产生信号,但是其实除了 ctrl c 以外,ctrl \ 也可以终止程序:

在这里插入图片描述
那么问题来了, ctrl c 和 ctrl \ 的区别在哪里

ctrl c 实际上是发送 2 号信号SIGINT,而 ctrl \ 实际上是发送3号信号SIGQUIT。查看这两个信号的默认处理动作,可以看到这两个信号的 Action 是不一样的,2 号信号是 Term,而 3 号信号是 Core

在这里插入图片描述
Term 和 Core 都代表终止进程,但是 Core 在终止进程时会进行 核 心 转 储 \color{red} {核心转储}

在这里插入图片描述

核心转储😋

嘛是核心转储?其实可以理解为运行时报错日志,我们知道在一个代码运行结束后如果有报错,我们可以通过退出码得知错误信息,但是如果是一个运行的代码错误,我们只能通过核心转储获悉错误原因

在运行中如果崩溃了,我们一般会直接进行 debug 调试,但是有些特殊情况下我们会用到核心转储,核心转储的本质是一个 磁 盘 文 件 \color{red} {磁盘文件} ,也叫核心转储文件,它是操作系统在进程收到信号而终止后,会将进程地址空间的内容,状态和其他信息转而存储到磁盘中,一般命名为core.pid

我们我们通过 ulimit -a 指令查看当前资源限制设定,这里我们看到在云服务器里面,核心转储功能是默认关闭的,因为 core 文件大小为 0:

在这里插入图片描述
我们可以自己通过 ulimit -c 来改变 core 文件的大小:

在这里插入图片描述
这里有了 core 文件就证明核心转储功能已经被我们开发♂出来了,再次使用 ctrl \ 就会出现 core dump 信息(因为我的服务器搞了汉化包,这里的硬核翻译比较尴尬,吐核就是 core dump):

在这里插入图片描述
且在当前目录下会产生一个 core+一串数字后缀的文件,这一串数字其实就是这次核心转储的进程的 PID

在这里插入图片描述
ulimit 指令改变的是 shell 的 Resource Limit,这里 signal 的 PCB 也是由 shell 复制来的,所以 signal 也具有和 shell 相同的 Resource Limit

如何调试🤔

我们下面这个简单的除0场景为例:

在这里插入图片描述
崩溃后就会出现报错信息:

在这里插入图片描述
接下来查看 core dump 文件:

在这里插入图片描述
在进入 gdb 调试模式,使用core-file + core文件名命令加载core文件,即可判断出该程序在终止时收到了8号信号,并且定位到了产生该错误的具体代码:

在这里插入图片描述
8 号信号对应的就是算术错误。

core dump 标识在讲 waitpid 时就有说过:

pid_t waitpid(pid_t pid, int *status, int options);

waitpid 的第二个参数 status 是一个输出型参数,他用于获取子进程的退出状态,他是一个整型参数,但是我们不能单纯将它看成一个整数,因为他的不同比特位有不同的含义:

在这里插入图片描述
我们只关注 status 的 16 位,如果进程时正常退出,次低 8 位就会记录退出状态,也就是我们说的退出码,如果是因为信号异常退出,低 8 位的第一位就是 core dump,记录了核心转储

在这里插入图片描述
我们此时就可以开启系统的核心转储功能,编写一个代码实现父进程 fork 子进程,并在子进程里面进行野指针的实现,在 *p = 100 执行时,系统会终止并且会立即进行核心转储,我们在 waitpid 后查看对应的终止信号:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
   
	if (fork() == 0){
   
		//child
		printf("I am running...\n");
		int *p = NULL;
		*p = 100;
		exit(0);
	}
	//father
	int status = 0;
	waitpid(-1, &status, 0);
	printf("exitCode:%d, coreDump:%d, signal:%d\n",
		(status >> 8) & 0xff, (status >> 7) & 1, status & 0x7f);
	return 0;
}

效果如下:
在这里插入图片描述
因此 core dump 标志就是表示当前进程是否进行了核心转储

那么还有其他组合键操作吗?我们可以通过代码捕捉所有信号,并将收到信号的默认动作改为直接打印出该信号的编号:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler(int signal)
{
   
	printf("get a signal:%d\n", signal);
}
int main()
{
   
	int signo;
	for (signo = 1; signo <= 31; signo++){
   
		signal(signo, handler);
	}
	while (1){
   
		sleep(1);
	}
	
  • 102
    点赞
  • 143
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乔乔家的龙龙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值