Linux 信号

信号的引入

生活中我们有很多和信号有关的例子,比如说,你在家点了个外卖,当外卖小哥到你家门口的时候,你正在打游戏,快递小哥给你敲门,相当于向你传递了一个信号,而你本身就是具有处理这种信号的能力的,但你不一定要立刻去拿,这就是异步性,因为此时的你正在打游戏,很忙,抽不开身,所以收到这个信号会暂时不处理,而当你拿到外卖后,又可以有三种选择:1.执行默认动作,把外卖吃掉 2.忽略,继续打游戏 3.执行自定义行为,把外卖送给别人吃。

操作系统层面的信号

信号是给进程发送的,而进程具有处理信号的能力。进程不仅能够识别对应的信号,也能根据信号做出相应的行为

进程具有异步性,进程是以不可预知的速度进行推进的,所以当进程收到一个信号时,可能此时正在处理很重要的事情,不能马上处理信号,但是会先记录下这个信号,等做完该做的事情了再进行处理,而在处理时有三种选择:1. 执行默认行为 2.忽略 3. 执行自定义行为

进程是如何记住信号的呢?

进程的PCB中有一个 uint_32 sig;的数据,本质上是一个32位的位图,每个比特位对应有没有信号产生,而比特位的位置是对应信号的编号。

而只有操作系统有权利对该数据结构进行修改,也就是说无论信号怎么产生,都是操作系统帮我们进行设置的。

产生信号的方式

  1. 键盘产生,操作系统进行发送信号
  2. 系统调用
  3. 软件条件
  4. 硬件异常产生信号

在这里插入图片描述
kill -l 可以查看所有的信号

signal函数可以用于进行将对应的信号执行自定义行为
在这里插入图片描述
下面做一个实验:

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

void handler(int signo)
{
    cout << "我收到了一个信号 :" << signo << endl;

}

int main()
{   

    for(int sig = 1;sig<=31;sig++)
    {
        signal(sig,handler);
    }


    while(1)
    {
        cout << "这是一个正在运行的进程:" << getpid() << endl;
        sleep(5);

    }


    return 0;
}

执行下面的代码

在这里插入图片描述
*我们依次按下 ctrl+c ctrl+z ctrl+*

在这里插入图片描述
可以发现分别对应的信号是 SIGINT SIGTSTP SIGQUIT

那么问题来了,我们让所有信号都有了自己的自定义行为,那么岂不是任何信号都无法终止该进程了,该进程永远存在了?

我们输入 kill -9 4550

在这里插入图片描述
可以看到进程被杀掉了,说明该信号是不能被捕获并忽略的,是强制杀人手段,除非该进程在D状态!

下面介绍另一个接口,kill函数
在这里插入图片描述
传入指定的pid,和信号,可以对指定进程发送对应的信号
我们可以自己写一个kill 命令

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


void Usage()
{
    cout << "使用方法: ./mykill 信号编号 pid" << endl; 

}

int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage();
        exit(0);
    }
    pid_t id  =  static_cast<pid_t>(atoi(argv[2]));
    kill(id,atoi(argv[1]));



    return 0;
}

在这里插入图片描述

时钟中断信号
在这里插入图片描述
alarm函数传入一个剩余时间,(单位:秒) 时间到了像进程发送 SIGALARM信号(默认行为是终止进程)。

我们可以写一个程序来比较IO和CPU的速度

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

void handler(int signo)
{
    cout << "我收到了一个信号 :" << signo << endl;
    exit(0);

}

int main()
{   
    signal(SIGALRM,handler);
    alarm(1);
    int cnt = 1;
    while(1)
    {
        cnt ++;
        cout << cnt << endl;
    }
    //看看一秒内cnt能++多少次
   


    return 0;
}

在这里插入图片描述
1秒内加加 75713次左右

如果我们把cout去掉,只放到handler里面呢?

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

int cnt = 1;
void handler(int signo)
{
    cout << "我收到了一个信号 :" << signo <<"cnt:"<< cnt <<endl;
    exit(0);

}

int main()
{   
    signal(SIGALRM,handler);
    alarm(1);
   
    while(1)
    {
        cnt ++;
        
    }
    //看看一秒内cnt能++多少次
   


    return 0;
}

在这里插入图片描述
4亿多次,可以看出cpu的速度比IO速度快多少了!

程序崩溃的本质

