Linux——信号的保存与处理

前言:本文主要介绍信号的保存与处理过程。

一、信号阻塞与信号底层逻辑

        在linux下面的进程控制块(PCB),存在一个pending变量用于存放接收到的信号,该变量有32位,变量的位代表信号的类别,变量的值代表是否收到信号。进程会根据该变量上的信号做出响应(亦称为递达),响应的方式有三种(默认,忽略,自定义),若进程需忽略某个信号,即不对该信号做任何响应,称为信号阻塞。

        在PCB中还存在一个变量block,其与pending类似,有32位,每一位代表一种信号,位的值代表是否阻塞该信号。当某个信号被阻塞时,即使进程收到该信号,进程也无法递达。信号在产生到递达之间的状态称为未决,显然,若进程收到被阻塞的信号,该信号会始终处于未决状态,直至解除对该信号的阻塞。

       还有一个函数指针数组,包含31个元素,每个元素下标代表的是一种信号,指针指向该信号响应该信号的函数。(以上皆为简化处理,linux经过多层封装,具体实现可能不同,但基本原理类似)

 二、对信号内部数据结构的操作

        信号集

             上面的block和pending位图也称为信号集,在linux下可使用sigset_t数据类型表示,为了操作sigset_t类型的数据,提供了一系列接口,后续对信号集的操作都需要通过sigset_t数据来完成。

#include <signal.h>
//清空信号集
int sigemptyset(sigset_t *set);
//填充信号集
int sigfillset(sigset_t *set);
//添加信号,signo表示将该位置1
int sigaddset (sigset_t *set, int signo);
//删除信号,signo表示将该位置0
int sigdelset(sigset_t *set, int signo);
//判断该位是否为1
int sigismember(const sigset_t *set, int signo); 

         修改阻塞信号集

        使用系统调用sigprocmask可以读取或更改进程的阻塞信号集,其中9号和19号信号无法阻塞。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//how:表示更改的方式,
    SIG_BLOCK表示新增阻塞信号;
    SIG_UNBLOCK表示删除阻塞信号;
    SIG_SETMASK表示设置为set的信号集
//set:需要修改的信号集
//oset:输出型参数,表示修改前的block信号集
//修改成功返回值为0,否则为-1

        获取未决信号集

#include <signal.h>
int sigpending(sigset_t set);
//set:输出型参数,获取所有未决信号
//获取成功返回值为0,否则为1

        修改信号响应

        自定义信号的响应,同样9号信号的响应不能被修改。

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//signum:需要修改的信号编号
//handler:函数指针,修改后的响应操作

代码实例

        实例代码创建一个子进程,子进程阻塞所有信号,并且每隔一秒打印未决信号集,父进程每隔1秒给子进程发送

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


int main()
{
    pid_t id = fork();
    
    //子进程阻塞所有信号,并且打印未决信号集
    if(id == 0)
    {
        //创建两个信号集
        sigset_t block,oblock;
        //初始化清零
        sigemptyset(&block);
        sigemptyset(&oblock);

        //阻塞所有信号
        for(int i=1;i<=31;i++)
        {
            sigaddset(&block,i);
        }
        //写入阻塞信号集
        sigprocmask(SIG_BLOCK,&block,&oblock);

        sigset_t pending;
        sigemptyset(&pending);
        while(true)
        {
            //每隔一秒获取一次未决信号集
            sleep(1);
            sigpending(&pending);
            print(pending);
        }
    }

    //挂载2s,防止子进程信号阻塞还未完成提前被父进程发送的信号终止
    sleep(2);
    //父进程给子进程发送除9,19外的所有信号
    for(int sig=1;sig<=31;sig++)
    {
        if(9!=sig && 19!=sig)
        {
            kill(id,sig);
        }
        sleep(1);
    }
    //等待10s,直接终止子进程
    sleep(10);
    kill(id,9);
    int wstatus;
    waitpid(id,&wstatus,0);
    return 0;
}

        观察以下运行结果,除了9号进程和19号进程以外的所有进程,都成功完成过阻塞,但是18号进程开始是阻塞,后面发送20号信号SIGTSTP后,未决信号集种不再有18号信号,其原因是18号信号为SIGCONT(继续信号),当停止信号来临的时候,会将继续信号清除。

三、信号递达

        内核态与用户态

        内核态和用户态本质上是cpu运行的两种状态,两个状态的本质区别就是二者对运行指令集的权限不同,在内核态下,cpu可以运行所有的指令集,而用户态下指令集是受限的。

        在进程地址空间种,内存被划分为内核空间和用户空间,其中用户空间用于存放用户的代码和数据等,内核空间中存放了系统内核所需的数据,包括内核空间中维护的系统调用表,中断表等,用户态无法直接进行系统调用函数,需要将用户态中系统调用所需的数据拷贝到内核空间中,并且在内核态下运行系统调用。所以简单讲就是当CPU执行内核空间的代码时,处于内核态;执行用户空间的代码时,处于用户态。

        信号的捕捉过程

        在进程从内核态转化到用户态的时候,信号就会被检测并处理。信号的检测处理过程如下图

        在进入内核态完成相应动作后准备返回时,检测信号,并且进行响应:如果信号响应为默认则在内核态下完成处理,如果是自定义,则会先切回用户态,执行自定义操作,再切回内核态,调用系统调用sys_sigreturn()返回代码中。

信号递达

        信号递达分为三种默认、忽略和自定义,其中可以使用signal或者sigaction系统调用来自定义响应行为。

        使用signal系统调用来自定义对信号的处理

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//signum参数为需要自定义处理的信号
//handler是一个参数为int,无返回值的函数指针
//返回值是在修改前的处理方式

         使用sigaction系统调用来自定义对信号的处理

#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);
struct sigaction 
{
   void     (*sa_handler)(int);     //不可靠信号自定义响应
   void     (*sa_sigaction)(int, siginfo_t *, void *);//可靠信号自定义响应
   sigset_t   sa_mask;              //在递达过程中需要阻塞的信号
   int        sa_flags;              
   void     (*sa_restorer)(void);
};
//在信号递达的过程中,操作系统会自动阻塞引起当前递达的信号,递达结束后取消阻塞
//若在递达过程中,还需阻塞其他信号,可通过sa_mask添加
自定义递达代码实例

        自定义2号信号响应,同时屏蔽3,4,5信号,期间通过kill向该进程发送信号,并且打印未决信号集。

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

void handler(int signo)
{
    cout<<"signo: "<<signo<<endl;
    sigset_t pending;
    while(true)
    {
        //每隔1s打印一次未决信号集
        sigpending(&pending);
        print(pending);
        sleep(1);
    }
}

int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    //创建屏蔽信号集
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask,3);
    sigaddset(&mask,4);
    sigaddset(&mask,5);
    act.sa_mask = mask;
    //设置自定义响应和屏蔽信号集
    sigaction(2,&act,&oact);
    
    while(true);
    return 0;
}

        观察现象,在2信号递达期间,再次发送2,3,4,5信号都被阻塞

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值