进程信号--Linux

news2024/11/25 20:48:22

文章目录

  • 信号?
  • kill -l 指令查看所有信号
  • 信号的工作流程
    • 信号产生
      • 1.通过终端按键产生信号
      • 2.通过系统调用接口产生信号
      • 3.通过软件产生信号
      • 4.硬件异常产生信号
    • 信号接收
    • 信号处理
  • 总结

信号?

进程间的通信我们了解到有管道通信,有共享内存的通信。这些都是利用一些内存空间从而实现通信内容的交互,这些信息可以是字符、数字、字符串、甚至可以是结构体或者是类,但今天介绍的信号并没有那么复杂的信息量,信号作为通信方式,有其独特的特点:简洁、含义明确、系统级别……

首先在讲解之前,得首先了解一下信号到底是个什么东西。在生活中,我们可能遇到很多信号:红绿灯、旗帜、烽火(古时传讯)等等,这些信号我们在遇到时候马上就会明白到底有什么含义,因为这些信号在产生的时候就被赋予了特定的含义,并被所有的人所知晓与认可。所以在遇到信号时,根本就没有异议,大家只要是正常人都会按照确定好的含义来处理。所以,我个人认为,信号的定义就是事先约定好的,广为知晓并被接受,用来处理特定事件而产生的唯一辨识现象。

知道了信号的含义,那么将这个含义应用到进程中,就会衍生出一种单独的通信方式。并不像我们自己在程序中定义的某些变量充当信号,由于要被所有的进程所识别,所以这些信号是要被系统收纳并写死的,不可更改。也就是我们作为用户,只是知道这些信号,并且只有使用权,并没有修改权。因此,信号就是属于系统级别的通信了。不同进程只要处理各自的信号,就能知道外界给这个进程传递了什么信息。

kill -l 指令查看所有信号

说起信号,之前在学习进程pid的时候,我们会通过kill -9 +进程pid的指令杀死一个进程,这个过程其实就是像一个进程发送信号,使其终止,该信号就是9。现在我们要学习更多的信号,就需要了解所有常用的信号。具体方式就是在终端上通过指令kill -l来查看:

image-20230113145255604

现在我只需要学习到普通信号,以后有机会的话会更新实时信号的相关知识。

更详细的要查看信号属性的话使用指令:man 7 signal

image-20230113150054299

信号的工作流程

信号的产生是异步的,并不是信号产生就立马就被处理了,进程可能有更加重要的事情要做,因此信号会被放置一段时间再处理。这就会导致信号必定有其特殊的存储结构以及处理流程。

image-20230114141416710

信号产生

1.通过终端按键产生信号

平常我们可以通过ctrl + c来终止正在运行的进程,这其实就是通过键盘产生的信号。而我们知道,ctrl + c的结果就是终止进程,而在信号中SIGINT的作用就是终止进程(默认的处理结果),我们可以试着自定义一个处理动作,这就需要借助系统给我们提供的函数接口了。

image-20230113201543610

函数名:signal(信号捕捉函数)

参数:

​ signum:信息序号

​ handler:处理方法,一个参数为int返回值为void类型的函数指针。

返回值:函数指针

可以看出,由于传参的原因,我们自定义处理方法时,只能回调系统写死的函数类型,即参数为int返回值为void类型的函数。注意:handler函数属于回调函数,只有在接收到对应的信号时才会调用该函数。

以下是对自定义信号处理方法的测试:

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handler(int signo)
{
    cout<<"接收到信号:"<<signo<<endl;
}
int main()
{
    signal(SIGINT,handler);
    while(1)
    {
        cout<<"等待捕捉信号……"<<endl;
        sleep(1);
    }
    return 0;
}

image-20230113202523215

