信号


什么是信号

用户或者操作系统通过发送一定的信号,通知进程,让进程做出相应的处理,这就是信号

进程要处理信号,必须要具有识别他的能力

信号产生之后,进程可以找个时间进行处理,不需要立即进行处理——那么此时我们就要记录下来这个信号——记录这个信号我们可以用位图结构

常见的信号:

进程信号_自定义


1到31为普通信号

34到64为实时信号

每个信号其实就是一个宏,它有自己对应的值

进程信号_自定义_02


这里的Core 为核心转储

信号如何产生


键盘产生

进程信号_用户态_03


核心转储

我们在学习进程等待的时候,当一个进程被杀死的时候,第8位为core dump标志,为是发生核心转储。

进程信号_自定义_04


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

用命令ulimit -a进行查看,可以用ulimit -c 数字进行修改

它会产生一个文件,文件名为core.进程pid主要是为了调试

进程信号_自定义_05


核心转储演示:

int main()
{
    pid_t pid=fork();
    if(pid==0)
    {
        int i=0;

        while(true)
        {
            cout<<"我是子进程:"<<getpid()<<endl;
            sleep(1);

            if(i==10)
            {
                int a=1;
                a/=0;
            }
            ++i;
        }
    }
    int stat;
    waitpid(pid,&stat,0);
    //提取code dump
    cout<<"我是父进程:"<<getpid()<<"是否发生核心转储:"<<((stat>>7)&1)<<endl;
    return 0;
}

就会发现有这个,除0,发送的8号信号,浮点错误,行为会发送核心转储。

进程信号_自定义_06


调用系统函数向进程发信号

当我们在命令行上输入kill命令的时候,其实是调用的kill函数实现的,kill函数可以向指定进程发送信号。

进程信号_用户态_07


成功返回0,失败返回-1。

模拟一个像命令一样的程序

int main(int argc, char *argv[])
{
    if(argc!=3)
    {
        cout<<"命令输入有误"<<endl;
        exit(1);
    }
    kill(atoi(argv[2]),atoi(argv[1]));
    return 0;
}

还有raise函数

进程信号_#include_08


它的作用就是给当前进程发送信号

还有一个abort函数

进程信号_自定义_09


就像exit函数一样,abort函数其实发送的是6号信号。

如何理解系统调用接口向进程发送信号:


由软件条件产生信号

在管道中,如果读端不读而且关闭,写端一直写,那么就会被os会自动终止写端进程,发送的是SIGPIPE信号
下面就进行验证:

int main()
{
    int fd[2];
    int ret=pipe(fd);
    if(ret!=0)
    exit(-1);
    pid_t pid=fork();

    //child
    if (pid == 0)
    {
        close(fd[0]);
        for (int i = 0; i < 100000; i++)
            write(fd[1], "h", 1);
        exit(0);
    }

    close(fd[0]);
    close(fd[1]);
    int stat;
    waitpid(pid,&stat,0);
    cout<<"退出信号:"<< (stat&0x0000007f)<<endl;
    return 0;
}

看运行的结果:

[lighthouse@VM-4-8-centos 信号]$ ./signal退出信号:13

为什么是父进程读取呢?如果是父进程写,子进程读。因为子进程把读端关闭,父进程写没有意义,就会把父进程终止,那么我们无法拿到信号,而反过来就可以的。

还有一种由软件异常产生的信号,就是时钟信号,当时间到了,系统发送时钟信号SIGALRM14号信号。

该函数

进程信号_用户态_10


这个是设置的是秒级别的定时器。

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

  1. os先识别某种软件条件触发或者不满足
  2. os构建信号,发送给指定的进程

硬件异常产生信号

除0发生的异常以及野指针、越界问题导致的硬件异常。
除0发送的是8号信号,越界、野指针发送的是11号信号

在cpu中有一个标志寄存器,当发生除0错误的时候,在寄存器中会进行标记,os会自动识别检测。当识别出有问题的时候,os会向当前进程发送信号,进程在合适的时候,进行处理

如何理解野指针、越界我问题导致的硬件异常?

  1. 都必须通过地址,找出目标位置
  2. 语言上面的地址,全部都是虚拟地址
  3. 将虚拟地址转换成物理地址,需要用到页表+MMU(这是一个硬件,分页内存管理单元)
  4. 转换的时候,MMU会发送异常,导致报错。

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

信号的处理

信号处理的常见方式:

  1. 默认的,进程自带的,也就是程序员写好的逻辑
  2. 忽略
  3. 自定义动作(捕捉信号)

对于自定义动作的演示

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

void handler(int signum)
{
    cout<<"获得一个信号:"<<signum<<endl;
}

