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

[Linux——Lesson21.进程信号:信号概念 信号的产生]

目录

前言

一、🤔什么是信号

1-1🍕生活中的信号

🚀技术应用角度的信号

二、🧐信号及信号的产生

2-1 🍟信号的概念

2-2 🍔信号的处理方式

1️⃣信号捕捉(Catch)

2-3 🌭信号的产生方式

2-3-1🐕键盘输入

2-4 🍳系统调用产生信号

2-5 🥐软件条件产生信号

2-5 🍞异常产生信号

三、🤓对信号产生方式的理解

3-1🥗键盘产生信号

3-2 🥪异常信号的理解

结束语


前言

在 Linux 操作系统的进程管理体系中,信号是贯穿始终的核心通信与控制机制,它像一套精密的 “指令集”,协调着进程间的交互、响应系统事件并处理异常场景。理解信号的运作逻辑,是打通进程管理、系统调用及异常处理等核心知识点的关键纽带🗽。

本节将围绕信号的核心脉络展开,从基础概念切入,逐步深入信号的产生、阻塞与递达机制,再到信号捕捉的实践方法,同时关联中断原理、系统调用机制及可重入函数等关键知识点,最终结合 SIGCHLD 等具体信号场景落地实践,助力构建完整的 Linux 进程信号知识体系。

一、🤔什么是信号

1-1🍕生活中的信号

在生活中存在各种各样的信号,例如:红绿灯、闹钟、手势……每当我们接收到一个信号,我们就会执行对应的操作,例如红灯停、绿灯行……

为什么我们会对不同的信号有对应的执行动作呢?原因是:

  1. 我们能够识别一个信号,知道其中的含义
  2. 我们从小接受的教育告诉我们应当如何去做

但是我们收到一个信号之后必须去执行相应的动作吗?那也不一定。

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

在Linux中,我们常常通过键盘按下 Ctrl + c 来终止一个前台进程。

用户按下Ctrl + c ,这时键盘会产生一个硬件中断,被OS获取,解释为信号,发送给目标进程,前台进程因为收到这个信号,进而引起进程退出。

前台进程与后台进程

./filename 启动一个进程,该进程为前台进程,在这条命令后加一个 & 可以将该进程放到后台运行;

$ ./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 🍔信号的处理方式

 信号的 常见处理方式 有以下几种:

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

我们以信号的方式来终止进程,我们通过一下代码进行测试:

#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 接口:

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

  • signum参数传入需要捕捉的信号(名字或编号),当进程收到与其相匹配的信号时则会调用第二个参数,否则不会有任何动作
  • handler参数handlder方法,此方法为自定义方法,当收到signum信号则不会执行该信号的默认动作,变为执行该方法
  • 返回值返回前一个信号处理方法

值得注意的是,我们在设置信号捕捉时,并不需要将此接口放入循环之中,只需要调用该接口一次,在整个程序中则一直循环有效。我们设置一个signal方法做测试:

#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 函数的基本信息:

    #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调用:

    #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]: 进程pidint 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 接口:

    int raise(int sig);
    

    • 功能传入信号给当前进程(调用此接口的进程)
    • sig参数需要传入的信号
    • 返回值0表示成功,非0为失败

     我们编写一段代码测试raise调用:

    #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 接口:

    void abort(void)
    

    • 功能对自己发送指定的信号,为6号信号 SIGABRT

    我们编写一段代码测试abort调用:

    #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号信号) 信号:

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

    • 参数

      • seconds设置定时器的时间间隔,单位是秒。当定时器计时到达指定的秒数后,将发送 SIGALRM 信号给进程。
    • 返回值:

      • 返回之前设置的剩余秒数,如果之前没有设置定时器,则返回 0。
    • 功能:

      • alarm 函数用于设置一个定时器,当定时器计时到达指定的秒数后,进程将收到 SIGALRM 信号。该信号默认会终止进程,但可以通过注册信号处理函数来改变其行为。
    • 注意事项:

      • 如果之前已经设置了定时器,调用 alarm 函数将取消之前的定时器,并用新的时间间隔重新设置。
      • 如果将 seconds 参数设置为 0,表示取消之前的定时器,即不再发送 SIGALRM 信号。

     我们在代码上直观感受一下闹钟的响应:

    #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秒响一次闹钟。

    #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方法的回调,返回值是什么呢?

    #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一定要对这些闹钟做管理,如何管理?先描述,再组织!

      根据以往经验,闹钟一定是有自己的结构体,结构体内有着必要的属性成员,那么在结构体的属性成员当中,就必定有闹钟的过期时间成员,用来记录闹钟的过期时间!
     

    struct alarm
    {uint64_t expired_time;// 闹钟过期时间// ...其他属性字段
    }
    

     而闹钟的过期时间实际上是一个时间戳,一个线性增长的时间。那我们应该以什么样的结构组织起来这些闹钟呢?经常看我博客的小伙伴第一反应很可能是链表。设置一个双链表,按照闹钟过期时间来排序,之后我只要找到第一个过期的闹钟,那么在此之后必然全部都是过期闹钟。

      虽然这种想法很好,但是我们有更优解,我在很早之前写过一篇博客:堆与堆排序 而操作系统就是采用最小堆的方法组织闹钟结构!以最小堆的堆顶一定是最近一次即将超时的闹钟。所以往后,OS只需要查找堆顶元素,过期了就释放掉,再通过堆调整,将次要过期的闹钟调整到堆顶。

    2-5 🍞异常产生信号

    第五种信号的产生方式,程序出了异常,操作系统定然不会在放任这个问题进程不管,会采取一定的措施,OS为了能让程序员知道程序出了问题,于是设置了一些常出现的异常信号,当进程出现异常时,OS将会对进程发送异常信号,爆出异常缘由。

    异常并不一定是由程序的语法、逻辑问题带来的,可很有可能是外部设备出了问题,所以异常又被分为 软件异常 和 硬件异常

    软件异常通常有除零错误或者溢出错误引起的。而硬件异常通常是有进程访问无效地址引起的,一般有段错误等具体情况具体分析,我们先对除零错误进行模拟:

    #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号信号)。我们再来模拟一下野指针异常

    #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号信号

     当我们对除零错误的异常信号进行捕捉,并且保持进程一直在运行状态:

    #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文件系统】进程信号:信号概念 & 信号的产生

    感谢您的三连支持!!!

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

    相关文章:

  • 浙江英文网站建设嘉兴高档网站建设
  • ERP与WMS一体化构建方案
  • python+django/flask的眼科患者随访管理系统 AI智能模型
  • 实战案例:用 Guava ImmutableList 优化缓存查询系统,解决多线程数据篡改与内存浪费问题
  • AR短视频SDK,打造差异化竞争壁垒
  • 什么是AR人脸特效sdk?
  • Angular由一个bug说起之二十:Table lazy load:防止重复渲染
  • 从0到1做一个“字母拼词”Unity小游戏(含源码/GIF)- 字母拼词正确错误判断
  • 网站建设自查情况报告做淘宝联盟网站要多少钱?
  • 重新思考 weapp-tailwindcss 的未来
  • RuoYi .net-实现商城秒杀下单(redis,rabbitmq)
  • Langchain 和LangGraph 为何是AI智能体开发的核心技术
  • C++与C#布尔类型深度解析:从语言设计到跨平台互操作
  • 贵阳 网站建设设计企业门户网站
  • Rust 练习册 :Matching Brackets与栈数据结构
  • Java基础——常用算法3
  • 【JAVA 进阶】SpringAI人工智能框架深度解析:从理论到实战的企业级AI应用开发指南
  • 对话百胜软件产品经理CC:胜券POS如何用“一个APP”,撬动智慧零售的万千场景?
  • 用ps怎么做短视频网站建立网站的步骤 实湖南岚鸿
  • wordpress使用latex乱码长沙优化网站厂家
  • 【uniapp】解决小程序分包下的json文件编译后生成到主包的问题
  • MySQL-5-触发器和储存过程
  • HTTPS是什么端口?443端口的工作原理与网络安全重要性
  • 从零搭建一个 PHP 登录注册系统(含完整源码)
  • Android 端离线语音控制设备管理系统:完整技术方案与实践
  • 网站流量一般多少合适asp网站实例
  • 想学网站建设与设计的书籍基于网站开发小程序
  • 【双指针类型】---LeetCode和牛客刷题记录
  • h5单页预览PDF文件模糊问题解决
  • LeetCode 每日一题 2025/11/3-2025/11/9