【Linux】【信号】

news2024/9/23 9:32:55

文章目录

  • 一、信号是什么
    • 1.生活中的信号
    • 2.什么是Linux信号
    • 3.信号处理的常见方式
    • 4.Linux当中的信号
  • 二、信号的产生
    • 1.signal函数
    • 2.核心转储
    • 3.验证进程等待中的core dump标记位
  • 三、信号的系统调用接口
    • 1.kill
    • 2.raise
    • 3.abort
  • 四、由软件条件产生信号
    • alarm
  • 五、硬件异常产生信号
    • 1.除零异常
    • 2.野指针(段错误11号信号)
  • 六、阻塞信号
    • sigset_t信号集
    • 信号集的处理函数
      • 1.sigpending
      • 2.sigprocmask
  • 七、信号处理

一、信号是什么

1.生活中的信号

信号和我们上一章节中刚说的信号量是没有任何关系的,食欲两套不同的体系。
信号属于通信范畴,信号量属于用于互斥和同步通信体系的。

在生活中有哪些跟信号相关的场景:

红绿灯、请求集合信号、短信的提示音、狼烟、

  1. 你为什么会知道这些信号呢?

因为你记住了这些对应场景下的信号+后序是有动作需要你执行的。
(闹钟响了,你就知道你需要起床了)
这样我们就能够识别这些信号

  1. 我们再我们的大脑中,能识别这个信号的。
  2. 如果特定的信号没有产生,但是我们依旧知道我们应该如何处理这个信号。
  3. 我在收到这个信号的时候,可能不会立即处理这个信号。
    (外卖到了,单手我手头的活还没做完,我们就没有办法立即处理这个信号,我们还需要等一等)
  4. 信号本身在我们无法立即被处理的时候,也一定要先被临时地记住
    (我们需要记着我们的外卖已经送到哪里了,也就是先记住这个信号)

2.什么是Linux信号

本质是一种通知机制。用户或者操作系统通过发送一定的信号,通知进程,某些事件已经发生,你可以在后序进行处理。

结合进程,信号结论

  1. 进程要处理信号,必须具备信号“识别”的能力(a.看到这个信号b.处理这个信号)
  2. 凭什么进程能够“识别”这个信号呢?
    一定是在进程内部提前规定了这个信号应该如何被处理。
  3. 曾经我们使用过kill -9来杀死一个进程,本质就是对进程发送了9号新号来杀死进程,这里的9就是一个信号
  4. 信号是随机产生的,进程可能正在忙自己的事情。所以新号的处理可能不是立即处理的
  5. 信号会临时地记录一下对应的信号,方便后序的处理。
  6. 在什么时候处理呢?合适的时候
  7. 一般而言,信号的产生相对于进程而言是异步

我们不妨编写一个死循环然后我们ctrl c一下,终止这个进程

