【Linux】:信号的保存和信号处理

朋友们、伙计们,我们又见面了,本期来给大家带来信号的保存和信号处理相关代码和知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

C + + 专 栏   :C++

Linux 专 栏  :Linux

​ 

目录

1. 信号的保存

1.1 信号相关概念

1.2 信号的保存 

1.3 处理位图的接口 

2. 信号的处理 

2.1 状态的切换

2.2 信号的处理

2.3 sigaction函数 

3. 信号的其他补充 

3.1 可重入函数

3.2 SIGCHLD信号 


1. 信号的保存

1.1 信号相关概念

  • 实际执行信号的处理动作称为信号递达(Delivery)。

信号递达的方式有三种:

  • ① 信号的默认处理
  • ② 信号的忽略
  • ③ 信号的自定义捕捉

当我们自定义捕捉信号的时候使用的signal接口就是对指定信号进行捕捉,然后去执行我们自定义的方法,下面再来介绍一下两种用法:

  • ① signal(signo, SIG_DFL):对指定信号恢复默认操作;
  • ② signal(signo, SIG_IGN):对指定信号进行忽略(忽略也算做对信号进行处理)。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

1.2 信号的保存 

当信号产生时,我们不一定要立即对信号进行递达,而是在合适的时候进行递达,那么在信号未决时期,我们要有能力将信号保存,所以在进程PCB中会存在三张位图表,用于保存信号:

信号屏蔽字(block表):比特位的位置表示信号的编号、比特位的内容表示是否对特定信号进行屏蔽(阻塞)。

未决位图表(pending表):比特位的位置表示信号编号、比特位的内容表示特定的信号时候被递达。

handler表(函数指针数组):比特位的位置表示信号编号、比特位的内容是一个函数指针,指向该信号的处理方法。

注意:常规信号在递达之前产生多次只记一次!

1.3 处理位图的接口 

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作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);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
  • 函数sigaddset用于向指定的信号集添加某种信号。
  • 函数sigdelset用于删除指定信号集中的某种信号。
  • 函数sigismember用于判断指定信号在指定信号集是否存在。

对block表进行操作:

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

参数:

① set:将要设置的新的信号屏蔽字

② oldset:获取旧的信号屏蔽字

③ how:修改block表的选项

SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask = mask l set
SIG_UNBLOCK

set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask = mask & ~set

SIG_SETMASK

设置当前信号屏蔽字为set所指向的值,相当于 

mask = set

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
 
对pengding表操作:
#include <signal.h>
int sigpending(sigset_t *set);

参数:

① set:获取当前进程的信号未决表

返回值:

成功返回0,出错返回-1

接下来通过这些接口我们可以实现一个动态的打印pending表的一个代码:

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

