【1++的Linux】之信号(一)

👍作者主页:进击的1++
🤩 专栏链接:【1++的Linux】

一,关于信号

1. 什么是信号

在我们的生活中存在许多的信号:红绿灯,闹钟等都是信号,这些信号的本质都是告诉我们对应的信号发送后,我们要有相应的动作应对。
Linux信号的本质也是如此:是一种通知机制,用户或者OS发送给进程信号,通知进程某件事已经发生,你要进行后续的处理

2. 信号和进程

就像红绿灯一样,我们要能够处理信号,必须具备能识别信号的能力。那么进程也是一样的,在处理信号前要能够识别相应信号是什么意思。
进程怎么能够识别信号呢?------通过程序员。
信号的产生是随机的,进程可能在忙 自己的事情,暂时没办法处理信号,那么就会在后续才进行处理,并不是要立刻进行处理。并且进程会临时记录下信号,方便后续的处理。
信号的产生一般而言对于进程是异步的。

3. 信号是怎么产生的

我们使用xshell时,经常会利用键盘组合键 ctrl+c进行终止程序的操作。
这本质就是向相应的进程发送信号。或者我们可以通过命令直接向对应的进程发送信号也是可以的。

如图就是我们Linux中常用的信号

其中1-31时我们的普通信号 34-64是我们的实时信号。

在这里插入图片描述

对于信号的处理我们有三种处理方式:

默认(进程自带的,已经写好的逻辑 )
忽略
自定义动作(捕捉信号)

组合键是如何变为信号的呢?

键盘的工作原理是:中断。
组合键在程序中已经有相应的解释。因此OS能够对其做出解释,接着OS会查找进程列表,找到前台运行的进程,最后将对应的信号写入该进程的位图结构中,等待该进程去处理。

4. 管理信号

信号是作用于进程的,OS又作为进程的管理者,因此信号最终都是要通过进程发送给进程的。而信号又不止一个,是不是要对信号进行管理?怎么管理?先描述,后组织。因此在每个进程的PCB中都必须要有保存信号的相关数据结构。Linux中有62种信号,我们在使用信号时,在乎的是信号有无。因此我们可以用位图来保存信号。

因此我们也就知道了发送信号的本质是:在OS直接将目标进程PCB中的信号位图中指定位置置为1 。

二,深剖信号的产生

1. 键盘组合建产生信号

我们前面说过ctrl +c 可以终止一个进程,并且其发送的是二号信号,那么该如何证明呢?
在这里插入图片描述
我们上面说过,信号 处理有三种方式,该函数可以修改我们信号对应的默认处理方式。

第一个参数:是int类型,刚刚查看的信号我们除了可以看见信号名称外还能看到编号,在系统中,信号编号就是整数,每一个信号都是被#define定义出来的,我们既可以使用信号名又可以使用数字。这里的参数就是信号编号。

第二个参数:是返回值为void,参数为int的函数指针

signal的返回值是指向之前的信号处理程序的指针。

下面我们来看代码:

#include<iostream>
#include<unistd.h>
#include<signal.h>

