Linux知识点 -- 进程信号(二)

Linux知识点 – 进程信号(二)


一、信号保存

1.相关概念

  • 信号递达(Delivery):实际执行信号的处理动作;

  • 信号未决(Pedning):信号从产生到递达之间的状态;信号未决就是进程收到了一个信号,但是未处理,就是临时保存到了进程PCB中的对应的位图中;

  • 进程可以选择阻塞(block)某个信号;

  • 被阻塞的信号产生时将保持在未决状态,直到进程解决对此信号的阻塞,才执行递达的动作;

  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在抵达之后可选的一种处理动作;

  • 在进程PCB内部有3张表:
    在这里插入图片描述
    其中,pending就是信号未决的位图,进程在收到一个信号后,会将pending表中相应的位置位;
    handler是函数指针数组 – 数组下标对应信号的编号,就是对应信号的处理方式;signal自定义捕捉就是将信号对应的方法填入handler表;

    在这里插入图片描述
    也可以设置信号的忽略和默认;IGN是忽略;DFL是默认;
    block表是阻塞表,结构和pending一摸一样,代表的含义是对应的信号是否被阻塞;

  • 信号的处理过程:
    进程在接受一个信号后,会将pending表中相应的位置位,然后先去block表中查看该进程是否被阻塞,如果被阻塞,就不做任何动作,如果没有阻塞,再去handler表中查询处理方法;

2.信号保存的相关接口

(1)语言会为我们提供.h.hpp和语言的自定义类型;
同时,操作系统也会给我们提供.h和自定义类型;

(2)OS向我们提供了接口,一定要提供相对应的类型;
语言提供了访问系统调用的接口,也一定会提供相对应的类型;

  • sigset_t类型:
    未决和阻塞标志可以使用相同的数据类型(位图),sigset_t称为信号集,这个类型可以表示每个信号的有效或无效状态;
    在阻塞信号集中有效和无效的含义是该信号是否被阻塞,阻塞信号集也叫做信号屏蔽字
    而在未决信号集中有效和无效的含义是该信号是否处于未决状态;

    注:
    sigset_t不允许用户自己进行位操作,OS为我们提供了对应的操作方法;
    sigset_t使用者可以直接使用该类型,和用内置类型、自定义类型没有任何差别;
    sigset_t一定需要对应的系统接口,来完成对应的功能,其中系统接口需要的参数,可能就包含了sigset_t定义的变量或者对象;

    扫描二维码关注公众号,回复: 16208812 查看本文章
  • OS提供的对sigset_t操作的接口:
    在这里插入图片描述
    分别是:
    全部位清0;
    全部位置1;
    某个信号置位;
    某个信号复位;
    判断信号是否存在;

    在这里插入图片描述
    sigpending函数获取当前调用进程的pending信号集;
    set是输出型参数;
    成功返回0,失败返回-1;

    在这里插入图片描述
    sigprocmask函数检查并更改block信号集;
    how参数:
    在这里插入图片描述
    set:根据how的不同的宏,有不同的功能;
    oldset:输出型参数,返回老的信号屏蔽字,不需要可以传空指针;

3.对所有的信号都进行自定义捕捉

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

using namespace std;


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


int main()
{
    
    
    for(int i = 1; i <= 31; i++)
    {
    
    
        signal(i, catchSig);
    }

    while(true)
    {
    
    
        sleep(1);
    }
    return 0;
}

运行结果:
在这里插入图片描述
可以发现,其他信号都被自定义捕捉了,只有9号信号杀死了该进程,因为9号信号是不能被捕捉的

4.将2号信号block,并打印pending信号集

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

using namespace std;

static void showPending(sigset_t &pending)
{
    
    
    for(int sig = 1; sig <= 31; sig++)
    {
    
    
        if(sigismember(&pending, sig))
        {
    
    
            cout << "1";
        }
        else
        {
    
    
            cout << "0";
        }
    }
    cout << endl;
}

int main()
{
    
    
    //1.定义信号集对象
    sigset_t bset, obset;
    sigset_t pending;
    //2.初始化
    sigemptyset(&bset);
    sigemptyset(&obset);
    sigemptyset(&pending);
    //3.添加要进行屏蔽的信号
    sigaddset(&bset, 2);
    //4.设置set到内核中对应的进程内部
    int n = sigprocmask(SIG_BLOCK, &bset, &obset);
    assert(n == 0);
    (void)n;

    cout << "block 2号信号成功 " << endl;

    //5.重复打印当前进程的pending信号集
    while(true)
    {
    
    
        //获取当前进程的pending信号集
        sigpending(&pending);
        //显示当前进程的pending信号集
        showPending(pending);
        sleep(1);
    }

    return 0;
}

运行结果:
在这里插入图片描述
在这里插入图片描述
当发送了2号信号后,pending表中对应的位置1了,2号信号是被阻塞了,应该一直在pending表中,无法被递达;

在一定时间后恢复2号信号的block

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

using namespace std;