// 打印pending表
void PrintPending(const sigset_t &pending)
{
    for (int signo = 31; signo > 0; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << "\n";
}

int main()
{
    // 1. 屏蔽2号信号
    sigset_t set, oset;
    // 1.1 初始化信号集
    sigemptyset(&set);
    sigemptyset(&oset);
    // 1.2 添加信号
    sigaddset(&set, 2);
    // 1.3 修改信号集
    sigprocmask(SIG_BLOCK, &set, &oset);

    // 2. 让进程不断获取当前进程的pending
    int cnt = 0;
    sigset_t pending;
    while (true)
    {
        // 2.1 获取pending表
        sigpending(&pending);
        // 2.2 打印
        PrintPending(pending);

        sleep(1);

        cnt++;

        if (cnt == 10)
        {
            std::cout << "解除对2号信号的屏蔽, 2号信号准备递达" << std::endl;
            // 2.3 恢复原pending表
            sigprocmask(SIG_SETMASK, &oset, nullptr);
        }
    }
    return 0;
}

2. 信号的处理 

2.1 状态的切换

进程会在合适的时候处理信号,那么这个合适的时候是指什么时候呢?

进程从内核态返回到用户态时,进行信号的检测和处理。

  • 用户态:一种受控的状态,能够访问的资源是有限的。
  • 内核态:操作系统的工作状态,能访问大部分的系统资源,并且可以让用户以操作系统的身份访问内核空间。
  • ① 用户是无法直接访问OS底层资源,只能通过系统调用间接访问,所以用户调用系统调用,必然包含了身份的变化;
  • ② 进程要被调度首先得加载到内存然后通过页表映射到物理内存,那么操作系统也是需要被映射到物理内存的;
  • ③ 用户空间由用户级页表映射到物理内存;
  • ④ 内核空间由内核级页表映射到物理内存;
  • 所以在调用系统调用时访问OS直接在进程地址空间内进行跳转,就如同函数调用一样,调用系统调用接口也是在进程地址空间内进行的。

① 操作系统的代码、系统调用、数据结构、数据在整个系统中只有一份,所以内核级页表只需要有一张即可;

② 如果有多个进程,只需将内核空间通过内核级页表映射到物理内存,尽管有多个进程,使用的也是同一份系统调用接口;

③ 无论进程如何调度,CPU都可以直接找到操作系统!

④ 我们进程所有代码的执行,都可以在自己的进程地址空间内通过跳转的方式,进行调用和返回。

那么如何区分内核态和用户态呢?

CPU内存在的寄存器CS寄存器,CS寄存器用来保存代码段的,其中有两个比特位01表示内核态(1)、11表示用户态(3);切换用户的状态其实就是修改CS寄存器中对应的比特位。

CPU内部还存在一些CR寄存器:

CR3寄存器用于保存当前运行进程的用户级页表的物理地址;

CR1寄存器用于保存上一次引发缺页中断的虚拟地址。

2.2 信号的处理

用户在调用系统调用之后,在要完成调用任务时,会从用户态切换至内核态完成对应的任务,此时并不是直接切换回用户态,而是先要检测信号,如果有需要处理的信号,根据对信号的处理方法,如果是默认动作、忽略就直接处理,如果是用户自定义方法,那么此时不能在内核态处理,而是要返回用户态去执行用户自定义方法,在执行完之后,不能直接跳转到用户代码处,而是要再次返回内核态,再从内核态返回进入内核态的用户代码处。

简化的图就是一个♾️

在信号捕捉中,一共会涉及到4次状态的切换!

上述情况是只有一个信号需要被处理,那么如果存在多个需要处理的信号,那么在处理完一个信号之后会轮训式的检测需要处理的信号,在所有信号处理完之后再切换为用户态。

2.3 sigaction函数 

该函数是一个检测信号并改变处理动作的函数:

参数:

signum:要改变的信号的编号;

sigaction是一个结构体:

其中我们只需要关注sa_handler和sa_mask

sa_handler是要指定的处理动作;

sa_mask是要额外屏蔽的信号集。

act:表示要改变的新的处理方法;

oldact:表示被改变之前的处理方法。

Linux是不允许同一个信号已经在被处理的过程中,再次进行嵌套处理的,所以当某一个信号在被处理的过程中,内核会自动将该信号加入到信号屏蔽字中,当处理的函数返回之后,会对该信号进行恢复,除了当前处理的信号被屏蔽外,我们也可以通过sa_mask(信号集)添加一些额外的信号进行屏蔽。 

代码演示:

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

void Print(const sigset_t &pending);

void handler(int signo)
{
    std::cout << "get a sig: " << signo << std::endl;
    sleep(1);
    while (true)
    {
        sigset_t pending;
        sigpending(&pending);
        Print(pending);
        sleep(1);
    }
}

// 打印pending表
void Print(const sigset_t &pending)
{
    for (int signo = 31; signo > 0; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}
int main()
{
    std::cout << "pid: " << getpid() << std::endl;
    struct sigaction act, oact;
    // 自定义处理方法
    act.sa_handler = handler;
    // 初始化信号集
    sigemptyset(&act.sa_mask);
    // 添加3号信号
    sigaddset(&act.sa_mask, 3);
    // 自定义捕捉2号信号
    sigaction(2, &act, &oact);
    while (1)
        sleep(1);
    return 0;
}

3. 信号的其他补充 

3.1 可重入函数

将函数和信号结合起来研究:

在链表阶段我们实现了一个头插的函数接口,头插的阶段分为两步,做完第一步的时候由于某些硬件中断使进程回到了内核态,再次返回用户态时需要进行信号的检测与处理,如果此时的信号自定义方法中也调用了头插的函数,在做完头插的两步之后又重新返回用户态的代码处继续向下执行,那么在自定义方法中插入的头节点就会被丢失掉,此时这个头插的这个函数就是一个不可重入函数。

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
如果一个函数符合以下条件之一则是不可重入的 :
  • 调用了mallocfree,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

3.2 SIGCHLD信号 

在进程等待的章节说到过,子进程退出时父进程必须进行等待(waitpid()),否则会造成僵尸问题,并且我们有时还需要知道子进程的退出信息,另外在子进程退出的时候回向父进程发送SIGCHLD信号。

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

void handler(int signo)
{
    std::cout << "get a sig: " << signo << std::endl;
}

int main()
{
    std::cout << "pid: " << getpid() << std::endl;
    // 自定义捕捉信号
    signal(SIGCHLD, handler);
    pid_t id = fork();
    if (id == 0)
    {
        std::cout << "child is running" << std::endl;
        sleep(5);
        exit(10);
    }
    while (true)
        sleep(1);
    return 0;
}

父进程自定义捕捉SIGCHLD信号,子进程在运行5秒后退出,可以看到果然子进程给父进程发送了SIGCHLD信号。

所以我们就可以基于信号来对子进程进行回收等待了:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

void handler(int signo)
{
    std::cout << "get a sig: " << signo << std::endl;
    // 等待任意进程
    waitpid(-1, nullptr, 0);
}

int main()
{
    std::cout << "pid: " << getpid() << std::endl;
    // 自定义捕捉信号
    signal(SIGCHLD, handler);
    pid_t id = fork();
    if (id == 0)
    {
        std::cout << "child is running" << std::endl;
        sleep(5);
        exit(10);
    }
    while (true)
        sleep(1);
    return 0;
}

Linux支持手动忽略SIGCHLD,如果对其进行忽略,那么所有的子进程都不要父进程进行等待了,子进程会在终止时自动的清理。

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

int main()
{
    std::cout << "pid: " << getpid() << std::endl;
    // 手动忽略SIGCHLD
    signal(SIGCHLD, SIG_IGN);
    pid_t id = fork();
    if (id == 0)
    {
        std::cout << "child is running" << std::endl;
        sleep(5);
        exit(10);
    }
    return 0;
}

 

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!   

  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

stackY、

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

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

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

打赏作者

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

抵扣说明:

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

余额充值