【Linux系统】信号:认识信号 与 信号的产生

news2025/2/3 19:34:28




在这里插入图片描述



信号快速认识


1、生活角度的信号


在这里插入图片描述


异步:你是老师正在上课,突然有个电话过来资料到了,你安排小明过去取资料,然后继续上课,则小明取资料这个过程就是异步的

同步:小明取快递,你停下等待小明回来再接着上课,这就是同步



2、预备知识

1、为什么能识别信号:你怎么能识别信号呢? 识别信号,是内置的。进程认识信号,是程序员内置的特性

2、如何处理信号:信号产生之后,怎么处理你知道吗? 知道! 信号的处理方法,在信号产生之前,已经准备好了。

3、信号是否立即处理:处理信号? 立即处理吗? 我在做我的事情,优先级很高,信号处理,可能并不是立即处理。合适的时候:我就需要记录信号,等待下次处理

4、处理信号的方式:怎么处理信号呀? a. 默认行为 b. 忽略信号 c. 自定义动作



3、知识框架


在这里插入图片描述


进程信号分为三个部分:

  • 信号产生
  • 信号保存
  • 信号处理


信号产生


1、键盘产生


1.1 前台进程和后台进程的概念和区别

前台进程和后台进程是操作系统中用于描述进程运行状态的两个概念。它们的区别主要在于进程与终端的交互方式以及系统的调度策略。下面详细解释这两个概念及其区别:


前台进程

  1. 定义
    • 前台进程是指当前正在与用户交互的进程。用户可以通过终端输入命令来启动或控制这些进程。
    • 前台进程独占终端,用户必须等待前台进程完成或暂停才能继续输入新的命令。
  2. 特点
    • 用户可以直接与前台进程进行交互,例如输入数据或接收输出。
    • 前台进程通常会响应用户的键盘输入,如 Ctrl+C(中断)、Ctrl+Z(暂停)等。
    • 前台进程在运行期间会占用终端,用户不能在同一终端上启动其他前台进程。



后台进程

  1. 定义

    • 后台进程是指在后台运行的进程,不与用户直接交互。这些进程可以在用户不知情的情况下运行,不会占用终端。
    • 后台进程通常用于执行长时间运行的任务或服务,如数据库服务器、Web服务器等。
  2. 特点

    • 后台进程不独占终端,用户可以在同一终端上启动多个后台进程。
    • 后台进程通常不会响应用户的键盘输入,除非它们被配置为监听特定的信号。
    • 后台进程可以通过任务调度器(如 cron)或守护进程管理工具(如 systemd)来启动和管理。
  3. 示例

    • 使用 & 符号将命令放到后台运行,例如 sleep 100 &
    • 使用 nohup 命令使进程在后台运行并且不受终端关闭的影响,例如 nohup myscript.sh &
    • 使用 bg 命令将已暂停的前台进程放到后台继续运行。



如何管理前后台进程

  1. 将前台进程放到后台

    • 使用 Ctrl+Z 暂停前台进程,然后使用 bg 命令将其放到后台继续运行。

    • 例如:

      sleep 100  # 启动一个前台进程
      ^Z         # 暂停前台进程
      bg         # 将暂停的进程放到后台继续运行
      
  2. 将后台进程放到前台

    • 使用 fg 命令将后台进程放到前台。

    • 例如:

      sleep 100 &  # 启动一个后台进程
      jobs         # 查看当前终端中的所有作业
      fg %1        # 将作业编号为1的后台进程放到前台
      
  3. 查看当前的前后台进程

    • 使用 jobs 命令查看当前终端中的所有作业。
    • 使用 ps 命令查看系统中的所有进程。



1.2 拓展:默认作业


在 Unix 和 Linux 系统中,jobs 命令的输出中,默认作业(即最后一个在后台暂停的作业)通常会有一个特殊的标记。这个标记通常是 + 号,表示它是当前的默认作业。如果还有其他作业,它们可能会被标记为 - 号,表示它们是上一个默认作业。


在这里插入图片描述


杀死默认作业: 使用 kill 命令终止默认作业。默认作业可以用 %+ 来表示。

c++ kill %+

在这里插入图片描述



1.3 nohup 命令

nohup 是一个常用的 Unix/Linux 命令,用于使进程在后台运行并且不受终端关闭的影响。具体来说,nohup 命令可以让进程忽略挂断(SIGHUP)信号,这样即使用户退出终端或断开连接,进程仍然会继续运行。

nohup 的全称是 “no hang up”。这个名称来源于它的功能:使进程在启动后忽略挂断(SIGHUP)信号,从而能够在终端关闭后继续运行。

  • no: 表示“不”或“没有”。

  • hang up (SIGHUP): 挂断信号,通常在用户退出终端或断开远程连接时发送给所有与该终端关联的进程。



SIGHUP 信号
  • SIGHUP(Hang Up)信号通常在用户退出终端或断开远程连接时发送给所有与该终端关联的进程。
  • 默认情况下,进程接收到 SIGHUP 信号会终止运行。


nohup 命令的作用
  1. 忽略 SIGHUP 信号

    nohup 命令会修改进程的行为,使其忽略 SIGHUP 信号,这样即使终端关闭,进程也不会被终止。

  2. 后台运行

    nohup 命令本身并不会默认将进程放到后台运行,但它通常与 & 符号一起使用,以便将进程放到后台运行。

    前台运行:如果你只是使用 nohup 命令而没有添加 & 符号,进程将在前台运行。这意味着你会看到进程的输出,并且终端会被占用,直到进程结束或你手动停止它



使用方法
nohup command [arguments] &
  • command:你要运行的命令。

  • arguments:命令的参数。

  • &:将命令放到后台运行(可选)。



重定向输出

默认情况下,nohup 会将标准输出和标准错误输出重定向到一个名为 nohup.out 的文件中。你可以指定不同的输出文件:

nohup myscript.sh > output.log 2>&1 &

这条命令会将标准输出和标准错误输出重定向到 output.log 文件中。



注意事项
  1. 进程管理

    • 使用 nohup 启动的进程可以在后台长时间运行,但你需要确保这些进程不会消耗过多的系统资源。
  2. 输出文件

    • 如果你不希望输出被重定向到 nohup.out 文件,可以将输出重定向到 /dev/null 来丢弃输出:

      nohup myscript.sh > /dev/null 2>&1 &
      



1.4 实验 nuhup 命令时遇到的问题


我运行下面的程序,使用 nuhup 命令默认输出到 nuhup.out 文件,但是好像 cat nuhup.out 查询时没有输出结果

这是因为在默认情况下,标准输出在连接到终端时是行缓冲的,而在连接到文件或管道时是全缓冲的。因此可以主动使用 fflush(stdout) 刷新

#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
    while(1)
    {
        cout << "hello world!" << '\n';
        sleep(1);
    }
    return 0;
}


1.5 补充一些前后台进程的操作知识

五种命令及用法

在 Unix/Linux 系统中,你可以使用多种命令来管理进程的前台和后台运行状态。以下是一些常用命令及其用法:

1. 暂停进程

Ctrl + Z
  • 功能:发送一个 SIGTSTP(Signal TSTop)信号给当前的前台进程,暂停(挂起)当前正在运行的进程,并将其放到后台。

  • 示例

    cat
    ^Z  # 按 Ctrl + Z 暂停 `cat` 命令
    

2. 查看后台任务

jobs 命令
  • 功能:列出当前终端会话中所有的后台任务及其状态。

  • 示例

    jobs
    
jobs -l 命令
  • 功能:列出所有正在运行的作业,并显示每个作业的进程 PID。

  • 示例

    jobs -l
    

3. 将任务恢复到前台

fg 命令
  • 功能:将后台任务恢复到前台继续运行。

  • 示例

    • 如果只有一个后台任务,直接使用 fg 命令:

      fg
      
    • 如果有多个后台任务,可以通过指定任务编号来恢复特定的任务。例如,恢复任务编号为 [1] 的任务:

      fg %1
      

4. 将后台任务放到后台继续运行

bg 命令
  • 功能:将暂停的任务恢复到后台继续运行。

  • 示例

    • 如果有多个后台任务,可以通过指定任务编号来恢复特定的任务。例如,恢复任务编号为 [1] 的任务:

      bg %1
      

5. 直接将命令放到后台运行

& 符号
  • 功能:将命令放到后台运行。

  • 示例

    • 假设你有一个长时间运行的命令,如 sleep 60,你可以在命令末尾加上 & 来将其放到后台运行:

      sleep 60 &
      
    • 终端会立即返回提示符,并显示类似以下的输出:

      [1] 12345
      
    • 这里,[1] 是后台任务的编号,12345 是该后台进程的 PID(进程标识符)。

总结

  • Ctrl + Z:暂停当前前台进程并将其放到后台。
  • jobs:列出当前终端会话中的所有后台任务。
  • jobs -l:列出所有后台任务及其 PID。
  • fg:将后台任务恢复到前台继续运行。
  • bg:将暂停的任务恢复到后台继续运行。
  • &:将命令直接放到后台运行。

通过这些命令,你可以灵活地管理进程的前台和后台运行状态,提高工作效率。



1.6 键盘 ctrl+c:2号信号 SIGINIT

该命令用于终止正在运行的前台进程,本质是向该进程发送信号

证明是信号

系统调用:signal()

用于自定义信号处理,当信号发送给进程,进程会有一些默认固定的信号的处理方法,如kill -9 信号就是用于杀死该进程

而该系统调用可以自定义信号处理,使其处理信号时执行自定义的方法,而不是系统规定好的默认处理方法

用这个来检测 ctrl+c 是信号


在这里插入图片描述


前面讲解的信号处理,更准确来说是:信号捕捉

而系统调用:signal() 是信号自定义捕捉



Ctrl + C 发送的是 SIGINT(Signal Interrupt)信号。这个信号通常用于请求程序中断当前的操作,通常是终止或停止一个正在运行的进程。

  • SIGINT:这是一个中断信号,通常由用户通过按下 Ctrl + C 组合键来发送。这个信号的默认行为是终止进程。
  • 用途SIGINT 通常用于优雅地终止一个正在运行的进程,允许进程在退出前进行一些清理工作,如释放资源、保存状态等。

kill -l :查看系统中常见信号,其中 1~31 号是我们学习需要的信号,其他 32~64 号信号是实时信号,我们不学习


在这里插入图片描述


Ctrl + C 被OS接收并解释成为2号信号 SIGINT



使用系统调用:signal()

可以直接传递信号编号,或则信号名称:因为这些信号名底层其实就是宏,对应着信号编号,因此意义一致

signal(2, handler);
signal(SIGINT, handler);

代码如下:在循环打印语句时,我不断 Ctrl + C ,程序就会信号捕捉到该信号并执行我的自定义信号捕捉函数:打印语句 "get a signal, signum: 2"

#include<iostream>
#include<unistd.h>
#include <signal.h>

using namespace std;


typedef void (*sighandler_t)(int);
void handler(int signum)
{
    cout << "get a signal, signum: " << signum << '\n';
}

int main()
{
    //sighandler_t signal(int signum, sighandler_t handler);
    signal(2, handler);

    while(1)
    {
        cout << "hello world!" << '\n';
        sleep(1);
    }
    return 0;
}

代码运行结果如下:


在这里插入图片描述


如何终止这个进程:ctrl+\ (后面解释)

这就是:将2号信号的默认终止->执行自定义方法:handler

当对应的信号被触发,内核会将对应的信号编号,传递给自定义方法



查看每种信号的默认处理方法

