【Linux】进程通信 — 信号(上篇)

文章目录

  • 前言
  • 1. 什么是信号
    • 1.1 认识信号:
    • 1.2 信号的产生:
    • 1.3 信号的异步:
    • 1.4 信号的处理:
  • 2. 前后台进程
  • 3. 系统接口
    • 3.1 signal:
      • 3.1 - 1 不能被捕捉的信号
    • 3.2 kill:
      • 3.2 - 1 killall
    • 3.3 raise:
    • 3.4 abort:
    • 3.5 alarm:
  • 4. 崩溃的本质是什么
    • 4.1 Core Dump:
      • 4.1 - 1 Core Dump打开方式
      • 4.1 - 2 使用方式
      • 4.1 - 3 默认关闭的原因

前言

本章我们将讲解Linux信号这部分的内容,本章将介绍信号的产生,发送,信号的捕捉,屏蔽等操作,将对信号进行一些列系统的了解与学习。目标已经确定,接下来就要搬好小板凳,准备开讲了…


1. 什么是信号

1.1 认识信号:

  • 在我们学习信号之前,我们先来回忆一下生活中的各种信号,例如:红绿灯、铃声、闹钟……
  • 我们在能够认识这些场景下的信号以及所表示的含义:
    • 即便这个信号还没有产生,我们就已经具备了处理这个信号的能力。
  • 我们早就知道了信号产生后要做什么:
    • 即便当前信号还没有产生,我们已经提前知道了这个信号的处理方法。
  • 信号是给进程发送的,进程要具备处理信号的能力。
  • 可以说程序员通过编写代码来利用操作系统提供的接口和功能,实现了处理信号的能力。
    • 该能力一定是预先已经早就有了的。
    • 进程能够识别对应的信号。
    • 进程能够处理对应信号。

对于进程来讲,即便是信号还没有产生,我们进程已经具有识别和处理这个信号的能力了。

  • 信号的种类:

使用kill -l命令罗列出来的内容叫做信号,我们可以看到目前Linux系统下64种不同的类型:

【Linux】进程通信 — 信号(上篇)_第1张图片

  • 没有32、33、0号信号。
  • 第一批1 ~ 31(普通信号)
  • 第二批34 ~ 64(实时信号)

信号左侧的数字和右侧的名称是一回事,其实都是宏,大写的字母是宏名称,宏的值就是左侧对应的编号。

这二者的差别是:早期有实时操作系统,我们现在用的是分时操作系统。
基于时间片轮转,基于优先级抢占的调度算法。

1.2 信号的产生:

有很多情况会产生信号:

  1. 系统接口(kill命令)
  2. 键盘产生(Ctrl + C,Ctrl + \ )
  3. 软件条件(进程停止,进程运行完退出)
  4. 硬件异常(比如除0错误)

信号发送的本质:

  • 键盘是产生了信号,但是信号是操作系统发的。
  • 在位图中,将对应的位置设置为1,就完成了信号的发送。
  • 与其叫发送,不如叫操作系统向进程写入信号。

信号都是由操作系统向系统写入的:

  • 计算机要是想向一个PCB进程发信号,本质上因为操作系统是进程的管理者。
  • 可以直接以自身的身份来对进程的PCB数据结构的位图做任意修改。

崩溃现象就是底层代码引起了硬件的问题,进而被操作系统识别,然后操作系统将硬件问题识别成信号,然后向进程发送,然后终止进程。

1.3 信号的异步:

何为异步:

以点外卖为例,当外卖到了时,你可能正在忙着做其他事情,外卖员给你发了条取餐消息,但是你并不能立即去取。
此时我们知道自己的外卖到了(知道收到了信号),等手上的活忙完了再去取(过一会再去处理信号)。

同步和异步:

  • 当节奏会受某个因素影响时,这叫同步。
  • 当节奏不会受某个因素影响时,这叫异步。