#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
    while(1)
    {
        cout<<"hello world"<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

ctrl+c:本质就是向我们的进程发送2号信号,将其终止(进程退出了)。

3.信号处理的常见方式

  1. 默认的处理方式(每一种信号都有默认的处理动作,进程自带的,是程序要写好的逻辑)
  2. 忽略(闹钟响了,但是你还是不想醒来。)(将计算机中记住的信号忘掉)
  3. 自定义动作(捕捉信号)(闹钟响了,别人默认是起床,但是你想要跳一套广播体操,那这个就是自定义动作)

4.Linux当中的信号

查看Linux中对应的所有的信号

kill -l

在这里插入图片描述

一共是62个信号,没有32,33,没有0号信号

[1:31]号新号被称为普通信号
[34:64]信号中带有RT的称为实时信号
分时操作系统和实时操作系统
实时操作系统有严格的时序,需要立马严格地处理完成。
比方说汽车中的车载操作系统,有些操作系统是Linux,也会参与我们的汽车的操作,比方说刹车。
如果是分时操作系统,那么如果我们刹车的这个进程没有获得处理机的处理,那么久不会立马被刹车。
如果是实时操作系统的话,那么我们的刹车就会被立即执行。

查看每一个信号对应的具体的意义

man 7 signal

在这里插入图片描述

如何理解信号被进程保存呢?如何理解信号发送的本质?

a.什么信号
b.是否产生
进程必须具有保存信号的相关数据结构(位图结构,unisgned int用第几个比特位的位置表示第几个信号,0000 0010,比方说这个位图的从右往左第二个位置为1,表示2号新信号被接收到了)。
(信号本质都是给进程发送的)

位图在哪里保存呢?

进程的PCB内部会保存信号位图字段。
信号位图是在task_struct->task_struct属于内核数据结构->只有操作系统才有资格去修改操作系统内部的数据结构
所以所有的信号本质就是操作系统发送的,只有操作系统才有权限去修改PCB内部的相关字段。

信号发送的本质:OS向目标进程写信号,OS直接修改对应的PCB当中的指定的位图结构,完成发送信号的过程。

我们上面的组合键ctrl c就是发送了一个2号信号,那么我们如何理解组合键变成信号呢?

键盘的工作方式是通过:中断方式进行的
键盘可以识别abcd的字符,当然也能够是被组合键
当键盘识别到了ctrl c之后,OS解释组合键
OS查找进程列表,找到前台运行的进程
操作系统写入对应的信号到我们进程内部的位图结构当中。
(所以kill命令底层一定调用了系统接口)

二、信号的产生

1.signal函数

对特定的信号进行捕捉

signal

在这里插入图片描述
第一个参数:你要对哪一个信号进行捕捉
第二个参数:函数指针。
想一个函数传入另外一个函数的函数指针,就是回调函数。通过回调的方式,修改赌赢的信号捕捉方法。然后我们这个回调函数的参数为int,返回值类型为void。

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void catchSig(int signum)
{
    cout<<"进程捕捉到了一个信号,正在处理中"<<signum<<"Pid:"<<getpid()<<endl;
}
int main()
{
    //系统中信号对应的名称
    //只要我们收到了2号信号,就将这个信号传递给catchSig函数
    signal(SIGINT,catchSig);
    //系统中信号对应的编号
    // signal(2,catchSig);
    while(true)
    {
        cout<<"我是一个进程,我正在运行……,Pid:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
我们发现我们ctrl+c之后,我们的程序并没有停止。
因为以前对于2号信号的处理动作就是终止这个进程,现在我们改成了执行对应的函数
所以我们的进程就不退出了。

特定信号的处理动作,一般只有一个。

signal函数仅仅是修改进程对特定信号的后序处理动作,不是直接调用对应的处理动作。
(比方说我们上面的signal函数是写在最前面的,但并不是运行到这一行就直接调用我们的catchSig函数,而是只有捕捉到2号信号的时候才会调用我们的catchSig函数)
(如果我们没有发送这个信号,我们的这个catchSig函数就不会调用)

signal一般都是写在前面,就好比是我们先注册了一个方法。

向进程发送三号信号,同样也能让我们的进程退出

ctrl \

在这里插入图片描述
我们同样可以处理我们的3号信号

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void catchSig(int signum)
{
    cout<<"进程捕捉到了一个信号,正在处理中: "<<signum<<"Pid:"<<getpid()<<endl;
}
int main()
{
    //系统中信号对应的名称
    //只要我们收到了2号信号,就将这个信号传递给catchSig函数
    signal(SIGINT,catchSig);
    //ctrl \,处理三号信号
    signal(SIGQUIT,catchSig);
    while(true)
    {
        cout<<"我是一个进程,我正在运行……,Pid:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

2.核心转储

我们刚刚观察到我们的2,3号信号都能让我们的进程停止,那么我们用

man 7 signal

查看到的这里信号的动作分别是term和core,这个有什么区别吗?
(ign是忽略,cont是继续)
在这里插入图片描述
这个core就是我们core dump中的一个标志位的不同,代表是否发生和核心转储
在这里插入图片描述

可以查看这篇博文waitpid获取子进程退出结果的部分
进程

一般而言,我们云服务器(生产环境)的核心转储功能是被关闭的

查看操作系统对于我们进程的限制。

ulimit -a

在这里插入图片描述

ulimit -c 1024

在这里插入图片描述
然后我们编写下面的程序

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
int main()
{
    while(true)
    {
        cout<<"我是一个进程,我正在运行……,Pid:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

将我们的程序运行起来之后,我们执行ctrl+\,也就是发送一个三号信号
在这里插入图片描述
会生成一个core文件
在这里插入图片描述

du -k core.5991

在这里插入图片描述

核心转储的意思就是当进程出现某种异常的时候,是否由OS将当前进程在内存中的相关核心数据,转存到磁盘中,也就是生成我们的core文件。

为什么要转储呢?
主要是为了调试。

如何进行调试定位到出错的位置
首先编译我们的程序,生成我们的core文件
程序如下

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
int main()
{
    sleep(1);
    int a=100;
    a/=0;
    cout<<"hello world"<<endl;
    return 0;
}

在这里插入图片描述
然后使用gdb打开我们的程序

gdb signal

在这里插入图片描述
然后在我们的gdb中打开我们的core文件

core core.6828

浮点数错误
在这里插入图片描述

3.验证进程等待中的core dump标记位

在这里插入图片描述
所以我们这个core dump的标记位就是是否发生核心转储。

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
using namespace std;
int main()
{
    pid_t id=fork();
    //子进程制造一个除零错误(8号信号)
    if(id==0)
    {
        sleep(1);
        int a=100;
        a/=0;
        exit(0);
    }
    int status=0;
    //以阻塞的方式进行等待
    waitpid(id,&status,0);
    cout<<"父进程:"<<getpid()<<"子进程:"<<id<<"exit sig: "<<(status&0x7F)<<"is core: "<< ((status>>7)&1)<<endl;
    return 0;
}

在这里插入图片描述
也就是说,这个coredump标记位的意思就是当你子进程退出的时候,是否是用coredump的形式推出的。

那如果我们系统层面将我们的core dump给关闭的话,我们这里读取到的core就是0

ulimit -c 0

为什么生产环境一般要关闭core dump?
因为如果我们的生产环境中打开了core dump,那么可能就会生成大量的core文件,所以我们的磁盘非常容易被占满,然后导致我们的系统崩溃。

三、信号的系统调用接口

1.kill

在这里插入图片描述

编写一个程序向指定的进程发送指定的信号

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<string>
using namespace std;
// ./signal 2 pid
static void Usage(string proc)
{
    cout<<"Usage:\r\n\t"<<proc<<" signumber processid"<<endl;
}
int main(int argc,char *argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    //我们的信号
    int signumber=atoi(argv[1]);
    //我们的进程的pid
    int procid=atoi(argv[2]);

    kill(procid,signumber);
    return 0;
}

首先我们让一个进程进行睡眠
在这里插入图片描述
然后我们查看这个进程的pid
在这里插入图片描述
然后我们调用我们刚刚的程序将我们的这个sleep进行关闭
在这里插入图片描述

2.raise

自己给自己发送信号
在这里插入图片描述

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<string>
using namespace std;

int main(int argc,char *argv[])
{
    cout<<"我开始运行了"<<endl;
    sleep(1);
    raise(8);
    return 0;
}

在这里插入图片描述

3.abort

给自己发送确定的abort信号,也就是自己终止自己,我们的6号信号就是这个功能。
在这里插入图片描述

可以理解成自己给自己发送了6号信号
raise(6)

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<stdlib.h>
using namespace std;

int main(int argc,char *argv[])
{
    cout<<"我开始运行了"<<endl;
    abort();
    return 0;
}

在这里插入图片描述

我们的abort通常用来终止进程。

如何理解系统调用接口发送信号?

用户调用系统接口->    执行OS对应的系统调用代码->    OS提取参数,或者设置特定的数值->
OS先目标进程写信号->   修改对应进程的信号标记位->   进程后续会处理信号->
执行对应的处理动作

四、由软件条件产生信号

管道中,读端不光不读,而且还关闭了,此时写端一直在写会发生什么问题?
写没有意义!OS会自动终止对应的写端进程,通过发送信号的方式,SIGPIPE
也就是我们的13号信号

1.创建匿名管道
2.让父进程进行读取,子进程进行写入
3.让我们的父进程关闭读端,并且waitpid()等待子进程,子进程只要一直写入就行
4.子进程退出,父进程waitpid拿到子进程的退出status
5.提取出退出信号
#include<iostream>
#include<unistd.h>
#include<assert.h>
#include<string>
#include<cstdio>
#include<cstring>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main()
{
    //1.创建管道
    int pipefd[2]={0};//pipefd[0]:读端,pipefd[1]:写端
    int n=pipe(pipefd);
    //在debug模式下assert是有效的,但是release版本下是会无效的
    assert(n!=-1);
    //所以我们这里需要写下面的代码,证明n被使用过
    (void)n;
 
    //如果是DEBUG模式下就不打印了,相当于就是注释掉了
  #ifdef DEBUG
    cout<<"pipefd[0]"<<pipefd[0]<<endl;
    cout<<"pipefd[1]"<<pipefd[1]<<endl;
  #endif
 
    //2.创建子进程
    pid_t id=fork();
    assert(id!=-1);
    if(id==0)
    {

      close(pipefd[0]);
      string message="我是子进程,我正在给你发消息";
      int count=0;
      char send_buffer[1024];
      while(true)
      {
        snprintf(send_buffer,sizeof(send_buffer),"%s[%d]:%d",message.c_str(),getpid(),count++);
        write(pipefd[1],send_buffer,strlen(send_buffer));
        sleep(1);
      }
      
    }
    //父进程
    close(pipefd[1]);
    char buffer[1024];
    //从0号文件描述符中读取,读取到缓冲区buffer中
    size_t s=read(pipefd[0],buffer,sizeof(buffer)-1);
    if(s>0)
    {
      //添加\0
      buffer[s]=0;
      cout<<"father get a message["<<getpid()<<"] Child#"<<buffer<<endl;
    }
    sleep(10);
    close(pipefd[0]);

    int status;
    pid_t ret=waitpid(id,&status,0);
    cout<<"退出码为:"<<status<<endl;
    assert(ret<0);
    (void)ret;
    //子进程中的pipefd[0]关闭可写可不写,因为进程退出了,进程中的文件描述符也会被关掉
    return 0;
}


在这里插入图片描述

我们的pipe在一端被关闭后,就没有通信功能了。
这就称为我们的软件条件不满足,于是我们的子进程就被终止了,也就是被发送了13号信号。

alarm

在这里插入图片描述

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<string>
#include<stdlib.h>
using namespace std;

int main(int argc,char *argv[])
{
    //设置一个1秒的闹钟
    //一秒之后给我们发送13号信号
    //也就是验证1秒只能,我们一共会计算多少次count++
    alarm(1);
    int count=0;
    while(true)
    {
        cout<<"count: "<<count++<<endl;
    }
    return 0;
}

在这里插入图片描述

为什么我们只运行了10w+次左右?
1.因为cout,要打印出来 2(云服务器)网络发送
也就是说要通过大量的IO和长距离传输
所以就会非常慢。

那我们如果想单纯地计算算力呢?

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<string>
#include<stdlib.h>
using namespace std;
uint64_t count=0;
void catchSig(int signum)
{
    cout<<"final cout: "<<count<<endl;
}
int main(int argc,char *argv[])
{
    //设置一个1秒的闹钟
    //一秒之后给我们发送13号信号
    alarm(1);//我们设定了一个闹钟,这个闹钟一旦触发,就自动移除了
    signal(SIGALRM,catchSig);
    while(true)
    {count++;}
    return 0;
}

在这里插入图片描述

我们设定了一个闹钟,这个闹钟一旦触发,就自动移除了
我们可以周期性地定闹钟

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<string>
#include<stdlib.h>
using namespace std;
uint64_t count=0;
void catchSig(int signum)
{
    cout<<"final cout: "<<count<<endl;
    alarm(1);
}
int main(int argc,char *argv[])
{
    //设置一个1秒的闹钟
    //一秒之后给我们发送13号信号
    alarm(1);//我们设定了一个闹钟,这个闹钟一旦触发,就自动移除了
    signal(SIGALRM,catchSig);
    while(true)
    {count++;}
    return 0;
}

在这里插入图片描述
或者我们可以设置一个任务列表,周期性地执行任务

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<string>
#include<stdlib.h>
#include<vector>
#include<functional>
#include<sys/wait.h>
using namespace std;
typedef function<void ()> func;
vector<func> callbacks;
uint64_t count=0;

void showCount()
{
    cout<<"final cout: "<<count<<endl;
}
void showLog()
{
    cout<<"这个是日志功能"<<endl;
}
void loguser()
{
    if(fork()==0)
    {
        execl("/usr/bin/who","who","-a",nullptr);
        exit(1);
    }
    wait(nullptr);
}
void catchSig(int signum)
{
    for(auto&f:callbacks)
    {
        f();
    }
    alarm(1);
}
int main(int argc,char *argv[])
{
    //设置一个1秒的闹钟
    //一秒之后给我们发送13号信号
    alarm(1);//我们设定了一个闹钟,这个闹钟一旦触发,就自动移除了
    signal(SIGALRM,catchSig);
    callbacks.push_back(showCount);
    callbacks.push_back(showLog);
    callbacks.push_back(loguser);
    while(true){count++;}
    return 0;
}

在这里插入图片描述

如何理解软件条件给进程发送信号

a.OS先识别到某种软件条件触发或者不满足
b.OS构建信号,发送给指定的进程。

五、硬件异常产生信号

1.除零异常

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<stdlib.h>
#include<functional>
#include<sys/wait.h>
using namespace std;
void handler(int signum)
{
    sleep(1);
    cout<<"获得了一个信号:"<<signum<<endl;
    //
}
int main(int argc,char *argv[])
{
    signal(SIGFPE,handler);
    int a=100;
    a/=0;
    while(true) sleep(1);
    return 0;
}

除零错误给我们发送了8号信号,但是为什么我们的进程循环打印8号信号呢?
在这里插入图片描述

如何理解这里的除0呢?

为什么只有除0会被我们的操作系统发现,除23不会吗?
1.进行计算的是CPU,这个是硬件
2.CPU内部是有寄存器的,状态寄存器(不进行数值保存),用来保存本次计算的计算状态
(有没有出现进位,有没有出现溢出)
3.状态寄存器里面有对应的状态标记位,溢出标记位,操作系统会进行计算完毕之后的检测。
如果溢出标记位是1,操作系统里面识别到有溢出问题,立即只要找到当前谁在运行提取pid
操作系统完成信号发送的过程,进程会在合适的时候,进行处理。

一旦出现硬件异常,进程一定会退出码?

不一定。
因为硬件异常的默认行为是退出,但是如果你捕捉了这个异常就不会退出了。
但是溢出标记位是由CPU维护的
所以即便我们不退出,我们也做不了什么。
我们只能打印这个错误,然后进行退出。

那为什么会出现死循环呢?

我们的异常的退出变成了打印错误信号,但是不退出。
但是寄存器中的异常一直没有被解决!那我们的操作系统同会对其进行调度
那么所以会一直给打我们打印错误信号,除非我们将我们的进程退出。
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<stdlib.h>
#include<functional>
#include<sys/wait.h>
using namespace std;
void handler(int signum)
{
    sleep(1);
    cout<<"获得了一个信号:"<<signum<<endl;
    exit(1);
}
int main(int argc,char *argv[])
{
    signal(SIGFPE,handler);
    int a=100;
    a/=0;
    while(true) sleep(1);
    return 0;
}

在这里插入图片描述

2.野指针(段错误11号信号)

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<stdlib.h>
#include<functional>
#include<sys/wait.h>
using namespace std;
int main(int argc,char *argv[])
{
    int *p=nullptr;
    *p=100;
    while(true) sleep(1);
    return 0;
}

在这里插入图片描述

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<stdlib.h>
#include<functional>
#include<sys/wait.h>
using namespace std;
void handler(int signum)
{
    sleep(1);
    cout<<"获得了一个信号:"<<signum<<endl;
    exit(1);
}
int main(int argc,char *argv[])
{
    signal(SIGSEGV,handler);
    int *p=nullptr;
    *p=100;
    while(true) sleep(1);
    return 0;
}

在这里插入图片描述

如何理解野指针或者越界问题?

1.都必须通过地址,找到目标位置
2.我们语言上面的地址,全部都是虚拟地址
3.将虚拟地址转成物理地址
4.页表+MMU(Memory Manager Unit)(内存管理单元,这是一个硬件!)
5.野指针,或者是越界->非法地址->在MMU转化的时候,一定会报错!
6.操作系统将这个报错进行捕获。

所以说:所有的信号,都有他的来源,但最终全部都是被OS识别,解释并发送的!

上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者
信号的处理是否是立即处理的?在合适的时候

六、阻塞信号

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。(信号存在但是没有被处理)
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

进程PCB内部就有三张表
在这里插入图片描述
pending表:表中是无符号整数。其中为1的代表收到了该信号,信号为0代表没有收到该信号,也就是我们上面所说的位图结构
handler表:表中填充的全部都是函数的地址,当我们的进程收到了一个信号,我们只要按照这个信号的编号就能够在handler表中找到我们信号的对应的处理方法(在上面我们的代码中有实验。)

typedef(*hander_t)(int);
handler_t handler[32]; //函数指针数组,数组的下标就是信号的编号。

signal(signum,handler);//这个就是将我们的handler函数的指针填到我们这个数组的下标为signum的位置。
但是这样仅仅是满足了信号的自定义呀,那信号的忽略和默认应该怎么解决呢?
#include<iostream>
#include<signal.h>
int main()
{
    //信号默认0
    signal(2,SIG_DFL);
    //信号忽略1
    signal(2,SIG_IGN);
}
所以操作系统会先识别你的信号编号sigal
handler[signal]
进行强制类型转换
(int)handler[signal]==0;//执行默认动作,done结束(我们上面的SIG_DFL就是0)
(int)handler[signal]==1;//执行忽略动作,done结束(我们上面的SIG_IGN就是1)
如果上面两个都没有匹配上的话,就执行我们自定义的处理方法,调用我们的函数。
handler[signal]();

block表:block表中也是位图,这个结构跟我们的pending表的结构一模一样,里面也全部都是无符号整数。但是位图中的内容代表的含义是对应的信号是否被阻塞

操作系统给我们的pending位图发送信号
->处理信号-> 查看pending位图中哪些位置为1   
->看看对应的block是否为1,如果为1就是被屏蔽了,不处理
->如果没有被屏蔽,那么我们再去handler表的对应位置查找对应的信号的处理方法
也就是说:pending->block->handler

sigset_t信号集

基本上,语言会给我们提供.h,.hpp和语言的自定义类型(语言类的类型可能会包含系统提供的类型)
同时OS也会给我们提供.h和OS自定义的类型
sigset_t :是一种位图结构,也是操作系统给我们提供的一种类型,不允许用户自己进行位操作。操作系统给我们提供了对应的操作位图的方法

sigset_t :user是可以直接使用该类型的,和用内置类型和自定义类型没有任何差别
sigset_t :一定需要对应的系统接口来完成对应的功能,其中系统调用接口需要的参数,可能就包含了sigset_t定义的变量或者对象。

信号集的处理函数

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。
初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

1.sigpending

检查pending信号,也就是获取当前调用进程的pending信号集
返回值成功就是0,失败就是-1

在这里插入图片描述

2.sigprocmask

检查并且更改我们的阻塞信号集,能够对我们的block的信号集进行获取和更改

在这里插入图片描述

这个how参数有下面几个选择:

在这里插入图片描述

sigset_t *oldset是一个输出型参数,比方说你想对2,5,8信号进行屏蔽,但当你屏蔽完成,之后想要恢复的时候,就可以用到这个。
它会返回旧的信号屏蔽字

1.如果我们队所有的信号都进行了自定义捕捉–我们是不是就写了一个不会被异常或者用户杀掉的进程?可以吗?

#include<iostream>
#include<unistd.h>
#include<signal.h>
void catchSig(int signum)
{
    std::cout<<"获得了一个信号:"<<signum<<std::endl;
}
int main()
{
    for(int i=1;i<=31;i++) signal(i,catchSig);
    while(true) sleep(1);
}
我们发现我们的9号信号依旧能够杀死我们的进程
9号信号属于管理员信号,是不能够被捕捉的!

在这里插入图片描述

2.如果我们将2号信号block,并且不断获取并且打印当前进程的pending信号集,如果我们突然发送一个2号信号,我们就应该看到pending信号集中,有一个比特位0->1(该信号一直被阻塞,得不到处理,所以一直是没有被处理的状态,也就是1)

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<assert.h>
static void showPending(sigset_t &pending)
{
    for(int sig=1;sig<=31;sig++)
    {
        if(sigismember(&pending,sig)) std::cout<<"1";
        else std::cout<<"0";
    }
    std::cout<<std::endl;
}
static void handler(int signum)
{
    std::cout<<"捕捉信号:"<<signum<<std::endl;
}
int main()
{
    //0.方便测试,捕捉2号信号,不要退出
    signal(2,handler);
    //1.定义两个信号集对象(在栈区开辟了空间)
    sigset_t bset,obset;
    sigset_t pending;
    //2.初始化
    sigemptyset(&bset);
    sigemptyset(&obset);
    sigemptyset(&pending);
    //3.添加要进行屏蔽的信号
    sigaddset(&bset,2 /*SIGINT*/);
    //4.设置到对应的进程内部(默认进程不会对任何信号进行block)
    int n=sigprocmask(SIG_BLOCK,&bset,&obset);
    
    //assert是一个宏,在release版本是无效的,所以要定义一个void(n);
    assert(n==0);
    (void)n;

    std::cout<<"block 2号信号成功……  pid:"<<getpid()<<std::endl;
    //5.重复打印当前进程的pending信号集
    int count=0;
    while(true)
    {
        //5.1获取当前进程的pending信号集
        sigpending(&pending);
        //5.2显示pending信号集中没有被递达的信号
        showPending(pending);
        sleep(1);
        count++;
        if(count==20)
        {
            //默认情况下,恢复对于2号信号的block的时候,确实会进行递达
            //但是2号信号的默认处理动作是终止进程!
            //需要对2号信号进行捕捉
            std::cout<<"解除对于2号信号的block"<<std::endl;
            int n=sigprocmask(SIG_SETMASK,&obset,nullptr);
            //assert是一个宏,在release版本是无效的,所以要定义一个(void)n;
            assert(n==0);
            (void)n;
            
        }
    }
    return 0;
}

在这里插入图片描述

貌似没有一个接口用来设置pending位图(所有的信号发送方式,都是修改pending位图的过程),我们是可以获取的sigpending

3.如果我们对所有的信号都进行block,那么我们是不是就写了一个不会被异常或者用户杀掉的进程?可以吗

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<assert.h>
static void showPending(sigset_t &pending)
{
    for(int sig=1;sig<=31;sig++)
    {
        if(sigismember(&pending,sig)) std::cout<<"1";
        else std::cout<<"0";
    }
    std::cout<<std::endl;
}
static void handler(int signum)
{
    std::cout<<"捕捉信号:"<<signum<<std::endl;
}
//对指定信号进行屏蔽
static void blockSig(int sig)
{
    sigset_t bset;
    sigaddset(&bset,sig);
    int n=sigprocmask(SIG_BLOCK,&bset,nullptr);
    assert(n==0);
    (void)n;
}
int main()
{
    for(int sig=1;sig<=31;sig++)
    {
        blockSig(sig);
    }
    sigset_t pending;
    sigemptyset(&pending);
    while(true)
    {
        sigpending(&pending);
        showPending(pending);
        sleep(1);
    }
    return 0;
}

编写并且运行上面的程序之后,我们输入下面的bash命令,向我们的进程发送1-31号信号,看看会发生什么情况

i=1;id=$(pidof signal); while [ $i -le 31 ]; do kill -$i $id ; echo "send signal $i" ;let i++; sleep 1;done

在这里插入图片描述
在这里插入图片描述

这里我们不妨跳过919信号,来查看一下我们别的信号会发生什么情况
编写下面的bash脚本,运行。
#! /bin/bash

i=1
id=$(pidof signal)
while [ $i -le 31 ] 
do
	if [ $i -eq 9 ];then
		let i++
		continue
	fi
	if [ $i -eq 19 ];then
		let i++
		continue
	fi
	kill -$i $id 
	echo "kill - $i  $id" 
	let i++
	sleep 1
done

在这里插入图片描述

我们发现919信号会杀死我们的进程,或者让我们的进程stop
我们的20号信号在pending中不会变成1

七、信号处理

信号产生之后,信号可能无法立即被处理,在合适的时候(是什么?)
1.在合适的时候(是什么?)

信号相关的数据字段都是在进程PCB内部,属于内核的范畴。
内核态 vs 用户态
只有在内核态,从内核态返回用户态的时候,才进行信号检测和处理。

我为什么会进入内核态呢?
进行系统调用,缺陷陷阱异常等。

在汇编语言上有一个中断编号int 80,内置在我们的系统调用函数中。

在这里插入图片描述
在这里插入图片描述

如果存在一个open的系统调用,我们只需要在内核级页表中查找到对应的方法就可以了

在这里插入图片描述

内核也是在所有进程的地址空间上下文中跑的

那么我们可以执行进程切换的代码吗?

当然可以。
操作系统直接在进程地址空间中找到对应的进程,将其数据保存,然后切换上我们想要执行的代码。

我凭什么有权利执行OS的代码呢?

凭的就是我们处于内核态还是用户态。
cpu内的寄存器分为两类,一套是可见的,一套是cpu不可见的(自用的)
cpu中有一个CR3寄存器,其中有若干个比特位表示当前CPU的执行权限,
	比方说用1表示内核,3表示用户态
所以我们调我们上面介绍的寄存器指令int 80的时候,我们就会修改这个寄存器中的这个标志位。
然后我们就可以访问内核级页表,然后进行内核级操作了。

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

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

相关文章

从编译到可执行,eBPF 加速容器网络的原理分析 | 龙蜥技术

编者按&#xff1a;eBPF(extended Berkeley Packet Filter) 是一种可以在 Linux 内核中运行用户编写的程序&#xff0c;而不需要修改内核代码或加载内核模块的技术。简单说&#xff0c;eBPF 让 Linux 内核变得可编程化了。本文整理自龙蜥大讲堂第 57 期&#xff0c;浪潮信息 SE…

HTML防数据采集

什么是防采集 就是我们想利用爬虫工具采集某个网站的数据&#xff08;前提当然是公开合法数据&#xff09;&#xff0c;但网站不想给你采集而设置的技术阻挡措施。 常见的防止采集方案 利用输入验证码框验证&#xff0c;在采集某些网站过程中&#xff0c;要求你输入验证码&a…

电源特性测试测试哪些方面?电源特性自动测试系统NSAT-8000介绍

假设电源适配器厂家对电源适配器进行了很合理的测试验证工作&#xff0c;那么电源适配器输出的电压应该是个稳定的电源输出。那么对于一些小型设备而言&#xff0c;电源测试就主要测试设备电源端的测试工作。下面纳米软件Namisoft小编将带大家一起看看&#xff0c;关于电源特性…

Android Jetpack Compose——一个简单的笔记APP

一个简单的笔记APP简述效果视频Hilt提供依赖对象Room CRUD接口实现类内容封装查询所有查询删除插入笔记内容效果图ViewModel依赖注入数据初始化数据处理View标题栏排序组件笔记列表新建&编辑笔记效果图ViewModel依赖注入初始化数据处理View背景颜色条标题保存笔记路由导航建…

动态规划 0-1背包问题(滚动数组思想优化)

目录 125 背包问题&#xff08;二&#xff09;LintCode 炼码 0-1背包滚动数组优化 0-1背包问题&#xff08;一&#xff09;LintCode 炼码 【解法一】二维数组 【解法二】滚动数组 125 背包问题&#xff08;二&#xff09;LintCode 炼码 class Solution { public:/*** para…

HADOOP-3.2.2安装

HADOOP-3.2.2安装一. 准备工作二.安装阶段1. 创建安装目录并安装解压包2.修改配置文件core-site.xml3. 修改hdfs-site.xml4. 修改修改yarn-site.xml5.修改workers文件6.修改hadoop-env.sh7.修改mapred-site.xml8.递归创建目录9.分发文件三.运行阶段1.启动hdfs2.启动yarn3.启动j…

F280049C Crossbar X-BAR

文章目录X-BAR9.1 输入X-BAR9.2 ePWM、CLB和GPIO输出X-BAR9.2.1 ePWM X-BAR9.2.1.1 ePWM X-BAR架构9.2.2 CLB X-BAR9.2.2.1 CLB X-BAR架构9.2.3 GPIO输出X-BAR9.2.3.1 GPIO输出X-BAR架构9.2.4 X-BAR标志总结X-BAR 交叉开关&#xff08;在本章中称为X-BAR&#xff09;提供了以各…

一年风雨几度寒,一杯浊酒敬虎年

我是谁大家好&#xff0c;我是凡夫贩夫&#xff0c;真实姓名不值一提&#xff0c;我的履历也很不值一提&#xff0c;非名校非大厂非专家&#xff0c;一名三非野生java开发者&#xff0c;现居住地河南郑州&#xff0c;就职于一家外包公司。的确&#xff0c;我是一个普通人&#…

(02)Cartographer源码无死角解析-(46) 2D栅格地图→CastRay()函数与贝汉明(Bresenham)算法

讲解关于slam一系列文章汇总链接:史上最全slam从零开始&#xff0c;针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下: (02)Cartographer源码无死角解析- (00)目录_最新无死角讲解&#xff1a;https://blog.csdn.net/weixin_43013761/article/details/127350885 文末…

PCB设计完成后,为什么经常要拼版及拼版注意事项

通常我们在完成PCB设计的时候&#xff0c;有一些板子我们通常是需要进行拼版的&#xff0c;那么我们为什么要拼版&#xff0c;哪种情况下需要拼版呢&#xff1f;不拼是否可以呢&#xff1f;1、PCB生产制作尺寸要求 一般来说面积比较小的板子我们是需要进行拼版&#xff0c;一般…

MATLAB APP 设计实践(一)UART通信(下篇)

引言上篇介绍了 MATLAB App 的基本内容&#xff0c;本篇就结合UART发送数据的具体案例介绍开发过程。文末给出设计源文件、设计的可执行文件的下载链接&#xff0c;以及App的实际使用视频&#xff08;与FPGA开发板进行调试验证&#xff09;。前文链接&#xff1a;MATLAB APP 设…

MySQL 分区(innode引擎的讲解)

目录 一.InnoDB逻辑存储结构 段 区 页 二.分区概述 分区 三.分区类型 一.InnoDB逻辑存储结构 首先要先介绍一下InnoDB逻辑存储结构和区的概念&#xff0c;它的所有数据都被逻辑地存放在表空间&#xff0c;表空间又由段&#xff0c;区&#xff0c;页组成。 段 段就是…

【Python】sklearn机器学习之层次聚类算法AgglomerativeClustering

文章目录基本原理绘图层次定义距离基本原理 和Birch聚类相似&#xff0c;层次聚类也是一种依赖树结构实现的聚类方法&#xff0c;其核心概念是相似度。根据相似度&#xff0c;可以将所有样本组织起来&#xff0c;从而构建一棵层次聚类树。 其中Birch算法的核心&#xff0c;叫…

MyBatis【多表查询与动态SQL使用】

MyBatis【多表查询与动态SQL使用】&#x1f34e;一.MyBatis多表查询&#x1f352;1.1 一对一查询&#x1f352;1.2 一对多查询&#x1f34e;二.动态SQL使用&#x1f352;2.1 if 标签使用&#x1f352;2.2 trim 标签使用&#x1f352;2.3 where 标签使用&#x1f352;2.4 set 标…

ARX测试_绘制道路横断面

本文迁移自本人网易博客&#xff0c;写于2011年1月12日&#xff0c;ARX测试_绘制道路横断面 - lysygyy的日志 - 网易博客 (163.com)1、已提供道路的图形&#xff0c;获取用户输入的两点&#xff0c;并在两点间画一条虚线。计算这条直线与多少条直线相交&#xff0c;若数量不等于…

振弦采集模块多通道专用寄存器

振弦采集模块多通道专用寄存器 多通道频率、温度值寄存器 51~58&#xff08; 0x33~0x3A&#xff09; 位 符号 值 描述 默认值 bit15:0 频率/温度值 0 单通道模块时&#xff0c;寄存器 51 内为频率值&#xff0c;寄存器 55 内为温度值 4 通道模块时&#xff0c;寄存器 51~54 内…

如何去学习PMP考试的《PMBOK》

首先&#xff0c;是PMP考试的核心教材&#xff1a;《PMBOK指南》&#xff0c;目前已经出道第七版了&#xff0c;大家如果有备考的需要要赶紧买一本来学习。 其次&#xff0c;是《汪博士解读PMP》&#xff0c;目前出到第6版&#xff0c;这本书是对PMBOK中各领域知识点的深入浅出…

机械臂速成小指南(二十一):几何雅可比矩阵

&#x1f468;‍&#x1f3eb;&#x1f970;&#x1f973;需要机械臂相关资源的同学可以在评论区中留言哦&#x1f916;&#x1f63d;&#x1f984;指南目录&#x1f4d6;&#xff1a;&#x1f389;&#x1f389;机械臂速成小指南&#xff08;零点五&#xff09;&#xff1a;机…

煤矿皮带跑偏撕裂智能检测算法 opencv

煤矿皮带跑偏撕裂智能检测算法能够通过pythonopencv深度学习技术实时监测运输皮带的状况&#xff0c;当监测到皮带出现撕裂跑偏时&#xff0c;立刻抓拍告警并中止皮带的运输。OpenCV基于C实现&#xff0c;同时提供python, Ruby, Matlab等语言的接口。OpenCV-Python是OpenCV的Py…

动态博客系统

Halo 是我折腾过的众多博客系统里面&#xff0c;最好、最容易上手的动态博客系统之一&#xff08; solo 也是&#xff09;&#xff0c;轻快&#xff0c;简洁&#xff0c;功能强大。 正文 上周末正在募集团队一起写算法题&#xff0c;群里讨论需要一个网站来存放文章&#xff…