命令:man 7 signal,然后一直向下翻


其中,2号信号 SIGINT 的行为为 Term 就是 terminal 终止的意思


在这里插入图片描述




1.7 键盘ctrl+\:3号信号 SIGQUIT


在这里插入图片描述



3号信号 SIGQUIT 就是键盘:ctrl+\

验证一下:将3号信号也自动捕捉一下

代码如下

#include<iostream>
#include<unistd.h>
#include <signal.h>

using namespace std;


typedef void (*sighandler_t)(int);
void handler(int signum)
{
    cout << "get a signal, signum: " << signum << '\n';
}

int main()
{
    //sighandler_t signal(int signum, sighandler_t handler);
    signal(3, handler);
    while(1)
    {
        cout << "hello world!" << '\n';
        sleep(1);
    }
    return 0;
}


运行结果


在这里插入图片描述




1.8 问题:signal怎么不放在循环里面?

答:因为这个信号捕捉只需要设定一次,一次就能完成信号自定义处理的设置,参考文件重定向(只需要重定向一次)



1.9 问题:全部信号自定义捕捉,是否可以实现进程永生?


我们 man 7 signal 查看手册,可以发现,几乎所有信号都是用于终止进程,当我们使用系统调用 signal 将 1~31 号信号全捕捉了,岂不是这个进程就杀不死了?


代码:循环自定义捕捉所有信号



运行结果演示:在新终端上通过 kill 命令尝试杀掉该进程

新终端


在这里插入图片描述




旧终端

在这里插入图片描述


回答原因:

上面的运行结果展示了较大一部分的信号可以被捕捉以自定义处理,但还是有例外的。
其实操作系统设计者早就考虑了这点,这些信号中有几个信号是无法被捕捉的,其中 9 号信号一定不能被捕捉,因此我们一定可以通过 9 号信号杀死该进程



1.10 问题:在软件层面,如何理解键盘信号处理?


键盘如何发送信号给进程:准确来说,是键盘的组合键被操作系统先识别到的,因为操作系统是键盘真正的管理者,所以当你的进程在运行时,操作系统检测键盘上有没有信息,当键盘上 ctrl+c ,操作系统把 ctrl+c 这样的组合键解释成了对应的信号发送给进程

因为操作系统本身就属于硬件的管理者,硬件上做任何行为首先是一定是先给操作系统识别到,所以不要看到有人说键盘上可以发信号,其实最根本的是键盘先把对应的组合键信息交给了操作系统



1.11 进程如何保存信号

前面讲过:信号不一定要被立即处理,可以先保存下来,再合适的时机再处理

那进程如何保存信号:位图


在这里插入图片描述


信号产生有很多种,但是信号发送只能由 OS 来做


进程内部还会维护一张函数指针数组,对应每种信号需要的信号处理函数

通过信号位图查询哪些信号需要被处理,则到这张表中查询并执行对应的 处理方法

我们自定义信号处理函数也是写在这张表中(具体后面再讲解)


在这里插入图片描述




1.12 硬件中断机制

问题:OS怎么知道键盘上面有数据了?

有人说:操作系统会不断轮询键盘设备,当有数据产生时,就会被操作系统读取

答:其实这样非常消耗性能



实际上,根据冯诺依曼体系,硬件设备会和CPU中的控制器相连,当外部设备产生数据时,

外部设备会通过一些连接的针脚,给 cpu 发送我们对应的硬件中断,则操作系统就会知道外部设备数据就绪,然后操作系统就拷贝该数据,操作系统无需主动轮询所有的外设


在这里插入图片描述



  1. 硬件设备与中断控制器
    • 键盘等外部设备通过中断控制器(Interrupt Controller)与 CPU 连接。
    • 中断控制器负责管理来自各种外部设备的中断请求,并将这些请求传递给 CPU。
  2. 中断请求
    • 当键盘上有按键被按下时,键盘控制器会生成一个中断请求(Interrupt Request, IRQ)。
    • 这个中断请求通过中断控制器传递给 CPU。
  3. 中断处理
    • CPU 收到中断请求后,会暂停当前正在执行的指令,保存当前的状态(如寄存器内容),然后跳转到中断处理程序(Interrupt Service Routine, ISR)。
    • 中断处理程序通常是由操作系统提供的,负责读取键盘缓冲区中的数据,并进行相应的处理。
    • 处理完毕后,CPU 恢复之前的状态,继续执行被中断的指令。


其他硬件也是如此:

网卡:当网卡接收到网络数据时,通过中断控制器将硬件中断传递给 CPU,操作系统才会拷贝读取对应数据

磁盘:我们需要通过 LBA 地址在磁盘中寻找对应位置数据时,也是先让磁盘自己找,找到了,磁盘才会通过中断控制器将硬件中断传递给 CPU,操作系统才会拷贝读取对应数据


这样使得,硬件和OS并行执行



1.13 信号 vs 硬件中断


  • 信号,纯软件,模拟中断的行为
  • 硬件中断,纯硬件

硬件中断像不像一种信号机制,通过中断向CPU发送”信号“,告知有数据需要读取,其实软件层面的信号就是模仿了中断的行为

中断是信号的老祖宗



2、指令发信号

也就是通过命令行命令,至于一个命令如何让系统向对应进程发送信号,后文会提及


在这里插入图片描述




3、系统调用发信号


3.1 系统调用 kill


在这里插入图片描述



通过这个我们可以实现自制 kill 命令

代码演示使用该系统调用自制 kill 命令程序:

#include <iostream>
#include <sys/types.h>
#include <signal.h>
using namespace std;

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        cout << "usage:./mykill signum pid\n";
        return 1;
    }

    pid_t pid = atoi(argv[2]);
    int sig = atoi(argv[1]+1);   // 因为输入选项的形式为:-9,因此数字是从第二个字符开始的

    // int kill(pid_t pid, int sig);
    int n = kill(pid, sig);
    if(n < 0){
        cout << "kill error\n";
        return 1;
    }


    return 0;
}