进程崩溃的本质是收到了异常的信号,从而导致程序的强制结束,我们拿除零错误举例:
当进程执行时发现除零错误时:CPU内的状态寄存器会被设置被有报错,浮点数越界,os会识别CPU的寄存器中的状态发现错误,像对应的进程发送信号。

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


void handler(int signo)
{
    cout << "我收到了一个信号 :" << signo <<endl;
    exit(0);

}

int main()
{   
    
    for(int sig = 1;sig<=31;sig++)
    {
        signal(sig,handler);
    }

    int *p = nullptr;
    *p = 100;

    
   


    return 0;
}

这是一个野指针的错误,我们运行在这里插入图片描述
会发现是11号信号,也就是SIGSEGV 天天见的段错误

我们再试试除零

在这里插入图片描述

8号信号,也就是SIGFPE 浮点数越界

在这里插入图片描述

abort函数,像进程发送 SIGABRT信号 6号信号

阻塞信号

相关概念
  1. 实际执行信号的处理动作称为信号递达(Delivery)
  2. 信号从产生到递达之间的状态,称为信号未决(Pending)
  3. 进程可以选择阻塞(Block)某个信号
  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作
  5. 阻塞与忽略不同,忽略是在递达之后可选的一种处理动作
在内核中的表示

在这里插入图片描述

可以看到在task_struct结构体中,有两个标志位block和pending,他们都是位图结构,block中0表示该比特位对应的信号没有被阻塞,而1表示该比特位对应的信号被阻塞了,pending中0表示该比特位的信号没有产生或者已经处理完,而handler是一个函数指针数组,对应每种信号的处理方式。

拿SIGINT举例子,pending为1 说明SIGINT已经产生,但是还是未决状态,没有递达,而此时block对应的是1,也就是说该信号会被阻塞,暂时不能递达,直到解除阻塞。

sigset_t

未决和阻塞状态都可以用相同的数据类型sigset_t来存储,sigset_t也被称为信号集,这个类型可以表示每个信号的状态。

信号集操作函数

在这里插入图片描述

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

sigprocmask

用于读取或更改进程的信号屏蔽字(阻塞信号集)

在这里插入图片描述

如果oldset参数不为空,则读取进程当前的阻塞信号集,并从oldset传出,如果set不为空,则更改当前进程的阻塞信号集,更改方法由how决定。

在这里插入图片描述

sigpending

在这里插入图片描述

读取当前的信号屏蔽集,通过set传出。

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


void handler(int signo)
{
    cout << "我收到了一个信号 :" << signo <<endl;
    exit(0);

}

void showPending(sigset_t* pendings)
{
    for(int sig = 1;sig <= 31;sig ++)
    {
        if(sigismember(pendings,sig))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }


    }
    cout << endl;
}

int main()
{   
    sigset_t bsignal,oldbsignal;
    sigemptyset(&bsignal);
    sigemptyset(&oldbsignal);


    for(int sig = 1;sig<=31;sig++)
    {   
        sigaddset(&bsignal,sig);
        signal(sig,handler);
    }

    sigprocmask(SIG_SETMASK,&bsignal,&oldbsignal);
    
    sigset_t pendings;
    sigemptyset(&pendings);

    
    int cnt = 0;
    while(1)
    {
        sigemptyset(&pendings);

        sigpending(&pendings);

        showPending(&pendings);

        sleep(2);

        cnt ++ ;
        if(cnt == 10)
        {
            cout << "开始解除对所有信号的block..." << endl;
            sigset_t sigs;
            sigemptyset(&sigs);
            for(int i = 1;i<=31;i++)
            {
                sigaddset(&sigs,i);
            }
            sigprocmask(SIG_UNBLOCK,&sigs,nullptr);   
            showPending(&pendings);

        }




    }
   


    return 0;
}

core dump

core dump就是所谓的 信息转储,当进程在运行过程中发生异常而终止,会把上下文数据core dump到磁盘上,便于调试。 也就是在进程等待waitpid接口中,status参数的低16为中的低八位的第八位

在这里插入图片描述
但是一般在云服务器上面 core dump会被关掉。

在这里插入图片描述

ulimit -c 可以查看是否打开了 core dump
ulimit -c unlimited可以打开core dump
ulimit -c 0 可以关闭core dump

我们写一个异常导致coredump
在这里插入图片描述
在这里插入图片描述


处理信号的过程

在这里插入图片描述

sigaction 函数

在这里插入图片描述
在这里插入图片描述
结构体的第二个 第四个 第五个成员我们可以不用管,和实时信号有关(sa_flags设置为0)

