


目录
[2-1 🍟信号的概念](#2-1 🍟信号的概念)
[2-2 🍔信号的处理方式](#2-2 🍔信号的处理方式)
[2-3 🌭信号的产生方式](#2-3 🌭信号的产生方式)
[2-4 🍳系统调用产生信号](#2-4 🍳系统调用产生信号)
[2-5 🥐软件条件产生信号](#2-5 🥐软件条件产生信号)
[2-5 🍞异常产生信号](#2-5 🍞异常产生信号)
[3-2 🥪异常信号的理解](#3-2 🥪异常信号的理解)
前言
在 Linux 操作系统的进程管理体系中,信号是贯穿始终的核心通信与控制机制,它像一套精密的 "指令集",协调着进程间的交互、响应系统事件并处理异常场景。理解信号的运作逻辑,是打通进程管理、系统调用及异常处理等核心知识点的关键纽带🗽。
本节将围绕信号的核心脉络展开,从基础概念切入,逐步深入信号的产生、阻塞与递达机制,再到信号捕捉的实践方法,同时关联中断原理、系统调用机制及可重入函数等关键知识点,最终结合 SIGCHLD 等具体信号场景落地实践,助力构建完整的 Linux 进程信号知识体系。
一、🤔什么是信号
1-1🍕生活中的信号
在生活中存在各种各样的信号,例如:红绿灯、闹钟、手势......每当我们接收到一个信号,我们就会执行对应的操作,例如红灯停、绿灯行......
为什么我们会对不同的信号有对应的执行动作呢?原因是:
- 我们能够识别一个信号,知道其中的含义;
- 我们从小接受的教育告诉我们应当如何去做;
***但是我们收到一个信号之后必须去执行相应的动作吗?***那也不一定。
- 假设你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能"识别快递"。
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成"在合适的时候去取"。
- 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你"记住了有一个快递要去取"。
- 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)。
- 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话。
🚀技术应用角度的信号
在Linux中,我们常常通过键盘按下Ctrl + c 来终止一个前台进程。
用户按下Ctrl + c,这时键盘会产生一个
硬件中断,被OS获取,解释为信号,发送给目标进程,前台进程因为收到这个信号,进而引起进程退出。前台进程与后台进程
./filename 启动一个进程,该进程为
前台进程,在这条命令后加一个&可以将该进程放到后台运行;
bash$ ./filename # 启动后,进程在前台运行 $ ./filename & # 启动后,进程在后台运行注意
:
Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
Shell可以同时运行 一个前台进程 和任意 多个后台进程,只有 前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。在许久之前,我们也曾用过kill -9 的指令来终止一个进程,它的本质也是给进程发送 9 号信号来让进程终止。
二、🧐信号及信号的产生
2-1 🍟信号的概念
信号是进程之间事件异步通知的一种方式,属于 软中断 。
Linux中存在许多信号,我们可以使用 kill -l命令查看Linux中有哪些信号:
Linux中,有 62种信号 ,前31种(1~31)信号被称为 标准信号 ,每个信号都有特殊的含义及用途。后31种(34~64)信号被称为 实时信号 ,可用于实时应用程序的拓展信号,提供了更多的灵活性。注意中间并没有32和33号信号 。
我们可以通过man手册来查询不同信号的含义man 7 signal:
并且每个信号的编号都有自己的名字,这些 名字 其实就 是 C 语言的 宏 ,如果调用信号,既可以通过信号的名称调用,也可通过信号的编号调用。当然,这么多的信号并不需要你全部记下来,我们在运用的过程中就会知道哪些信号常用,哪些不常用。这些宏定义可以在
signal.h中找到。
2-2 🍔信号的处理方式
信号的 常见处理方式 有以下几种:
- 忽略此信号。
- 执行该信号的默认处理动作。
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。
我们以信号的方式来终止进程,我们通过一下代码进行测试:
cpp#include <iostream> #include <unistd.h> #include <sys/types.h> int main() { while(true) { std::cout << "I'am a proc, I'm running now..., pid: " << getpid() << std::endl; sleep(1); } return 0; }我们使用9号信号 和2号信号杀死了进程,这种发命令做出对应行为的方式 就是进程执行了信号的默认动作。就像我们潜意识知道在十字路口红灯停、绿灯行一样。
1️⃣信号捕捉(Catch)
但是今天我们以 信号捕捉 的方式来处理发来的信号,我们就需要用到 signal 接口:
cpp#include <signal.h> void (*signal(int signum, void (*handler)(int)))(int);
- signum参数 :传入需要捕捉的信号(名字或编号),当进程收到与其相匹配的信号时则会调用第二个参数,否则不会有任何动作。
- handler参数 :handlder方法,此方法为自定义方法,当收到signum信号则不会执行该信号的默认动作,变为执行该方法。
- 返回值 :返回前一个信号处理方法。
值得注意的是,我们在设置信号捕捉时,并不需要将此接口放入循环之中,只需要调用该接口一次,在整个程序中则一直循环有效。我们设置一个signal方法做测试:
cpp#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> void handler(int signo)// 回调函数,收到信号则执行handler方法 { std::cout << "get a sig, number is: " << signo << std::endl; } int main() { signal(SIGINT, handler);// 设置信号捕捉 while(true) { std::cout << "I'am a proc, I'm running now..., pid: " << getpid() << std::endl; sleep(1); } return 0; }在这个例子中,程序注册了一个用于处理
SIGINT信号的处理函数handler。当用户按下Ctrl+C发送SIGINT信号时,程序不会终止而是会调用这个处理函数来执行相应的操作。注意
- 9号信号是不能被捕捉、阻塞和忽略的。
*** signal函数执行时并不会立刻对handler执行回调,只有当收到对应的信号时,才会执行回调。*** 比如,你跟室友打赌输了,接下来一周等红绿灯需要听室友的,本来红灯停变为了红灯唱歌,但是我们并不知道室友什么时候让你唱歌。也就是说,因为信号是异步触发,所以只有收到信号时才会执行回调
信号的默认处理方式还剩下忽略信号,也就是信号发送到进程,但是进程对其不管不顾。在程序中,使用捕捉的方式实现信号忽略,那么signal第二个参数应该调用 SIG_IGN (signal ignore):
在底层,SIG_IGN 是将整形1强转为 __sighandler_t 类型,于是就可以告诉OS,进程运行时以忽略的方式将 signal 函数的第一个参数忽略:
上⾯的所有内容,我们都没有做⾮常多的解释,主要是先⽤起来,然后渗透部分概念和共识,下⾯我 们从理论和实操两个层⾯,来进⾏对信号的详细学习、论证和理解。为了保证条理,我们采⽤如下思 路来进⾏阐述:
2-3 🌭信号的产生方式
2-3-1🐕键盘输入
前面我们已经提到了两种信号产生 的方式:键盘、kill 指令。
当你在键盘上按下Ctrl+C 时,这个动作实际上会产生一个中断信号,这个信号被称为SIGINT(Interrupt Signal)。操作系统内核(kernel)通过键盘驱动程序来监测键盘的输入事件。当Ctrl+C组合键被按下,键盘驱动程序会通知内核,内核然后生成一个SIGINT信号并将其发送给与当前前台进程相关联的终端。
以下是简要的工作流程:
键盘特定输入 ------> OS解释为信号 ------> 向目标进程发送信号 ------> 进程收到信号 ------> 进程做出响应
- 键盘输入: 当你按下键盘上的键时,键盘控制器检测到这个事件,并将相应的扫描码发送到计算机。
- 中断请求(IRQ): 键盘控制器通过硬件中断请求(IRQ)通知CPU有一个新的中断事件发生。
- 中断服务程序: 操作系统内核中有一个与键盘输入相关的中断服务程序,它被调用以处理键盘中断。
- 生成信号: 中断服务程序检测到Ctrl+C组合键后,它会生成一个SIGINT信号。
- 信号传递: 生成的SIGINT信号被发送给当前前台进程的进程组。前台进程是与终端相关联的活跃进程。
- 信号处理: 如果前台进程注册了SIGINT的信号处理函数,该函数将被调用以执行相应的操作。如果没有注册处理函数,则默认操作是终止进程。
- 总体来说,键盘输入被硬件中断机制捕获,通过中断服务程序和信号机制,通知操作系统内核产生了一个SIGINT信号,最终传递给前台进程。
比如:当我们把二号信号进行捕捉,并且回调函数只对信号进行打印动作,我们再使用 Ctrl-C 就杀不死该进程了:
而键盘中并不只有 Ctrl-C 组合,还有 Ctrl-\ 组合,这里我使用的就是 Ctrl-\ 退出进程的,其对应信号的 3号信号。
2-4 🍳系统调用产生信号
我们可以用kill 想指定进程发送指令。我们除了可以直接使用kill指令 向进程发送信号外,还可以在程序中调用kill函数来进行指令发送。
**kill 函数是一个用于向指定进程发送信号的系统调用。它可以用于向指定进程发送任何一个有效的信号,例如终止进程、挂起进程、继续执行进程等。**以下是 kill 函数的基本信息:
cpp#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);参数:
- pid:要发送信号的目标进程的进程ID。
- sig:要发送的信号的编号,可以是标准信号(如 SIGKILL)或用户自定义的信号。
返回值:
- 如果成功,返回0;如果失败,返回-1,并设置相应的错误码(errno)。
功能:
- kill 函数用于向指定进程发送信号。信号可以是预定义的标准信号(如 SIGKILL、SIGTERM)或用户自定义的信号。通常用于进程间通信、控制进程的行为,或者强制终止进程等。
注意事项:
- 如果将 pid 参数设置为0,则信号会发送给调用进程的进程组中的所有成员。
- 如果将 pid 参数设置为-1,则信号会发送给调用进程有权发送信号的任意进程(权限通常由 effective user ID 决定)。
- 如果发送 SIGKILL 信号(编号为9),则表示强制终止目标进程,目标进程将无法捕获或忽略该信号。
我们编写一段代码测试kill调用:
cpp#include <iostream> #include <unistd.h> #include <signal.h> #include <cerrno> #include <cstring> #include <sys/types.h> int main(int argc, char* argv[]) { if(argc != 3)// 参数控制为三个 { std::cout << "Usage: " << argv[0] << " -signumber pid" << std::endl; return 1; } // argv[0]: ./testSig 运行可执行程序 // argv[1]: 发送的信号 // argv[2]: 进程pid int signumber = std::stoi(argv[1]);// argv[1] int pid = std::stoi(argv[2]);// argv[2] int n = kill(pid, signumber); if(n < 0) { std::cerr << "kill error, " << strerror(errno) << std::endl; } return 0; }除了kill 系统调用之外,Linux还提供了一个 raise 接口:
cppint raise(int sig);
- 功能 :传入信号给当前进程(调用此接口的进程)。
- sig参数 :需要传入的信号。
- 返回值 :0表示成功,非0为失败。
我们编写一段代码测试raise调用:
cpp#include <iostream> #include <unistd.h> #include <signal.h> #include <cerrno> #include <string> #include <cstring> #include <sys/types.h> int main(int argc, char* argv[]) { int cnt = 0; while(true) { std::cout << "cnt: " << cnt++ << std::endl; sleep(1); if(cnt == 5)// 5s 后退出进程 { std::cout << "send 9 signal to caller" << std::endl; raise(9); } } return 0; }除了raise系统调用之外,Linux还提供了一个 abort接口:
cppvoid abort(void)
- 功能 :对自己发送指定的信号,为6号信号 SIGABRT。
我们编写一段代码测试abort调用:
cpp#include <iostream> #include <unistd.h> #include <signal.h> #include <cerrno> #include <string> #include <cstring> #include <sys/types.h> int main(int argc, char* argv[]) { int cnt = 0; while(true) { std::cout << "cnt: " << cnt++ << std::endl; sleep(1); if(cnt == 5)// 5s 后abort进程 { std::cout << "send 6 signal to caller" << std::endl; abort(); } } return 0; }
2-5 🥐软件条件产生信号
我们曾经学过管道,而管道四种特性有一种是 当没有读端时,写端会被终止。管道不具备写的软件条件了,所以触发了终止信号,这种信号为 SIGPIPE(13号信号) 。
除此之外,由软件条件产生的信号还有alarm 函数 和 SIGALRM(14号信号) 信号:
cpp#include <unistd.h> unsigned int alarm(unsigned int seconds);
参数:
seconds:设置定时器的时间间隔,单位是秒。当定时器计时到达指定的秒数后,将发送SIGALRM信号给进程。
返回值:
- 返回之前设置的剩余秒数,如果之前没有设置定时器,则返回 0。
功能:
- alarm 函数用于设置一个定时器,当定时器计时到达指定的秒数后,进程将收到
SIGALRM信号。该信号默认会终止进程,但可以通过注册信号处理函数来改变其行为。
注意事项:
- 如果之前已经设置了定时器,调用
alarm函数将取消之前的定时器,并用新的时间间隔重新设置。- 如果将
seconds参数设置为 0,表示取消之前的定时器,即不再发送SIGALRM信号。我们在代码上直观感受一下闹钟的响应:
cpp#include <iostream> #include <unistd.h> #include <signal.h> #include <cerrno> #include <string> #include <cstring> #include <sys/types.h> int main(int argc, char* argv[]) { // 设置一个闹钟 alarm(1); int cnt = 0; while(true) { std::cout << "cnt: " << cnt++ << std::endl; } return 0; }大概有3万七千次左右,虽然我们云服务器配置不高,但是3万7千次未免太少了。我们把打印信息注释掉,并且设置一个全局变量,让其在循环内一直做++,对14号信号再进行捕捉,捕捉回调方法打印全局变量:
这次运行居然有5亿多次累加,至于为什么我们前面打印次数如此的少,这里可以给出两个原因:
1️⃣在Linux下,一切皆文件,前面对屏幕打印文字的行为,本质上是对显示器文件进行疯狂打印。在前后对比下,我们能直观的发现其实 IO很慢 。
2️⃣由于我们是使用了云服务器,真正运行并不在本地运行,我们向云服务器发送命令,以及云服务器将信息从远端发到本地,都是经过网络的。
闹钟在被设置的时候,其默认动作只会响一次!如果我们想要设置多个闹钟,我们可以在回调handler方法里再加上n秒的闹钟,这样,第一次闹钟响了之后,进程收到闹钟信号执行回调方法,而main函数是被循环卡死的,所以往后就每隔n秒响一次闹钟。
cpp#include <iostream> #include <unistd.h> #include <signal.h> #include <cerrno> #include <string> #include <cstring> #include <sys/types.h> void handler(int signo)// 收到闹钟信号进行回调 { std::cout << "get a sign: " << signo << std::endl; int n = alarm(2); // exit(0); } int main(int argc, char* argv[]) { signal(SIGALRM, handler); // 设置一个闹钟 alarm(4); int cnt = 0; while(true) { sleep(1); std::cout << "cnt: " << cnt++ << std::endl; } return 0; }如果我们设定一个闹钟需要很久之后才会响应,但是我在此期间发送14号信号提前对SIGALRM进行捕捉,执行handler方法的回调,返回值是什么呢?
cpp#include <iostream> #include <unistd.h> #include <signal.h> #include <cerrno> #include <string> #include <cstring> #include <sys/types.h> void handler(int signo)// 收到闹钟信号进行回调 { std::cout << "get a sign: " << signo << std::endl; unsigned int n = alarm(2); std::cout << "闹钟剩余时间: " << n << std::endl; // exit(0); } int main(int argc, char* argv[]) { signal(SIGALRM, handler); // 设置一个闹钟 alarm(100); int cnt = 0; while(true) { sleep(1); std::cout << "cnt: " << cnt++ << ", pid is: " << getpid() << std::endl; } return 0; }我们只对闹钟进行了一次kill信号,我们第一个闹钟设置了100s,而提示信息是每隔一秒打印一次,算下来刚好过去5s,打印出的返回值为95s,而第二次回调的时候闹钟剩余时间就变为0了,这也就证实了alarm接口返回值是上一次闹钟剩余时间。
接下来我来解释一下为什么闹钟也能作为软件条件?我们知道,alarm接口是系统调用接口,也就是说,设定闹钟,实际上是在操作系统内部设定的。而操作系统中存在的闹钟定然不止一个,所以OS一定要对这些闹钟做管理,如何管理?先描述,再组织!
根据以往经验,闹钟一定是有自己的结构体,结构体内有着必要的属性成员,那么在结构体的属性成员当中,就必定有闹钟的过期时间成员,用来记录闹钟的过期时间!
cppstruct alarm { uint64_t expired_time;// 闹钟过期时间 // ...其他属性字段 }而闹钟的过期时间实际上是一个时间戳,一个线性增长的时间。那我们应该以什么样的结构组织起来这些闹钟呢?经常看我博客的小伙伴第一反应很可能是链表。设置一个双链表,按照闹钟过期时间来排序,之后我只要找到第一个过期的闹钟,那么在此之后必然全部都是过期闹钟。
虽然这种想法很好,但是我们有更优解,我在很早之前写过一篇博客:堆与堆排序 而操作系统就是采用最小堆的方法组织闹钟结构!以最小堆的堆顶一定是最近一次即将超时的闹钟。所以往后,OS只需要查找堆顶元素,过期了就释放掉,再通过堆调整,将次要过期的闹钟调整到堆顶。
2-5 🍞异常产生信号
第五种信号的产生方式,程序出了异常,操作系统定然不会在放任这个问题进程不管,会采取一定的措施,OS为了能让程序员知道程序出了问题,于是设置了一些常出现的异常信号,当进程出现异常时,OS将会对进程发送异常信号,爆出异常缘由。
异常并不一定是由程序的语法、逻辑问题带来的,可很有可能是外部设备出了问题,所以异常又被分为 软件异常 和 硬件异常 。
软件异常通常有除零错误或者溢出错误引起的。而硬件异常通常是有进程访问无效地址引起的,一般有段错误等 。具体情况具体分析,我们先对除零错误进行模拟:
cpp#include <iostream> #include <unistd.h> #include <signal.h> #include <cerrno> #include <string> #include <cstring> #include <sys/types.h> int main(int argc, char* argv[]) { int a = 10; a /= 0; while(1) sleep(1); return 0; }除零错误会在Shell上爆出 Floating point exception 错误信息 ,并且除零错误对应的信号是 SIGFPE(8号信号) 。我们再来模拟一下野指针异常 :
cpp#include <iostream> #include <unistd.h> #include <signal.h> #include <cerrno> #include <string> #include <cstring> #include <sys/types.h> int main(int argc, char* argv[]) { int* p = nullptr; *p = 50; return 0; }野指针异常 通常会爆Segmentation fault 错误 ,想必学C/C++的小伙伴对这种报错会经常见到,而其对应的异常信号为 SIGSEGV(11号信号) 。
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
三、🤓对信号产生方式的理解
3-1🥗键盘产生信号
键盘产生数据毫无问题,但是操作系统是如何对组合键做出特殊处理的,OS怎么知道我使用
Ctrl C就是要发送2号信号给进程呢?我们都知道,操作系统向下管理软硬件资源,那么键盘硬件资源当然也属于操作系统管理范畴。所以,我们从键盘输入组合键,是由操作系统说的算的,而你使用的组合键是被OS解释为了命令。输入方在与键盘,解释方为操作系统,也就是说,键盘输入的到底是普通数据还是特殊命令,是由键盘驱动和OS联合解释的 。
这么看来操作系统只需要解释我们输入的信息即可,但是对于OS来说,用户输入的动作一定是异步的。而我们在日常使用键盘的时候,操作系统似乎是优先处理的。这样对于操作系统的压力是不是有些大了?要知道OS可不止需要监测键盘,还有其他异步硬件,网卡就是典型的例子。OS并不能保证何时会不会有信息从网络发来。
显然操作系统如果一直对这些异步发生的硬件进行监视的话,会将操作系统的性能拉低,而操作系统存在的意义就是,向下对软硬件资源管理,向上为用户提供良好服务。所以定然不会无时无刻监视这些硬件,于是就有人提出了 硬件中断技术 。
早在我们电脑开机的时候,操作系统就给我们生成了一张 中断向量表 ,这张表提前注册对软硬件资源操作的方法。比如此表的二号下标的方法就是用来读取键盘输入的数据。而所谓的中断向量表实际上是一个 函数指针数组 。那么其究竟是如何实现通过程序来访问硬件资源的呢?
通过冯诺依曼结构,我们知道,CPU在数据层面只和内存级打交道。但是在每个CPU上都存在许多的针脚,这些针脚是物理性的,在主板上可以和各个硬件相连接,包括键盘也是通过CPU的针脚连接的。每个针脚都有自己的编号。而未来我们在按键盘时,通过针脚,使 CPU触发硬件中断!当然也没这么简单,他们之间可能存在 8259可中断控制器,这个我们目前不需要了解。
不管如何,键盘和CPU可以通过针脚相互连接,而用户在键盘上输入数据时(发送高电平),就会触发硬件中断,此时CPU就可以检测到这个针脚有高电平,从而识别到键盘。而这时,CPU中的寄存器会将中断号(针脚编号)保存在寄存器内部,至此,硬件的动作就完成了!
寄存器收到中断号后,被操作系统检测到,此时操作系统就会停下手头的工作。拿着这个中断号从中断向量表中查询**(中断号就是中断向量表的下标索引)** 对应处理键盘资源的方法,进而调用这个方法去收集键盘发来的数据了。于是就可以把从键盘输入的数据读取到内存当中了。严格意义上来说,键盘文件也是文件,OS会先将数据读入到键盘文件的缓冲区里。
读取到的键盘数据经过操作系统对字符的判定,判定为数据则发送到当前进程打开的键盘文件缓冲区中,而被判定为控制命令的组合键,则会被解释为信号,比如我输入了Ctrl C,那么OS就会将其解释为2号信号,而并非普通字符信息。这时,这个信号就会发送给调用键盘文件的进程,从而执行对应的动作。
*** 那么操作系统如何解释控制命令❓***实际上,信号在到来时,我们在处理更重要的事情,暂时不能处理到来的信号,所以我们一定需把信号保存到PCB中 !要知道信号可是有整整62种,一个进程可能会存在多个信号,所以OS定要对这些信号做管理,如何管理?先描述,再组织 !
而这些信号则是由位图这结构描述组织的!并且这个位图只需要32位比特位,因为标准信号只有31种,所以32位比特位完全够用。如果用户通过键盘对当前用户输入了Ctrl C,则会被操作系统解释为2号信号,通过位图,将第对应的比特位由0置1即可完成OS对进程发送信号。也就是说,操作系统向进程发送信号的本质是对进程PCB的位图进行写入操作 !
3-2 🥪异常信号的理解
前面出现了 除零错误(SIGFPE) ,以及野指针错误(SIGSEGV) 都属于异常产生的信号,首先我们来分析除零错误。
计算错误:
除零错误,实际上也就是计算错误,在硬件方面,计算错误表现在CPU的寄存器上,我们知道,程序的计算都是在寄存器内完成的,寄存器可以存贮少量数据,而当计算发生错误,CPU停止对进程的操作,转而告诉操作系统当前处理的进程发生了计算错误。
在CPU中存在一个标志寄存器,EFLAGS/RFLAGS标志寄存器 ,其 用于检测有符号数运算是否发生了溢出 。这个寄存器存在一个名为 OF(OverFlow) 的标志位。当发生溢出错误时,OF被置为1,否则被置为0。
操作系统收到CPU发来的信息,发现进程不再被调度了,于是操作系统就会检查EFLAGS/RFLAGS寄存器的溢出标记位OF,从而检测出当前进程出了计算异常,于是 OS就对当前进程发送(向pcb内写入)8号信号 !
当我们对除零错误的异常信号进行捕捉,并且保持进程一直在运行状态:
cpp#include <iostream> #include <unistd.h> #include <signal.h> #include <cerrno> #include <string> #include <cstring> #include <sys/types.h> void handler(int signo) { std::cout << "get a sig: " << signo << std::endl; } int main(int argc, char* argv[]) { signal(SIGFPE, handler); int a = 10; int b = 0; a = a / b; while(true) sleep(1); return 0; }我们把异常信号的默认执行方式进行了捕捉,从而执行我们的handler方法回调。而handler方法我们仅仅打印了一句话,所以这个异常信号依旧存在。我们都知道,在CPU中寄存器只有一套,而 寄存器的数据可以有很多 ,这些数据我们称为 进程的上下文数据 。
但是当前进程被我们设置为一直在运行,异常在进程中仍然存在,这个时候OS又会向OF读取异常数据,进而再一次的对当前进程发送8号信号,这样不断的循环,就导致了上图的结果。
野指针异常:
野指针异常是因为非法访问地址,而我们通过之前的学习,我们知道野指针内的地址一定是虚拟地址,而虚拟地址要映射到物理内存需要经过 由OS和CPU(MMU) 的 转化。而既然存在转化,就一定存在转化成功或者失败,我们来讨论一下转化失败的情况。
在CPU中还存在两个很重要的寄存器:CR2 和 CR3 寄存器,其中cr2寄存器 用于存储导致页表映射错误的虚拟地址 。而cr3寄存器用于 存储页表的基地址,指向当前页表 。现代电脑上的 MMU单元 都是被集成的CPU上的,其用于 虚拟到物理地址之间的转换 。
当进程出现了野指针异常时,当前进程就会停止调度,OS就会来检查为何当前进程停止调度,而CPU对cr2寄存器进行读取,发现当前进程出现了野指针错误,于是OS就对当前进程发送11号信号(SIGSEGV)从而终止进程!
总结:
** 所以产生信号不论是系统调用还是软件条件,亦或者是键盘、异常产生的信号,都是由操作系统同一发送的,因为OS作为软硬件资源的管理者,当进程出现异常时,需要对进程做相应的处理,这也就是为什么我们在windows下运行一些带有错误的程序时,进程会直接终止。**
结束语
以上是我对于【Linux文件系统】进程信号:信号概念 & 信号的产生
感谢您的三连支持!!!