static void showPending(sigset_t &pending)
{
    
    
    for(int sig = 1; sig <= 31; sig++)
    {
    
    
        if(sigismember(&pending, sig))
        {
    
    
            cout << "1";
        }
        else
        {
    
    
            cout << "0";
        }
    }
    cout << endl;
}

int main()
{
    
    
    //1.定义信号集对象
    sigset_t bset, obset;
    sigset_t pending;
    //2.初始化
    sigemptyset(&bset);
    sigemptyset(&obset);
    sigemptyset(&pending);
    //3.添加要进行屏蔽的信号
    sigaddset(&bset, 2);
    //4.设置set到内核中对应的进程内部
    int n = sigprocmask(SIG_BLOCK, &bset, &obset);
    assert(n == 0);
    (void)n;

    cout << "block 2号信号成功 " << endl;

    //5.重复打印当前进程的pending信号集
    int count = 0;
    while(true)
    {
    
    
        //获取当前进程的pending信号集
        sigpending(&pending);
        //显示当前进程的pending信号集
        showPending(pending);
        sleep(1);
        count++;
        if(count == 20)
        {
    
    
            int n = sigprocmask(SIG_SETMASK, &obset, nullptr);//将原来的信号集附上去
            assert(n == 0);
            (void)n;
            cout << "接触对2号信号的block " << endl;
        }
    }

    return 0;
}

运行结果:
在这里插入图片描述
结果是没有看到pending表从1变为0;
默认情况下,回复对于2号信号block的时候,确实会进行递达;
但是2号信号的默认处理动作是终止进程,将进程直接终止;

我们需要对2号信号进行捕捉:

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

using namespace std;


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

static void showPending(sigset_t &pending)
{
    
    
    for(int sig = 1; sig <= 31; sig++)
    {
    
    
        if(sigismember(&pending, sig))
        {
    
    
            cout << "1";
        }
        else
        {
    
    
            cout << "0";
        }
    }
    cout << endl;
}

int main()
{
    
    
    signal(2, catchSig);

    //1.定义信号集对象
    sigset_t bset, obset;
    sigset_t pending;
    //2.初始化
    sigemptyset(&bset);
    sigemptyset(&obset);
    sigemptyset(&pending);
    //3.添加要进行屏蔽的信号
    sigaddset(&bset, 2);
    //4.设置set到内核中对应的进程内部
    int n = sigprocmask(SIG_BLOCK, &bset, &obset);
    assert(n == 0);
    (void)n;

    cout << "block 2号信号成功 " << endl;

    //5.重复打印当前进程的pending信号集
    int count = 0;
    while(true)
    {
    
    
        //获取当前进程的pending信号集
        sigpending(&pending);
        //显示当前进程的pending信号集
        showPending(pending);
        sleep(1);
        count++;
        if(count == 20)
        {
    
    
            int n = sigprocmask(SIG_SETMASK, &obset, nullptr);//将原来的信号集附上去
            assert(n == 0);
            (void)n;
            cout << "接触对2号信号的block " << endl;
        }
    }

    return 0;
}

在这里插入图片描述
注:
没有一个接口时用来设置pending位图的,这是因为所有信号的发送方式,都是修改pending位图的过程;

5.将所有信号都block

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

using namespace std;

static void showPending(sigset_t &pending)
{
    
    
    for (int sig = 1; sig <= 31; sig++)
    {
    
    
        if (sigismember(&pending, sig))
        {
    
    
            cout << "1";
        }
        else
        {
    
    
            cout << "0";
        }
    }
    cout << endl;
}