运行结果如下:启动一个休眠进程放到后台运行,通过自制 kill 命令,选择 9 号信号杀掉该进程


在这里插入图片描述


这里可以讲解一个结论:指令底层也是使用这个 kill 系统调用!



3.2 系统调用 raise

谁调用我,我就给自己发送某信号


在这里插入图片描述




代码演示使用该系统调用:

#include <iostream>
#include <signal.h>

int main(int argc, char *argv[]) {
    int cnt = 5;

    while(true) {
        std::cout << "hahaha alive" << std::endl;
        cnt--;
        
        if(cnt <= 0) {
            raise(9);
        }
    }

    return 0;
}


运行结果如下


在这里插入图片描述




3.3 系统调用 abort

实际上,这个系统调用已经被C库封装了

作用:谁调用我,我就给谁发 abort 信号终止掉谁,相当于给终止自己


在这里插入图片描述


在这里插入图片描述




代码演示使用该系统调用:

#include <iostream>
#include <signal.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    int cnt = 5;

    while(true) {
        std::cout << "hahaha alive" << std::endl;
        cnt--;
        
        if(cnt <= 0) {
            abort();
        }
    }

    return 0;
}


运行结果如下


在这里插入图片描述




前面文章我们讲解了 信号产生的硬件条件,下面我们讲解一下,在软件层面,如何由软件触发信号:



4、软件条件


4.1 软件条件一:管道被信号杀死


匿名管道:当管道的读端关闭,操作系统会识别到当前管道里没有读端了,此时写端如果还要写就是一个非法操作,此时操作系统会把这个进程给杀掉

怎么杀掉该进程:其实是操作系统向我们的目标进程发送 13 号信号

管道是文件,文件是软件

管道文件写入条件不具备,就叫做软件条件不具备

那么软件条件的字面意思是:在操作系统当中某些对应的软件本身没有准备好或者条件不具备时,我们可以向目标进行发送信号



4.2 软件条件二:闹钟(重点)


在这里插入图片描述



alarm 是一个系统调用,用于在指定的时间后向进程发送一个 SIGALRM 信号。这个信号通常用于实现定时任务或超时机制。下面是 alarm 系统调用的详细解释和使用方法。

  • 函数原型
unsigned int alarm(unsigned int seconds);
  • 参数

    seconds:指定在多少秒后发送 SIGALRM 信号。如果 seconds 为 0,则取消任何已设置的定时器。

  • 返回值
    返回前一次调用 alarm 设置的剩余时间(以秒为单位)。如果之前没有设置定时器,则返回 0。
    多次调用 alarm:如果在定时器到期前再次调用 alarm,新的定时器会覆盖旧的定时器。alarm 函数会返回前一次设置的剩余时间。

  • 信号处理
    当指定的时间到达时,内核会向进程发送一个 SIGALRM 信号。默认情况下,SIGALRM 信号会导致进程终止。但是,你可以在程序中设置信号处理函数来捕获和处理 SIGALRM 信号。


上面这些概念,后续文章会对某些概念进行进一步讲解:



一秒的闹钟计数器

我们定一个一秒的闹钟计数器,一秒后,发送 SIGALRM 信号终止本进程:

#include <iostream>
#include <cstdio>
#include <signal.h>
#include <unistd.h>

int number = 0;

int main()
{
    alarm(1); // 我自己,会在 1s 之后收到一个SIGALRM信号
    
    while (true)
    {
        printf("count: %d\n", number++);
        number++;
    }

    return 0;
}

在这一秒中,计数器 cnt 不断计数:最后大概一秒钟累计 9万多次


在这里插入图片描述


但是这样有点奇怪,我们的计算机是不是有点慢了!!!

问题:现代计算机计算速度能达到上亿级别的,为什么这个好像有点慢:

答:因为 printf 进行 IO交互,IO 是比较耗时的!!



新版本:去掉 IO,仅在最后打印

代码:捕获信号 SIGALRM ,自定义处理

#include <iostream>
#include <cstdio>
#include <signal.h>
#include <unistd.h>

int number = 0;

void handler(int sig)
{
    printf("Received signal %d, count = %d\n", sig, number);
    kill(getpid(), SIGKILL);  // kill 自己给自己发信号,杀死自己
}



int main()
{
    alarm(1); // 我自己,会在1S之后收到一个SIGALRM信号
    signal(SIGALRM, handler);
    
    while (true)
    {
        //printf("count: %d\n", number++);
        number++;
    }

    return 0;
}

运行结果如下:直接累加到 4 亿多次

在这里插入图片描述


OS对定时器的管理

通过 alarm 设定个闹钟,最终其实在底层操作设置了一个定时器

如何去理解这个定时器:张三可以设置一个 5 秒的闹钟,李四可以设置一个 10 秒的闹钟……

操作系统内可以同时存在多个定时器,操作系统就要管理对应的定时器。

如何管理:先描述再组织!