信号可能在任何时候都能产生,可能是用户产生,也可能是操作系统产生的,这个产生对进程来讲是异步的。

因为信号产生是异步的:

  • 当信号产生的时候,对应的进程可能正在做更重要的事情,我们进程可以暂时不处理这个信号!
  • 也就是说进程可能不需要立即处理这个信号!
  • 但是并不代表这个信号不会被处理!

1.4 信号的处理:

处理信号的三种行为:

  1. 默认动作。
  2. 忽略。
  3. 自定义动作。

信号的处理,也叫做信号的的捕捉,递达处理动作。

必须记住这个信号有没有,是什么信号:

  • 信号被记录在了进程的PCB当中的:
    • 有没有产生【比特位的内容1/0】
    • 是什么信号产生【比特位的位置】

只有操作系统有这个权利,能直接修改这个task_struct内的数据位图!
OS是进程的管理者,进程的所有的属性的获取和设置,只能由OS来!!
无论信号怎么产生,最终一定只能是OS帮我们进行信号的设置的!


2. 前后台进程

Ctrl + C的本质是向前台进程发送信号。

【Linux】进程通信 — 信号(上篇)_第2张图片
我们死循环打印Hello World,在一直死循环期间,我们输入命令ls并不会列出该目录下的文件名。

myproce跑起来之后,再输入其他指令是没用的,因为这个进程占用了前面bash所对应的终端。当前bash没法做命令行响应,此时这种进程叫做前台进程。

将进程放到后台:

【Linux】进程通信 — 信号(上篇)_第3张图片

26733是进程编号。

后台进程,可以执行命令行指令,但是用Ctrl + C终止不了了。

【Linux】进程通信 — 信号(上篇)_第4张图片
jobs查看后台进程:

【Linux】进程通信 — 信号(上篇)_第5张图片

fg 1就将该进程提至前台了,再次Ctrl + C就可以了。

补充:

  • 前后端混打的时候,虽然会打印乱掉,混乱是很正常的因为缺少访问控制,信息交叉在一起。根据冯·诺依曼体系,我们输入的内容一定是先被进程拿到的,显示器之所以能看到,是因为给显示器也拷贝了一份,这叫回显。
  • 不回显也可以的,就像Linux输入密码,不回显但是确实输进去了。

任务管理:

【Linux】进程通信 — 信号(上篇)_第6张图片
在Linux中,作业列表中的符号+-表示了作业的状态。

  • 符号"+"表示当前前台运行的作业。
  • 符号"-"表示当前后台运行的作业。

如果没有"+“和”-"符号显示在作业列表中,则表示当前没有前台或后台运行的作业。作业列表可能是空的,也就是没有任何正在运行的作业。这通常发生在你没有在前台执行命令或将任何作业放到后台时

bg指令:
要将一个正在前台运行的作业切换到后台运行,可以按下"Ctrl + Z",这会将该作业暂停,并返回到命令行界面。然后,可以使用"bg"命令将作业放到后台继续运行,此时作业会继续执行,但不会再占用终端。


3. 系统接口

3.1 signal:

【Linux】进程通信 — 信号(上篇)_第7张图片

Ctrl + C是向前台发送二号信号。

代码演示:

#include 
#include 
#include 

using namespace std;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}

