Linux系统编程:进程信号的保存和阻塞

目录

一. 信号保存和阻塞的相关概念

二. 进程信号的表示

2.1 进程信号在内核中的表示

2.2 sigset_t 类型

三. 信号集操作相关函数

3.1 sigset_t 类型参数设置相关函数

3.2 sigprocmask 函数

3.3 sigpending 函数

四. 演示代码

4.1 将所有信号的处理方式都注册为不退出进程

4.2 显示未决信号集

4.3 所有信号设置为阻塞

五. 总结


一. 信号保存和阻塞的相关概念

  • 信号递达:进程实际执行信号处理的动作叫做信号的递达(Delivery)
  • 信号未决:信号从实际产生到递达之间的状态,称为信号未决(Pending)
  • 进程可以阻塞某个信号(block),即:即使产生了某个信号也不执行相应的处理动作。
  • 被阻塞信号在产生后,只要不接触阻塞状态,就一直不会递达。
  • 信号阻塞和忽略不是一个概念,信号阻塞是不对进行处理,信号处于未决状态,而忽略信号是指进程接收到了信号,信号也会递达,只是没有实质性的处理动作。

二. 进程信号的表示

2.1 进程信号在内核中的表示

每一个进程的PCB中,都会存储三张用于表示信号状态的表(如图2.1所示),他们分别为block、pending和handler,其中:

  • block:阻塞状态表,其底层实现是位图,如果设置某个进程信号阻塞,block中对应的二进制位就由0变1,block标志位如果为1,信号就不能够递达。
  • pending:未决信号,底层实现也是位图,如果OS检测到了信号,但还没有对信号进行处理,那么在其pending位图中的对应bit位就会被设置为1,如果信号递达,pending位图就会由1变0,如果其block中的二进制位也为1,那么信号就不会递达,pending中的二进制位永远都会为1,直到阻塞状态解除信号递达。
  • handler:是一个函数指针数组,指向其对应的信号处理方法的函数指针,图中SIG_DEF和SIG_IGN分别对应默认处理方式和忽略信号。
  • SIG_DFL和SIG_IGN在源码中的定义为:#define SIG_DEF ((__sighandler_t ) 0) 和 #define SIG_IGN ((__sighandler_t ) 0),进程收到信号,在递达是判断用哪个函数对信号进行处理的流程大概为:先通过 if 和 else if 判断是否选取默认处理方法或者忽略,如果都不是,则走到else执行用于自定义的处理方法。
图2.1 进程信号在内核中的表示

2.2 sigset_t 类型

OS不允许用于直接对block和pending位图中的二进制位进行修改,因此提供了一个OS类型sigset专门用于设置用于进程信号表示的相关位图,sigset_t 可以被称为信号集,sigset_t 的底层是位图结果,用 0/1 来表示状态。

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)。

三. 信号集操作相关函数

3.1 sigset_t 类型参数设置相关函数

  • int sigemptyset(sigset_t *set) -- 让set中不包含任何信号,即全部二进制位都是0。
  • int sigfillset(sigset_t *set) -- 让set包含全部信号,即让全部二进制位都是1。
  • int sigaddset(sigset_t *set, int signo) -- 将set指定信号的状态设置为有效1。
  • int sigdelset(sigset_t *set, int signo) -- 删除set指定信号的状态无效0。
  • int sigismember(sigset_t *set, int signo) -- 判断set中某个信号的状态是否为有效1,如果有效,函数返回1,无效返回0。

上面的这些函数,都是成功执行返回0,失败返回-1。 

3.2 sigprocmask 函数

sigprocmask函数 -- 读取或更改信号的阻塞状态

函数原型:int sigprocmask( int how,  sigset_t *set, sigset_t *obset )

函数参数:

        how -- 方法选择,添加阻塞、删除阻塞或设置阻塞

        set -- 如果选择设置阻塞,则用set中的信息设置

        obset -- 输出型参数,读取原来的阻塞信息

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

在sigprocmask函数中,参数 how 用于方法的选择,how有三个可选项,分别为:SIG_BLOCK、SIG_UNBLOCK 和 SIG_SETMASK。

表3.1 sigprocmask函数参数how的选择及对应功能
how功能
SIG_BLOCK添加对特定信号的阻塞状态,相当于mask | set
SIG_UNBLOCK取消对特定信号的阻塞状态,相当于mask & ~set
SIG_SETMASK使用set设置信号阻塞状态,相当于mask = set

3.3 sigpending 函数

sigpending函数 -- 获取进程当前的未决信号集

函数原型:int sigpending(sigset_t *set)

函数参数:set为输出型参数,用于获取当前进程的未决信号集

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

四. 演示代码

4.1 将所有信号的处理方式都注册为不退出进程

如果我们在程序中将所有信号的处理方法都设置为不退出,并在之后执行死循环,那么,是不是进程就无法被杀死了呢?

