【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 arr[32] 函数指针数组,根据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 \( •̀ ω •́ )/

相关推荐
孙克旭_20 分钟前
day024-网络基础-TCP与UDP、DNS
linux·运维·网络·网络协议·tcp/ip·udp
板鸭〈小号〉25 分钟前
进程间通信及管道(理论)
运维·服务器
qq_2430507938 分钟前
thc-ssl-dos:SSL 压力测试的轻量级工具!全参数详细教程!Kali Linux教程!
linux·网络·安全·网络安全·压力测试·ssl·kali linux
regedit8042 分钟前
Centos7升级openssl
linux·运维·服务器
bigdata-rookie1 小时前
kafka SASL/PLAIN 认证及 ACL 权限控制
大数据·运维·服务器·分布式·zookeeper·kafka
渡我白衣1 小时前
Linux操作系统之进程(四):命令行参数与环境变量
linux
藥瓿亭1 小时前
2024 CKA模拟系统制作 | Step-By-Step | 18、题目搭建-备份还原Etcd
linux·运维·服务器·ubuntu·kubernetes·etcd·cka
angushine2 小时前
Docker设置代理
运维·docker·容器
洁✘2 小时前
lvs-keepalived高可用群集
linux·服务器·lvs
不会c嘎嘎2 小时前
Linux --进度条小程序更新
linux·小程序·apache