【Linux】【信号】

一、信号是什么

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的时候,我们就会修改这个寄存器中的这个标志位。
然后我们就可以访问内核级页表,然后进行内核级操作了。

用户->内核,为什么?

有时候有一些功能在用户态是没办法执行的。
用户不能绕过操作系统直接去访问底层的硬件等资源。
所以必须要切换到内核态。

用户如何切换到内核态

用系统调用接口

信号捕捉的基本流程

在这里插入图片描述

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

执行对应的信号捕捉处理方法,我当前是什么状态

我们当前是kernel状态。也就是我们再进行信号检测之后,
还在内核态的时候,继续执行对应的处理方法

我当前的状态能不能执行user handler方法

我当前是内核态,是可以执行user handler方法的,我可以直接访问用户的0-3G空间的。
可以再用户级页表中找到对应的方法的。

OS能做到帮用户执行对应的handler方法,但是OS不愿意,也不想。
如果我们以OS的身份去执行handler方法,那如果我们的方法中有非法的操作呢?
操作系统不相信任何人,这个handler是用户写的,所以不能帮用户执行handler
(不能用内核态执行用户的代码)
执行用户态代码->陷入内核->处理信号->信号检测->处理handler方法->
再次进入内核,进行信号的一些收尾性工作(修改pending位图等等)->返回用户态,继续向后执行

在这里插入图片描述

信号的操作

这是我们之前使用的

signal(int,void*(int*));

sigaction方法

sigaction也是一个信号捕捉的方法

在这里插入图片描述

这里的参数中的sigaction是一个结构体。
*act是一个输入型参数,用来传入新的action
*oldact是一个输出型参数,用来保存之前旧的action

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。
signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。
act和oact指向sigaction结构体:
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,
赋值为常数SIG_DFL表示执行系统默认动作,
赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,
该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,
这样就可以用同一个函数处理多种信号。
显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
struct sigaction {
      void     (*sa_handler)(int);//回调函数
      void     (*sa_sigaction)(int, siginfo_t *, void *);//暂时不考虑
      sigset_t   sa_mask;
      int        sa_flags;//暂时不考虑
      void     (*sa_restorer)(void);//暂时不考虑
  };

基本的用法

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int signum)
{
    cout<<"获取了一个信号: "<< signum<<endl;
}
int main()
{
    //内核数据类型,用户栈结构上定义的。也就是属于0-3G的空间中
    struct sigaction act,oact;
    act.sa_flags=0;
    //初始化为空
    sigemptyset (&act.sa_mask);
    act.sa_handler=handler;

    //设置进当前进程调用的pcb中
    sigaction(2,&act,&oact);

    cout<<"default action: "<<(int)(oact.sa_handler)<<endl;
    while(true) sleep(1);
    return 0;
}

在这里插入图片描述

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int signum)
{
    cout<<"获取了一个信号: "<< signum<<endl;
}
int main()
{
//这里我们将2号信号的处理动作变成SIG_IGN
    signal(2,SIG_IGN);
    //内核数据类型,用户栈结构上定义的。也就是属于0-3G的空间中
    struct sigaction act,oact;
    act.sa_flags=0;
    //初始化为空
    sigemptyset (&act.sa_mask);
    act.sa_handler=handler;

    //设置进当前进程调用的pcb中
    sigaction(2,&act,&oact);

    cout<<"default action: "<<(int)(oact.sa_handler)<<endl;
    while(true) sleep(1);
    return 0;
}

在这里插入图片描述

处理信号的时候,执行自定义动作,如果在处理信号期间,又来了同样的信号,OS如何处理?

Linux在任何时候只能处理一层信号。

本质:为什么要有block(信号屏蔽)?

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,
当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,
如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 
如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,
则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 
sa_flags字段包含一些选项

