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




在这里插入图片描述



信号快速认识


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.mzph.cn/bicheng/69911.shtml

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

相关文章

stm32硬件实现与w25qxx通信

使用的型号为stm32f103c8t6与w25q64。 STM32CubeMX配置与引脚衔接 根据stm32f103c8t6引脚手册&#xff0c;采用B12-B15四个引脚与W25Q64连接&#xff0c;实现SPI通信。 W25Q64SCK&#xff08;CLK&#xff09;PB13MOSI&#xff08;DI&#xff09;PB15MISO(DO)PB14CS&#xff08…

22.Word:小张-经费联审核结算单❗【16】

目录 NO1.2 NO3.4​ NO5.6.7 NO8邮件合并 MS搜狗输入法 NO1.2 用ms打开文件&#xff0c;而不是wps❗不然后面都没分布局→页面设置→页面大小→页面方向→上下左右&#xff1a;页边距→页码范围&#xff1a;多页&#xff1a;拼页光标处于→布局→分隔符&#xff1a;分节符…

it基础使用--5---git远程仓库

it基础使用–5—git远程仓库 1. 按顺序看 -git基础使用–1–版本控制的基本概念 -git基础使用–2–gti的基本概念 -git基础使用–3—安装和基本使用 -git基础使用–4—git分支和使用 2. 什么是远程仓库 在第一篇文章中&#xff0c;我们已经讲过了远程仓库&#xff0c;每个本…

aitraderv4.2开发计划,整合QMT。年化39.9%的因子与年化19.3%的策略哪个优?

原创内容第784篇&#xff0c;专注量化投资、个人成长与财富自由。 昨天我们发布的aitrader v4.1的代码&#xff1a;aitrader_v4.1系统更新|含年化39.1%的组合策略代码|backtraderopenctp实盘&#xff08;代码数据&#xff09; 星球下周代码计划&#xff1a; 1、考虑整合back…

玩转大语言模型——使用langchain和Ollama本地部署大语言模型

系列文章目录 玩转大语言模型——使用langchain和Ollama本地部署大语言模型 玩转大语言模型——ollama导入huggingface下载的模型 玩转大语言模型——langchain调用ollama视觉多模态语言模型 玩转大语言模型——使用GraphRAGOllama构建知识图谱 玩转大语言模型——完美解决Gra…

word2vec 实战应用介绍

Word2Vec 是一种由 Google 在 2013 年推出的重要词嵌入模型,通过将单词映射为低维向量,实现了对自然语言处理任务的高效支持。其核心思想是利用深度学习技术,通过训练大量文本数据,将单词表示为稠密的向量形式,从而捕捉单词之间的语义和语法关系。以下是关于 Word2Vec 实战…

yes镜像站群/PHP驱动的镜像站群架构实践

▍当前站群运维的三大技术困局 在近期与多个IDC服务商的交流中发现&#xff0c;传统站群系统普遍面临&#xff1a; 同步效率瓶颈&#xff1a;跨服务器内容同步耗时超过行业标准的42%SEO权重稀释&#xff1a;镜像站点重复率导致70%的站点无法进入百度前3页运维成本失控&#x…

走向基于大语言模型的新一代推荐系统:综述与展望

HightLight 论文题目&#xff1a;Towards Next-Generation LLM-based Recommender Systems: A Survey and Beyond作者机构&#xff1a;吉林大学、香港理工大学、悉尼科技大学、Meta AI论文地址&#xff1a; https://arxiv.org/abs/2410.1974 基于大语言模型的下一代推荐系统&…

Verilog语言学习总结

Verilog语言学习&#xff01; 目录 文章目录 前言 一、Verilog语言是什么&#xff1f; 1.1 Verilog简介 1.2 Verilog 和 C 的区别 1.3 Verilog 学习 二、Verilog基础知识 2.1 Verilog 的逻辑值 2.2 数字进制 2.3 Verilog标识符 2.4 Verilog 的数据类型 2.4.1 寄存器类型 2.4.2 …

智慧园区综合管理系统如何实现多个维度的高效管理与安全风险控制

内容概要 在当前快速发展的城市环境中&#xff0c;智慧园区综合管理系统正在成为各类园区管理的重要工具&#xff0c;无论是工业园、产业园、物流园&#xff0c;还是写字楼与公寓&#xff0c;都在积极寻求如何提升管理效率和保障安全。通过快鲸智慧园区管理系统&#xff0c;用…