int main()
{
    // 给该信号设置了回调捕捉,自定义动作
    // 这里不是调用handler方法,这里只是设置了一个回调,让SIGINT产生的时候,该方法才会被调用。
    // 如果不产生SIGINT,该方法不会被调用!
    // 当二号信号产生的时候,才调用后面的方法。
    signal(SIGINT, handler);
    signal(3, handler);

    sleep(3);
    cout << "进程已经设置完了" << endl; 

    sleep(3);

    while (true)
    {
        cout << "我是一个正在运行中的进程: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

注意:

  • 给该信号设置了回调捕捉,自定义动作。
  • 这里不是调用handler方法,这里只是设置了一个回调,让SIGINT产生的时候,该方法才会被调用。
  • 如果不产生SIGINT,该方法不会被调用!!
  • 当二号信号产生的时候,才调用后面的方法。

函数指针,回调函数:

  • 函数指针类型,允许用户对信号自定义处理,忽略,自定义,默认。
  • 大部分信号都有默认动作,而signal方法可以让进程对特定的信号自定义设置。

【Linux】进程通信 — 信号(上篇)_第8张图片

  • Ctrl + C本质是给前台产生了2号信号,发送给目标进程,其中目标进程默认对2号信号的处理,是终止自己。
  • 更改了对二号信号处理,设置了用户自定义处理方法。
  • 还有一种终止进程的方法是发送3号信号

3.1 - 1 不能被捕捉的信号

  • 到这里我们不禁思索一番,我们之前讲过kill -9 + 进程ID信号可以杀掉进程,那我们能否将kill -9 + 进程ID的信号捕捉了呢?
  • 经过实验,得到结论,kill -9 + 进程ID该信号并不能被捕捉,这是为了防止一些恶意进程杀不掉的情况。
  • 因为9号信号不能被设置捕捉动作。永远都是默认动作,叫做管理员信号。
  • 9号信号几乎可以杀掉所有进程,除了曾经讲的D状态的进程。进程状态复习-传送门
  • 在Linux中,有一些信号被称为"不可捕捉信号",它们无法被用户进程捕捉或处理。这些信号是:
  • SIGKILL (信号编号为9):用于立即终止一个进程。无论进程是否希望接收该信号,都无法阻止或忽略它。
  • SIGSTOP (信号编号为19或17):用于暂停一个进程的执行。与SIGKILL类似,无法被捕捉或忽略。
  • SIGCONT (信号编号为18或19):用于继续一个被暂停的进程的执行。与前两个信号不同,SIGCONT是可以被捕捉的,但在默认情况下,它会立即恢复进程的执行。

这些不可捕捉信号通常由操作系统或其他系统级实体发送,用于管理进程的状态和行为。在正常情况下,用户进程无法阻止或修改这些信号的执行。

3.2 kill:

kill不仅是命令而且也是系统调用接口:

【Linux】进程通信 — 信号(上篇)_第9张图片

  • 向指定进程发送指定信号,成功了返回0,失败了返回-1。
  • 支持向任意进程发送任意信号。
  • 杀进程也是要有权限的。

不能杀掉不是自己的进程:

在这里插入图片描述
有了上述接口,再加上我们之前学的main函数的几个参数,我们可以手搓一个kill指令:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

static void Usage(const string& proc)
{
    cout << "Usage:\n\t" << proc << "signo pid" << endl;
}

// 自己实现一个kill命令
// mykill 9 1234
int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    if (kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == -1)
    {
        cerr << "kill: " << strerror(errno) << endl;
        exit(2);
    }
    
    return 0;
}

3.2 - 1 killall

根据进程名字杀掉某个进程:killall + 进程名

  • 在Linux中,killall命令用于终止同名进程。
  • killall命令默认会发送SIGTERM(信号编号为15)信号给目标进程。
  • 不过,你也可以使用参数"-s"或"–signal"来指定其他信号,例如SIGKILL(信号编号为9)。
  • 这个命令非常有用,特别是当你想要快速终止所有同名进程时。
  • 需要注意的是,使用killall命令要小心,确保只终止你想要终止的进程,以免造成意外的影响。

3.3 raise:

kill是给任意进程发任意信号,raise是给自己发任意信号:

【Linux】进程通信 — 信号(上篇)_第10张图片
进程不断地给自己发送2号信号:

#include 
#include 
#include 

using namespace std;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}

int main()
{
    // 这里没有调用对应的handler方法,仅仅是注册
    
    signal(2, handler);

    while (true)
    {
        // 每次循环都给自己发送2号信号
        sleep(1);
        raise(2);
    }
    // 每隔1秒都会收到一个2号信号

    return 0;
}