sa_sigaction:如果设置了SA_SIGINFO标志位,则会使用sa_sigaction处理函数,否则使用sa_handler处理函数。
sa_mask:定义一组信号,在调用由sa_handler所定义的处理器程序时将阻塞该组信号,不允许它们中断此处理器程序的执行,只有在调用handler方法的时候才会阻塞
sa_flags:位掩码,指定用于控制信号处理过程的各种选项。
SA_NODEFER:捕获该信号时,不会在执行处理器程序时将该信号自动添加到进程掩码中。
SA_ONSTACK:针对此信号调用处理器函数时,使用了由sigaltstack()安装的备选栈。
SA_RESETHAND:当捕获该信号时,会在调用处理器函数之前将信号处置重置为默认值(即SIG_IGN)。
SA_SIGINFO:调用信号处理器程序时携带了额外参数,其中提供了关于信号的深入信息
SA_RESTART:执行信号处理后自动重启动先前中断的系统调用

#include<iostream>
using namespace std;
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
void handler(int signo)
{
    cout << "我是一个进程,我收到了一个信号:" << signo << endl;
    sleep(20);
    

}



int main()
{   
    //signal(2,handler);
   // signal(3,handler);
    //signal(4,handler);
    //signal(5,handler);




    struct sigaction act ,oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,3);
    sigaction(2,&act,&oact);
    
    while(1)
    {
        cout << "running" << endl;
        sleep(4);
    }



    return 0;
}
#include<iostream>
using namespace std;
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
void handler(int signo)
{
    cout << "我是一个进程,我收到了一个信号:" << signo << endl;
    sigset_t pendings;
    while(true)
    {
        cout << "." << endl;
        sigpending(&pendings);
        for(int i = 1;i<=31;i++)
        {
            if(sigismember(&pendings,i))
            {
                cout << "1" ;
            }
            else
            {
                cout << "0";
            }
            

        }
        cout << endl;
        sleep(3);
    }


}





int main()
{   
    //signal(2,handler);
   // signal(3,handler);
    //signal(4,handler);
    //signal(5,handler);




    struct sigaction act ,oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,3);
    sigaction(2,&act,&oact);
    
    while(1)
    {
        cout << "running" << endl;
        sleep(4);
    }



    return 0;
}


观察上述代码,运行会发现,我们如果多次按ctrl+c 也就是发送二号信号的时候,会自动阻塞二号信号,直到当前二号信号的handler函数处理完毕,但不阻塞其他信号

在这里插入图片描述


可重入函数

在这里插入图片描述
考虑这样一个场景,当我们在进行链表头插时,调用insert函数进行头插,而此时进程突然收到一个信号,并且该信号被捕捉,调用处理信号的自定义方法,而在自定义方法中又有一个链表的insert方法被调用,此时又头插了一个node2,信号处理结束,回到原进程继续insert node1,此时问题出现了,node2变成了野指针。

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

在这里插入图片描述


volatile

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

int flags = 0;

void handler(int signo)
{
    flags = 1;
    cout << "收到信号,并更改flags:"<< signo << endl;

}

int main()
{   
    signal(2,handler);
    while(!flags);
    printf("进程exit\n");



    return 0;
}

观察上述代码,按理来说程序被运行起来会卡在while循环处,因为flags的值一直为0,而收到2号信号后,flags值改变,循环结束,可以结束进程,但如果我们编译的时候把优化等级提高会怎样呢?
在这里插入图片描述
发送二号信号但是进程却无法结束,这是为什么呢???

原因是:当优化等级提升后,编译器为了优化,会“自作聪明”,不会每次都去内存中取flags的值,而是直接把flags优化到了cpu的寄存器中,这样即使收到信号后,在内存中更改了flags的值,cpu也不会察觉了,这时候 volatile关键字就派上用场了,volatile可以让cpu强制从内存中取数据
在这里插入图片描述


子进程退出会给父进程发送信号

子进程退出时会给父进程发送17号信号,代码验证:

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


void handler(int signo)
{
   
   cout << "我是父进程,我确实收到了子进程给我的信号:" << signo << endl;

}

int main()
{   
    signal(SIGCHLD,handler);
    pid_t id = fork();
    if(id == 0)
    {
        while(1)
        {
            cout << "我是子进程:" << getpid() << endl;
            sleep(2);
        }
        exit(0);
    }
    else
    {
        while(true)
        {
            cout << "我是父进程" << getpid() << endl;
            sleep(2);
        }

    }



    return 0;
}

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值