也就是说正在处理2号信号的时候,再不断地传2号信号,是不会被处理的

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void showPending(sigset_t *pending)
{
    for(int sig=1;sig <=31;sig++)
    {
        if(sigismember(pending,sig)) cout<<"1";
        else cout<<"0";
    }
    cout<<endl;
}
void handler(int signum)
{
    cout<<"获取了一个信号: "<< signum<<endl;
    
    sigset_t pending;
    int c=10;
    while(true)
    {
        sigpending(&pending);
        showPending(&pending);
        c--;
        if(!c) break;
        sleep(1);
    }
}
int main()
{
    signal(2,SIG_DFL);
    //内核数据类型,用户栈结构上定义的。也就是属于0-3G的空间中
    struct sigaction act,oact;
    act.sa_flags=0;
    //初始化为空
    sigemptyset (&act.sa_mask);
    act.sa_handler=handler;

    //设置进当前进程调用的pcb中
    sigaction(2,&act,&oact);

    cout<<"default action: "<<(int)(oact.sa_handler)<<endl;
    while(true) sleep(1);
    return 0;
}

在这里插入图片描述

如何在处理一个信号的同时屏蔽别的信号

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void showPending(sigset_t *pending)
{
    for(int sig=1;sig <=31;sig++)
    {
        if(sigismember(pending,sig)) cout<<"1";
        else cout<<"0";
    }
    cout<<endl;
}
void handler(int signum)
{
    cout<<"获取了一个信号: "<< signum<<endl;
    
    sigset_t pending;
    int c=20;
    while(true)
    {
        sigpending(&pending);
        showPending(&pending);
        c--;
        if(!c) break;
        sleep(1);
    }
}
int main()
{
    cout<<"pid: "<<getpid()<<endl;
    signal(2,SIG_DFL);
    //内核数据类型,用户栈结构上定义的。也就是属于0-3G的空间中
    struct sigaction act,oact;
    act.sa_flags=0;
    //初始化为空
    sigemptyset (&act.sa_mask);
    act.sa_handler=handler;

    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);
    sigaddset(&act.sa_mask,5);
    sigaddset(&act.sa_mask,6);
    sigaddset(&act.sa_mask,7);

    //设置进当前进程调用的pcb中
    sigaction(2,&act,&oact);

    cout<<"default action: "<<(int)(oact.sa_handler)<<endl;
    while(true) sleep(1);
    return 0;
}

在这里插入图片描述

可重入函数

信号捕捉,并没有创建新的进程或者线程
这里我们假设我们的链表如下,是有头结点的
下面我们将我们的链表进行头插

在这里插入图片描述

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数

重入:在同一个时间,被多个执行流重复进入
可重入函数 vs 不可重入函数
是一种函数的特征,在不同的场景下有不同的应用。
目前我们使用的函数一般都是不可重入的函数。

如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile关键字

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
int flag=0;

void changeFlag(int signum)
{
    (void)signum;
    cout<<"change flag"<<flag;
    flag=1;
    cout<<"->"<<flag<<endl;
}

int main()
{

    signal(2,changeFlag);
    while(!flag);
    cout<<"进程正常退出 : "<<flag<<endl;
}

在这里插入图片描述

编译器有时候会自动地给我们进行代码优化

这里我们再编译的时候,指定更高的优化等级

g++ -o signal signal.cc -O3 -std=c++11 

然后我们就会观察到下面的情况
在这里插入图片描述

如果我们不做优化,我们的edx就会正常地修改flag
但是如果我们进行了优化,我们就将我们的flag优化到了寄存器当中,但是我们后序的更改的都是内存中的flag,所以我们的优化导致了cpu无法看到内存中的情况了,所以就出现了我们上面的情况。
在这里插入图片描述

我们为了保证内存的可见性,我们就引入了volatile关键字,然后就能够解决我们这里的问题。

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
volatile int flag=0;

void changeFlag(int signum)
{
    (void)signum;
    cout<<"change flag"<<flag;
    flag=1;
    cout<<"->"<<flag<<endl;
}

int main()
{

    signal(2,changeFlag);
    while(!flag);
    cout<<"进程正常退出 : "<<flag<<endl;
}

在这里插入图片描述

这个编译器的优化发生在什么时候?

