【linux】进程信号——信号的产生

进程信号

  • 一、信号概念
    • 1.1 信号理解
  • 二、产生信号
    • 2.1 通过键盘产生信号
    • 2.2 捕捉信号自定义signal
    • 2.3 系统调用接口产生信号
      • 2.3.1 向任意进程发送任意信号kill
      • 2.3.2 给自己发送任意信号raise
      • 2.3.3 给自己发送指定信号abort
      • 2.3.4 理解
    • 2.4 硬件异常产生信号
      • 2.4.1 除0异常
      • 2.4.2 野指针异常
      • 2.4.3 总结
    • 2.5 软件条件产生信号
      • 2.5.1 定时器软件条件alarm
      • 2.5.2 alarm的深层理解
    • 2.6 核心转储Core Dump

一、信号概念

首先要知道查看信号的指令:kill -l
在这里插入图片描述
通过观察发现没有0和32和33号信号,只有1 ~ 31, 34 ~ 64的信号。我们把
【1 ~ 31】叫做普通信号
【34 ~ 64】叫做实时信号

1.1 信号理解

在日常生活中有很多的信号,例如红路灯、裁判哨声、闹钟,这些都是给我们人类看的,当这些场景触发的时候,我们人类立马就知道要做什么,并且产生行动

而我们为什么能识别这些信号呢?
我们对特定事件的反应,是被教育的结果,本质是我们记住了。

还有一种情况:当信号传来的时候我们可能正在做更重要的事情,所以不一定会立马处理信号,此时信号的产生和我们正在做的事情称为异步
我们把信号传递过来到处理之前的这段时间称为时间窗口。在时间窗口我们必须得记住这个信号

在我们处理信号的时候,我们可以有不同的处理方式,比方说我们早上听到闹钟响起,会直接起床,这里叫做默认动作,而听到闹钟后先做十个俯卧撑再起床,这叫做自定义动作,当然我们也可以不理会闹钟,这叫做忽略动作

把概念迁移到进程中:

1️⃣ 进程能认识信号并产生动作是因为程序员编码完成的。
2️⃣ 当进程收到信号,进程可能在执行更重要的代码,所以信号不一定被立即处理。 所以进程要有对信号的保存能力。
3️⃣ 进程在处理信号的时候,一般有三种动作:默认、自定义、忽略,有个专业名词叫:信号被捕捉

那么信号是怎么被捕捉的呢?

信号发给进程,而进程需要保存到PCB中,那么如何保存呢?
是否收到信号具有原子性,只有两太,而我们知道普通信号是1 ~ 31,所以我们可以在PCB中创建一个unsigned int的变量,有32个比特位,刚好用这些比特位来标记接收的信号。比特位的位置代表信号的编号。0表示没有,1表示有。

所以发送信号的本质:修改PCB中的信号位图。

而只有操作系统才能修改PCB,发信号本质就是给操作系统发信号,那么操作系统就必须要提供发送信号、处理信号的相关调用接口,我们以前的kill指令就是调用了底层接口。

二、产生信号

2.1 通过键盘产生信号

ctrl + c:是一个组合键,OS将它解释成3号信号(SIGQUIT)
在这里插入图片描述
我们知道每个信号有三种处理动作,那么怎么查看信号的默认动作是什么呢?
ctrl + \:是一个组合键,OS将它解释成2号信号(SIGINT)
在这里插入图片描述

指令:man 7 signal
在这里插入图片描述
可以看到二号信号的动作是:Term(终止),描述是:Interrupt from keyboard(从键盘中断)

2.2 捕捉信号自定义signal

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

RETURN VALUE
signal() returns the previous value of the signal handler, or SIG_ERR on error.  
In the event of an error, errno is set to indicate thecause.

参数说明:
signum:指定的信号。
handler:设置自定义动作,就是一个回调函数,函数内我们可以自定义我们想要的动作。

void handler(int sig)
{
    std::cout << "进程捕捉到信号,编号是:" << sig << std::endl;
}

