Linux——进程信号-CSDN博客
文章目录
目录
文章目录
前言
一、创建进程的信号
1、键盘输入方式
2、系统接口
3、指令获得信号
4、硬件异常产生信号
5、软件条件产生信号
二、信号的保存
1、pending表
2、阻塞信号(block位图表)
信号集:(可以说是阻塞集)
信号屏蔽字:
三、信号的捕捉
2. sigaction函数
四、结合地址空间谈信号
信号产生的位置:
总结
前言
上一篇博客讲解了3种创建信号的方式
1、键盘输入的方式;比如ctrl+c等
2、系统调用接口;kill、raise、 abort这3个接口
3、命令行;kill -9 pid
这篇再介绍最后两种方式
一、创建进程的信号
回忆前三种常见信号方式
1、键盘输入方式
在进程运行时,我们输入ctrl+c(2号型号)依次方式获得信号;
2、系统接口
3、指令获得信号
在我们进程在运行时,我们输入指令加上所需要的信号再加需要接受信号的pid即可;
现在我们学习信号获得信号方式
4、硬件异常产生信号
我们在学习c语言、c++还有java时,我们接触过异常这个概念,我们知道被除数不可以为0,还有野指针的存在,一但出现就会出现抛异常,抛了异常进程就会崩溃。
那么为什么出现除零和野指针会让进程奔溃呢?
- 当 CPU 执行到除零操作时,如前所述,硬件(算术逻辑单元 ALU)会检测到这个非法的算术运算。除了可能将状态寄存器的相关标志位(如在某些架构中的溢出标志位或特定的算术异常标志位)置位来表示出现了异常情况外,CPU 会暂停当前指令序列的正常执行流程。这是因为除零操作在数学上是无定义的,硬件无法按照正常的算术规则完成这个运算。
- 然后产生溢出标识位,操作系统检查到让进程奔溃。
- 硬件会产生一个中断信号来通知操作系统内核发生了异常。这个中断信号携带了关于异常类型(除零异常)的基本信息。在 x86 架构中,通过中断向量表(IDT - Interrupt Descriptor Table)来确定如何处理这个中断。当这个除零异常对应的中断向量被触发时,CPU 会跳转到内核预先设置好的中断处理程序入口点。
- 内核的中断处理程序会获取硬件传递过来的异常信息,包括异常发生的指令地址(通过 CPU 的一些寄存器,如在 x86 架构中的 CS 和 EIP 寄存器组合可以确定异常指令的位置)和异常类型代码。内核根据这些信息来确定是哪个进程触发了这个除零异常。
- 对于除零异常,在 Linux 系统中,内核会生成一个
SIGFPE
(浮点异常信号)信号。然后,内核通过进程控制块(PCB)找到与触发异常的进程相关的信息,将SIGFPE
信号发送给该进程。 - 如果进程没有为
SIGFPE
信号注册自定义的信号处理程序,那么操作系统会采用默认的信号处理方式。默认情况下,这个信号会导致进程终止。这是因为除零错误通常被认为是一个严重的错误,表明程序的逻辑出现了问题。而且,继续执行可能会导致不可预测的结果,所以操作系统选择终止进程来防止可能出现的更严重的问题,如数据损坏或系统不稳定。同时,如果系统配置允许,操作系统可能会生成一个核心转储文件(core dump)。这个文件包含了进程在崩溃瞬间的内存映像、寄存器状态等信息,用于后续的调试,以帮助开发者确定除零错误发生的具体原因。
只有操作系统可以管理进行和硬件,cup只是硬件,不可以管理进程和cpu;
我们只需要知道标黄色部分的过程即可;
代码及操作示范
#include<iostream>
#include<unistd.h>
#include<signal.h>using namespace std;int main()
{int x=4;int a=x/0;return 0;
}
我们man 7 signal查看报错码,发现是8号指令
#include<iostream>
#include<unistd.h>
#include<signal.h>using namespace std;
void handler(int sign)
{cout<<"接收到的信号为 "<<sign<<endl;
}
int main()
{signal(8,handler);int x=4;int a=x/0;return 0;
}
我们发现一直在执行性我们信号方法,是因为我们操作系统一直检测cup的 状态寄存器溢出位一直是我们的1或者0,所以一直打印。
#include<iostream>
#include<unistd.h>
#include<signal.h>int main()
{char* ch;*ch=10;cout<<ch<<endl;return 0;
}
野指针报错是一个从进程错误使用指针开始,通过 CPU 执行操作触发内存访问异常,再由操作系统的内存保护机制检测和解释为段错误信号,最终可能导致进程终止的过程。这涉及到操作系统对进程地址空间的管理和保护,以及 CPU 与操作系统之间的硬件异常通知和处理机制,它们共同确保系统的安全性和稳定性。
5、软件条件产生信号
介绍信号在 Linux 操作系统中的重要性,引出软件条件产生信号的话题,说明为什么软件条件产生的信号对于进程控制和通信非常重要。
- alarm () 函数:
- 参数为倒计时时间,返回值为上一个闹钟剩余值
-
void work() {cout << "print log..." << endl; }// 信号为什么会一直被触发?? void handler(int signo) {// work();cout << "...get a sig, number: " << signo <<endl; //我什么都没干,我只是打印了消息// exit(1);int n = alarm(5);cout << "剩余时间:" << n << endl; }int main() {signal(SIGALRM, handler);int n = alarm(50);while(1){cout << "proc is running..., pid: " << getpid() << endl;sleep(1);}sleep(1);return 0; }
unlimited可以为任何数字
- 通常,当进程接收到某些信号且没有处理这些信号时,会产生 Core Dump 文件。例如,以下信号可能会触发 Core Dump:
SIGABRT
(进程异常终止):当程序调用abort()
函数或出现内部错误时,会触发此信号。SIGSEGV
(段错误):当进程试图访问不允许访问的内存区域时,如使用野指针、数组越界、栈溢出等,会触发此信号。SIGFPE
(浮点异常):当发生除零错误、浮点数溢出或其他算术异常时,会触发此信号。SIGILL
(非法指令):当进程执行非法的机器指令时,会触发此信号。
可直接跳转到错误地方
二、信号的保存
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
信号传达也就是handler实现方法
信号未决也就是是否收到某一个信号
信号阻塞就是哪些信号让动
我们的pcb里面有如下几个表
1、pending表
我们发现普通信号是0到31,而那么为了存储这些信号我们可以用一个位图来存储
我们的信号都是发给进程的pcb的而我们信号的cub的结构是有一个signal的整型,它是位图的形式接收信号的,如果有这个信号就是1,没有就是0
比特位的第几位就是几号信号
本质修改信号就是操作系统对task_stauct的信号比特位由0置1
这个记录几号信号的表就是pending这个位图表
代码演示
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void PrintPending(sigset_t &pending)
{for (int signo = 31; signo >= 1; signo--){if (sigismember(&pending, signo)){cout << "1";}else{cout << "0";}}cout << "\n\n";
}void handler(int signo)
{cout << "catch a signo: " << signo << endl;
}int main()
{// 4. 我可以将所有的信号都进行屏蔽,信号不就不会被处理了吗? 肯定的!9sigset_t bset, oset;sigemptyset(&bset);sigemptyset(&oset);for (int i = 1; i <= 31; i++){sigaddset(&bset, i); // 屏蔽了所有信号吗???}sigprocmask(SIG_SETMASK, &bset, &oset);sigset_t pending;while (true){// 2.1 获取int n = sigpending(&pending);if (n < 0)continue;// 2.2 打印PrintPending(pending);sleep(1);}return 0;
}
2、阻塞信号(block位图表)
信号集:(可以说是阻塞集)
-
信号集是一种数据结构,用于表示一组信号。在 Linux 中,使用
sigset_t
类型来表示信号集。可以使用以下函数对信号集进行操作:sigemptyset
:将信号集初始化为空集,即不包含任何信号。sigfillset
:将信号集初始化为包含所有信号。sigaddset
:将一个信号添加到信号集中。sigdelset
:从信号集中删除一个信号。sigismember
:检查一个信号是否在信号集中。
#include <stdio.h>
#include <signal.h>int main() {sigset_t set;// 初始化信号集为空集sigemptyset(&set);// 添加 SIGINT 信号到信号集sigaddset(&set, SIGINT);// 检查 SIGINT 是否在信号集中if (sigismember(&set, SIGINT)) {printf("SIGINT is in the set.\n");} else {printf("SIGINT is not in the set.\n");}return 0;
}
这就是这几个函数基本用法
信号屏蔽字:
-
每个进程都有一个信号屏蔽字,它是一个信号集,其中包含了当前被阻塞的信号。当一个信号被阻塞时,它不会被递送到进程,而是被暂时保存,直到进程解除对该信号的阻塞。
-
可以使用
sigprocmask
函数来检查和修改信号屏蔽字。
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
参数
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信 号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>void handler(int signum) {if (signum == SIGINT) {printf("Received SIGINT signal.\n");}
}int main() {sigset_t set;sigemptyset(&set);sigaddset(&set, SIGINT);// 阻塞 SIGINT 信号if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {perror("sigprocmask");return 1;}// 注册 SIGINT 信号的处理函数signal(SIGINT, handler);// 等待一段时间,此时发送 SIGINT 信号将被阻塞sleep(5);// 解除对 SIGINT 信号的阻塞if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {perror("sigprocmask");return 1;}// 再次等待一段时间,此时发送 SIGINT 信号将被处理sleep(5);return 0;
}
三、信号的捕捉
1. 内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码 是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行 main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号 SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler 和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返 回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复 main函数的上下文继续执行了。
2. sigaction函数
- 函数原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 功能:比
signal
函数更强大,允许更详细地设置信号处理的属性。 struct sigaction
结构体的定义如下:-
struct sigaction {void (*sa_handler)(int);void (*sa_sigaction)(int, siginfo_t *, void *);sigset_t sa_mask;int sa_flags;void (*sa_restorer)(void); };
sa_handler
是信号处理函数指针,与signal
函数中的handler
类似。sa_sigaction
可用于获取更多的信号信息,需要将sa_flags
设置为SA_SIGINFO
。sa_mask
是一个信号集,在执行信号处理函数时将阻塞这些信号。sa_flags
可设置不同的标志,如SA_RESTART
(使某些系统调用在信号处理后自动重启)。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>void my_handler(int signum) {if (signum == SIGINT) {printf("Caught SIGINT signal. Program will exit gracefully.\n");exit(0);}
}int main() {struct sigaction act;act.sa_handler = my_handler;sigemptyset(&act.sa_mask);act.sa_flags = 0;// 注册 SIGINT 信号的处理函数if (sigaction(SIGINT, &act, NULL) == -1) {perror("sigaction");return 1;}while (1) {printf("Program is running...\n");sleep(1);}return 0;
}
- 解释:
- 定义了
struct sigaction
结构体act
,设置sa_handler
为my_handler
。 - 使用
sigemptyset
清空sa_mask
信号集,防止在处理SIGINT
时其他信号的干扰。 - 将
sa_flags
设置为 0,使用默认的信号处理行为。 - 使用
sigaction
函数将act
结构体与SIGINT
信号关联,注册信号处理函数。
- 定义了
四、结合地址空间谈信号
我们操作系统中有很多的地址空间,而一个进程只有一个地址空间,而地址空的4gb空间用户级有3gb,内核1gb,当我们代码调用系统函数时,我们的的身份就变成了内核级,因为然后用完再跳回用户级
信号产生的位置:
- 用户空间触发信号:
- 一些信号可以由用户在用户空间的操作触发,例如:
- 程序错误:程序在用户空间执行时,如果发生错误,例如访问非法内存(使用野指针、数组越界等),会触发硬件异常,硬件将此异常通知给操作系统内核,内核会产生
SIGSEGV
信号。 - 终端按键:当用户在终端按下
Ctrl+C
时,会产生SIGINT
信号,这个操作是在用户空间发起的,但最终由操作系统内核将信号发送给当前正在运行的进程
- 程序错误:程序在用户空间执行时,如果发生错误,例如访问非法内存(使用野指针、数组越界等),会触发硬件异常,硬件将此异常通知给操作系统内核,内核会产生
- 内核空间触发信号:
- 内核可以基于多种条件向进程发送信号,例如:
- 定时器到期:使用
alarm()
或setitimer()
函数设置的定时器,是在内核中进行管理的。当定时器到期时,内核会向相应进程发送SIGALRM
信号。 - 系统调用失败:当进程进行系统调用(如
kill()
或fork()
)时,如果失败,内核可能会发送相应的信号给进程。
- 定时器到期:使用
- 内核可以基于多种条件向进程发送信号,例如:
- 一些信号可以由用户在用户空间的操作触发,例如:
-
内存保护机制:
- 操作系统的内存管理单元(MMU)通过页表将进程的虚拟地址空间映射到物理内存,并进行内存保护。当信号处理函数执行时,MMU 会确保它遵守地址空间的访问规则:
- 如果信号处理函数试图访问未分配的内存或超出地址空间范围的地址,会触发
SIGSEGV
信号。 - 如果信号处理函数试图修改只读区域(如代码段),也会触发
SIGSEGV
信号。
- 如果信号处理函数试图访问未分配的内存或超出地址空间范围的地址,会触发
- 操作系统的内存管理单元(MMU)通过页表将进程的虚拟地址空间映射到物理内存,并进行内存保护。当信号处理函数执行时,MMU 会确保它遵守地址空间的访问规则:
-
信号处理函数的安全考虑:
- 为保证地址空间的安全性和程序的稳定性,信号处理函数应该:
- 尽可能简洁,避免长时间占用处理器,防止影响进程的正常执行流程。
- 避免使用不可重入的函数,除非采取了特殊的保护措施,防止栈溢出和数据不一致。
- 避免修改正在使用的共享资源,除非使用了适当的同步机制,因为信号可能在任何时候触发,导致并发问题。
- 为保证地址空间的安全性和程序的稳定性,信号处理函数应该:
地址空间为进程提供了内存资源的存储和组织方式,而信号是操作系统用于进程间通信、进程控制和异常处理的一种机制。信号处理函数在进程的地址空间中运行,需要遵守地址空间的访问规则,同时可以对地址空间中的不同部分(代码段、数据段、堆、栈)进行操作。理解它们之间的逻辑关系对于编写稳定、可靠的程序,特别是在并发和异常处理方面非常重要。通过合理使用信号和处理好地址空间的关系,可以更好地实现进程的管理和资源的保护。
总结
信号是操作系统提供的一种强大的机制,它可以让进程响应各种事件,从用户操作到硬件异常,同时也支持进程间通信和进程的控制。合理使用信号处理函数、理解信号阻塞和信号集操作、关注信号处理函数的可重入性以及处理好与地址空间的关系,可以使程序更加健壮和可靠。通过掌握信号的产生、处理和应用,开发人员可以更好地实现进程管理、异常处理和资源管理,提高程序的性能和稳定性。
信号机制在 Linux 等操作系统中是一个复杂而重要的部分,涉及操作系统的多个方面,包括进程管理、内存管理和异常处理。熟练掌握信号的使用和处理对于编写高质量的系统级程序至关重要。