是在编译的时候还是在程序执行的过程中?
是在编译的时候进行优化的。程序执行的时候,只是按照编译的情况进行执行。

SIGCHLD信号

子进程退出会主动向父进程发送第17号信号,也就是我们的SIGCHLD信号
父进程在处理第17号信号的时候,默认是忽略
所以我们可以在子进程推出的时候,利用这个17号信号,来通知我们的父进程对子进程进行回收。

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

void handler(int signum)
{
    cout<<"子进程退出: "<<signum<<endl;
}
int main()
{
    signal(SIGCHLD,handler);
    if(fork()==0)
    {
        sleep(1);
        exit(0);
    }
    while(true) sleep(1);
}

在这里插入图片描述

所以我们这里证明了子进程退出会向父进程发送退出信号。
验证是不是父进程接受了信号
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

void handler(int signum)
{
    cout<<"子进程退出: "<<signum<<"  father:"<<getpid()<<endl;
}
int main()
{
    signal(SIGCHLD,handler);
    if(fork()==0)
    {
        cout<<"child pid :"<<getpid()<<endl;
        sleep(1);
        exit(0);
    }
    while(true) sleep(1);
}

在这里插入图片描述

那如果我们有10个子进程想要回收呢?

我同一时刻收到了10个SIGCHLD信号,
但是我的pending位图里面只有一个比特位表示是否收到了这个信号。
如果同时传过来了,我们的pending位图只会收到一次这个信号
所以如果有10个进程,我根本就不知道是哪一个退出了!

所以我们采用了一个while循环,依次等待每一个进程,退出了就回收,没退出就等待(阻塞式地等待)
或者我们直接用waitpid(-1,NULL,WNOHANG),也就是等待任意一个进程退出,(非阻塞式地等待)
我们不关心是哪一个退出了。

如果我们不想等待子进程,并且我们还想让我们的子进程退出之后,自动释放僵尸进程

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

int main()
{
    if(fork()==0)
    {
        cout<<"chils: "<<getpid()<<endl;
        sleep(5);
        exit(0);
    }
    while(true)
    {
        cout<<"parent: "<<getpid()<<" 执行我自己的任务!"<<endl;
        sleep(1);
    }
}

由于我们的子进程退出的时候,资源没有被回收,我们的子进程就变成了僵尸进程。

在这里插入图片描述
方案一:编写循环等待我们的子进程退出

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <iostream>
#include <unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
void handler(int sig)
{
    pid_t id;
    while( (id = waitpid(-1, NULL, WNOHANG)) > 0)
    {
        printf("wait child success: %d\n", id);
    }
    printf("child is quit! %d\n", getpid());
}
int main()
{
    signal(SIGCHLD, handler);
    pid_t cid;
    if((cid = fork()) == 0){//child
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }
    while(1){
        printf("father proc is doing some thing!\n");
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

方案二:在父进程中将17号信号进行忽略

编写脚本捕捉我们的进程

while :;do ps -axj|head -1 &&ps axj|grep signal ;sleep 1;done
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;

int main()
{
    signal(SIGCHLD,SIG_IGN);//手动设置对子进程进行忽略
    if(fork()==0)
    {
        cout<<"chils: "<<getpid()<<endl;
        sleep(5);
        exit(0);
    }
    while(true)
    {
        cout<<"parent: "<<getpid()<<" 执行我自己的任务!"<<endl;
        sleep(1);
    }
}

这样我们就不会有僵尸进程了
在这里插入图片描述

但是我们默认的第17号信号不也是将其忽略嘛?

在这里插入图片描述

但是操作系统默认也是忽略呀?
这个忽略跟我们手动进行忽略属于两个不同的级别。
我们操作系统级别的忽略就是默认的动作,该变成僵尸进程就变成僵尸进程,
操作系统并不知道你真的是不是要将其回收,所以就将子进程搁那里了,变成僵尸进程了。

如果我们自己设置忽略,就是用户告诉操作系统将其忽略,也就是直接告诉操作系统,
我忽略了,运行完毕你就直接回收吧。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

桜キャンドル淵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值