可以看出,重新自定义后的信号处理方式已经不再是退出程序了,而是回调用户自己定义的函数handler。那如果我们将1-31的信号全部都自定义,会不会出现一个无法以信号关闭的进程呢?

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handler(int signo)
{
    cout<<"接收到信号:"<<signo<<endl;
}
int main()
{
    //signal(SIGINT,handler);
    for(int i=1;i<=31;++i)
    {
        signal(i,handler);//将1-31的普通信号的处理方法都给重新自定义一下,包括9号信号
    }
    while(1)
    {
        cout<<"等待捕捉信号……"<<endl;
        sleep(1);
    }
    return 0;
}

image-20230113203811712

可以看出除了9号信号,其他信号确实是被重定义了,因此9号信号是保证进程安全的最后一道防线。🔺:9号信号不可被用户自定义处理方式!

2.通过系统调用接口产生信号

系统给我们用户提供了很多个函数来产生信号,其中有kill、raise、abort、exit等等,其中的kill可以给已知pid的进程发送信号,利用这个特性,我们可以模拟一个自己的kill命令。

image-20230114145806696

kill:

参数:pid:进程标识符,sig:信号

返回值:返回0代表成功,失败返回-1

作用:向某个指定进程发送某个信号

raise:

参数:sig:信号

返回值:成功返回0,失败返回非零数字。

作用:对调用该函数的进程发送某个信号

abort:

参数:无

返回值:无

作用:使调用该函数的进程退出。

exit:

参数:status:退出状态

返回值:无

作用:使得调用该函数的进程正常中止,status & 0377 的值被返回给父进程。注意区别返回值,这里返回给父进程的值类似于信号一类的信息。

正常运行的程序:

#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
    while(1)
    {
        cout<<getpid()<<" is running"<<endl;
        sleep(1);
    }
    return 0;
}

模拟的kill命令:

#include<iostream>
#include<string>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
using namespace std;
void Usage(const string& str)
{
    cerr<<"Usage:\t"<<str<<" + signo + pid"<<endl;
}
int main(int argc, char* argv[])
{
    if(argc<3)
    {
        Usage(argv[0]);
    }
    if(kill(static_cast<pid_t>(atoi(argv[2])),atoi(argv[1]))==-1)
    {
        cerr<<"kill: "<<strerror(errno)<<endl;
        exit(2);
    }
    return 0;
}

通过mykill进程杀掉myproc进程

image-20230114144443064

再试一下raise的使用结果:

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handler(int signo)
{
    cout<<"接收到信号:"<<signo<<endl;
}
int main()
{
    for(int i=1;i<=31;++i)
    {
        signal(i,handler);
    }
    while(1)
    {
        cout<<getpid()<<" is running"<<endl;
        sleep(1);
        raise(2);
    }
    return 0;
}

image-20230114152855334

3.通过软件产生信号

image-20230114161240761

软件产生信号,可以由alarm函数来实现。

参数:seconds(设置的秒数)

返回值:返回任何先前计划的警报之前剩余的秒数。如果之前没有计划的警报,则为零。

作用:安排在数秒内将SIGALRM信号传递到调用进程。一般而言SIGALRM信号会使得进程退出。

借助这个功能,我们可以做许多定时的工作,比如统计一秒内一个变量能够++多少次:

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
int cnt=1;
void handler(int signo)
{
    cout<<"接收到信号:"<<signo<<" cnt:"<<cnt<<endl;
    exit(2);
}
int main()
{
    signal(SIGALRM,handler);
    alarm(1);
    while(1)
    {
        cnt++;
    }
    return 0;
}

测试结果:

image-20230114165414384

但是如果是在++的过程中输出cnt,++次数就会变得很少:

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
int cnt=1;
int main()
{
    alarm(1);
    while(1)
    {
        printf("%d\n",cnt++);
    }
    return 0;
}

image-20230114165640543

由此也可以直观地感受出IO流确实特别慢,输出所耗费的时间严重影响了++的次数。

4.硬件异常产生信号

除了上述系统提供的一些接口,还有一些特殊的情况也会产生异常,比较直观的现象就是程序崩溃了。至于为什么会崩溃,代码本身格式问题除外,一般都是逻辑上的问题:除零、野指针、越界……

