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

【Linux系统篇】:什么是信号以及信号是如何产生的---从基础到应用的全面解析

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:Linux篇–CSDN博客

在这里插入图片描述

文章目录

  • 一.信号概念
    • 1.生活角度的信号
    • 2.技术应用角度的信号
    • 3.补充内容
      • 前台进程和后台进程
      • 硬件中断
  • 二.信号产生
    • 1.键盘组合键
    • 2.kill命令
    • 3.系统调用
    • 4.异常
    • 5.软件条件
  • 三.补充内容--核心转储

一.信号概念

1.生活角度的信号

在平常的生活中,信号无处不在,比如说上下课铃声,红绿灯,快递取件码,外卖电话,闹钟等等。

1.对于这些生活中的信号,我们是如何认识这些信号的

当然是有人教我们怎么识别信号,以及信号到来后怎么处理信号

2.即便是当前没有信号产生,我们也知道信号产生后应该做什么

比如点外卖,我们平常点外卖都是下单之后,过一段时间骑手才能给我们送过来,当我们收到骑手的电话后,我们就知道外卖到了要下楼取餐。但是我们在等待的这个过程中,骑手还没有给我们打电话之前,我们依然知道收到电话之后要做什么。

3.信号产生之后,我们可能并不会立即处理这个信号,而是选择在合适的时候处理,因为我们可能正在做其他更重要的事情。所以,信号产生之后直到信号处理时,存在一个时间窗口,在这个时间窗口这一时间段内,我们必须记住信号到来

还是点外卖这个例子,我们在收到骑手的电话后,可能并不是立即下楼取餐,比如此时我们还在打游戏,中间不能断,直到游戏结束后,我们才下楼取餐。在接收到骑手电话直到游戏结束,这个时间段就是一个时间窗口,在这个时间窗口中我们需要记住外卖到了,然后游戏结束后再处理。

2.技术应用角度的信号

如果站在技术应用角度看待信号,同样可以根据上面的内容得出结论:

在计算机中,程序也就是进程代表的就是我们用户的身份。

1.进程必须可以识别以及处理信号

2.进程即便是没有收到信号,也能知道哪些信号该怎么处理。信号的处理能力,属于进程内置功能的一部分

3.当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,而是选择在合适的时候处理

信号的处理方式:

  • 默认动作
  • 忽略
  • 自定义动作

4.进程在收到信号后,到信号开始被处理的时候,也一定会有一个时间窗口,进程具有临时保存哪些信号已经发生的功能

举个例子:

写一个死循环的程序,在命令行上采用前台进程运行;当用户在进程运行的过程中,按下Ctrl+c键,该进程就会终止退出

#include <iostream>
#include <unistd.h>
using namespace std;int main(){while(true){cout << "I am a process" << endl;sleep(1);}return 0;
}

在这里插入图片描述

直接说明结论:Ctrl+c键的本质是被进程解释成为收到了2信号,前面提到过进程对信号的处理方式有三种,如果没有设置自定义动作,进程就会采用默认动作处理信号,而2号信号的默认动作就是终止退出

下面图中表示的是所有的信号,其中1~31个是普通信号,而34~64则是实时信号(需要理解处理),后面讲解信号时主要围绕前31个普通信号来讲解。

信号以宏定义的形式表示,本质上就是数字编号

在这里插入图片描述

验证上面的结论,Ctrl+c组合键本质上是发送2号信号,并且默认处理动作是终止退出:

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

功能:用于修改特定进程对于指定信号的处理动作,也就是自定义动作,这个过程叫做信号捕捉

参数

  • sig:信号编号或者信号名。
  • handler:自定义处理函数指针,或特殊值 SIG_IGN(忽略信号,表示的是数字1)和 SIG_DFL(恢复默认行为,表示的是数字0)。

返回值:成功时返回之前的处理函数指针,失败时返回 SIG_ERR

测试代码:

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;void handler(int signo){cout << "process get a signal: " << signo << endl;
}int main(){signal(2, handler);while (true){cout << "I am a process, pid: " << getpid() << endl;sleep(1);}return 0;
}

用signal函数设置自定义动作,捕捉2号信号,因为自定义动作只是输出一条语句,所以处理完后(函数调用结束),并没有终止退出,因此可以验证2号信号的默认动作就是终止退出。