在这里插入图片描述

3.4 abort:

向自己发送6号SIGABRT信号:

#include 
#include 
#include 
#include 

using namespace std;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
}

int main()
{
    // 这里没有调用对应的handler方法,仅仅是注册
    
    signal(6, handler);

    while (true)
    {
        sleep(1);
        abort();// exit(), ahort();
    }

    return 0;
}

终止进程:

在这里插入图片描述
注意:

  • abort()是即使捕捉了,但是依然会退出进程。
  • 除了9号信号不能被捕捉,对6号信号进行捕捉,但是依旧会退出。

硬件是在推着操作系统做一系列动作:

  • 时钟硬件 —— 给操作系统发送时钟中断。
  • CPU主频越高调度的频率就越高,效率就越高。

3.5 alarm:

#include 
#include 
#include 
#include 

using namespace std;
    
int cnt = 0;

void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << "cnt: " << cnt << endl;
    exit(1);
}

// 信号闹钟
int main()
{
    // 未来一秒钟之后会超时

    signal(SIGALRM, handler);

    alarm(1);
    // 如果没有自定义操作,默认alarm会自定义终止,会收到SIGALRM信号

    // 统计该进程一秒钟cnt++多少次
    while (1)
    {
        cnt++;
        // cout << "hello: " << cnt++ << endl;
    }

    return 0;
}

相比于CPU独立做计算,IO非常慢。

【Linux】进程通信 — 信号(上篇)_第11张图片

  • 在Linux中,默认情况下,当alarm定时器到期时,会生成一个SIGALRM信号。 如果进程没有捕获和处理该信号,那么该进程会被终止。
  • SIGALRM信号是用于告知进程某个定时器已经超时的信号。它通常由内核或通过使用alarm函数设置的定时器触发。当定时器超时时,内核向进程发送SIGALRM信号,进程可以选择捕获和处理该信号,或者使用默认操作(即终止进程)。
  • 如果进程没有显式地设置对SIGALRM信号的处理方式(通过信号处理函数或信号处理器),那么SIGALRM信号将以默认操作的方式处理,即终止进程。这意味着如果定时器超时并且进程没有捕获该信号,进程会被终止。

4. 崩溃的本质是什么

在Linux中越界访问都叫段错误。

所谓的崩溃,本质是什么呢?

  • 是该进程收到了异常信号,应该叫进程崩溃了。

进程崩溃是因为收到了异常信号,那么为什么会收到异常信号呢?

  • 除零问题:计算是在CPU内部,内有状态寄存器,该寄存器是用来表征该本次计算是否出现问题。
  • 如果有问题,状态寄存器中特定标志位会被计位。

C++ try catch:

  • 崩溃了,一定会导致进程终止吗?不一定!!
    • 崩溃,本质是什么呢?
    • 进程崩溃的本质,是该进程收到了异常信号!
  • 为什么呢?
  • 因为硬件异常,而导致OS向目标进程发送信号,进而导致进程终止的现象!
    • 除零: CPU内部,状态寄存器,当我们除0的时候,CPU内的状态寄存器会被设置成为,有报错:浮点数越界,CPU的内部寄存器(硬件),OS就会识别到CPU内有报错啦:
      • 1.谁干的?2.是什么报错(OS -> 构建信号) -> 目标进程发送信号 -> 目标进程在合适的时候 -> 处理信号 -> 终止进程。
    • 越界 && 野指针: 我们在语言层面使用的地址(指针),实都是虛拟地址 -> 物理地址 -> 物理内存 -> 读取对应的数据和代码的。
    • 如果虚拟地址有问题,地址转化的工作是由(MMU(硬件) + 页表(软件)),转化过程就会引起问题 -> 表现在硬件MMU上 -> OS发现硬件出现了问题:
      • 1.谁干的?2.是什么报错(OS -> 构建信号) -> 目标进程发送信号 -> 目标进程在合适的时候 -> 处理信号 -> 终止进程。

