【Linux】进程 信号的产生

🌻个人主页路飞雪吖~

🌠专栏:Linux


目录

一、掌握Linux信号的基本概念

[🌠前台进程 VS 后台进程](#🌠前台进程 VS 后台进程)

[🌠 小贴士:](#🌠 小贴士:)

[🪄⼀个系统函数 --- signal()](#🪄⼀个系统函数 --- signal())

[🪄查看信号 --- man 7 signal](#🪄查看信号 --- man 7 signal)

[二、软硬件上理解 : OS如何进行信号处理](#二、软硬件上理解 : OS如何进行信号处理)

✨硬件上理解

[🌠 信号 VS 硬件中断](#🌠 信号 VS 硬件中断)

✨软件:如何理解信号处理?

三、✨信号产生的方式

[🚩1. 键盘产生](#🚩1. 键盘产生)

[🚩2. 系统指令产生 发送信号 ---- 底层使用的是系统调用​编辑](#🚩2. 系统指令产生 发送信号 ---- 底层使用的是系统调用编辑)

[🚩3. 系统调用 发送信号](#🚩3. 系统调用 发送信号)

[🌠 发送信号的其他函数:](#🌠 发送信号的其他函数:)

[🪄 raise --- 函数可以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号)](#🪄 raise --- 函数可以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号))

[🪄 abort --- 自己给自己发了一个特定的终止自己的信号 --- 6号信号SIGABRT](#🪄 abort --- 自己给自己发了一个特定的终止自己的信号 --- 6号信号[SIGABRT])

[🚩4. 由软件条件 产生信号](#🚩4. 由软件条件 产生信号)

🌠OS内定时器

[四、✨ alarm() 设置重复闹钟 --- 简单快速理解系统闹钟](#四、✨ alarm() 设置重复闹钟 --- 简单快速理解系统闹钟)

[🚩5. 异常 (野指针、 除 0)](#🚩5. 异常 (野指针、 除 0))

[🪄模拟 野指针](#🪄模拟 野指针)

[🪄 模拟 除 0](#🪄 模拟 除 0)

🌠OS怎么知道我们的进程内部出错了?为什么会陷入死循环?

[☄️ 除 0 :](#☄️ 除 0 :)

[☄️ 野指针](#☄️ 野指针)

[🌠core VS Term](#🌠core VS Term)

[✨core 的使用 --- 调试](#✨core 的使用 --- 调试)

[🌠 如果是子进程异常了呢? core 会不会出现?](#🌠 如果是子进程异常了呢? core 会不会出现?)

🌠小结:

五、信号处理

[1. 默认处理动作](#1. 默认处理动作)

[2. 忽略:本身就是一种信号捕捉的方法,动作就是忽略](#2. 忽略:本身就是一种信号捕捉的方法,动作就是忽略)

[3. 自定义 捕捉一个信号](#3. 自定义 捕捉一个信号)


• 信号是内置的,进程认识信号,是程序员内置的特性;

• 信号的处理方法,在信号产生之前,就已经准备好了;

• 何时处理信号?先处理优先级很高的,可能并不是立即处理,在合适的时候处理;

• 怎么处理信号:a. 默认行为,b. 忽略信号,c. 自定义动作

信号产生 ---> 信号保存 ---> 信号处理

一、掌握Linux信号的基本概念

cpp 复制代码
// Makefile

BIN=sig
CC=g++
SRC=$(shell ls *.cc)
OBJ=$(SRC:.cc=.o)

$(BIN):$(OBJ)
	$(CC) -o $@ $^ -std=c++11

%.o:%.cc
	$(CC) -c $< -std=c++11

.PHONY:clean
clean:
	rm -f $(BIN)
cpp 复制代码
// Signal.cc

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

int main()
{
    while(true)
    {
        std::cout << "hello world" << std::endl;
        sleep(1);
    }

    return 0;
}
🌠前台进程 VS 后台进程

• 前台进程【./sig】:在命令行所输入的所有东西都不能执行。

> Ctrl + c 终止前台进程:给的是shell,shell在前台 --> 信号发给进程。

• 后台进程【./sig &】:bash进程依旧可以进行命令行解释。

> kill -9 pid 终止后台进程

> fg 作业号

🌠 小贴士:

<1>

【1 -- 31】:普通信号;

【34 -- 64】:实时信号。

<2>

ctrl + c --> 信号发给进程,被OS接受并解释成为2号【SIGINT】信号,发送给目标进程 -- 对2号信号的默认处理动作是 终止自己!。

🪄⼀个系统函数 --- signal()

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void Handler(int signo)
{
    // 当对应的信号被触发,内核会将对应的信号编号,传递给自定义方法
    std::cout << "Get a signal, signal number is : " << signo << std::endl;
}

int main()
{
    signal(SIGINT, Handler);// 默认终止 --改成了--> 执行自定义方法:Handler 
    while(true)
    {
        std::cout << "hello world" << std::endl;
        sleep(1);
    }

    return 0;
}
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void Handler(int signo)
{
    // 当对应的信号被触发,内核会将对应的信号编号,传递给自定义方法
    std::cout << "Get a signal, signal number is : " << signo << std::endl;
}

int main()
{
    signal(SIGQUIT, Handler);// 默认终止 --改成了--> 执行自定义方法:Handler 
    while(true)
    {
        std::cout << "hello world" << std::endl;
        sleep(1);
    }

    return 0;
}

ctrl + \ :3号信号,默认也是终止进程的。

🪄查看信号 --- man 7 signal

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void Handler(int signo)
{
    // 当对应的信号被触发,内核会将对应的信号编号,传递给自定义方法
    std::cout << "Get a signal, signal number is : " << signo << std::endl;
}

int main()
{
    // signal 为什么不放在循环里面? 不需要,只需要设置一次就可以了
    // signal:如果没有产生2或者3号信号呢? handler不被调用!
    // signal(SIGINT, Handler);// 默认终止 --改成了--> 执行自定义方法:Handler
    // signal(SIGQUIT, Handler);// 默认终止 --改成了--> 执行自定义方法:Handler

    for (int signo = 1; signo < 32; signo++)// 捕捉 1--31 号的信号,使得很多方法杀不掉进程,但是有一些信号捕捉不到
    {
        signal(signo, Handler);
        std::cout << "自定义捕捉信号:" << signo << std::endl;
    }

    while (true)
    {
        std::cout << "hello world" << std::endl;
        sleep(1);
    }

    return 0;
}

二、软硬件上理解 : OS如何进行信号处理

✨硬件上理解

OS怎么知道键盘上面有数据?

当按下键盘 首先是被驱动程序识别到 按下的按键,OS如何知道键盘被按下了?--- 硬件中断 ,键盘一旦按下,在硬件上 键盘 和 CPU 是连接的,在控制信号,输入设备是直接和中央处理器连接的,输入设备首先会给中央处理器发送一个中断信号【硬件电路】,CPU中央处理器收到这个硬件电路之后,立马就会告诉OS,当前外设有一个外设准备好了,接下来让OS主动的去把外设的数据拷贝到内存里,此时OS再也不用主动轮询检测任何输入/输出设备了,只需要等待发生中断信号 --- 硬件和OS可以并行执行了!

🌠 信号 VS 硬件中断

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

• 硬件中断,纯硬件。

✨软件:如何理解信号处理?

• 键盘上的组合键 是先被OS系统识别到【OS是键盘真正的管理者,当系统在运行的时候,OS一直在检测键盘上有没有信息】再发给进程,当进程不能立即处理这个信号时,进程就会记录下这个信号【信号是从 1-31 连续的数字,进程是否收到1-31这个数字的信号 --- 在 task_struct 里通过位图记录 0000 0000 0000 0000 0000 0000 0000 0000,比特位的位置:信号的编号,比特位的内容:是否为0/1,是否收到对应的信号】发送信号的本质是什么?【写入信号】OS修改目标进程的PCB中的信号位图【0 -> 1】,操作系统有没有权利修改进程PCB的位图?有,OS是进程的管理者,所以 无论以什么方式发送信号,最终 都是转换到OS,让OS写入信号,因为OS是进程的唯一管理者。

信号产生有很多种,但是信号发送只有OS;

• 判断进程是否收到信号:进程里面有信号处理的表结构 sighandler_t arr32 函数指针数组,根据PCB里面去查位图,根据位图就可以检测出那个比特位为1,拿到比特位为1,这个数字就可以作为该数组的下标【下标-1】,直接去调用函数指针上的方法。

三、✨信号产生的方式

🚩1. 键盘产生

当在可执行程序在前台进程产生之后,按住 ctrl+c ,键盘被按下,计算机的CPU识别到键盘又被按下的动作,唤醒OS,让OS去读取键盘山的 ctrl+c ,读到后将 ctrl + c 解释成 2号 信号【if(案件 == ctrl + c)】,OS会把 ctrl + c 转化成一段代码【向目标前台进程PCB写入2号信号,即把比特位 0 --> 1,OS信号发送完成】,发送完成之后,该进程在后续合适的时候调度运行,发现自己收到一个信号,当前进程默认就要执行自己对 2号 信号的处理动作。

🚩2. 系统指令产生 发送信号 ---- 底层使用的是系统调用

🚩3. 系统调用 发送信号

cpp 复制代码
// Makefile

BIN=mykill
CC=g++
SRC=$(shell ls *.cc)
OBJ=$(SRC:.cc=.o)

$(BIN):$(OBJ)
	$(CC) -o $@ $^ -std=c++11

%.o:%.cc
	$(CC) -c $< -std=c++11

.PHONY:clean
clean:
	rm -f $(BIN)
cpp 复制代码
// Signal.cc

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


// 设置自己的kill

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " signumber processid " << std::endl;
}


// ./mykill  1信号    12345进程号
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    int signumber = std::stoi(argv[1]);// 转换为整数
    pid_t id = std::stoi(argv[2]);

    int n = ::kill(id, signumber);
    if(n < 0)
    {
        perror("kill");
        exit(2);
    }

    exit(0);
}

信号发送 本质都是操作系统OS发的!!!

🌠 发送信号的其他函数:

🪄 raise --- 函数可以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号)
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

int main()
{
    int cnt = 5;
    while(true)
    {
        std::cout << "hahaha alive" << std::endl;
        cnt--;
        if(cnt <= 0)
            raise(9);
        sleep(1);
    }
}
🪄 abort --- 自己给自己发了一个特定的终止自己的信号 --- 6号信号SIGABRT
cpp 复制代码
int main()
{
    int cnt = 5;
    while(true)
    {
        std::cout << "hahaha alive" << std::endl;
        cnt--;
        if(cnt <= 0)
            // abort();
        sleep(1);
    }
}

🚩4. 由软件条件 产生信号

SIGPIPE 是⼀种由软件条件产生的信号。

**🪄 alarm 函数 ---**调⽤ alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发 (14号)SIGALRM 信号,该信号的默认处理动作是终⽌当前进程。

cpp 复制代码
int main()
{
    // 统计我的服务器 1s 可以将计数器累加多少!
    alarm(1);// 我自己,会在 1s 之后收到一个SIGALARM信号
    int number = 0;
    while(true)
    {
        printf("count: %d\n", number);
    }
    
}
cpp 复制代码
int number = 0;
void die(int signumber)
{
    printf("get a sig : %d, count: %d\n", signumber, number);
    exit(0);
}

int main()
{
    // 统计我的服务器 1s 可以将计数器累加多少!
    alarm(1);// 我自己,会在 1s 之后收到一个SIGALARM信号
    signal(SIGALRM, die);
    while(true)
    {
        number++;
    }
    
}

这两个的写法 计数器累加的次数为什么会差别这么大呢?

【printf("count: %d\n", number);】里面的 IO 影响了计算的速度!,【while(true) number++;】纯CPU计算。

🌠OS内定时器

• 在操作系统内,要对 进程管理、文件管理、多线程、内存管理 进行管理,同时也要做定时管理,OS在开机之后,是会维护时间的,所以设置的闹钟,在OS底层就是设置了一个定时器

• 许多进程都可以设置闹钟,OS里可以同时存在很多被设置的闹钟,所以OS就要管理定时器【先描述,再组织】struct timer{} ,OS在每次检测超时的时候,会把定时器所对应的节点,按顺序进行升序排序,变成有序的列表,当有超时的时候,只需要从前往后遍历,遇到第一个没有超时的,之前的全部是超时的,遍历的同时,把超时的【struct timer】执行对应的 【func_t f;函数,给目标进程发送SIGALRM信号】在操作系统内给相应的进程发信号。

• 用堆理解定时器的排序:维护成一个最小堆,用超时时间作为键值,所以要想知道定时器有没有超时,只需要查堆顶就可以了,若堆顶没有超时,则所有的节点都没有超时;若堆顶超时,就把堆顶pop出来【堆再重新构建】,去执行相应的操作,重复操作,直到不再超时。

• 所以当我们在调用alarm函数时,就相当于在OS内给我们获取当前时间【currenttime】和 当前的超时时间【seconds】,然后在内核中设置一个节点,放在堆里面,OS就会在超时之后,直接向目标进程发送SIGALRM信号,并且把该节点释放掉。

• 闹钟的返回值 是什么?闹钟剩余时间

cpp 复制代码
int main()
{
    alarm(10);
    sleep(4);
    int n = alarm(0);// 0:取消闹钟
    std::cout << "n: " << n << std::endl;
}

当软件条件就绪时【超时、闹钟、定时器】,OS就可以向目标进程发送信号。

四、✨ alarm() 设置重复闹钟 --- 简单快速理解系统闹钟

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string>
#include <signal.h>
#include <functional>
#include <vector>


// 闹钟
using func_t = std::function<void()>;

int gcount = 0;
std::vector<func_t> gfuncs;

// 把信号 更换 成为 硬件中断
void handler(int signo)
{
    for(auto &f : gfuncs)
    {
        f();
    }
    std::cout << "gcount : " << gcount << std::endl;
    alarm(1);// 上面的闹钟响了之后,继续执行
}

int main()
{
    gfuncs.push_back([]()
                     { std::cout << "我是一个内核刷新操作" << std::endl;});

    gfuncs.push_back([]()
                     { std::cout << "我是一个检测进程时间片的操作,如果时间片到了,我会切换进程" << std::endl;});

    gfuncs.push_back([]()
                     { std::cout << "我是一个内存管理操作,定期清理操作系统内部的内存碎片" << std::endl;});

    alarm(1);// 一次性的闹钟,超时alarm会自动被取消
    signal(SIGALRM, handler);

    while(true)  // gcount++;
    {
        pause();
        std::cout << "我醒来了..." << std::endl;
        gcount++;
    }
}

• 操作系统其实就是一个死循环,当OS启动之后,会接收外部固定的事件源【时钟中断,集成在CPU内部】,每隔很短的时间,向OS触发硬件中断,让OS去执行中断方法。

• OS的调度、切换、内存管理、系统调用,全都是依靠中断来完成的。所以OS不应该叫OS,应该叫一个中断程序。

🚩5. 异常 (野指针、 除 0)

🪄模拟 野指针

cpp 复制代码
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);
}

🪄 模拟 除 0

cpp 复制代码
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);
}

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

🌠OS怎么知道我们的进程内部出错了?为什么会陷入死循环?
☄️ 除 0 :

状态寄存器里会有各种描述状态计算的结果,其中有一个叫做 溢出标记位,正常情况下计算完毕,溢出标记位为0,说明整个计算结果没有溢出,结果可靠,此时把结果写回内存里,就完成对应的复制。若对应的溢出标记位为1,就证明CPU内部计算结果出错了。

当出现溢出,即CPU内部结果出错了,OS就会知道【CPU硬件的中断有 内部中断 和 外部中断,CPU内部一旦出现溢出的错误,CPU内部硬件就会触发内部中断,就会告诉OS,OS是硬件的管理者,OS在调度、切换,就是在合理的使用CPU资源】,是哪个进程引起的硬件错误,OS就会通过信号杀掉进程!即当计算出现硬件错误,OS要识别到,就会给目标进程发送对应的信号,杀掉进程!

为什么会陷入死循环【不退出进程】?

这个进程一直没有退出,还要被调度,进程就会被切换、执行等等,【a /= 0】CPU里面的状态寄存器的 Eflags的溢出标记位一直都是1,CPU里面的数据一直都是这个进程的上下文内容不会更改,所以就会一直被捕捉,被调度,就会出现死循环,并且一直在触发8号信号。

☄️ 野指针

CR3 寄存器:保存页表的起始地址,负责虚拟地址和物理地址转化。

MMU【硬件】被集成在CPU内部:完成虚拟地址和物理地址转化。

ELP:读取可执行程序的起始地址。

当传递一个 0 地址,在MMU转化出来 0,我们没有权利访问最低的地址0,MMU这个硬件经过 转化或权限 方面上, 发现根本不能转化出来这个地址,使得MMU【硬件】报错,即CPU内部出错【与上面的类似】,OS就会知道,OS就会向目标进程去识别,就会杀掉这个进程。也要类似 状态寄存器的东西,转化错误,所以就会死循环。

OS怎么知道我们的进程内部出错了?程序内部的错误,其实都会表现在硬件错误上,OS就会知道是哪个地方出错,进而给对应的目标进程发送信号。

🌠core VS Term

Term :正常终止进程【不需要进行debug】。

Core:也是终止进程,但会多做一些处理。核心转储,在当前目录下形成文件【pid.core】,OS在进程崩溃的时候,将进程在内存中的部分信息保存起来【保存在磁盘上,持久化起来】,方便后续调试!这个文件【pid.core】一般都会被云服务器关掉。

把core功能关闭的原因:在云服务器上,若野指针、除 0 出现错误等等,一般都会在后端立即重启这个服务。若不关闭,当某个错误,一直重复 重启后挂掉,磁盘上就会出现大量的core文件,甚至磁盘爆满,就会影响系统 或 某些应用服务直接就挂掉,即 会因为系统问题,导致磁盘爆满

✨core 的使用 --- 调试

🌠 如果是子进程异常了呢? core 会不会出现?

子进程会不会出现core取决于:退出信号是否终止动作 && 服务器是否开启 core 功能。

🌠小结:

• 键盘、系统指令、系统调用、软件条件、异常 的信号产生,最终都要有OS来执行,因为OS是进程的管理者,统一由OS来向目标进程发信号;

• 信号的处理方式:不是立即处理,而是在合适的时候处理;

• 信号如果不是被立即处理,信号就会暂时被进程记录下来,保存在进程对应的PCB中的信号位图;

• 一个进程在没有收到信号的时候,知道自己应该对合法信号作何处理:默认、忽略、自定义;

• OS向目标进程发送信号:实则是OS向目标进程写信号。

五、信号处理

1. 默认处理动作

cpp 复制代码
void handler(int signo)
{
    std::cout << "get a signal: " << signo << std::endl;
    exit(1);
}

int main()
{
    signal(2, SIG_DFL);// default:默认

    while(true)
    {
        pause();
    }
}

2. 忽略:本身就是一种信号捕捉的方法,动作就是忽略

cpp 复制代码
void handler(int signo)
{
    std::cout << "get a signal: " << signo << std::endl;
    exit(1);
}

int main()
{
   
    signal(2, SIG_IGN);// 忽略(不做处理),本身就是一种信号捕捉的方法,动作就是忽略

    while(true)
    {
        pause();
    }
}

3. 自定义 捕捉一个信号

cpp 复制代码
void handler(int signo)
{
    std::cout << "get a signal: " << signo << std::endl;
    exit(1);
}

int main()
{
    // 信号捕捉:
    // 1. 默认
    // 2. 忽略
    // 3. 自定义
    ::signal(2, handler);// 自定义

    while(true)
    {
        pause();
    }
}

如若对你有帮助,记得关注、收藏、点赞哦~ 您的支持是我最大的动力🌹🌹🌹🌹!!!

若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢 ヾ(≧▽≦*)o \( •̀ ω •́ )/

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式