操作系统会给我们创建一个定时器对象,通常包含下面几种属性:对应进程的pid(who)、进程的 task_struct、时间戳、链表节点指针、对应的处理方法(如默认向对应进程发送信号SIGALRM


在这里插入图片描述




操作系统将定时器描述成一个个结构,并连接组织成链表结构,将对定时器的管理转为对链表节点的增删查改操作


在这里插入图片描述




检测定时器是否超时

有这么多定时器,都是不同的时间,是不是需要遍历一遍所有的定时器才能知道是否超时,这样比较影响效率

因此,我们一般会通过排序的方式管理定时器结构,操作系统底层是通过哈希等结构管理的

我这里为了方便理解,可以理解成,系统将定时器结构以一个小顶堆的方式管理起来

每次只需查看最小的定时器是否超时即可,这样提高了效率



闹钟的返回值

返回值:返回前一次调用 alarm 设置的剩余时间(以秒为单位)。如果之前没有设置定时器,则返回 0。

多次调用 alarm:如果在定时器到期前再次调用 alarm,新的定时器会覆盖旧的定时器。alarm 函数会返回前一次设置的剩余时间。

总结:表示上一次设置闹钟的剩余时间。



闹钟和信号产生有什么关系

设置定时器,当软件条件就绪时比如超时,那么我们的操作系统就可以向目标进行发送信号

所以闹钟定时器那么它本身属于软件条件满足或不满足而触发的让操作系统向目标进程发信号

这种策略叫软件条件!


说白了,就是因为软件问题,而导致的操作系统项目标进行发信号

不管是管道读端关闭或者是定时器超时,操作系统把你干掉了,这都叫做软件条件,跟硬件无关



4.3 闹钟的小项目:理解定时器的真正作用


#include<iostream>
#include<functional>
#include<vector>
#include<unistd.h>
#include <signal.h>
using namespace std;

// 定义一个函数指针类型,用于处理信号
typedef void (*sighandler_t)(int);
// 定义一个函数对象类型,用于存储要执行的函数
using func = function<void()>;
// 定义一个函数对象向量,用于存储多个要执行的函数
vector<func>funcV;
// 定义一个计数器变量
int count = 0;

// 信号处理函数,当接收到信号时,执行向量中的所有函数
void Handler(int signum)
{
    // 遍历函数对象向量
    for(auto& f : funcV)
    {
        // 执行每个函数
        f();
    }
    // 输出计数器的值和分割线
    cout << "—————————— count = " << count << "——————————" << '\n';
    // 设置一个新的闹钟,1 秒后触发
    alarm(1);
}

int main()
{
    // 设置一个 1 秒后触发的闹钟
    alarm(1);
    // 注册信号处理函数,当接收到 SIGALRM 信号时,调用 Handler 函数
    signal(SIGALRM, Handler); // signal用于整个程序,只会捕获单个信号

    // 向函数对象向量中添加一些函数
    funcV.push_back([](){cout << "I am 存储 work" << '\n';});
    funcV.push_back([](){cout << "I am 数据库更新 work" << '\n';});
    funcV.push_back([](){cout << "I am 拷贝 work" << '\n';});

    // 进入一个无限循环,程序不会退出
    while(1){
        count++;
    }; //  死循环,不退出

    return 0;
}



这个代码的作用是:

先向进程定一个 1 秒的定时器,通过 signal 捕获定时器信号,进入自定义处理函数

在 main 函数结尾定义死循环,使程序不会退出

同时 signal 的自定义处理函数又定义 1 秒的定时器

使得整个程序处于无限循环:不断定义 1 秒的定时器,不断触发 signal 捕获定时器信号

同时死循环中的计数器不断递增,最后打印出来,为了能看到时间的变化

相当于每一秒钟执行一次 signal 的自定义处理函数

在这里插入图片描述



进一步优化:添加 pause

当信号没有产生时,通过 pause 不让死循环跑,只有信号来了才继续

pause() 函数

在C语言中,pause() 函数是一个系统调用,用于使当前进程暂停执行,直到接收到一个信号(signal)。这个函数通常用于等待某个外部事件的发生,比如用户输入或定时器到期等。

函数原型

pause() 函数的原型定义在 <unistd.h> 头文件中:

#include <unistd.h>

int pause(void);

功能

  • 暂停进程:调用 pause() 后,进程会进入等待状态,直到接收到一个信号。
  • 信号处理:当进程接收到一个信号并且该信号没有被忽略(即有对应的信号处理函数),则 pause() 会返回,并且控制权会传递给相应的信号处理函数。
  • 返回值pause() 总是返回 -1,并且设置 errnoEINTR,表示调用被中断。

因为进程 alarm 设置定时器,定时器是操作系统在进程外管理的,定时器到时了自然会发信号给原进程,此时进程就会被该信号唤醒



优化部分:

while(1){
    pause();
    cout << "我醒来了~" << '\n';
    count++;
};



运行结果如下:计数器count如愿的一秒一秒的递增

在这里插入图片描述


到这里我们就完成了一个:需要靠外部信号唤醒去执行对应工作的进程,只有外部信号到来才能唤醒该进程,否则进程暂停



最后的升华:操作系统运行的本质?

我们将执行函数的打印代码换一下:

funcV.push_back([](){cout << "我是一个内核刷新操作" << '\n';});
funcV.push_back([](){cout << "我是一个检测进程时间片的操作,如果时间片到了,我会切换进程" << '\n';});
funcV.push_back([](){cout << "我是一个内存管理操作,定期清理操作系统内部的内存碎片" << '\n';});


运行结果如下:

在这里插入图片描述


实际上,这就是模拟了操作系统的底层运行!!!

操作系统就是通过不断发送时间中断(看作一种信号),来使操作系统进程一直运行,不断进行:进程调度、进程切换、内存管理等操作!!!!


在这里插入图片描述


关于真正讨论操作系统的底层原理,后面再讲解



5、异常


5.1 野指针:段错误


代码:

#include<iostream>
using namespace std;

int main()
{
    int *p = nullptr;
    *p = 10;

    while(true){};
    return 0;
}

在这里插入图片描述



程序遇到野指针直接崩溃,报段错误

实际上,程序是因为接收到操作系统发送的 11 号信号 SIGSEGV

在这里插入图片描述

验证确实是 11 号信号 SIGSEGV 的原因:通过信号自定义捕捉

#include<iostream>
#include<signal.h>
using namespace std;

void handler(int signo)
{
    std::cout << "get a signo: " << signo << std::endl;
    // 我捕捉了11号新号,没执行默认
}

int main()
{
    signal(11, handler);
    
    int *p = nullptr;
    *p = 100;
    
    while (true){};
}

运行结果:确实没有程序退出,而是循环打印自定义处理函数的语句

在这里插入图片描述



5.2 除零异常:浮点异常


代码:

#include<iostream>
#include<signal.h>
using namespace std;

int main()
{
    int a = 10;
    a /= 0;
    
    while (true){};
}

在这里插入图片描述



程序遇到除零直接崩溃,报浮点异常

实际上,程序是因为接收到操作系统发送的 8 号信号 SIGFPE

在这里插入图片描述


验证确实是 8 号信号 SIGFPE的原因:通过信号自定义捕捉

#include<iostream>
#include<signal.h>
using namespace std;

void handler(int signo)
{
    std::cout << "get a signo: " << signo << std::endl;
    // 我捕捉了8号新号,没执行默认
}

int main()
{
    signal(8, handler);
    
    int a = 10;
    a /= 0;
    
    while (true){};
}

运行结果:确实没有程序退出,而是死循环打印自定义处理函数的语句

在这里插入图片描述



5.3 问题:OS 怎么知道程序运行出错了?为什么会死循环?


先拿除零异常解释一下:

溢出标记位


在这里插入图片描述



CPU中使用多个寄存器参与运算工作

其中有一个状态寄存器中存有一个 “溢出标记位”:用于记录当前运算是否溢出或出错,若是则记为 1,否则为 0


溢出标记位记录到出错了,CPU就会向OS发信息,并说明这是由除零导致的异常,OS会向该进程发送8号信号,而我们程序把8号信号捕捉了(自定义处理过了),

当 CPU 继续调度进程,继续执行程序剩余部分时,会把进程的上下文数据放到CPU的各个寄存器中,其中状态寄存器也属于上下文数据的一部分!!!

上一轮溢出标记位记录到出错了,此时的还是记录为 1,则后果可想而知,CPU 继续报错!!

因为你处理了我的 8 号信号,但并没有将 CPU 内部的 溢出标记位 重置为 0,从而导致操作系统不断循环的向 进程发送 信号的现象


在这里插入图片描述




野指针异常也同理:


在这里插入图片描述


CPU 内部有专门处理内存地址的寄存器,其中一个关键是指向特定地址数据的虚拟地址指针,这个指针会放到寄存器 EIP 中。通过内存管理单元 MMU(一种硬件设施)以及 CR3 寄存器来查找页表,如果发现该虚拟地址没有对应的物理地址映射,MMU 里类似状态寄存器的东西就会记录下这次错误的查询。

这时候,硬件 MMU 也会通知操作系统出现了错误,操作系统接着会给相关的进程发送信号,通常情况下会导致该进程被终止。

但是,如果我们捕捉并自定义处理了这个信号,当 CPU 再次尝试调度这个进程时,由于导致问题的野指针还未得到解决,MMU 中记录的错误依旧存在,这将导致 CPU 持续报错。最终造成操作系统不断循环地向该进程发送信号的现象。




总结一下,操作系统是如何知道我们的进程内部出错了呢?实际上,并不是你在编程语言层面直接让进程崩溃了,而是程序内部的问题反映为硬件级别的错误,这些错误信息反馈给操作系统,再由操作系统发送信号给进程,从而导致进程被终结或崩溃!

明白了这一点很重要!!并不是因为进程自身的语法错误直接导致其崩溃,而是操作系统基于硬件报告的错误决定终止进程的运行!



5.4 总结一下

C/C++中,常见的异常,导致进程崩溃了,其实是OS给目标进程发送对应信号,进而导致该进程退出



6、信号的两种 ActionCore / Term


查看手册:man 7 signal

可以看到信号有几种 Action,这里讲解一下 Core / Term

在这里插入图片描述


6.1 认识 Core

直入主题:

Term 就是正常的终止

Core 除了终止,还会多做一件事,程序崩溃时生成了一个核心转储文件(core dump),该文件用于调式代码。

核心转储是程序崩溃时内存状态的一个快照,包括程序计数器、寄存器状态以及内存数据等信息。

这个文件对于调试是非常有用的,因为它可以用来分析程序崩溃的原因。



我们演示 “野指针” 和 “除零错误” 的信号都是 Core 的信号:

在这里插入图片描述



但是我们根据前面文章的讲解:

好像这两个错误信号发送给进程了,但是并没有生成什么核心转储文件???

答:这是因为云服务器上的 Linux 系统,默认将生成 核心转储文件 的服务给禁用掉



6.2 为什么要禁用 Core

命令:ulimit -a

ulimit -a 命令用于显示当前 shell 及其子进程的所有资源限制。这些限制通常由操作系统或系统管理员设置,以防止某个进程占用过多资源,影响系统的稳定性和性能。


在这里插入图片描述



我们可以手动设置 core 文件的允许生成:命令 ulimit -c 10240

ulimit -c 10240 命令用于设置最大核心文件大小为 10240 块。这里的“块”通常是指 512 字节,因此 10240 块等于 5120 KB(即 5 MB)。

在这里插入图片描述


此时我们再次触发野指针或除零异常

在这里插入图片描述


发现报错多了一个词:core dumped


同时当前目录下生成了一个 core 文件

在这里插入图片描述



那为什么要禁用 core 文件呢?

当程序运行出现除零错误或野指针这类问题时,这会导致程序直接崩溃。作为程序员,我们通常会主动处理这些问题。但如果这个服务是在半夜挂掉的,并且没有人去处理,它可能会不断重启又不断崩溃,每次崩溃都会生成一个 core 文件。如果这种情况持续一段时间,磁盘空间会被这些 core 文件迅速占满,不仅没有给你留下调试和排错的机会,还可能导致服务器先一步挂掉。

因此,较新的 Linux 系统内核默认会禁用生成核心转储文件的服务,就是为了防止上述情况的发生。

另一种处理方法是,如上面提到的,在 Ubuntu 下生成的 core 文件就简单地命名为 core。无论有多少个程序生成了 core 文件,它们都统一叫做 core。这意味着在同一目录下最多只会有一个 core 文件存在,这样就不会因为多个 core 文件而造成磁盘空间阻塞的问题。

在不同的 Linux 平台和不同版本的 Linux 内核中,核心转储文件(core dump)的命名规则可能有所不同:

  • Ubuntu 的某些内核版本下:生成的 core 文件就直接命名为 core
  • CentOS 的某些内核版本下:生成的 core 文件则会带上进程的 PID,例如命名为 core.<PID>

在这里插入图片描述



6.3 Core 文件的作用演示

为了方便演示效果,我这里加上多几句打印语句,其中真正出错的语句在第25行:除零异常

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

int main()
{

    cout << "hello world" << endl;
    cout << "hello world" << endl;
    cout << "hello world" << endl;
    cout << "hello world" << endl;
    cout << "hello world" << endl;
    cout << "hello world" << endl;
    cout << "hello world" << endl;
    cout << "hello world" << endl;
    cout << "hello world" << endl;
    cout << "hello world" << endl;
    cout << "hello world" << endl;
    cout << "hello world" << endl;
    cout << "hello world" << endl;
    cout << "hello world" << endl;
    
    int a = 10;
    a /= 0;

    while (true)
    {};
}


编译该文件:同时加上 -g

如果你的可执行文件没有包含调试信息,GDB 将无法提供源代码级别的调试信息。确保在编译时添加 -g 选项以包含调试信息。

gcc -g -o myprogram myprogram.c


运行该可执行文件:生成核心转储文件 core

在这里插入图片描述


GDB 调试该可执行文件,输入 core-file core:表示将 核心转储文件 加载到GDB调试器中

在这里插入图片描述



作用:将我们代码出错程序的行号及相关信息直接展示出来!

印证了我们前面讲解的,核心转储文件的作用就是方便我们调试



6.4 进程退出码中的 core


在这里插入图片描述



当进程被信号所杀时,进程退出码的 0~7 为信号编号

当系统的 core 开放后,当进程被 core 类型的信号所杀时,进程退出码就是 core dump 标志,等于 1

当系统的 core 不开放, core dump 标志一直置为 0



综上所述,一个进程是否会出现 core dump,取决于两个条件:

  • 1、退出信号是否终止动作是core
  • 2、服务器是否开启core功能!

在这里插入图片描述




验证:

代码:创造除零错误的场景

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;

int main()
{

    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return 1;
    }

    if(id == 0)
    {
        // 子进程除零错误,会被信号杀死
        int a = 10;
        a /= 0;
    }
    else if(id > 0)
    {
        int status = 0;
        waitpid(id, &status, 0);
        cout << "eixt signal: " << WTERMSIG(status) << endl;
        cout << "core dump: " << ((status >> 7) & 1) << endl; 
    }
}