MMU是内存管理单元(Memory Management Unit)的简称。

实操注意:

  • 当进程崩溃时,对某个信号进行时捕捉时:
    • 要将自定义的handler函数最后exit(1);退出进程,不然会一直发信号,就会一直调用handler函数。
    • 因为一般进程崩溃时,操作系统会给进程发送对应的信号并终止进程。

【Linux】进程通信 — 信号(上篇)_第12张图片

  • 如果不加上最后的退出进程,会一直打印刷屏。
  • 一旦我们不进行信号捕捉,会直接终止。
  • 而我们捕捉之后如果没有对信号做处理,没有终止进程的话,会一直刷屏,进程没有被终止。

因为没有解决这个问题,这个异常一直都在,所以操作系统一直给进程发信号,所以刷屏了。

4.1 Core Dump:

Core Dump会把进程在运行中,对应的异常上下文数据,core dump到磁盘上,方便调试。

发上云服务器是设置成0的,禁止发生core dump(一般是关掉的),但是可以打开。

4.1 - 1 Core Dump打开方式

【Linux】进程通信 — 信号(上篇)_第13张图片
Core不光光要终止,还要发生Core dump。

【Linux】进程通信 — 信号(上篇)_第14张图片
8号信号本身就要产生core文件的,然后指令发现多了一个文件,里面是乱码。

在这里插入图片描述
当一个进程异常退出时,收到了某些信号,系统为了便于用户调试,会告诉用户触发core dump机制,core dump机制叫做核心转储

4.1 - 2 使用方式

代码演示:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

int main()
{   
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
       int *p = nullptr;
       *p = 1000; // 野指针问题
       
        exit(1);
    }

    // 父进程
    int status = 0;
    // nullptr, NULL, 0, '\0' == 0;
    waitpid(id, &status, 0); // nullptr

    // core dump表明当前进程在退出时,是否发生core dump
    printf("exitcode: %d, signo: %d, core dump flag: %d\n",
           (status >> 8) & 0xFF, status & 0x7F, (status >> 7) & 0x1);

    return 0;
}

【Linux】进程通信 — 信号(上篇)_第15张图片

核心转储(Core Dump)是指在程序运行过程中发生了严重错误导致程序崩溃时,系统将程序内存的完整快照保存到一个核心转储文件中。

  • 这个文件包含了程序崩溃时的内存状态、寄存器的内容以及其他相关的调试信息。
  • 核心转储文件主要用于程序崩溃分析和调试目的。通过分析核心转储文件,开发人员可以了解程序崩溃时的内存状态,定位错误的原因,并进行问题排查和修复。
  • 核心转储文件通常具有可读性较低的二进制格式,需要使用调试工具或分析器来解析和分析。
  • 在许多操作系统上,默认情况下,当程序崩溃时会自动生成核心转储文件。开发人员也可以在程序中通过设置相应的参数或使用调试工具来控制核心转储的生成及其行为。
  • 需要注意的是,由于核心转储文件可能会包含敏感信息,如内存中的数据,因此在进行排查和分析时需要遵守相应的隐私保护规定,并确保核心转储文件的安全性。

生成的Core Dump文件很大:

在这里插入图片描述

Core Dump一般配台gdb使用:

【Linux】进程通信 — 信号(上篇)_第16张图片
每次执行出错都会产生core dump文件,这种调试策略叫做事后调试。

4.1 - 3 默认关闭的原因

云服务器关掉的原因:

因为如果大型程序,一旦代码有问题,会自动重启程序,如果一重启就挂掉,就会产生core文件,所以一直重启就会产生大量的core文件,就很大概率将磁盘空间被打满,此时就会危及到操作系统正常工作了。

你可能感兴趣的:(Linux,linux)