JavaFX - 事件处理

在 JavaFX 中&#xff0c;我们可以开发 GUI 应用程序、Web 应用程序和图形应用程序。在此类应用程序中&#xff0c;每当用户与应用程序 &#xff08;节点&#xff09; 交互时&#xff0c;都会称其发生了事件。 例如&#xff0c;单击按钮、移动鼠标、通过键盘输入字符、从列表中…

小米CR6606,CR6608,CR6609 启用SSH和刷入OpenWRT 23.05.5

闲鱼上收了一台CR6606和一台CR6609, 一直没时间研究, 趁春节假期把这两个都刷成 OpenWRT 配置说明 CPU: MT7621AT&#xff0c;双核880MHz内存: NT5CC128M16JR-EKI 或 M15T2G16128A, 256MB闪存: F59L1G81MB, 128MB无线基带芯片(BB): T7905DAN无线射频芯片(RF): MT7975DN无外置F…

使用windows笔记本让服务器上网

使用windows笔记本让服务器上网 前言准备工具开始动手实践1.将手机热点打开&#xff0c;让Windows笔记本使用无线网卡连接上网2.使用网线将Windows笔记本的有线网卡和服务器的有线网卡直连3.在Windows笔记本上按winR输入ncpa.cpl打开网卡设置界面4.在Windows笔记本上右键“无线…

2007-2019年各省科学技术支出数据

2007-2019年各省科学技术支出数据 1、时间&#xff1a;2007-2019年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;行政区划代码、地区名称、年份、科学技术支出 4、范围&#xff1a;31省 5、指标解释&#xff1a;科学技术支出是指为促进科学研究、技术开发…

6. 使用springboot做一个音乐播放器软件项目【1.0版项目完结】附带源码~

#万物OOP 注意&#xff1a; 本项目只实现播放音乐和后台管理系统。 不分享任何音乐歌曲资源。 上一篇文章我们 做了音乐播放器后台的功能。参考地址&#xff1a; https://jsonll.blog.csdn.net/article/details/145214363 这个项目已经好几天也没更新了&#xff0c;因为临近放…

【Rust自学】15.4. Drop trait:告别手动清理,释放即安全

喜欢的话别忘了点赞、收藏加关注哦&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 15.4.1. Drop trait的意义 类型如果实现了Drop trait&#xff0c;就可以让程序员自定义当值离开作用域时发生的操作。例如文件、网络资源…

2025年1月22日(网络编程 udp)

系统信息&#xff1a; ubuntu 16.04LTS Raspberry Pi Zero 2W 系统版本&#xff1a; 2024-10-22-raspios-bullseye-armhf Python 版本&#xff1a;Python 3.9.2 已安装 pip3 支持拍摄 1080p 30 (1092*1080), 720p 60 (1280*720), 60/90 (640*480) 已安装 vim 已安装 git 学习…

嵌入式C语言:大小端详解

目录 一、大小端的概念 1.1. 大端序&#xff08;Big-endian&#xff09; 1.2. 小端序&#xff08;Little-endian&#xff09; 二、大小端与硬件体系的关系 2.1. 大小端与处理器架构 2.2. 大小端与网络协议 2.3. 大小端对硬件设计的影响 三、判断系统的大小端方式 3.1.…

当WebGIS遇到智慧文旅-以长沙市不绕路旅游攻略为例

目录 前言 一、旅游数据组织 1、旅游景点信息 2、路线时间推荐 二、WebGIS可视化实现 1、态势标绘实现 2、相关位置展示 三、成果展示 1、第一天旅游路线 2、第二天旅游路线 3、第三天旅游路线 4、交通、订票、住宿指南 四、总结 前言 随着信息技术的飞速发展&…

85.[1] 攻防世界 WEB easyphp

进入靶场 属于代码审计 <?php // 高亮显示当前 PHP 文件的源代码&#xff0c;常用于调试或展示代码 highlight_file(__FILE__);// 初始化两个标志变量&#xff0c;用于后续条件判断 $key1 0; $key2 0;// 从 GET 请求中获取参数 a 和 b $a $_GET[a]; $b $_GET[b];// 检…