代码结果如下:父进程获取子进程退出码,得知 8 号信号杀死该进程,同时 core dump = 1

在这里插入图片描述


如果我将系统的 core 关掉

命令:ulimit -c 0

core dump 就置为 0 了!

在这里插入图片描述



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2291408.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一、html笔记

(一)前端概述 1、定义 前端是Web应用程序的前台部分,运行在PC端、移动端等浏览器上,展现给用户浏览的网页。通过HTML、CSS、JavaScript等技术实现,是用户能够直接看到和操作的界面部分。上网就是下载html文档,浏览器是一个解释器,运行从服务器下载的html文件,解析html、…

PyQt5超详细教程终篇

PyQt5超详细教程 前言 接&#xff1a; [【Python篇】PyQt5 超详细教程——由入门到精通&#xff08;序篇&#xff09;](【Python篇】PyQt5 超详细教程——由入门到精通&#xff08;序篇&#xff09;-CSDN博客) 建议把代码复制到pycahrm等IDE上面看实际效果&#xff0c;方便理…

洛谷 P8724 [蓝桥杯 2020 省 AB3] 限高杆

洛谷题目传送门 题目描述 某市有 n 个路口&#xff0c;有 m 段道路连接这些路口&#xff0c;组成了该市的公路系统。其中一段道路两端一定连接两个不同的路口。道路中间不会穿过路口。 由于各种原因&#xff0c;在一部分道路的中间设置了一些限高杆&#xff0c;有限高杆的路…

