当前位置: 首页 > news >正文

Linux(17)——Linux进程信号

目录

一、信号速识

✍️生活中的信号

✍️技术上的信号

✍️信号的发送和记录

✍️信号处理概述

二、产生信号

✍️通过终端产生信号

✍️通过函数发送信号

✍️通过软件产生信号

✍️通过硬件产生信号


一、信号速识

✍️生活中的信号

  • 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
  • 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
  • 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是,你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
  • 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1.执行默
  • 认动作(幸福的打开快递,使用商品)2.执行自定义动作(快递是零食,你要送给你你的女朋友),3.忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
  • 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

✍️技术上的信号

我们写个代码来看看:

#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
int main() {while(true) {printf("I am a process, I am waiting signal!\n");sleep(1);}return 0;
}

这个代码是一个死循环,我们最好的终止代码的方式是ctrl+c。

为什么我们的进程被终止了呢?

实际上这是因为我们给进程发送了一个信号,这个信号就是我们的ctrl+c的动作,只不过这个行为被操作系统翻译成了2号信号了,然后操作系统给目标前台进程发送了这个信号,前台进程收到了2号信号之后就会退出了。

我们可以使用signal函数对信号进行捕捉,以说明我们的ctrl+c操作使进程收的的确是2号信号,这里简单介绍一下这个signal函数。下面是函数的原型:

typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);

参数说明:

第一个参数signum,指的是需要我们捕捉的信号。

第二个参数handler,指的是对信号的处理方法,也就是可以传一个参数是int,返回值是void的函数指针。

我们可以对上面的代码进行一下改写,对2号信号进行捕捉,当进程运行起来之后,如果进程收到了2号信号,那么就可以打印出相关的信息了。

这个时候我们运行我们的代码:

这也就证明了,当我们按下ctrl+c的时候进程的确是收到了2号信号。

敲黑板:

✍️信号的发送和记录

这里我们可以使用kill -l命令来查看我们的信号列表:

  1. ctrl+c产生的信号只能是发给前台进程的,在一个命令后面加上一个&就可以将其放到后台来运行了,这样就可以接受新的命令,开启新的进程了。
  2. shell只能运行一个前台进程,但是它可以同时运行多个后台进程。我们这里可以看到我们可以随时按下ctrl+c来产生一个信号给琴台进程终止,也就是说信号相对进程是异步的。

这里我们要解释一下,1~31号信号是普通信号,34~64号信号是实时信号,这两种信号各有31个。

那么信号是怎么记录下来的呢?

实际上我们的进程接收到某种信号后,该信号是被记录在了该进程的进程控制块中的,进程控制块的本质就是一个结构体变量,对于信号而言我们就是记录某种信号是否产生,因此我们使用32位的位图来记录信号是否产生的。

其中比特位的位置就是代表信号的编号,而比特位的内容就是是否收到了这个信号。

信号是怎么产生的?

实际上我们也是应该能推测出来的,进程收到了信号本质上就是进程内对应位置的信号位图被修改了,也就是进程数据被修改了,而只有操作系统才有资格修改进程的数据,这也就说明了信号的产生就是操作系统取修改了进程PCB的信号位图。

✍️信号处理概述

信号默认会执行其默认操作,处理信号函数实质上是就是要求内核在处理信号是切换到用户太来执行信号函数,这种行为就是catch(捕捉)。

我们可以在man手册中查看一下各个信号的默认处理行为:
 

man 7 signal

这里简单的说明一下:

  • Term:表示终止(Terminate),这个信号会导致进程终止。
  • Core:表示生成核心转储(Core Dump),通常表示程序崩溃时将内存状态写到文件中,便于调试。
  • Ign:表示忽略(Ignore),即进程会忽略该信号。
  • Cont:表示继续(Continue),用来恢复进程的执行,通常用在进程暂停后。
  • Stop:表示暂停(Stop),让进程暂停执行,常见于SIGSTOP。

二、产生信号

✍️通过终端产生信号

我们这里还是用我们之前的代码:

#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
int main() {while(true) {printf("hello signal!\n");sleep(1);}return 0;
}

实际上我们除了可以使用ctrl+c终止进程之外,我们还可以使用ctrl+\来终止进程。

那么这两个操作有什么区别呢?

实际上,ctrl+c/是向进程发送2号信号SIGINT,而ctrl+\实际上发送的是3号信号SIGQUIT。其实之前的表格里面也是有展示的:

他们两个一个行为是Term(2号信号),一个是Core(3号信号)。Term是将进程终止,而Core则是表示核心转储。

那么什么是核心转储呢?

在云服务器之中,核心转储默认是关闭的,我们可以通过使用ulimit -a命令来查看当前的资源限制的设定。

我们可以看到第一行显示的是core文件的大小是0,也就表示我们的核心转储是关闭的。

我们可以是用命令ulimit -c size来设置core文件的大小。


设置好了之后,就相当于是将核心转储的功能打开了,这个时候我们再使用ctrl+\来对进程进行终止。这个时候我们就会在在当前路径下面生成一个core文件(没有生成的话可以检查一下这个路径:/proc/sys/kernel/core_pattern,然后echo "core.%e.%p" > /proc/sys/kernel/core_pattern
),这里的文件后缀的一串数字实际上是发生这次核心转储的进程的PID。

核心转储的作用是什么呢?

其实核心转储主要是为了我们方便调试代码的,如果我们代码出现了问题,我们最关心的就是我们的代码是什么原因出错的,当我们的程序运行过程中崩溃了,我们一般会通过调试来进行逐步的查找程序的崩溃的原因。而在一些特殊情况下我们就会用到核心转储,核心转储就是我们的操作系统在进程收到信号终止以后,将进程地址空间的内容以及有关的进程状态的其他信息转而存储到了一个磁盘文件当中,这个磁盘文件也叫做核心转储文件。

如何调试呢?

这里我们写个错误的代码:

#include <stdio.h>
#include <unistd.h>int main() {printf("I am running...\n");sleep(3);int r = 10 / 0;return 0;
}

很明显这个代码会执行崩溃的,我们可以在当前目录下面看到核心转储是生成的core文件。

使用gdb可以对当前的可执行程序调试,我们直接使用生成的core文件,在gdb中执行命令core-file + core文件的命令。

core dump标志

我们之前在说进程等待的时候,用到了一个函数叫waitpid:

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

我们当时重点介绍了这个函数的第二个参数,这个参数是一个输出型参数,是用来获取子进程的退出状态的,status是一个整型变量,但是事实上我们不是将它当成一个整型而是一个位图(我们只关注了低的16位):

若进程是正常退出的,那么status的次低8位就是进程的退出状态,即退出码。若进程是被信号所杀,那么status的低7位表示的就是终止信号了,而第8位就是core dump标志位,即进程终止时是否有进行核心转储操作。