在这里插入图片描述

  • signal函数的注意事项

1.信号处理的三种方式只能三选一,使用自定义动作,就不能再执行默认动作;

2.signal函数只需要设置一次,在进程整个生命周期往后都会有效;

3.signal函数中设置的自定义动作函数handler是在执行后续代码的过程中收到信号才调用,没有收到就不会调用;

4.对于普通信号,除了9号和19号信号不能自定义动作捕捉外,其他都可以(可以将所有普通信号使用signal函数捕捉,然后依次发送每个信号进行测试);

5.多个信号可以设置同一个自定义动作,所以handler函数需要带一个参数,用来指定具体的目标信号。

3.补充内容

前台进程和后台进程

前台进程和后台进程是操作系统中两种不同的进程运行方式,在Linux中,一次登录,一个终端,一般会配上一个命令行解释器bash,每一个登录,只允许一个进程是前台进程,但是可以允许多个进程是后台进程。

  • 前台进程

    • 在终端中直接启动命令时默认为前台运行;

    • 一旦前台进程启动后,bash进程变成后台进程,就不再接收任何指令了,需要等待其完成才能执行其他指令

    • 前台进程直接与用户交互,可以接受用户的输入(比如键盘输入)

    • 可以通过Ctrl+c组合键终止

在这里插入图片描述

  • 后台进程
    • 在Linux中,可以通过在命令行后添加&符号来启动后台进程(比如./test &
    • 一旦后台进程启动后,bash进程还是前台进程,可以继续执行用户输入的指令
    • 后台进程不与用户直接交互,通常无法接受用户输入
    • 无法通过Ctrl+c组合键终止

在这里插入图片描述

前后台进程的区分就是谁来获取键盘的输入,键盘输入首先是被前台进程收到的

因为前台进程可以接收到用户输入的Ctrl+c组合键,通过硬件中断转变为接收到2号信号,执行默认动作处理从而使进程终止退出。

硬件中断

在上面的讲解例子中,键盘数据是如何输入给内核的,Ctrl+c组合键又是如何变成信号的?这就需要来了解一下关于硬件部分了。

进程是由CPU来执行的,在数据层面上,CPU不和外设直接交互;但是在控制层面上,CPU可以和外设直接交互,当外设某种事件完成后,就可以发起硬件中断,经过中断单后,向CPU发送中断信号,CPU根据中断请求线的电平变化来接收中断信号。

通过中断机制,外设可以需要时获得CPU的注意力,而不需要CPU不断检查外设状态,这样设计可以使CPU能够高效管理多个外设,而不必浪费时间在不需要服务的设备上

此外在内存中存储了一张中断向量表,中断向量表中存放的是中断服务程序的入口地址(也就是函数指针),系统通过CPU收到的中断信号,以中断信号为索引到表中找到对应的处理中断方法,然后执行对应的操作。

当我们从键盘中输入时,键盘被按下,存有数据时,键盘立刻会发起硬件中断,通过中断单元向CPU发送中断信号,中断信号被CPU保存下来,同时操作系统会立刻读取中断信号,以此为索引到中断向量表中找到对应的处理中断方法(从键盘读取到内存中),键盘上的数据在读取到内存上时,系统会进行判断,如果是控制类型的数据,比如Ctrl+c,系统就会将2号信号发送给进程。进程收到2号信号后,如果没有自定义捕捉,就会执行默认动作终止退出。

操作系统就是不断接受中断信号来处理外设的

对于外设和CPU的直接交互也仅仅局限于控制层面上的

除了有硬件中断,当然还存在软件中断:信号就是用软件方式,对进程模拟的硬件中断,虽然两者没有任何关联,但设计方式却是很相似(后面信号保存的时候再讲解)

当程序正在执行代码时,可能会产生信号,信号的产生和我们自己写的代码运行是异步的。信号就是进程之间事件异步通知的一种方式—属于软中断

二.信号产生

1.键盘组合键

常用的几个:

1.Ctrl+c

  • 信号SIGINT,信号编号为2
  • 默认行为:终止进程
  • 用途:强制中断前台进程的执行

2.Ctrl+\

  • 信号SIGQUIT,信号编号为3
  • 默认行为:终止进程并生成核心转储(暂时了解即可,后面会讲什么是核心转储)
  • 用途:强制终止进程并生成调试用的核心文件。

3.Ctrl+z

  • 信号SIGSTOP,信号编号为19
  • 默认行为:暂停进程,将进程放入后台(无法被捕捉)
  • 用途:暂停进程

2.kill命令

通过kill指令同样可以给指定进程发送信号:

kill [-信号编号或信号名] PID

注意:如果省略信号参数,或默认向指定进程发送SIGTERM(编号为15),优雅的终止进程。

常用信号及其用途

信号名编号默认行为典型场景
SIGTERM15优雅终止进程允许进程清理资源后退出
SIGKILL9强制终止进程立即杀死无响应的进程
SIGINT2终止进程(同 Ctrl+C手动终止前台进程
SIGSTOP19暂停进程(不可被捕获)调试时冻结进程状态
SIGCONT18恢复已暂停的进程继续运行被 SIGSTOP 暂停的进程

3.系统调用

1.kill函数

#include <signal.h>int kill(pid_t pid, int sig);

功能

向指定进程发送目标信号(类似于指令kill发送信号)。

参数

  • pid:发送信号的目标进程
  • sig:要发送的信号名或信号编号

返回值

成功返回0,失败返回-1并设置errno。

通过kill函数实现一个自己的kill指令:

signal.cc文件:

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;void handler(int signo){cout << "process get a signal: " << signo << endl;
}int main(){// 所有普通信号全部捕捉for (int i = 1; i < 31; i++){signal(i, handler);}while (true){cout << "I am a process, pid: " << getpid() << endl;sleep(2);}return 0;
}

mykill.cc文件:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
using namespace std;void handler(int signo){cout << "process get a signal: " << signo << endl;exit(0);
}int main(int argc, char *argv[]){if(argc!=3){cout << "Usage:\n\t" << "kill signal pid\n" << endl;exit(1);}int signum = stoi(argv[1]);pid_t pid = stoi(argv[2]);int n = kill(pid, signum);if(n == -1){perror("kill");exit(2);}return 0;
}

在这里插入图片描述

2.raise函数

#include <signal.h>int raise(int sig);

功能

向调用这个函数的当前进程发送目标信号(等价于kill(getpid(), sig))。

参数

sig:要发送的信号名或信号编号

返回值

成功返回0,失败返回-1并设置errno。

raise函数发送的信号,被捕捉后,只会执行自定义动作:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
using namespace std;void handler(int signo){cout << "process get a signal: " << signo << endl;exit(0);
}int main(){signal(2, handler);int cnt = 5;while (true){cout << "I am a process, pid: " << getpid() << endl;sleep(1);cnt--;if(cnt==0){raise(2);}  }return 0;
}

这里对2号信号自定义动作添加了退出语句,所以执行完后回退出:

在这里插入图片描述

3.abort函数

#include <signal.h>int abort(void);

功能

向调用这个函数的当前进程发送6号信号(等价于kill(getpid(), 6))。

无参数无返回值

abort函数和raise不一样,被捕捉后,如果自定义动作没有退出,执行完自定义动作,还会执行默认动作,终止退出:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
using namespace std;void handler(int signo){cout << "process get a signal: " << signo << endl;// exit(0);
}int main(){signal(6, handler);int cnt = 5;while(true){cout << "I am a process, pid: " << getpid() << endl;sleep(1);cnt--;if(cnt==0){abort();}}return 0;
}

对6号信号的自定义动作取消退出,但是最后依然会终止退出,就是因为被捕捉后,如果自定义动作没有退出,执行完自定义动作,还会执行默认动作,终止退出

在这里插入图片描述

4.异常

在刚开始学习进程的时候,在进程终止的那里讲解过进程有三种退出情况:1.代码执行完,结果正确退出;2.代码执行完,结果不正确退出;3.执行过程中出现错误,异常退出,代码没有执行完。

现在学到信号这里,就可以明白,进程在执行过程中出现错误,本质上是收到了对应的异常信号,因为信号的默认动作是终止退出,所以当前进程就结束执行退出了。

举例:

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;int main(){// 除零错误// int a = 10;// int b = 0;// int c = a / b;// cout << c << endl;// 野指针错误// int *p = nullptr;// *p = 10;// cout << *p << endl;// return 0;
}

在这里插入图片描述

这两个错误分别对应信号SIGFPE(浮点异常)信号编号为8,以及信号SIGSEGV(段错误)信号编号为11.

任何异常只会影响当前进程本身,并不影响整个操作系统以及其它进程

这是因为CPU执行当前进程时,CPU寄存器中的数据只属于当前进程的上下文;

异常信号可以来自硬件异常(比如除0或者野指针错误),通常由CPU指令执行失败触发中断,系统是软硬件的管理者,系统就会知道对应的硬件错误信息,然后向当前进程发送对应的异常信号,当前进程收到异常信号,就会执行默认动作终止退出;而CPU则会获取下一个进程的上下文开始执行,所以并不会受到影响。

如果进程捕获了硬件异常信号但未终止退出,CPU会恢复该进程的上下文,使其从触发异常的指令位置继续执行。如果错误原因未修复,CPU调度执行该进程时,重新执行同一指令会再次触发异常,导致信号循环发送,形成死循环。

以上面的除0错误,进行捕捉但不退出为例:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
using namespace std;void handler(int signo){cout << "process,pid: "<<getpid()<<" get a signal: " << signo << endl;// exit(0);
}int main(){// 硬件异常// 除零错误signal(8, handler);int a = 10;int b = 0;int c = a / b;cout << c << endl;return 0;
}

在这里插入图片描述

所以异常信号并不是为了让用户解决问题,而是提示用户当前进程终止退出的错误原因,尽管可以捕捉设置自定义动作,但是如果不退出就会一直报错陷入死循环,并不能解决什么问题,所以对于大部分处理最后都是要终止进程

5.软件条件

异常不光只会由硬件产生,也可能会是软件。

比如之前讲解进程通信时的管道,当通过管道进行通信的两个进程,读端关闭,写端就不能再继续写入,系统就会给写入的进程发送异常信号然后终止退出。

进程由系统进行管理,管道也是由系统创建的共享资源,所以这种就属于软件上的异常;除了这个,还有向未打开的文件进行写入和读取时也是属于软件上的异常。

而软件上除了异常可以产生信号,还有特殊事件—软件条件,比如闹钟就是一种软件条件

在操作系统中,“闹钟”是一种定时器机制,主要通过SIGALRM信号(编号为14)实现:

闹钟是进程可以设置的一种定时器,当指定时间到时,系统会向该进程发送14号信号,在Linux中通过alarm函数实现

#include <unistd.h>unsigned int alarm(unsigned int seconds);

参数seconds指定多少秒后向该进程发送14号信号

返回值:闹钟响时剩余的秒数,如果没有则返回0;如果连续设置多个闹钟,返回值就是上一次闹钟的剩余时间。

进程可以捕捉该信号并自定义处理函数,默认处理会终止进程;闹钟的存在可以实现超时处理机制或者定时执行某些操作

示例:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
using namespace std;void handler(int signo){cout << "process,pid: "<<getpid()<<" get a signal: " << signo << endl;
}int main(){// 软件条件---闹钟signal(14, handler);int n = alarm(5);while(true){cout << "I am a process, pid" << getpid() << endl;sleep(1);}return 0;
}

在这里插入图片描述

闹钟不属于软件异常,是系统层面上的一种软件条件

上面5种就是常见的产生信号方式,最后总结一下:无论是哪种方式产生信号,本质上都是由系统发送给进程的,因为操作系统是进程的管理者

三.补充内容–核心转储

下面图中是所有的普通信号,其中大多数信号默认处理动作都是终止退出,但是有的是Term类型的退出(比如2号,9号等),有的是Core类型的退出(比如8号,11号等)

在这里插入图片描述

其中core类型的终止退出表示具有核心转储功能。

1.什么是核心转储

在父进程回收子进程,也就是调用waitpid函数时,用一个输出型参数int status用来表示子进程的退出信息。

其中有一个core dump标志位,如果当前进程收到的异常信号是Trem类型该标志位上就是0,Core类型的就是1。

用一段代码进行测试,父进程创建子进程后,在子进程运行时,向子进程分别发送Trem类型的2号信号和Core类型的8号信号,然后父进程回收子进程打印出status,观察两个不同类型信号最后status的core dump标志位。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;int main(){pid_t id = fork();if(id==0){// childint cnt = 100;while(cnt--){cout << "I am a child process, pid: " << getpid() << endl;sleep(1);}exit(0);}// fatherint status = 0;pid_t rid = waitpid(id, &status, 0);if(rid==id){cout << "child quit info, rid: " << rid << " exit code: " << ((status >> 8) & 0xFF) <<" exit signal: " << (status & 0x7F) <<" core dump: " << ((status >> 7) & 1) << endl;}return 0;
}

在这里插入图片描述

因为我这里使用的是云服务器,而云服务器上的core功能默认是关闭的,所以会看到两个都是0。

想要打开需要在命令行中输入下面的指令:

#查看特定的资源
ulimit -a#其中有一个core file size,大小为0,表示关闭
ulimit -c#开启core dump 设置对应的大小
ulimit -c 10240#关闭core dump 设置大小为0
ulimit -c 0

在这里插入图片描述

打开core dump之后再重新发送2和8号信号:

在这里插入图片描述

发送8号信号,core dump标志位变为1,并且生成了一个core.27353的文件。

打开core dump功能后,一旦进程出现异常,系统会将该进程在内存中的运行信息,dump(转储)到进程的当前目录(磁盘),并且形成core.pid文件,这就是核心转储功能

2.为什么要进行核心转储

一旦进程出现错误而异常终止,前面提到过异常信号只是让用户知道进程终止退出的错误原因,而核心转储可以具体定位原始代码中,在运行过程中哪方面出错误。

用一个简单的除0错误代码来测试:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;int main(){int a = 10, b = 0;int c = a / b;      // 11行这里的除0错误cout << c << endl;return 0;
}

使用g++编译时带上-g选项,编译后生成可执行文件并运行,最后除0错误而终止退出,生成一个core.1263文件,打开gdb进入调试

直接输入core-file core.1263后,就会自动打印出错误信息,收到8号信号而终止退出,出错原因在第11行。

在这里插入图片描述

核心转储功能加上调试,可以直接复现问题之后,直接定位到出错行,也就是先运行,再core-file事后调试。

所以核心转储功能就是方便进行事后调试。

以上就是关于信号第一部分:信号概念以及信号产生的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!

相关文章:

  • echart实现柱状图并实现柱子上方需要显示指定文字,以及悬浮出弹框信息,动态出现滚动条,动态更新x,y轴的坐标名称
  • linux sudo 命令介绍
  • NVIDIA高级辅助驾驶安全报告解析
  • 差分信号抗噪声原理:
  • 浔川代码编辑器v2.0(测试版)更新公告
  • 基于事件驱动的云原生后端架构设计:从理念到落地
  • 【多源01BFS】Codeforce:Three States
  • 基于Vulkan Specialization Constants的材质变体系统
  • JDK(java)安装及配置 --- app笔记
  • 低代码平台开发胎压监测APP
  • redis经典问题
  • 【星海出品】Calico研究汇总
  • hackmyvm-atom
  • 快速体验tftp文件传输(嵌入式设备)
  • 位运算题目:解码异或后的排列
  • PostgreSQL 数据库备份与恢复全面指南20250424
  • Dockerfile指令
  • 知识图谱火了?
  • 【Java面试笔记:进阶】16.synchronized底层如何实现?什么是锁的升级、降级?
  • 医学图像(DICOM数据)读取及显示(横断面、冠状面、矢状面、3D显示)为什么用ITK+VTK,单独用ITK或者VTK能实一样功能吗?
  • 中宣部版权管理局:微短剧出海面临版权交易不畅、海外维权较难等难题
  • 神舟二十号载人飞船成功飞天,上海航天有何贡献?
  • 第四届全民阅读大会在太原举办,李书磊出席并讲话
  • 湖南永州公安全面推行“项目警官制”,为重点项目建设护航
  • 商务部:一季度社零总额12.47万亿元,同比增长4.6%
  • 蚌埠一动物园用染色犬扮熊猫引争议,园方回应:被投诉已撤走