虚幻UE5手机安卓Android Studio开发设置2025

一、下载Android Studio历史版本 步骤1&#xff1a;虚幻4.27、5.0、5.1、5.2官方要求Andrd Studio 4.0版本&#xff1b; 5.3、5.4、5.5官方要求的版本为Android Studio Flamingo | 2022.2.1 Patch 2 May 24, 2023 虚幻官网查看对应Andrd Studiob下载版本&#xff1a; https:/…

JavaWeb入门-请求响应(Day3)

(一)请求响应概述 请求(HttpServletRequest):获取请求数据 响应(HttpServletResponse):设置响应数据 BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器就可访问,应用程序的逻辑和数据都存储在服务端(维护方便,响应速度一般) CS架构:Client/ser…

【Rust】18.2. 可辩驳性:模式是否会无法匹配

喜欢的话别忘了点赞、收藏加关注哦&#xff08;加关注即可阅读全文&#xff09;&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 18.2.1. 模式的两种形式 模式有两种形式&#xff1a; 可辩驳的&#xff08;可失败的&…

【SLAM】于AutoDL云上GPU运行GCNv2_SLAM的记录

配置GCNv2_SLAM所需环境并实现AutoDL云端运行项目的全过程记录。 1. 引子 前几天写了一篇在本地虚拟机里面CPU运行GCNv2_SLAM项目的博客&#xff1a;链接&#xff0c;关于GCNv2_SLAM项目相关的介绍请移步此文章&#xff0c;本文不再重复说明。 GCNv2: Efficient Corresponde…