下面我们尝试着把所有的信号都自定义一下,然后再写出上述问题中的某些问题,看是否产生信号,以及产生了哪些信号。

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handler(int signo)
{
    cout<<"接收到信号:"<<signo<<endl;
    exit(2);
}
int main()
{
    for(int i=1;i<=31;++i)
    {
        signal(i,handler);
    }
    //int a=1/0;//除零问题
    int *ptr=nullptr;
    *ptr=10;//野指针问题
    return 0;
}

经测试,除零以及野指针问题的信号分别是:8、11

image-20230114182153604

而通过man 7 signal指令查看可以发现,8号信号对应的是SIGFPE,意思是浮点运算例外;而11号信号对应的是SIGSEGV,意思是无效内存引用。与对应的问题一致。可见硬件的异常确实会导致信号的产生。

信号接收

解决了信号产生的问题,我们就顺势引出了一个新的问题:信号的来源有了,但是怎样才能将信号传达给对应的进程呢?答案是操作系统负责信号之间的传递。但是被传递信号进程怎么接收信号呢?要想讲清楚这个问题,就涉及到了信号一个进程中是如何被存储起来的。

事实上,信号的存储位置还是在进程的PCB中,用一种名为位图的数据结构来进行信号存储。我们知道无论是什么类型的数据,最终都是由一个个的bit位所构成。信号在最前面就提到过是一种标识,因此一个bit位的大小就能表示一个信号的有无。(0:无;1:有)所以使用位图这种结构无疑是特别省空间的做法。关于信号的存储与后续的处理,是由三个表组成的,分别是block(信号屏蔽字)、pending(信号集)、handler(方法表)。

image-20230115095024326

由上图可以看出,block表和pending表都是位图的数据结构,而handler表则是一个函数指针数组。信号的接收主要是靠pending表,block与handler表主要在信号的处理阶段使用。其中pending表中的0、1就分别表示对应的信号是否存在,而block表中的0、1代表后面的信号是否能够被使用,1表示可以,0表示不可以。上述结构都是在进程PCB中,用户是没有权限直接访问或是修改对应的数据,因此就给我们用户提供了一个专门访问该信号结构的数据类型:sigset_t,同时也衍生出了一些针对该数据类型的操作函数:

image-20230115164213268

🔺注意:由于sigset_t是系统提封装好的数据类型,因此不可以直接通过位移操作去增添删改信号,只能由提供的上述函数进行操作。

对于信号存储的三个表,我们已知可以通过signal对处理方法进行自定义操作,事实上,要想对block表和pending表进行读取和修改,就得利用另外两个函数:sigpending(获取调用该函数进程的pending表)、sigprocmask(获取、修改调用该函数进程的block表)。

image-20230115165044722

其中sigpending的用法比较简单,将传入的set信号集设置成为调用该函数进程的pending表,成功返回0,失败返回-1,一般配合着sigemptyset使用。而sigprocmask函数就有点复杂了,这涉及到其中一个参数how(怎样改变对应进程的block表)。

函数名:sigprocmask

参数:

​ set:若该参数为非空参数,则配合how参数改变调用该函数进程的block表。

​ oldset:若该参数为非空参数,则使调用该函数进程的旧的block表拷贝给oldset信号集;为空的话忽略oldset参数。也就是说, 这个参数为输出型参数。

​ how:有三个选项,分别是SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK,分别带有不同的含义与作用。

image-20230115171434291

作用:获取、修改调用该函数进程的信息屏蔽字。