答案显然是否定的,OS的设计者早就考虑到了这一点,9号信号SIGKILL为管理员权限信号,用户不可以重新注册其处理方法,代码和运行结果如下。

代码4.1:所有信号重新注册方法为不退出

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

// 用户自定义的信号处理函数
void siganl_handler(int sig)
{
    std::cout << "recieve a signal, signum:" << sig << std::endl;
}

int main()
{
    std::cout << "this is a process, pid:" << getpid() << ", ppid:" << getppid() << std::endl;

    // 1. 将全部普通信号处理函数重新注册
    for(int sig = 1; sig <= 31; ++sig)
    {
        signal(sig, siganl_handler);
    }

    // 2. 执行死循环
    while(true) 
    { }

    return 0;
}
图4.1 代码4.1的运行结果

4.2 显示未决信号集

代码4.2通过sigprocmask函数,在第5s设置2号SIGINT信号为阻塞状态,,在第8s通过raise函数向进程发送2号SIGINT信号,在第15s取消SIGINT信号的阻塞状态,并实时输出未决状态信息,代码运行结果如图4.2,2号信号的阻塞状态由0至1,并在阻塞状态取消后,执行用户自定义的2号SIGINT信号处理函数,退出进程。

代码4.2:未决信号集的打印

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

// 用户自定义的信号处理函数
void siganl_handler(int sig)
{
    std::cout << "recieve a signal, signum:" << sig << std::endl;
    exit(0);
}

// 未决状态打印函数
void showPending(const sigset_t& pending)
{
    for(int sig = 1; sig <= 31; ++sig)
    {
        if(sigismember(&pending, sig)) std::cout << 1;
        else std::cout << 0;
    }    
    std::cout << std::endl;
}

int main()
{
    // 重新注册2号信号的处理方法
    signal(SIGINT, siganl_handler);

    // bset用于设置阻塞状态,obset用于接收原阻塞状态
    sigset_t bset, obset; 
    // pending用于获取阻塞集
    sigset_t pending;  

    //对信号集设置初始状态
    sigemptyset(&bset);
    sigaddset(&bset, SIGINT);

    int count = 0;
    while(true)
    {
        sleep(1);
        std::cout << ++count << ": " << std::flush;
        
        sigpending(&pending);   // 接收阻塞状态
        showPending(pending);   // 阻塞状态打印

        if(count == 5)
        {
            std::cout << "设置SIGINT信号处于阻塞状态 ..." << std::endl; 
            sigprocmask(SIG_BLOCK, &bset, &obset);
        }

        if(count == 8)
        {
            std::cout << "发送SIGINT信号" << std::endl;
            raise(SIGINT);
        }

        if(count == 15)
        {
            std::cout << "取消对SIGINT信号的阻塞" << std::endl;
            sigprocmask(SIG_SETMASK, &obset, nullptr);
        }
    }

    return 0;
}
图4.2 代码4.2的运行结果

4.3 所有信号设置为阻塞

设置所有信号阻塞,然后死循环,是不是发送任何信号都无法终止进程的运行?

答案当然也不是,9号SIGKILL信号和19号SIGSTOP信号都无法被阻塞,在19号信号使进程终止运行期间,18号SIGCONT信号也无法被阻塞。

代码4.3:验证9号SIGKILL信号无法被阻塞

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

// 未决状态打印函数
void showPending(const sigset_t& pending)
{
    for(int sig = 1; sig <= 31; ++sig)
    {
        if(sigismember(&pending, sig)) std::cout << 1;
        else std::cout << 0;
    }    
    std::cout << std::endl;
}

int main()
{
    // 设置所有信号阻塞
    sigset_t bset, obset;
    sigset_t pending;
    sigfillset(&bset);

    sigprocmask(SIG_BLOCK, &bset, &obset);

    // 依次向进程发送1 - 31号信号,如何输出未决状态
    for(int sig = 1; sig <= 31; ++sig)
    {   
        std::cout << "kill -" << sig << std::endl;
        raise(sig);

        // 获取阻塞信号集并打印
        sigpending(&pending);
        showPending(pending);

        sleep(1);
    }

    return 0;
}
图4.3 代码4.3的运行结果

五. 总结

  • 进程处理信号的动作叫做信号递达、信号从产生到递达的状态叫做信号未决、被阻塞的信号无法递达。
  • 进程PCB中有三张表:block、pending、handler,block和pending为阻塞和未决信号集,handler为指向信号处理函数的函数指针数组。
  • Linux操作系统提供了内置信号集类型sigset_t,用于信号状态的设置,sigprocmask 函数用于读取或设置阻塞信号集,sigpending用于获取阻塞信号集。
  • 9号SIGKILL信号具有管理员权限,用户无法自定义其处理动作,也无法被阻塞。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值