【自然语言处理(NLP)】基于Transformer架构的预训练语言模型:BERT 训练之数据集处理、训练代码实现

文章目录 介绍BERT 训练之数据集处理BERT 原理及模型代码实现数据集处理导包加载数据生成下一句预测任务的数据从段落中获取nsp数据生成遮蔽语言模型任务的数据从token中获取mlm数据将文本转换为预训练数据集创建Dataset加载WikiText-2数据集 BERT 训练代码实现导包加载数据构建…

41【文件名的编码规则】

我们在学习的过程中&#xff0c;写出数据或读取数据时需要考虑编码类型 火山采用&#xff1a;UTF-16 易语言采用&#xff1a;GBK php采用&#xff1a;UTF-8 那么我们写出的文件名应该是何种编码的&#xff1f;比如火山程序向本地写出一个“测试.txt”&#xff0c;理论上这个“测…

使用MATLAB进行雷达数据采集可视化

本文使用轮趣科技N10雷达&#xff0c;需要源码可在后台私信或者资源自取 1. 项目概述 本项目旨在通过 MATLAB 读取 N10 激光雷达 的数据&#xff0c;并进行 实时 3D 点云可视化。数据通过 串口 传输&#xff0c;并经过解析后转换为 三维坐标点&#xff0c;最终使用 pcplayer 进…

沙皮狗为什么禁养?

各位铲屎官们&#xff0c;今天咱们来聊聊一个比较敏感的话题&#xff1a;沙皮狗为什么会被禁养&#xff1f;很多人对沙皮狗情有独钟&#xff0c;但有些地方却明确禁止饲养这种犬种&#xff0c;这背后到底是什么原因呢&#xff1f;别急&#xff0c;今天就来给大家好好揭秘&#…

Dest1ny漏洞库:用友 U8 Cloud ReleaseRepMngAction SQL 注入漏洞(CNVD-2024-33023)

大家好&#xff0c;今天是Dest1ny漏洞库的专题&#xff01;&#xff01; 会时不时发送新的漏洞资讯&#xff01;&#xff01; 大家多多关注&#xff0c;多多点赞&#xff01;&#xff01;&#xff01; 0x01 产品简介 用友U8 Cloud是用友推出的新一代云ERP&#xff0c;主要聚…

DeepSeek-R1模型1.5b、7b、8b、14b、32b、70b和671b有啥区别?

deepseek-r1的1.5b、7b、8b、14b、32b、70b和671b有啥区别&#xff1f;码笔记mabiji.com分享&#xff1a;1.5B、7B、8B、14B、32B、70B是蒸馏后的小模型&#xff0c;671B是基础大模型&#xff0c;它们的区别主要体现在参数规模、模型容量、性能表现、准确性、训练成本、推理成本…

#define,源文件与头文件,赋值表达式

1.#define 1.1定义 #define 是一个预处理指令&#xff0c;用于定义宏 宏&#xff0c;是预处理阶段&#xff08;在编译之前&#xff09;由预处理器处理的代码片段 1.2使用 1.2.1 #define 可以定义常量 #define PI 3.14159 1.2.2 #define 可以定义宏函数 #define SQUARE(x) ((…

5分钟在本地PC上使用VLLM快速启动DeepSeek-R1-Distill-Qwen-32B

5分钟在本地PC上使用VLLM快速启动DeepSeek-R1-Distill-Qwen-32B 前言环境准备所需工具创建虚拟环境安装VLLM及依赖库 模型下载安装Hugging Face CLI下载DeepSeek-R1-Distill-Qwen-32B 模型启动启动命令启动确认 模型验证发送API请求示例输出 注意事项参考链接 前言 VLLM 是一个…

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】1.13 降维打击:扁平化操作的六种武器

1.13 降维打击&#xff1a;扁平化操作的六种武器 目录 #mermaid-svg-bbLxDryjxBbXe3tu {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-bbLxDryjxBbXe3tu .error-icon{fill:#552222;}#mermaid-svg-bbLxDryjxBbXe3tu…

Oracle Primavera P6 最新版 v24.12 更新 2/2

目录 一. 引言 二. P6 EPPM 更新内容 1. 用户管理改进 2. 更轻松地标准化用户设置 3. 摘要栏标签汇总数据字段 4. 将里程碑和剩余最早开始日期拖到甘特图上 5. 轻松访问审计数据 6. 粘贴数据时排除安全代码 7. 改进了状态更新卡片视图中的筛选功能 8. 直接从活动电子…

AI-on-the-edge-device - 将“旧”设备接入智能世界

人工智能无处不在&#xff0c;从语音到图像识别。虽然大多数 AI 系统都依赖于强大的处理器或云计算&#xff0c;但**边缘计算**通过利用现代处理器的功能&#xff0c;使 AI 更接近最终用户。 本项目演示了使用 **ESP32**&#xff08;一种低成本、支持 AI 的设备&#xff09;进行…

Openfga 授权模型搭建

1.根据项目去启动 配置一个 openfga 服务器 先创建一个 config.yaml文件 cd /opt/openFGA/conf touch ./config.yaml 怎么配置&#xff1f; 根据官网来看 openfga/.config-schema.json at main openfga/openfga GitHub 这里讲述详细的每一个配置每一个类型 这些配置有…

C++模板编程——可变参函数模板之折叠表达式

目录 1. 什么是折叠表达式 2. 一元左折 3. 一元右折 4. 二元左折 5. 二元右折 6. 后记 上一节主要讲解了可变参函数模板和参数包展开&#xff0c;这一节主要讲一下折叠表达式。 1. 什么是折叠表达式 折叠表达式是C17中引入的概念&#xff0c;引入折叠表达式的目的是为了…