int main()
{
    signal(2, handler);
    while(true)
    {
        std::cout << "in service: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

这里要注意是signal函数的调用,不是handler的调用。这个函数仅仅是对2号信号的捕捉,并不代表被调用了。只有收到对应信号才会被调用。

在这里插入图片描述
在这里插入图片描述
可以看到发送2号信号并不能导致进程被终止了。

这里有个问题:如果我们对所有的信号都进行了信号捕捉,那我们是不是就写了一个不会被异常终止或者用户杀掉的进程呢?我们通过代码来验证一下!

void Catchsig(int sig)
{
    std::cout << "捕捉到了一个信号: " << sig << " pid: " << getpid() << std::endl;
}

int main()
{
    for(int i = 1; i <= 31; ++i)
        signal(i, Catchsig);

    while(1)  sleep(1);

    return 0;
}

在这里插入图片描述
操作系统的设计者也考虑到了上述的情况,所以就让 9 号信号无法被捕捉,9 号信号是管理员信号

2.3 系统调用接口产生信号

2.3.1 向任意进程发送任意信号kill

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

RETURN VALUE
On success (at least one signal was sent), zero is returned.  
On error, -1 is returned, and errno is set appropriately.

参数说明:
pid:目标进程的pid。
sig:向目标进程发送指定信号。

所以我们可以自己写一个kill的进程。

// mykill.cc
void Usage(const std::string& proc)
{
    std::cout << "\nerror, format: " << proc << " pid sig" << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    pid_t pid = atoi(argv[1]);
    int sig = atoi(argv[2]);
    kill(pid, sig);
    return 0;
}

再写一个永远运行的进程,让mykill进程来杀死它。

// myproc.cc
int main()
{
    while(true)
    {
        std::cout << "please kill me, my pid: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

2.3.2 给自己发送任意信号raise

#include <signal.h>

int raise(int sig);
RETURN VALUE
raise() returns 0 on success, and nonzero for failure.

sig就是发送的信号。

int main(int argc, char* argv[])
{
    int cnt = 0;
    while(++cnt < 10)
    {
        std::cout << "cnt: " << cnt << std::endl;
        sleep(1);
        if(cnt == 5)
        {
            raise(9);
        }
    }
    return 0;
}

在这里插入图片描述
其实这里raise也可以写成:kill(getpid(), 9)

2.3.3 给自己发送指定信号abort

#include <stdlib.h>

void abort(void);
int main(int argc, char* argv[])
{
    int cnt = 0;
    while(++cnt < 10)
    {
        std::cout << "cnt: " << cnt << std::endl;
        sleep(1);
        if(cnt == 5)
        {
            abort();
        }
    }
    return 0;
}

在这里插入图片描述
而发送的指定信号就是6号(SIGABRT)
所以这里abort也可以自己用kill封装:kill(getpid(), 6)

2.3.4 理解

我们可以看到进程收到的大部分信号,默认处理动作都是终止进程
信号的不同代表了不同的事件,但是它们的处理动作可以一样

2.4 硬件异常产生信号

2.4.1 除0异常

信号的产生不一定需要用户手动发送。

int main(int argc, char* argv[])
{
    while(true)
    {
        std::cout << "in service" << std::endl;
        sleep(1);
        int a = 1;
        a /= 0;
    }
    return 0;
}

在这里插入图片描述
这里为什么/0会导致进程终止呢?
因为进程会收到来自操作系统的8号信号(SIGFPE)。

我们可以用前面学的捕捉信号进行验证:

void handler(int sig)
{
    std::cout << "捕获到信号:" << sig << std::endl;
    sleep(1);
}

int main(int argc, char* argv[])
{
    signal(8, handler);
    while(true)
    {
        std::cout << "in service" << std::endl;
        sleep(1);
        int a = 1;
        a /= 0;
    }
    return 0;
}

在这里插入图片描述

这次我们把/0放到循环前

在这里插入图片描述
在这里插入图片描述
可以看到这里还是循环打印,好像一直在调用捕获函数。
这里就要先知道操作系统是如何得知要给进程发送八号信号的呢?(怎么知道的/0

这里1/0会被放进CPU中的寄存器中,0相当于无穷小的数字,这样就会导致CPU的状态寄存器中的溢出标记由0变为1。这样就发生了CPU的运算异常,操作系统就会知道(操作系统是软硬件的管理者),然后向目标进程发送8号信号。而收到信号进程不一定退出,没有退出说明还会被继续调度。而寄存器的内容属于当前进程上下文信息,但是进程没有能力把状态标识符置为0,所以进程切换的时候就有无数次的状态寄存器被保存和恢复(上下文信息),每次恢复就会发送信号。导致捕获函数一直被调度。

2.4.2 野指针异常

int main(int argc, char* argv[])
{

    while(true)
    {
        std::cout << "in service" << std::endl;
        sleep(1);
        int *ptr = nullptr;
        *ptr = 2;
    }
    return 0;
}

在这里插入图片描述
这里为什么空指针会导致进程终止呢?
因为进程会收到来自操作系统的11号信号(SIGSEGV)。

利用signal函数证明:

void handler(int sig)
{
    std::cout << "捕获到信号:" << sig << std::endl;
    sleep(1);
}

int main(int argc, char* argv[])
{
    signal(11, handler);
    while(true)
    {
        std::cout << "in service" << std::endl;
        sleep(1);
        int *ptr = nullptr;
        *ptr = 2;
    }
    return 0;
}

在这里插入图片描述
那么操作系统是如何知道发生了野指针情况呢?

我们知道指针本质上是个虚拟地址,而我们知道虚拟地址需要转化成物理地址,通过页表+MMUMMU是集成在CPU中的硬件,通过访问通过页表的内容形成物理地址,再访问物理地址。而我们解引用空指针,MMU就会发生异常,然后被操作系统得知,然后发送信号给进程。

2.4.3 总结

大部分信号会导致进程退出,我们需要捕获这个异常,因为异常的不同代表不同的原因导致的,进而让我们能够追溯原因,让我们能够反向定位问题。比如说我们收到的信号是段错误,我们就会想到可能是野指针,收到浮点数溢出报错就会想到可能是除0错误。

2.5 软件条件产生信号

我们以前学过管道:【linux】进程间通信——管道通信
当两个进程正在利用管道进行读写,此时把读端关闭,操作系统就会终止掉写进程(发送SIGPIPE信号)。这种情况称为软件条件产生信号

2.5.1 定时器软件条件alarm

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

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

int main(int argc, char* argv[])
{
    alarm(1);
    int cnt = 0;
    while(true)
    {
        std::cout << cnt++ << std::endl;
    }
    return 0;
}

在这里插入图片描述
这个进程的目的就是统计1s的时间内计算机能将数据叠加多少次。

而如果我们这么写:

int cnt = 0;

void handler(int sig)
{
    std::cout << "捕获到信号:" << sig << std::endl;
    std::cout << "cnt: " << cnt << std::endl;
}

int main(int argc, char* argv[])
{
    signal(14, handler);
    alarm(1);
    while(true)
    {
        ++cnt;
    }
    return 0;
}

在这里插入图片描述

从这里就可以看到IO跟不IO的效率差距相当大。

而只打印了一次说明是收到了一个SIGALRM信号,闹钟响过一次就不再响了
那如果我们想让它一直打印呢?
在这里插入图片描述
相当于在handler内部又要调用handler。这样就类似于sleep(1)
在这里插入图片描述

unsigned int alarm(unsigned int seconds);

当然alarm也可能提前响起。比方说有可能手动发送SIGALRM,他就会返回剩余多少时间。当我们把seconds设置为0,表示取消闹钟

2.5.2 alarm的深层理解

我们知道每个进程都可能通过alarm接口设置闹钟,所以可能会存在很多闹钟,那么操作系统一定要管理起来它们。
先用一个结构体描述每个闹钟,其中包含各种属性:闹钟还有多久结束(时间戳)、闹钟是一次性的还是周期性的、闹钟跟哪个进程相关、链接下一个闹钟的指针…… 然后我们可以用数据结构把这些数据连接起来。
接下来操作系统会周期性的检查这些闹钟,当前时间戳和结构体中的时间戳进行比较,如果超过了,说明超时了,操作系统就会发送SIGALRM给该进程。

为了方便检查是否超时,可以利用堆结构来管理。

2.6 核心转储Core Dump

核心转储:

当进程出现异常的时候,我们可以将该进程在对应时刻的内容数据保存到磁盘上,文件名通常是 core。

在这里插入图片描述
这里的Term和Core都表示进程退出,Trem表示正常结束,操作系统不会做额外的工作,如果是Core退出,我们暂时看不到明显的现象,如果想要看到,我们可以打开一个选项:ulimit -a(可以看到操作系统给用户所设置的资源上限)

在这里插入图片描述
可以看到第一行core file size的大小为0,因为云服务器默认关闭了core file这个选项。
如果我们想修改我们就可以用后边的参数进行修改(-c)。ulimit -c
在这里插入图片描述
打开了以后我们继续解引用空指针:
在这里插入图片描述

可以发现比以前多了一点内容。在查看当前目录:
在这里插入图片描述
多了一个core文件
我们把core dumped叫做核心转储,core文件后面的数字就是问题进程的pid。

那么为什么要有核心转储?
我们需要知道程序为什么崩溃,在哪崩溃?而核心转储就是为了支持我们进行调试
那么如何调试呢?
第一步先编译的时候带上-g选项
第二步使用gdb调试
在这里插入图片描述
第三步直接输入core-file core.17633
在这里插入图片描述
从结果可以看出代码终止的原因是收到了11号信号,引发了段错误。在mykill.cc的17行。
我们把这种处理错误的方法叫做事后调试

总结一下:当程序出现异常,我们先确定是几号信号,然后man 7 signal查看是core还是Trem,如果是core,直接打开核心转储,然后gdb调试直接定位错误。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯刷题冲刺 | 倒计时17天

作者&#xff1a;指针不指南吗 专栏&#xff1a;蓝桥杯倒计时冲刺 &#x1f43e;马上就要蓝桥杯了&#xff0c;最后的这几天尤为重要&#xff0c;不可懈怠哦&#x1f43e; 文章目录1.长草2.分考场1.长草 题目 链接&#xff1a; 长草 - 蓝桥云课 (lanqiao.cn) 题目描述 小明有一…

Feign远程调用

之前在一篇博客中写了利用RestTemplate发起远程调用的代码&#xff0c;但是存在一下问题&#xff1a;代码可读性差&#xff0c;编程体验不统一&#xff1b;如果参数特别多的话&#xff0c;参数复杂URL难以维护。Feign官方地址&#xff1a;https://github.com/OpenFeign/feignFe…

行业观察 | 来了解一下AI加速器

本文参考网上可查询到的资料简要总结AI加速器的概念和应用等信息 1。 未完待续 更新&#xff1a; 2023 / 3 / 22 行业观察 | 来了解一下AI加速器前言加速器处理器处理器是什么&#xff1f;处理器进化史加速器架构指令集 ISA特定领域的指令集 ISA超长指令字&#xff08;VLIW&a…

如何使用子项目管理方案?

在项目进行中经常发生这样的情况&#xff1a;当你开始为一个项目制定时间表时&#xff0c;你会发现任务的数量太多。你需要把它们全部分组到一些摘要任务中。但随后你看到一堆摘要任务&#xff0c;也想把它们再分组。 这样一来&#xff0c;该项目变得很麻烦&#xff0c;甚至项目…

Matlab进阶绘图第10期—带填充纹理的柱状图

带填充纹理的柱状图是通过在原始柱状图的基础上添加不同的纹理得到的&#xff0c;可以很好地解决由于颜色区分不足而导致的对象识别困难问题。 由于Matlab中未提供纹理填充选项&#xff0c;因此需要大家自行设法解决。 本文使用Kesh Ikuma制作的hatchfill2工具&#xff08;Ma…

gin框架使用websocket实现进入容器内部执行命令

文章目录1. 先决条件2. gin框架实现3. 测试用html文件4. 需要完善1. 先决条件 docker开放远程API端口 2. gin框架实现 type GetCommandResultRequire struct {IpAddr string json:"ip_addr" //传入要控制容器的ip地址ContainerUuid string json:"cont…

对堆题的总体思路

浅说一下pwn堆并用一个简单的例子具体说明给刚入坑堆的小朋友说的一些思路说一下堆是什么堆你可以看成一个结构体数组&#xff0c;然后数组里每个元素都会开辟一块内存来存储数据那么这块用来存储数据的内存就是堆。结构体数组在BSS段上&#xff0c;其内容就是堆的地址&#xf…

动态SQL必知必会

动态SQL必知必会1、什么是动态SQL2、为什么使用动态SQL3、动态SQL的标签4、if 标签-单标签判断5、choose标签-多条件分支判断6、set 标签-修改语句7、foreach标签7.1 批量查询7.2 批量删除7.3 批量添加8、模糊分页查询1、什么是动态SQL 动态 SQL 是 MyBatis 的强大特性之一。如…

阿里巴巴2017实习生笔试题(二)——总结

具体题目来自阿里巴巴2017实习生笔试题&#xff0c;本文仅为整理与汇总。 本题应该往C的多态性进行理解&#xff0c;多态中的动态链接在执行时进行&#xff0c;静态链接在编译时进行。其中A、C、D 都是动态链接的优点&#xff0c;B 时静态链接的优点。 减少页面交换可从如下角…

nginx-动静分离-防盗链-location-4

动静分离 为了加快网站的解析速度&#xff0c;可以把动态页面和静态页面有不同的服务器来解析&#xff0c;加快机械速度。降低原来单个服务器的压力。在动静分离的tomcat时候比较明显&#xff0c;因为tomcat解析静态很慢&#xff0c;其实这些原理的话很好理解&#xff0c;简单…

Baumer工业相机堡盟万兆网相机如何使用千兆网网卡环境保持帧率不变(C++)

项目场景 Baumer工业相机堡盟相机是一种高性能、高质量的工业相机&#xff0c;可用于各种应用场景&#xff0c;如物体检测、计数和识别、运动分析和图像处理。 Baumer的万兆网相机拥有出色的图像处理性能&#xff0c;可以实时传输高分辨率图像。此外&#xff0c;该相机还具…

IP、MAC和端口

IP&#xff0c;MAC和端口的概念MAC地址也叫物理地址、硬件地址&#xff0c;由网络设备厂家直接烧录在网卡上的&#xff0c;理论上Mac地址是唯一-的。 但因为Mac地址可以通过程序修改&#xff0c;所以也有可能会重复。IP地址是互联网上的每台设备都规定了-一个唯一的地址, 这个地…

网络安全之认识勒索病毒

一、什么是勒索病毒 勒索病毒&#xff0c;是一种新型电脑病毒&#xff0c;伴随数字货币兴起&#xff0c;主要以邮件、程序木马、网页挂马、服务器入侵、捆绑软件等多种形式进行传播&#xff0c;一旦感染将给用户带来无法估量的损失。如果遭受勒索病毒攻击&#xff0c;将会使绝…

如何用C语言实现渣男通讯录

注意&#xff1a;纯属玩笑&#xff0c;博大家一乐&#xff0c;切勿当真&#x1f4d6;首先我们要知道一个渣男通讯录有哪些信息要包含哪些功能1.你的通讯录要装多少个女朋友你得规定吧&#xff1b;2.每个女朋友的姓名&#xff0c;年龄&#xff0c;电话&#xff0c;爱好这些要有吧…

第29次CCFCSP认证经验总结

鄙人有幸参加了由中国计算机学会举办的第29次计算机软件能力认证考试&#xff0c;在此进行一些考试细节和经验的总结。 如果没有仔细了解过的小白去网上搜索CCFCSP&#xff0c;可能出现的是CSP-J/S&#xff0c;但是详细了解会发现&#xff0c;首先CSP-J/S分初试和复试&#xff…

.NET/C#/GC与内存管理(含深度解析)

详情请看参考文章&#xff1a;.NET面试题解析(06)-GC与内存管理 - 不灬赖 - 博客园 (cnblogs.com)一、对象创建及生命周期一个对象的生命周期简单概括就是&#xff1a;创建>使用>释放&#xff0c;在.NET中一个对象的生命周期&#xff1a;new创建对象并分配内存对象初始化…

【Linux】浅谈shell命令以及运行原理

前言&#xff1a;上篇博文把linux下的基本指令讲解完了。本期我们聊聊Linux下【shell】命令及其运行原理。 目录 Shell的基本概念与作用 原理图展示 shell命令执行原理 Shell的基本概念与作用 Linux严格意义上说的是一个操作系统&#xff0c;我们称之为“核心&#xff08;ker…

文心一言 VS ChatGPT,国产大模型和国外的差距有多大?

3月16号&#xff0c;百度正式发布了『文心一言』&#xff0c;这是国内公司第一次发布类ChatGPT的产品。大家一定非常好奇文心一言和chatgpt之间的差距有多大&#xff1f;国产大模型还有多少路可走&#xff1f;本文就全面测评这两款产品&#xff01; 目录 体验网址 1、旅游攻…

【vue2】vue2中的性能优化(持续更新中)

⭐ v-for 遍历避免同时使用 v-if ⭐ v-for 中的key绑定唯一的值 ⭐ v-show与v-if对性能的影响 ⭐ 妙用计算属性 ⭐ 使用防抖与节流控制发送频率 ⭐ 路由守卫处理请求避免重复发送请求 ⭐ 使用第三方UI库的引入方式 【前言】 该系列是博主在使用vue2开发项目中常用上的一…

这些IT行业趋势,将改变2023

上一周&#xff0c;你被"AI"刷屏了吗&#xff1f; 打开任何一家科技媒体&#xff0c;人工智能都是不变的热门话题。周初大家还在用ChatGPT写论文、查资料、写代码&#xff0c;到周末的时候大家已经开始用GPT-4图像识别来做饭、Microsoft 365 Copilot 来写PPT了。 GP…