static void blockSig(int sig)
{
    
    
    sigset_t bset;
    sigemptyset(&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;
    while(true)
    {
    
    
        sigpending(&pending);
        showPending(pending);
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
当发到9号信号的时候,进程停止,9号信号是不能被屏蔽的;
在这里插入图片描述
跳过9号信号:
在这里插入图片描述
19号也是无法屏蔽
在这里插入图片描述

二、处理信号

1.信号处理的时机

  • 信号产生之后,可能无法被立即处理,要在合适的时候处理;
  • 因为信号的相关数据字段都是在进程PCB内部,这属于内核范畴,进程在运行时会从内核范畴 -> 内核状态 -> 用户态 -> 内核状态 -> 内核范畴;
    在内核态中,从内核态返回用户态的时候,进行信号的检测和处理
  • 当我们进行系统调用的时候,比如缺陷异常等,会进入内核态;int 80是一个系统中断语句,可以陷入内核;
  • 用户态是一个受管控的状态,内核态是一个操作系统执行自己代码的一个状态,具备非常高的优先级;
  • CPU的寄存器是由两套的,一套用户可见,另一套不可见,CPU自用;
  • CR3表示当前CPU的执行权限,1表示内核,3表示用户;
  • 在进程地址空间中,不光有用户地址空间,还有内核地址空间,内核地址空间使用的是内核级的页表,该页表是整个OS只有一份的,能够被所有的进程看到,因此所有进程看到的都是一个操作系统;
    在这里插入图片描述
    当我们进程需要调用系统接口时,就跳转到进程的内核地址空间,根据内核级页表,在内存中找到系统调用的相关方法;
  • 当我们有权限进入内核态时,进程使用的页表就是内核级页表了,就能够访问 OS的方法了,这也就意味着进程进入了内核态,可以处理信号了;

2.信号处理的流程

在这里插入图片描述

  • 注意:
    (1)在第二步时,进程在内核态处理完成系统任务后,会在重回用户态的时候进行信号的检测和处理;
    (2)在第三步检测到信号,并处理时,如果信号的处理方式时系统默认方式,就直接在内核态处理了,然后返回用户态的执行流继续执行;如果信号的处理方式是用户自定义的,就需要返回用户态去执行相应的方法;这时进程的状态时用户态,能够执行自定义信号处理,但是系统不会去在内核态执行用户代码,因为涉及到系统安全问题;
    (3)在第四步返回用户态执行信号处理后,进程会再次进入内核态,从内核态在返回用户态进程中断处继续执行;
    (4)一共四次状态切换;

3.sigaction

在这里插入图片描述

  • 参数:
    signum:信号编号;
    act:信号处理动作;struct sigaction是一个结构体,里面包含用户自定义的信号处理方式的函数指针等数据;
    在这里插入图片描述
    oldact:信号过去的处理方式;
#include<iostream>
#include<signal.h>
#include<unistd.h>

using namespace std;

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

int main()
{
    
    
    //内核数据类型,用户栈定义的
    struct sigaction act, oact;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = handler;

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

    while(true) sleep(1);

    return 0;
}

运行结果:
在这里插入图片描述
捕获2号信号并执行自定义处理方式;

  • 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的的信号屏蔽字,这样就保证了在处理某个信号时,如果该信号再次产生,那么它就会被阻塞到当前信号处理结束为止;如果在调用信号处理函数时,还希望屏蔽除当前信号的其他信号,就可以使用sigaction函数的sa_mask参数,来指定希望额外屏蔽的信号;
#include<iostream>
#include<signal.h>
#include<unistd.h>

using namespace std;


static 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()
{
    
    
    //内核数据类型,用户栈定义的
    struct sigaction act, oact;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = handler;

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

    while(true) sleep(1);

    return 0;
}

运行结果:
在这里插入图片描述
第二次获取二号信号的时候,就进行了屏蔽;

如果需要同时添加对其他信号的屏蔽:

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

using namespace std;


static 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()
{
    
    
    //内核数据类型,用户栈定义的
    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);

    while(true) sleep(1);

    return 0;
}

运行结果:
在这里插入图片描述

三、可重入函数

在这里插入图片描述
在main函数调用insert方法时,信号来了,调用handler,handler也去调用insert,那么像这样被多个执行流调用insert就叫做函数重入
在这里插入图片描述
函数重入出问题的叫做不可重入函数;
不出问题的叫做可重入函数;

函数的可重入性是函数的一种特征,我们目前使用的大多数函数,都是不可重入的;

四、volatile关键字

当接收到2号信号时,将flag置1,进程退出;
在这里插入图片描述
运行结果:
在这里插入图片描述
如果我们更改编译选项,让g++对代码作出一定的优化:
在这里插入图片描述
运行结果:
在这里插入图片描述
现在进程就无法退出了,但是flag还是变成了1;

这是因为在优化了代码之后,后面的语句没有更改flag,在后面检测flag的时候,就不访问内存中的flag了,而是检测寄存器edx中的flag;而寄存器中的flag是第一次读取的0,因此进程就不会退出了;
在变量定义的时候加上volatile关键字:
在这里插入图片描述
这个关键字的作用是**保持变量在内存中的可见性;**
运行结果:
在这里插入图片描述
注:优化是在编译时就完成的;

五、SIGCHILD信号

在这里插入图片描述
在这里插入图片描述
如果我们需要等待子进程退出,10个子进程5个退出,后面的信号还需要进行wait检测是否退出;
因为5个进程都发送了sigchild信号,但是OS只能收到一个;
这时主进程只能阻塞等待该子进程退出;
我们也可使用vector保存进程pid,来进行非阻塞遍历所有进程,这样不会被阻塞;
也可以在waitpid时候传入-1, 就可以等待任意一个退出的进程,进程也不会被阻塞;

  • 如果我们不想等待子进程,还想在子进程退出之后,自动释放僵尸子进程:可以设置对SIGCHILD信号的忽略
    在这里插入图片描述
    运行结果:
    子进程退出后自动回收僵尸子进程;
    在这里插入图片描述
    sigchild的默认动作就是忽略,但是为什么要再加一个忽略呢?
    因为这两个忽略时不同等级的,OS的忽略就是默认动作,不会回收子进程,会形成僵尸进程;
    而自己设置的忽略,告诉OS不光要忽略子进程,还要回收资源;

猜你喜欢

转载自blog.csdn.net/kissland96166/article/details/132201568