接下来是测试:

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
    cout << "接收到信号:" << signo << endl;
}
void showSigset(sigset_t *sigset)//通过调用sigismember函数来查看信号集。
{
    cout<<getpid()<<" sigset: ";
    for (int sig = 1; sig <= 31; ++sig)
    {
        if (sigismember(sigset, sig))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}
int main()
{
    sigset_t bsig,obsig;
    sigemptyset(&bsig);
    sigemptyset(&obsig);
    sigfillset(&bsig);//屏蔽所有的信号
    sigprocmask(SIG_SETMASK,&bsig,&obsig);//obsig拿到了之前的信号屏蔽字
    for (int i = 1; i <= 31; ++i)
    {
        signal(i, handler);//将所有的信号都自定义一下
    }
    sigset_t pendings;
    int times=0;
    while (true)
    {
        sigemptyset(&pendings);
        if (sigpending(&pendings) == 0)
        {
            showSigset(&pendings);//输出当前的pending表
        }
        sleep(1);
        times++;
        if(times==20)//20秒之后解除所有信号的屏蔽。
        {
            sigprocmask(SIG_SETMASK,&obsig,nullptr);
        }
    }
    return 0;
}

image-20230116101020009

信号处理

现在我们知道信号确实可以PCB中的数据结构所存储,但是信号的处理并不是我们想象中的那么简单。信号的处理称为递达,信号已经被进程接收,但是还没被处理,称之为未决。信号肯定是要被进程处理的,但是何时处理,处理的状态这些都是未知。事实上,信号被处理的时间是内核态切换到用户态的时候。什么是内核态,什么是用户态呢?

内核态:处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态。

用户态:处于用户态的 CPU 只能受限的访问内存,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。

更直观的可以用虚拟地址空间来辅助理解:

image-20230116164218706

内核态可以访问所有的代码和数据,具备更高的权限,而用户态只能访问自己的代码和数据。

内核态与用户态的切换说白了就是页表的切换与CPU状态的切换。CPU中有一个组件为cr3,标识为0时是内核态,标识为3时是用户态。什么时候会进行内核态与用户态之间的转换呢?情况有很多:1.系统调用时;2.时间片到了;……接下来,我们画个图来更见形象的理解信号处理的过程。

image-20230117104109077

现在有一个问题:如果在陷入内核处理信号时,又有信号被发送和接收,那么会不会出现递归式的陷入内核的处理现象呢?答案是不会的!操作系统在被设计的时候就考虑到了这种问题,在处理信号时,操作系统默认将这个正在被处理的信号所对应的block值置1,即拦截所有后续出现的相同的信号,直到处理动作完成,才会将值置为0,又可以重新接收信号。就比如用户一直在给一个进程发送ctrl c这样的指令,但是ctrl c所对应的信号处理方法由于是自定义的,所以耗时很长,后续的ctrl c就只会使pending表中的0置1,但并不会去调用处理方法。前面我们讲了signal这个函数,事实上有一个功能更加完善的函数:sigaction

image-20230117112649683

其中的oldact是获取之前的信号处理方法,与sigprocmask中的oldset是一样的作用,属于输出型参数。使用效果:

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void showSigset(sigset_t *sigset)
{
    cout<<getpid()<<" sigset: ";
    for (int sig = 1; sig <= 31; ++sig)
    {
        if (sigismember(sigset, sig))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}
void handler(int signo)
{
    cout << "接收到信号:" << signo << endl;
    sigset_t pendings;
    while(true)//不会退出handler
    {
        sigemptyset(&pendings);
        sigpending(&pendings);
        showSigset(&pendings);
        sleep(1);
    }
}

int main()
{
    struct sigaction act,oact;
    act.sa_flags=0;
    act.sa_handler=handler;//自定义
    //act.sa_handler=SIG_DFL;//默认
    //act.sa_handler=SIG_IGN;//忽略
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,3);//添加屏蔽信号
    sigaction(2,&act,&oact);
    while(true)
    {
        cout<<"main running"<<endl;
        sleep(1);
    }
    return 0;
}

image-20230117130422464

可以看出,将三号信号屏蔽后,后续在处理2号信号时,再发出2号3号信号就不会被递达了,因为之前的2号信号的方法还在被执行!

总结

信号的三个重要知识点:产生、接收、处理。其中衍生出来了许多的附带知识点,重点掌握系统函数以及信号集相关的函数。

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

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

相关文章

flowable编译

git clone -b flowable-release-6.7.2 https://github.com/flowable/flowable-engine.git下载之后File-Open&#xff0c;打开工程&#xff0c;modules是核心代码模块 找到flowable-root.xml按下altf12 &#xff0c;启动Terminal终端输入命令&#xff1a;mvn clean package -Ds…

《Buildozer打包实战指南》第三节 安装Buildozer打包所需的依赖文件

目录 3.1 安装依赖软件包 3.2 安装Cython 3.3 设置环境变量 3.4 安装p4a、Android SDK、NDK以及其他编译文件 Buidozer这个打包库下载安装完毕之后&#xff0c;我们还需要下载一些打包安卓apk所需要的依赖文件。 3.1 安装依赖软件包 首先输入以下命令更新Ubuntu上的软件包…

使众人行:如何带领一群人把事做成?

你好&#xff0c;我是雷蓓蓓&#xff0c;一名程序员出身的项目经理&#xff0c;曾任网易杭研项目管理部总监。 我所带领的网易杭研项目管理部&#xff0c;从2011年成立以来&#xff0c;就一直在互联网项目管理领域深耕&#xff0c;为网易云音乐、网易严选、云计算、智慧企业等…

智慧社区管理系统改造方案

伴随着城市发展的持续加速&#xff0c;许多在建智慧社区和老旧小区智能化改造都在有规划的展开着。如今许多老旧小区在展开设备升级&#xff0c;许多小区智能安防设备、物业管理服务系统软件、社区综合服务平台及其监控器设备等都会展开智能化改造。但是&#xff0c;很多人对老…

17.优于select的epoll

优于select的epoll epoll 理解及应用 select复用方法其实由来已久&#xff0c;因此&#xff0c;利用该技术后&#xff0c;无论如何优化程序性能也无法同时接入上百个客户端&#xff08;当然&#xff0c;硬件性能不同&#xff0c;差别也很大&#xff09;。这种select方式并不适…

IIC驱动中景园0.96寸OLED

驱动硬件介绍 1、驱动电压3.3到5,但是正点的也是这个芯片说用3.3 5会烧坏掉。 2、RST 上的低电平,将导致OLED 复位,在每次初始化之前,都应该复位一下 OLED 模块。而我们使用四线,里面就没有复位了 3、裸屏有多种接口方式(驱动芯片为SSD1306) 6800、8080 两种并行接口方…

Redis应用2(Redison)

不推荐使用application的配置方式,因为会替代spring内部的对于redis的配置方式 注意:如果redis数据库没有密码,不要使用 config.useSingleServer().setPassword("") 的形式,直接跳过setPassword()就可以,配置类写法如下: Configuration public class RedisConfig…

MySQL进阶——索引

一、索引及其分类 1.索引的概念 索引是一种特殊的文件&#xff0c;包含着对数据表中所有记录的引用指针通俗点说&#xff0c;索引就好比是一本书的目录&#xff0c;能加快数据库的查询速度例如需要遍历 200 条数据&#xff0c;在没有索引的情况下&#xff0c;数据库会遍历全部…

Spring之事务编程概述

目录 一&#xff1a;基本概念 搭建测试环境 基于xml声明式事务控制 二&#xff1a;事务相关配置 ​编辑 基于注解声明式事务控制 三&#xff1a;Spring事务角色 四&#xff1a;事务传播行为 五&#xff1a;案例&#xff1a;转账业务追加日志 一&#xff1a;基本概念 事…

Vue.nextTick核心原理

相信大家在写vue项目的时候&#xff0c;一定会发现一个神奇的api&#xff0c;Vue.nextTick。为什么说它神奇呢&#xff0c;那是因为在你做某些操作不生效时&#xff0c;将操作写在Vue.nextTick内&#xff0c;就神奇的生效了。那这是什么原因呢&#xff1f; 让我们一起来研究一…

手把手教你写Dockerfile以及测试

Dockerfile是什么&#xff1f; dockerfile就是用来构建docker镜像的构建文件,命令参数脚本。 如何使用Dockerfile&#xff1f; 1、编写一个Dockerfile文件2、docker build构建成 基础使用&#xff08;此处罗列一些我们经常用到的&#xff09; # 指定依赖镜像版本&#xff…

【附代码】十大主流聚类算法

准备工作安装必要的库pip install scikit-learn准备数据集使用 make _ classification ()函数创建一个测试二分类数据集。数据集将有1000个示例&#xff0c;每个类有两个输入要素和一个群集。这些群集在两个维度上是可见的&#xff0c;因此我们可以用散点图绘制数据&#xff0c…

第18章_JDBC

一、JDBC概述JDBC概述什么是JDBCJDBC&#xff08;Java DataBase Connectivity, Java数据库连接&#xff09; ,是一种用于执行SQL语句的Java API&#xff0c;为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成有了JDBC&#xff0c;程序员只需用JDBC API写一个…

夜深忽梦少年事,7年又一年,来看看95年那个小伙现在怎么样了

2022年已到尾声&#xff0c;疫情也结束了&#xff0c;这三年太不容易了&#xff0c;今年也是一样在疫情的艰难的度过&#xff0c;就是做了两件事&#xff0c;防疫和上班&#xff0c;没什么可写的。但是在一个深夜晚上&#xff0c;想了很多以前的事&#xff0c;想想还是写一点东…

亚马逊云科技Amazon DeepRacer互联网行业全国冠军诞生

1月11日&#xff0c;首届亚马逊云科技Amazon DeepRacer自动驾驶赛车互联网行业全国总决赛圆满结束&#xff0c;从全国各地选拔出的9支冠军队伍齐聚滨海三亚&#xff0c;向总决赛的桂冠发起了冲击。 本次比赛沿袭了Amazon DeepRacer League全球赛事标准&#xff0c;使用了全新的…

Vue.js的this如何取到data和method里的属性?

本篇文章介绍的是Vue.js如何取到data和methods里的属性&#xff1f; 准备工作 克隆源码到本地 git clone https://github.com/vuejs/vue.git 下载完毕后&#xff0c;用vscode打开&#xff0c;目光移动到package.json的scripts属性&#xff0c;我们看到有dev和build&#xff0…

Golang -- openwechat发送消息、自动回复

开篇 马上就要到农历新年了&#xff0c;不妨写一段代码准时为好友们送上祝福。 该 Demo 使用开源项目 openwechat &#xff0c;实现获取好友列表、为好友发送消息、图片或文件&#xff0c;接收来自好友或群组的消息并设置自动回复等功能。 openwechat Github地址 openwechat 文…

CSS设置元素内边距(padding)、外边距(margin)

设置元素内边距padding 所有的 HTML 元素基本都是以矩形为基础。 每个 HTML 元素周围的矩形空间由三个重要的属性来控制&#xff1a; padding&#xff08;内边距&#xff09; margin&#xff08;外边距&#xff09; border&#xff08;边框&#xff09; padding控制着元素内容…

产品经理需要懂的专业术语有哪些?

不同的行业都有着不同的专业术语&#xff0c;掌握专业术语不仅是个人专业能力的体现&#xff0c;还可以进一步促进工作中的交流&#xff0c;提高工作效率。 1、工作类 BRD&#xff1a;商业文档&#xff0c;包含了商业几乎&#xff0c;产品背景&#xff0c;可行性说明&#xff…

Redis底层数据结构简介

目录 1、Redis存储结构 2、数据结构 2.1、简单动态字符串(SDS) 2.2.1、SDS数据结构 2.2.2、编码 2.2.3、SDS与C字符串对比 2.2、链表(Linkedlist) 2.2.1、链表数据结构(双向链表) 2.2.2、特性 2.3、跳表(Skiplist) 2.3.1、数据结构 2.3.2、特点 2.3.3、增删查操作…