int main()
{
    for(int i=1;i<32;i++)
    {
        signal(i,handler); 
    }
    

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

看似是把所有的信号都自定义捕捉了,但是当我们发生9号信号的,依然可以终止进程,os是不可能让一个进程无法终止的。


阻塞信号


一些常见的概念:

信号递达:执行信号的处理动作
信号未决(Pending):信号产生到递达之间的状态
阻塞:阻塞就是进销存阻塞该信号——阻塞信号集也叫做信号屏蔽字

当一个信号被阻塞的时候,当产生这个信号的时候,那么这个信号一直处于未决状态,直到进程解除对此信号的阻塞,才能执行递达的动作。阻塞和忽略是不同的,阻塞之后信号就不会被递达,而忽略是递达之后的一种处理动作


信号在内核中的样子:

进程信号_用户态_11


在block中,0表示没有阻塞,1表示阻塞

在pending中,0表示没有接收到信号,1表示接受到信号

hander表示处理动作——忽略,默认,自定义


sigset_t类型

该类型不允许用户自己进行位的操作,os会给我们提供对应的操作位图的方法sigset_t一定需要对应的系统接口,来完成对应发功能。

像block,pending这样的信号我们用sigset_t来存储,sigset_t称为信号集,他的本质也就是一个位图结构。
对于这个类型的结构我们要学会怎么使用它,我们不需要关注它内部是怎么实现的。
下面是操作它的几个函数:

#include <signal.h>
int sigemptyset(sigset_t *set);//初始化,都清0
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);//检测信号集中是否包含该信号

上面这些函数虽然操作信号了,但是并没有在系统的角度设计信号,下面介绍几个系统接口来设置信号。
sigprocmask

#include <signal.h>
int sigpromask(int how,const sigset_t* set,sigset_t* oset);

how的参数选择:SIG_BLOCK,把set里面的信号写入到系统中进行屏蔽SIG_UNBLOCK,把set里面的信号从系统中解除屏蔽SIG_SETMASK,把系统的屏蔽字设置成set指向的set为我们设置的信号屏蔽字,oset为我们旧的信号屏蔽字。对于返回值,成功为0,失败为-1。

sigpending

#include <signal.h>
int sigpending(sigset_t *set);

函数 sigpending 用于获取当前被阻塞且未决的信号集。这个函数可以用来查询那些在当前进程中被阻塞但尚未处理的信号。

进程信号_#include_12

返回值:成功返回0,失败返回-1。


信号的保存

进程信号_自定义_13


脚本发送信号

信号发送的本质就是:OS向目标进程写信号,OS直接修改pcb中的指定位图结构,完成信号发送的过程

进程信号_用户态_14


进程信号_#include_15


进程信号_自定义_16


捕捉信号

信号产生之后,信号可能无法被立即处理,在合适的时候会被处理。

  1. 在合适的时候(是什么时候?)
  2. 信号处理的整个流程

下面对这两个问题进行回答:

  1. 与信号相关的数据字段都是在进程的PCB内部的,当接收到一个信号的时候,会从用户态切换到内核态,进行修改内核中有关信号的数据结构,之后会从内核态再次切换到用户态,在切换的时候(内核->用户),就会对信号进行检测和处理!因为此时已经处理好系统的各种逻辑了。
  2. 在CPU中也有2套,1套是可见的,一套是不可见的,用来自用的,这个自用的里面,其中有有关CR3的寄存器表示当前cpu的执行权限——1为内核,3为用户

进程信号_自定义_17


内核是如何实现信号捕捉的?

  • 捕捉信号

如果信号的处理动作是用户自己定义的,那么在信号递达的时候调用这个函数,这就称为捕捉信号。

信号捕捉的一个流程:

  1. 进程在执行的时候,由于中断、异常或者系统调用等原因进入内核开始执行代码
  2. 内核开始执行自己的代码,当处理完成的时候,准备返回用户态的时候
  3. 在返回用户态的时候,就会进行信号的处理,检查pending位图中,是否存在信号,如果不存在信号,之间返回用户态;如果存在,在去看它的block位图是否存在被阻塞,如果阻塞,那么也直接返回用户态;如果不阻塞,就进行信号的处理,信号处理有3种——忽略,默认,自定义捕捉。如果为忽略,把pending位图置成0,然后直接进行返回到用户层;如果为默认,把pending位图置成0,执行它的默认动作,如果有核心转储,就进行核心转储,然后杀掉进程;如果是自定义①,

①:因为自定义的代码在用户态,那么内核态可以执行用户态的代码吗?——是可以的,但是我们不能去执行,因为用户写的代码可以存在非法的情况,必须要切换到用户态去执行自定义捕捉的代码。在执行自定义代码的时候,在返回的时候会执行特殊的系统调用,然后再次进入内核。最后再从内核返回用户,执行用户的代码,在返回的时候还要在做检查,如果有其他信号,就按照上面的方式继续进行信号的处理;如果没有可以返回用户态了。