void func(int signnum)
{
    std::cout<<"我是 "<<getpid()<<"我正在处理信号: "<<signnum<<std::endl;
}
int main()
{
    signal(2,func);
    while(true)
    {
        std::cout<<"hellow world"<<std::endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
当我们按下组合键ctrl c 时我们发现其对应的确实是2号信号。

ctrl + \ 也可以终止进程
Ctrl + \ 对应3号
Ctrl + z (暂停)对应20号。

下面我们再来谈谈singal这个函数。
在这里插入图片描述
当我们调用这个函数时,系统并不会立刻执行func这个回调函数,只有接收到信号的时候才会调用,也就是说singal只修改了对信号的处理方法。

若我们通过./mytest + &使得进程在后台运行
在这里插入图片描述

此时我们发现通过组合键就无法发送信号了
此时我们通过命令来直接杀死该进程
在这里插入图片描述
键盘产生的信号,只能用来终止前台进程,也就是说只能用来终止那些阻塞你执行命令的程序。

ps: 9号信号不可以被捕捉(自定义)!

2.核心转储

我们通过man 7 signal就可以查看信号的默认行为

在这里插入图片描述
我们在前面说过:status我们只研究其低十六位:次低八位表示退出状态;最低七位表示退出信号,中间那一位叫做core-dump我们现在只需知道它是用于调试的。
core dump就是我们所说的核心转储.
一般而言云服务器,其默认核心转储功能是关闭的。

core dump标志位代表了是否发生了核心转储
我们来看下面这段代码:

int main()
{
    pid_t pid=fork();
    while(pid==0)
    {
        sleep(1);
        std::cout<<"我是子进程:"<<getpid()<<std::endl;
        int a=10;
        a/=0;
    }

    int status=0;
    waitpid(-1,&status,0);
    std::cout<<"退出信号: "<<(status & 0x7f)<<"是否发生了核心转储:"<<((status>7)&1)<<std::endl;
}

在这里插入图片描述
我们可以看到其标志位显示其发生了核心转储,并且生成了core文件。那么这个文件有什么用呢?
我们可以用waitpid()中的status的次低7位获取到进程退出的信号,知道信号我们就知道了崩溃的原因。除此之外,我们还想知道进程是在哪一行崩溃的。因此通过核心转储生成的该文件中就会有记录。

我们可以使用ulimit -a 进行查看core dump。
通过ulimit -c 来进行设置。

在linux中,当一个进程退出的时候,它的退出码和退出信号都会被设置(正常情况)。

当一个进程异常的时候,进程的退出信号会被设置,表明当前进程退出的原因。

如果必要,OS会设置退出信息中的core dump标志位,并将进程在内存中的数据转储到磁盘当中,方便我们后期调试。

看下面这段代码:

int main()
{
  int a=0;
  a/=0;
  return 0;
}

在这里插入图片描述
我们的云服务器中的core dump默认是关掉的
在这里插入图片描述
打开后其也确实生成了core文件。
在这里插入图片描述
我们加-g选项后生成可执行程序。gbd ./test 进行调式,再输入core-file core.pid 就可以看到奔溃原因和第几行奔溃的了。

我们先让程序出异常,然后在用gdb调试,直接用core-file 命令得到了错误的原因和错误的行数。这种方案我们称为事后调试。

3. 系统调用接口产生信号

在这里插入图片描述
我们可以使用系统调用接口kill来发送命令

参数pid: 进程pid
sig: 发送几号信号
返回值:成功返回0,失败返回-1

下面我们通过系统调用接口来模拟实现一个kill命令。

#include<iostream>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
using namespace std;
int main(int argv,char* argc[])
{
     if(argv!=3)
    {
        cout<<"argv!=3 "<<"proc "<<"sign "<<"who"<<endl;
    }
    
    int sign=atoi(argc[1]);
    int who=atoi(argc[2]);
    kill(who,sign);
    cout<<sign<<" "<<who<<endl;
    return 0;
}

我们用自己模拟实现的kill去杀掉sleep进程。
在这里插入图片描述

除了上述这样的系统调用,我们还有raise , abort等这样的系统调用接口。

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

#include<iostream>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
using namespace std;
int main()
{
   int count=5;
   while(count)
   {
    cout<<"等待:"<<count--<<endl;
    sleep(1);
   }

    cout<<"发送信号"<<endl;
    raise(8);
}

在这里插入图片描述

abort:给自己发送六号信号,若未捕获,则终止当前进程
在这里插入图片描述

int main()
{
   int count=5;
   while(count)
   {
    cout<<"等待:"<<count--<<endl;
    sleep(1);
   }

    cout<<"发送信号"<<endl;
    abort();
}

在这里插入图片描述

如何理解系统调用接口发送信号:用户调用系统接口—》执行OS对应的系统调用代码—》OS提取参数,或者设置特定值—》OS向对应进程写信号—》修改对应进程的信号标记为—》等待进程后续处理----》处理成功。

4. 由软件条件产生信号

我们在前面提到过匿名管道,当读写的任意一端关闭后,都会造成对立的进程退出,下面我们进行测试。

#include<iostream>
#include<unistd.h>
#include<cassert>
#include<cstring>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;
int main()
{
    int pipefd[2];
    int n=pipe(pipefd);
    assert(n!=-1);
    pid_t pid=fork();
        if(pid==0)
        {
            close(pipefd[0]);
            const char* buffer="hellow world";
           while(true)
           {
             int s=write(pipefd[1],buffer,strlen(buffer));
             assert(s!=0);
             sleep(1);
           }
           exit(1);
        }

        char buffer[1024];
        memset(buffer,'\0',1024);
        close(pipefd[1]);
        //close(pipefd[1]);
        while(true)
        {
            int n=read(pipefd[0],buffer,sizeof(buffer)-1);
            if(n>0)
            {
                printf("%s\n",buffer);
            }
            else
            {
                cout<<"退出"<<endl;
                break;

            }

            sleep(1);
            close(pipefd[0]);
        }

        int status;
        waitpid(-1,&status,0);
        close(pipefd[0]);
        cout<<"退出信号"<<(status&(0x7f))<<" "<<"是否核心转储"<<((status>>7)&1)<<endl;

    return 0;
}

在这里插入图片描述

我们发现当关闭父进程中的读端时,OS会向子进程发送13号信号(SIGPIPE),终止子进程。

在这里插入图片描述

设置一个计时器,会延迟的向我们发送一个信号。比如second设置成10,就代表着过10秒后给我们发送一个sigalrm信号也就是14号信号。

这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方 , 某人要小睡一觉 , 设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了 , 还想多睡一会儿 , 于是重新设定闹钟为 15 分钟之后响 ,“ 以前设定的闹钟时间还余下的时间 ” 就是10 分钟。如果 seconds 值为 0, 表示取消以前设定的闹钟 , 函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

void handler(int signum)
{
    cout<<"闹钟响了"<<endl;

}
int main()
{
    signal(14,handler);
    alarm(5);
    while(true)
    {
        cout<<"I am process"<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
若我们取消进程对alarm信号的自定义处理方式,那么其默认处理方式为:
让进程退出。
在这里插入图片描述

我们还可以在对信号的自定义处理方式中再定闹钟进行循环

void handler(int signum)
{
    cout<<"闹钟响了"<<endl;
    alarm(5);

}
int main()
{
    signal(14,handler);
    alarm(5);
    while(true)
    {
        cout<<"I am process"<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

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

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

如何理解由软件条件发信号

OS先检测到某种软件条件出发或者不满足,然后构建相关信号发送给对应的进程。

5. 硬件异常产生信号

如何理解除0错误
根据冯诺依曼原理,我们现代计算机在计算时都得要通过cpu来进行运算。
cpu内部有寄存器,寄存器中也有位图,有对应的状态标记位。OS会在计算完成之后对位图进行检测。若发生除0错误,对应的标记位会被置为1,OS进行检测发现后便会找到当前运行的进程,发送相应的信号。
出现硬件异常,进程不一定会退出,一般默认时退出,但我们可以自定义处理行为,但不退出,我们也做不了什么。

我们来看下面这个例子。

oid handler(int signum)
{
    cout<<"除0错误"<<endl;
    sleep(1);
    //alarm(5);

}
int main()
{
    signal(8,handler);
    int a=1;
    a/=0return 0;
}

在这里插入图片描述
我们发现若是,处理处理错误0信号时,我们自定义处理不退出,便会一直进行处理。
这是因为寄存器中的对应的标记位仍然是1,显示异常。OS会不断的去检测,所以才会一直在处理。

如何理解野指针或指针越界问题?
出现这样的错误,其本质都是访问量非法的地址。
所以何时报错,就是何时能够检查出他们访问的地址是非法的。
首先我们在语言层面上的地址都是虚拟地址,其在通过地址找目标位置的时候,虚拟地址会经过页表+MMU映射到物理内存上,而MMU(硬件)就能够判断其地址是否合法,在遇到非法地址,MMU在转化时,便会报错。
OS检测到后便会发送信号给对应的进程,该进程进行后续的处理。

所有信号都有来源,都是通过OS识别,解释,并发送的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

进击的1++

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

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

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

打赏作者

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

抵扣说明:

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

余额充值