当前位置:   article > 正文

【Linux】【信号】_linux sig = 61

linux sig = 61

一、信号是什么

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在这里插入图片描述

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

3.信号处理的常见方式

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

4.Linux当中的信号

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

kill -l
  • 1

在这里插入图片描述

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

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

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

man 7 signal
  • 1

在这里插入图片描述

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

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
  • 1

在这里插入图片描述
第一个参数:你要对哪一个信号进行捕捉
第二个参数:函数指针。
想一个函数传入另外一个函数的函数指针,就是回调函数。通过回调的方式,修改赌赢的信号捕捉方法。然后我们这个回调函数的参数为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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

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

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

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

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

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

ctrl \
  • 1

在这里插入图片描述
我们同样可以处理我们的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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在这里插入图片描述

2.核心转储

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

man 7 signal
  • 1

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

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

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

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

ulimit -a
  • 1

在这里插入图片描述

ulimit -c 1024
  • 1

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

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
int main()
{
    while(true)
    {
        cout<<"我是一个进程,我正在运行……,Pid:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

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

du -k core.5991
  • 1

在这里插入图片描述

核心转储的意思就是当进程出现某种异常的时候,是否由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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

gdb signal
  • 1

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

core core.6828
  • 1

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

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

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

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

ulimit -c 0
  • 1

为什么生产环境一般要关闭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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

首先我们让一个进程进行睡眠
在这里插入图片描述
然后我们查看这个进程的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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在这里插入图片描述

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在这里插入图片描述

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

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

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

四、由软件条件产生信号

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

1.创建匿名管道
2.让父进程进行读取,子进程进行写入
3.让我们的父进程关闭读端,并且waitpid()等待子进程,子进程只要一直写入就行
4.子进程退出,父进程waitpid拿到子进程的退出status
5.提取出退出信号
  • 1
  • 2
  • 3
  • 4
  • 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;
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

在这里插入图片描述

我们的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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在这里插入图片描述

为什么我们只运行了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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在这里插入图片描述

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

#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

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

#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

在这里插入图片描述

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

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

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

如何理解这里的除0呢?

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

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

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

我们的异常的退出变成了打印错误信号,但是不退出。
但是寄存器中的异常一直没有被解决!那我们的操作系统同会对其进行调度
那么所以会一直给打我们打印错误信号,除非我们将我们的进程退出。
  • 1
  • 2
  • 3
#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在这里插入图片描述

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这里插入图片描述

#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
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在这里插入图片描述

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

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

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

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

六、阻塞信号

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

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

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

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

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

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

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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

函数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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
我们发现我们的9号信号依旧能够杀死我们的进程
9号信号属于管理员信号,是不能够被捕捉的!
  • 1
  • 2

在这里插入图片描述

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

在这里插入图片描述

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

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

编写并且运行上面的程序之后,我们输入下面的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
  • 1

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

这里我们不妨跳过919信号,来查看一下我们别的信号会发生什么情况
编写下面的bash脚本,运行。
  • 1
  • 2
#! /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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在这里插入图片描述

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

七、信号处理

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

信号相关的数据字段都是在进程PCB内部,属于内核的范畴。
内核态 vs 用户态
只有在内核态,从内核态返回用户态的时候,才进行信号检测和处理。
因为在返回的时候,也就是是说我们要做的事情已经处理完成了。

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

在汇编语言上有一个中断编号int 80,内置在我们的系统调用函数中。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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

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

在这里插入图片描述

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

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

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

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

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

用户->内核,为什么?

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

用户如何切换到内核态

用系统调用接口
  • 1

信号捕捉的基本流程

在这里插入图片描述

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

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

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

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

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

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

在这里插入图片描述

信号的操作

这是我们之前使用的

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

sigaction方法

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

在这里插入图片描述

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

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

基本的用法

#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在这里插入图片描述

#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

在这里插入图片描述

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

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

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

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

也就是说正在处理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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

在这里插入图片描述

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

#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

在这里插入图片描述

可重入函数

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

在这里插入图片描述

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

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

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

如果一个函数符合以下条件之一则是不可重入的:
调用了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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在这里插入图片描述

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

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

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

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

如果我们不做优化,我们的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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在这里插入图片描述

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

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

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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在这里插入图片描述

所以我们这里证明了子进程退出会向父进程发送退出信号。
验证是不是父进程接受了信号
  • 1
  • 2
#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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在这里插入图片描述

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

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

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

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

#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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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

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

#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

在这里插入图片描述

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

编写脚本捕捉我们的进程

while :;do ps -axj|head -1 &&ps axj|grep signal ;sleep 1;done
  • 1
#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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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

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

在这里插入图片描述

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

如果我们自己设置忽略,就是用户告诉操作系统将其忽略,也就是直接告诉操作系统,
我忽略了,运行完毕你就直接回收吧。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小舞很执着/article/detail/864838
推荐阅读
相关标签
  

闽ICP备14008679号