上面的信号处理的逻辑可以用下面的简图来解释:

进程信号_#include_18


sigaction函数

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

sigaction函数可以读取和修改与指定信号相关的处理动作。调用成功返回0,失败返回-1。
signo是指定的信号编号。 struct sigaction是一个结构体类型,用于定义信号处理程序的属性和行为 下面是这个结构体的内容

struct sigaction {
   void     (*sa_handler)(int);//信号处理动作
   void     (*sa_sigaction)(int, siginfo_t *, void *);
   sigset_t   sa_mask;//信号屏蔽字
   int        sa_flags;
   void     (*sa_restorer)(void);
};

在这里,我们只需要了解void (*sa_handler)(int);sigset_t sa_mas即可,act为需要修改的信号动作,oact为旧的,可以联想sigpromask

处理信号的时候,执行自定义动作,如果在处理信号期间,又来了同样的信号,os将怎么办呢?

当某个信号的处理函数被调用时,内核自动将当前信号加入到进程的信号屏蔽字,处理完成后解除。这样就可以保证在处理该信号的时候,再次来相同的信号,就会被阻塞到当前处理结束为止。

验证一下:

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

void printsig(sigset_t &set)
{
    for(int i=1;i<=31;i++)
    {
        if(sigismember(&set,i))
        cout<<1;
        else
        cout<<0;
    }
    cout<<endl;
}
void handler(int sig)
{
    cout<<"这是"<<sig<<"号信号"<<endl;
    sigset_t pending;
    int c=0;
    while(true)
    {
        sigpending(&pending);
        printsig(pending);
        c++;
        if(c==10)
        break;
        sleep(1);
    }


}
int main()
{
    struct sigaction act,oact;
    act.sa_handler=handler;
    sigemptyset(&act.sa_mask);

    sigaddset(&act.sa_mask,2);
    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);
    sigaddset(&act.sa_mask,5);
    //把2号设置到当前进程的pcb中,其中2号进程为自定义捕捉动作;3,4,5为默认的动作
    sigaction(2,&act,&oact);

    cout<<"进程pid:"<<getpid()<<endl;
    while(true)
    {
        ;
    }
    return 0;
}


可重入函数

什么叫做重入函数:同一个函数被多个执行流进入,那么这个函数就是重入函数
可以让多个执行流进入的重入函数叫做可重入函数;不可以让多个执行流进入的,叫做不可重入函数。
为什么不可以让多个执行流进入,就是因为不出现不好的结果。
可重入、不可重入是函数的一种特征。
比如一个函数实现链表的头插,那么这个函数就是不可重入函数


volatile

c语言中的关键字volatile,它的作用就是保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在内存级别进行操作。
下面进行验证一下:

int flag=0;

void hendler(int sig)
{
    cout<<"flag的值进行进行修改"<<flag;
    flag=1;
    cout<<"->"<<flag<<endl;
}


int main()
{
    signal(2,hendler);
    while(!flag);

    cout<<"flag的最终值:"<<flag<<endl;
    return 0;
}

当我们对上面的代码进行普通的编译g++ -o $@ $^ -std=c++11它的运行结果如下:

[ml@VM-4-8-centos 信号]$ ./mysignal 
^Cflag的值进行进行修改0->1
flag的最终值:1

当我们加入优化选项的时候g++ -o $@ $^ -std=c++11 -O3它的运行结果如下:

[ml@VM-4-8-centos 信号]$ ./mysignal 
^Cflag的值进行进行修改0->1
^Cflag的值进行进行修改1->1
^Cflag的值进行进行修改1->1

接收2号信号的时候,会陷入死循环
为什么会出现这种情况呢?

当加入优化选项之后,在循环中flag的值会存放在寄存器中,而改变flag值的时候,直接改的是内存中flag的值,寄存器中的值是木有改变的。所以会一直进入循环。下面我们对flag加上关键字volatile的时候,对变量使用会从内存中进行找。下面是代码改变的部分volatile int flag=0;,还是按照刚才的优化进行编译。结果如下:

[ml@VM-4-8-centos 信号]$ ./mysignal 
^Cflag的值进行进行修改0->1
flag的最终值:1


SIGCHLD信号

当子进程退出或者被终止的时候,会给父进程发送SIGCHLD信号,该信号的动作是忽略——让子进程陷入僵尸状态。父进程可以自己定义SIGCHLD信号的处理函数,如果自己设置忽略,就会让子进程退出,和系统的忽略还是不一样的。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6