我们这里可以写个代码来验证一下,这里我们还是用父子进程来举例子,我们在代码中父进程创建出一个子进程,子进程的执行过程中出现除0异常,这个时候就会被操作系统终止进行核心转储。此时父进程使用waitpid等待子进程退出,使用ststus来获取出相关信息:

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>int main() {if(fork() == 0) {// 子进程printf("I am running...\n");int t = 100 / 0;exit(0);}// 父进程int status = 0;waitpid(-1, &status, 0);printf("exitcode:%d, core dump:%d, signal:%d\n", (status >> 8), (status >> 7), (status & 0x7f));return 0;
}

我们运行之后可以发现我们的代码是进行了核心转储的,所以说core dump标志就是用来表示进程崩溃时候进行核心转储的。

小扩展

我们可以通过下面的代码来看看我们的组合按键对应的型号类型,也就是使用signal函数来捕捉对应的信号。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>void handler(int signal) {printf("我是%d, 我获得了一个信号%d\n", getpid(), signal);
}int main() {for(int i = 1; i <= 31; i++) {signal(i, handler);}while(1) {sleep(1);}return 0;
}

这个时候,我们就可以知道我们的组合键ctrl + c、ctrl + \和ctrl + z的组合键给前台进程发送的几号信号了。

这个时候可能就有人问了,我们在这样的情况下该如何退出呢?

实际上我们只要发送9号信号就可以是进程退出了:

敲黑板:

我们的信号里面有一些信号是不能被捕捉的,比如这里的9号信号,这样做主要是为了安全性考虑。

✍️通过函数发送信号

kill函数

实际上我们之前调用的kill命令就是通过调用系统函数kill来实现的,函数的原型如下:

int kill(pid_t pid, int sig);

参数说明:

kill函数用来向进程ID为pid的进程发送sig信号,如果信号发送成功了,返回0,否则发送-1。

我们可以使用kill函数来写个模拟kill命令的代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>void usage(char* s) {printf("usage: %s pid signal!\n", s);
}int main(int argc, char* argv[]) {if(argc != 3) {usage(argv[0]);exit(1);}pid_t pid = atoi(argv[1]);int signal = atoi(argv[2]);kill(pid, signal);return 0;
}

我们这里为了更加的美观,可以将当前路径设置进环境变量PATH中去。

此时我们就可以模拟实现一个kill命令了:

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>void usage(char* s) {printf("usage: %s pid signal!\n", s);
}int main(int argc, char* argv[]) {if(argc != 3) {usage(argv[0]);exit(1);}pid_t pid = atoi(argv[1]);int signal = atoi(argv[2]);kill(pid, signal);return 0;
}

我们使用mykill 进程ID 进程编号就可以实现和kill命令一样的效果了。

raise函数

这个函数是用来给当前进程发送信号的,函数的原型如下:

int raise(int sig);

参数说明:

参数就是要发送的信号。

返回值说明:

如果信号发送成功了就返回0,否则就返回一个非零值。

下面我们写个代码来见一见:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>void handler(int signal) {printf("我是%d, 我获得了一个信号%d\n", getpid(), signal);
}int main() {signal(2, handler);while(true) {sleep(1);raise(2);}return 0;
}

运行的结果就是每秒钟都会收到一个2号信号,只不过触发的是信号函数。

abort函数

这个函数比较单一,它是给当前进程发送6号信号(SIGABRT)的,使得当前进程终止,函数的原型如下:

void abort(void);

参数说明:

这是一个无参无返回值的函数。

下面我们写个代码来见一见:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>void handler(int signal) {printf("我是%d, 我获得了一个信号%d\n", getpid(), signal);
}int main() {signal(6, handler);while(true) {sleep(1);abort();}return 0;
}

这里我们会发现一个很神奇的现象,代码并没有像我们预期的那样一直打印我们的函数调用的内容,而是终止了:

敲黑板:

这里的abort函数是通过信号机制终止进程的,即使捕捉了这个信号,进程仍然会被终止掉,因为这个信号的默认行为就是调用abort函数的内部处理程序使进程退出。

✍️通过软件产生信号

SIGPIPE信号

这个信号就是一个由软件产生的信号,我们使用管道通信的时候,读端进程将读端关闭,而写端进程还在向管道中写入,这个时候写端进程就会收到SIGPIPE信号而被操作系统终止。

我们可以写个代码来模拟一下上面这个过程:
 

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>int main() {int fd[2] = {0};if(pipr(fd) < 0) {perror("pipe error");exit(1);}pid_t id = fork();if(id == 0) {// 子进程close(fd[0]);const char* message = "hello father, I am child...";int count = 10;while(count--) {write(fd[1], message, strlrn(message));sleep(1);}close(fd[1]);exit(0);}// 父进程close(fd[1]);close(fd[0]);int status = 0;waitpid(id, &status, 0);printf("child get signal:%d\n", status & 0x7F);return 0
}

运行之后我们发现子进程推出的时候收到了13号信号,就是SIGPIPE信号。

SIGALRM信号

我们可以调用alarm函数给进程设定一个闹钟,经过设置的时间之后操作系统就可以发送一个SIGALRM信号给当前的进程,alarm函数的函数原型如下:

unsigned int alarm(usingned int seconds);

参数说明:

参数就是设置秒数

返回值说明:

  • 调用该函数之前,如果进程已经设置了闹钟,就返回上一个闹钟的剩余时间,并且本次的时间会覆盖掉上一次的时间。
  • 调用该函数之前,如果进程没有设置闹钟,就返回0值。

接下来我们可以写一个代码来见一见:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdbool.h>int main() {int count = 0;alarm(1);while(true) {count++;printf("count = :%d\n", count);}return 0;
}

这里我们可以看到,我们的服务器在一秒内加5万余次,然后就收到信号终止了。

这里也许有人认为5万已经是很大的数了,其实不然,我们在做算法题的时候经常要限制时间在1秒内,否则就会TLE,我们一般笼统的认为一秒内计算机执行的操作是一亿次,所以说这里的5万实际上是很小的。那么为什么呢?这是因为我的代码中存在了大量的IO操作,也就是打印,同时因为是云服务器,网络传输也需要消耗时间,所以这个数才会比较小。

下面是我们改进之后的代码:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdbool.h>
int count = 0;void handler(int signal) {printf("我是%d, 我获得了一个信号%d\n", getpid(), signal);printf("count = :%d\n", count);exit(1);
}int main() {signal(SIGALRM, handler);alarm(1);while(true) {count++;}return 0;
}

这里我们重新运行之后,结果一下子就变形成了5亿。

✍️通过硬件产生信号

我们其实会好奇,为什么我们的程序会崩溃呢?实际上是因为我们的进程收到了来自操作系统发来的信号儿终止的,那么操作系统是怎么识别的呢?

这个问题实际上就是计算机组成原理的基本常识了,我们知道,CPU 内部包含多个寄存器,当我们需要对两个数进行算术运算时,首先会将这两个操作数分别放入两个寄存器中,然后执行运算,并将结果写回寄存器。此外,CPU 中还有一组寄存器称为状态寄存器,用于记录当前指令执行结果的各种状态信息,例如是否发生了进位、溢出等情况。

操作系统作为软硬件资源的管理者,负责在程序运行过程中进行资源调度和异常处理。当操作系统检测到 CPU 内某个状态标志位被设置,并且该标志位是由于某种除以零的错误引起时,操作系统能够识别出是哪个进程引发了该错误。接着,操作系统将该硬件错误封装成信号,并发送给目标进程。具体来说,操作系统会通过查找该进程的 task_struct 结构体,识别出出错的进程,并向该进程的信号位图中写入 8 号信号(即除0错误信号)。一旦信号被写入,进程会在适当的时机被终止,从而避免继续执行错误的操作。

下面我们写一个野指针错误的代码来见一见:

#include <stdio.h>
#include <unistd.h>int main() {printf("I am running...\n");sleep(3);int *p = NULL;*p = 100;return 0;
}

运行效果:

这里我们都知道我们的地址呢实际上是通过页表映射到了,从虚拟内存映射到了物理地址的。从硬件的角度,这个操作实际上是由MMU所做的,它是一个负责处理CPU的内存访问请求的计算机硬件,也就是说MMU是虚拟地址到物理地址映射的中间件,但是这个硬件单元不仅仅是做映射的,还需要有相对应的状态信息,当我们访问到了不属于我们的虚拟地址的时候,MMU在虚拟地址映射的时候就会出错,然后将错误写到自己的状态信息里面,操作系统就会识别到这个信息,于是就会给进程发送SIGSEGV信号了。

http://www.dtcms.com/a/317801.html

相关文章:

  • C++ STL--> vector的模拟实现!
  • smart-water表设计方案
  • jdk-24的安装及环境变量配置
  • LazyLLM教程 | 第3讲:大模型怎么玩:用LazyLLM带你理解调用逻辑与Prompt魔法!
  • 【前端开发】四. JS内置函数
  • 芯片封装(DIP、SOP、QFP、QFN、BGA、LGA、PGA)
  • C++音视频流媒体开发面试题:音视频基础
  • OceanBase DBA实战营2期--自动分区分裂学习笔记
  • 机器翻译:语料库的定义与获取,及语料预处理
  • 安宝特方案丨工业AR+AI质检方案:致力于提升检测精度与流程效率
  • 无人机航拍数据集|第6期 无人机垃圾目标检测YOLO数据集772张yolov11/yolov8/yolov5可训练
  • LeetCode 分类刷题:611. 有效三角形的个数
  • 阿里云 Flink
  • 稀土新贵醋酸镥:高纯度材料的科技密码
  • 机器人定位装配的精度革命:迁移科技如何重塑工业生产价值
  • [特殊字符]企业游学 | 探秘字节,解锁AI科技新密码
  • 智慧养老破局:科技如何让“老有所养”变成“老有优养”?
  • 加载量化模型
  • 7.3 I/O方式 (答案见原书 P315)
  • HashMap 与 ConcurrentHashMap 深度解析
  • Java Stream (二)
  • 【模电笔记】—— 直流稳压电源——稳压电路
  • 从“T+1”到“T+0”:基于SQL构建MES到数据仓库的数据采集通道
  • 嵌入式学习---在 Linux 下的 C 语言学习 Day9
  • 时隔六年!OpenAI 首发 GPT-OSS 120B / 20B 开源模型:性能、安全与授权细节全解
  • PDW分选如何展示到界面上
  • MCU控制ADAU1701,用System Workbench for STM32导入工程
  • 力扣137:只出现一次的数字Ⅱ
  • 周志华院士西瓜书实战(二)MLP+SVM+贝叶斯分类器+决策树+集成学习
  • 一周学会Matplotlib3 Python 数据可视化-图形的组成部分