Linux进程信号处理:深入理解与应用(3)

news2024/11/15 15:35:24

 

                                               🎬慕斯主页修仙—别有洞天

                                              ♈️今日夜电波:it's 6pm but I miss u already.—bbbluelee

                                                                0:01━━━━━━️💟──────── 3:18
                                                                    🔄   ◀️   ⏸   ▶️    ☰  

                                      💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍


 

目录

前言

信号的保存

首先了解几个概念

什么是信号递达(Delivery)?

什么是信号未决(Pending)?

什么是阻塞 (Block )?

信号在内核中的表示

sigset

sigprocmask

信号的处理

信号的捕捉

使用 sigaction() 捕捉信号

信号处理时机


前言

        本文书接上回Linux进程信号处理:深入理解与应用(2),本文是Linux进程信号处理的最后一篇文章。主要介绍信号的保存以及信号的处理。其中较为重要的是sigset以及基于它的函数调用。

信号的保存

首先了解几个概念

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

        信号从产生到递达之间的状态,称为信号未决(Pending)

        进程可以选择阻塞 (Block )某个信号

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

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

什么是信号递达(Delivery)?

        实际执行信号的处理动作称为信号递达(Delivery),我们在前面的文章Linux进程信号处理:深入理解与应用(1)中有提到可以通过signal替换信号,实际上就是替换的处理动作。其中提到了有三种处理方式:

    • SIG_IGN:表示忽略该信号,即信号发生时不采取任何行动。
    • SIG_DFL:表示采用系统默认的处理方式,通常是终止进程或忽略该信号。
    • 自定义处理函数:如果你希望在信号发生时执行特定的操作,可以设置一个自定义的处理函数。这个函数通常需要接受一个整数参数(信号编号)并且返回void。

        信号的递达就是上述的三种处理,信号的递达就是处理信号!我们在认识signal的时候,对于函数原型的认识如下:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

        可以看到我们传递的SIG_IGN、SIG_DFL和自定义处理函数都是一个函数指针,自定义处理函数就是函数,那么对于SIG_IGN和SIG_DFL该怎么理解呢?实际上的定义如下:

#define SIG_DFL ((sighandler_t) 0)
#define SIG_IGN ((sighandler_t) 1)

        他们实际上就是对于0和1的强转,系统可以根据该特征来判断是默认还是自定义还是忽略,因为自定义的地址肯定不会是0和1。

什么是信号未决(Pending)?

        信号从产生到递达之间的状态,称为信号未决(Pending)。信号产生的时候,当前的进程可能在做更重要的事情,信号无法被立即处理,他需要在合适的时候的处理,所以需要有对信号进行保存的能力,这就是前面的文章Linux进程信号处理:深入理解与应用(1)中提到的系统可以保存信号!保持信号则是通过位图来进行保存对应的信号!

什么是阻塞 (Block )?

        信号的阻塞(Block)是指将某个信号设置为不可递达状态,即暂时忽略该信号。当一个信号被阻塞时,即使它被发送给进程,也不会立即递达,而是保持未决状态,直到信号解除阻塞。说大白话:阻塞实际上就是未决之后,暂时不递达,直到解决对信号的阻塞!

信号在内核中的表示

        如下,根据我们上面的了解,我们可以对以下这张图进行了解,每一个进程都会有这三张表,用于保存信号,这也是前面提到,为什么改变了其中的handler,其他的进程不会改变,因为handler的改变只是对于对应的进程而言的。我们通过横向的看着三张表,就可以很好的理解信号的保存以及后续的处理过程:

        每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

        SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

        SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

 

sigset

    sigset_t是Linux系统中用于表示信号集的数据类型。它用于存储一组信号,以便在进程之间进行信号的发送和接收操作。

   sigset_t实际上就是一个由操作系统提供的位图结构,实际上就是结构体里套了一个数组:

typedef struct
  {
    unsigned long int __val[_SIGSET_NWORDS];
  } __sigset_t;
typedef __sigset_t sigset_t;

        以下是一些常用的函数和操作与sigset_t相关的:

  1. sigemptyset(sigset_t *set): 初始化一个空的信号集,将所有位设置为0。
  2. sigfillset(sigset_t *set): 初始化一个包含所有信号的信号集,将所有位设置为1。
  3. sigaddset(sigset_t *set, int signum): 向信号集中添加一个信号。
  4. sigdelset(sigset_t *set, int signum): 从信号集中删除一个信号。
  5. sigismember(const sigset_t *set, int signum): 检查信号是否在信号集中。
  6. sigprocmask(int how, const sigset_t *set, sigset_t *oldset): 修改进程的信号掩码,即阻塞或解除阻塞信号集中的信号。
  7. sigpending(sigset_t *set): 获取当前进程未决的信号集。
  8. sigsuspend(const sigset_t *set): 暂停进程执行,直到收到信号集中的一个信号为止。
  9. sigwait(const sigset_t *set, int *sig): 等待信号集中的一个信号,并返回接收到的信号编号。

        下面他们通过sigprocmask和sigpending来理解这些操作:

sigprocmask

    sigprocmask()是一个用于修改进程信号掩码的函数,它允许进程阻止或解除阻止某些特定信号。

        函数原型:

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

        参数说明:

  • how:指定要执行的操作,可以是以下三个值之一:
    • SIG_BLOCK:将set中的信号加入到进程的信号掩码中,即阻塞这些信号。
    • SIG_UNBLOCK:从进程的信号掩码中移除set中的信号,即解除阻塞这些信号。
    • SIG_SETMASK:直接将set设置为进程的信号掩码,替换原有的信号掩码。

  • set:指向一个sigset_t类型的信号集,包含了要添加到或从进程信号掩码中移除的信号。根据how的值不同,该参数的含义也不同。
  • oldset:指向一个sigset_t类型的信号集,用于保存修改前的信号掩码。如果不需要保存旧的信号掩码,可以将此参数设置为NULL

        返回值:

  • 成功时返回0,失败时返回-1,并设置errno为相应的错误码。

        使用示例:

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

void handler(int signo)
{
    std::cout << "获得一个:NO." << signo << "信号" << std::endl;

}

int main()
{
    //替换2号信号的处理方法
    signal(2, handler);
    std::cout << "i am running,but block~,pid:" << getpid() << std::endl;

    // 初始化信号集
    sigset_t block,oblock;

    sigemptyset(&block);
    sigemptyset(&oblock);

    sigaddset(&block,2);
    // 阻塞2号信号
    sigprocmask(SIG_BLOCK,&block,&oblock);

    int time=5;
    while(time--)
    {
        sleep(1);
    }
    
    std::cout<<std::endl;
    std::cout << "i am running and do not block~,pid:" << getpid() << std::endl;

    //恢复原来的信号掩码
    //sigprocmask(SIG_UNBLOCK,&block,&oblock);
    sigprocmask(SIG_SETMASK,&oblock,&block);

    return 0;
}

        注意事项:

  • 在使用sigprocmask()函数时,需要注意避免产生死锁。例如,如果在信号处理函数中调用了sigprocmask(),则可能导致死锁。
  • 在多线程环境中,每个线程都有自己的信号掩码。因此,当在一个线程中调用sigprocmask()时,只会影响该线程的信号掩码。
  • sigprocmask()函数会返回之前的信号掩码,以便在后续操作中恢复。如果不需要在后续操作中恢复信号掩码,可以将oldset参数设置为NULL


sigpending

    sigpending()函数用于获取当前进程未决(pending)的信号集。

        函数原型:

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

        参数说明:

  • set:指向一个sigset_t类型的信号集,用于存储获取到的未决信号。

        返回值:

  • 成功时返回0,失败时返回-1,并设置errno为相应的错误码。

        使用示例:

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

void handler(int signo)
{
    std::cout << "获得一个:NO." << signo << "信号" << std::endl;
}

int main()
{
    // 替换2号信号的处理方法
    signal(2, handler);
    std::cout << "i am running,but block~,pid:" << getpid() << std::endl;

    // 初始化信号集
    sigset_t block, oblock;

    sigemptyset(&block);
    sigemptyset(&oblock);

    sigaddset(&block, 2);
    // 阻塞2号信号
    sigprocmask(SIG_BLOCK, &block, &oblock);

    sigset_t pend;
    while (true)
    {
        //获取当前进程未决信号集
        sigpending(&pend);
        //打印直观显示
        for (int i = 31; i > 0; i--)
        {
            if (sigismember(&pend, i))
            {
                std::cout << "1";
            }
            else
            {
                std::cout << "0";
            }
        }
        std::cout << std::endl;
        sleep(1);
    }

    return 0;
}

        注意事项:

  • sigpending()函数只能获取未决信号集,不能获取阻塞或忽略的信号。
  • 在多线程环境中,每个线程都有自己的未决信号集。因此,当在一个线程中调用sigpending()时,只会获取该线程的未决信号集。

信号的处理

信号的捕捉

        信号捕捉是操作系统提供的一种机制,允许进程指定特定函数(称为信号处理程序或信号处理器)来响应特定信号的接收。当一个进程收到一个它可以捕捉的信号时,操作系统会暂停进程当前的执行流程,转而调用与该信号关联的处理程序。一旦信号处理程序执行完毕,进程会继续执行被信号打断的操作。

        在Linux系统编程中,信号捕捉通常通过以下两个系统调用实现:

  1. signal() - 这是一个较老的系统调用,用于设置信号处理程序。
  2. sigaction() - 这是一个更现代、功能更丰富的系统调用,用于设置信号处理程序。

使用 sigaction() 捕捉信号

    sigaction() 提供了比 signal() 更多的控制,包括可以设置信号屏蔽、指定信号处理选项等。其原型如下:

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

    signum 参数是你想要捕捉的信号的编号。act 参数是一个指向 sigaction 结构体的指针,其中包含了新的信号处理程序、信号集和其他标志。oldact 参数是一个指向 sigaction 结构体的指针,用于接收旧的信号处理程序信息,如果不需要可以设置为 NULL

    sigaction 结构体定义如下:

struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};
  • sa_handler: 信号处理程序。
  • sa_mask: 信号屏蔽字,指定在处理信号时阻塞哪些其他信号。
  • sa_flags: 影响信号处理的一组标志。
  • sa_restorer: 不常用,通常设为 NULL

        信号处理程序的编写

        编写信号处理程序时,需要注意以下几点:

  1. 信号处理程序应尽可能短小,避免长时间阻塞。
  2. 信号处理程序不应调用非异步信号安全的函数。
  3. 不要在信号处理程序中进行复杂的逻辑操作,特别是涉及数据结构和锁定的操作。
  4. 尽量避免在信号处理程序中使用全局变量,因为信号处理程序可能会在任何时间运行,从而可能导致竞态条件。
  5. 如果需要修改全局状态,可以使用原子操作或者锁来保护共享数据。

        示例代码

        下面是一个简单的使用 sigaction 捕捉 SIGINT 信号并设置信号处理程序的示例:

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

void int_handler(int signum) {
    printf("Interrupt signal (%d) received.
", signum);
}

int main() {
    struct sigaction sa;
    sa.sa_handler = int_handler;
    sa.sa_flags = 0;
    sigemptyset(&sa.sa_mask);

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    while (1) {
        pause(); // 等待信号到来
    }

    return 0;
}

        在上面的代码中,我们创建了一个信号处理程序 int_handler 来处理 SIGINT 信号。我们使用 sigaction 系统调用来注册这个处理程序,并设置 sa_flags 为 0,表示使用默认的信号处理选项。我们还清空了 sa_mask,表示在处理信号时不阻塞任何其他信号。最后,我们使用无限循环和 pause 函数使进程等待信号的到来。

 

信号处理时机

        前面我们提到,信号会在合适的时候被处理,那么是什么时候呢?答案是在进程从内核态回到用户太的到时候进行信号的检测和信号的处理。这里牵扯到了用户态和内核态的互相切换,就不细展开了。我们只需要知道用户态是一种受控的状态,能够访问的资源是有限的。内核态是一种操作系统的工作状态,能够访问大部分的资源。系统调用的背后就包含了身份的变化。下图中3和5的交点即为信号处理的时候:

        对上图的解释:如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

 


                    感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o! 

                                       

                                                                        给个三连再走嘛~  

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1436268.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

编译原理与技术(三)——语法分析(二)自顶向下-递归下降

一、语法分析的两种方法 自顶向下&#xff08;Top-down&#xff09;&#xff1a; 针对输入串&#xff0c;从文法的开始符号出发&#xff0c;尝试根据产生式规则推导&#xff08;derive&#xff09;出该输入串。 从根部开始构造语法树。 自底向上&#xff08;Bottom-up&#…

双非本科准备秋招(18.1)—— 力扣二叉树

1、404. 左叶子之和 方法一&#xff1a; 可以在父节点判断一下&#xff0c;如果左子树不为null&#xff0c;并且左子树没有左右子树&#xff0c;说明这是个左叶子节点。 class Solution {public int sumOfLeftLeaves(TreeNode root) {if(root null) return 0;int LV sumOfL…

怎么把两段视频合成一个画面?教你4个合成方法

怎么把两段视频合成一个画面&#xff1f;在数字媒体时代&#xff0c;视频编辑已经成为一项必备技能。有时候&#xff0c;我们需要将两段或多段视频合并成一个画面&#xff0c;这在电影制作、自媒体内容创作、家庭相册制作等领域都有广泛应用。本文将为你介绍四种简单易行的方法…

挑战杯 python+opencv+机器学习车牌识别

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于机器学习的车牌识别系统 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;4分工作量&#xff1a;4分创新点&#xff1a;3分 该项目较为新颖&#xff0c;适…

PyTorch的10个基本张量操作

PyTorch是一个基于python的科学计算包。它的灵活性允许轻松集成新的数据类型和算法&#xff0c;并且框架也是高效和可扩展的&#xff0c;下面我们将介绍一些Pytorch的基本张量操作。 Tensors 张量Tensors是一个向量&#xff0c;矩阵或任何n维数组。这是深度学习的基本数据结构…

企业计算机服务器中了halo勒索病毒如何解密,halo勒索病毒数据恢复

对于众多的企业来说&#xff0c;数据是一个企业的发展的根基&#xff0c;通过数据可以更好地规划调整企业的发展方向&#xff0c;提高企业生产效率。但网络是一把双刃剑&#xff0c;网络技术的发展不仅会为企业带来极大便利&#xff0c;但也为企业数据安全带来严重威胁。近期&a…

谈谈BlueFS

目录 前言数据结构标识一个文件文件系统的全局记录事务记录超级块 启动流程磁盘管理读写流程创建文件流程为文件写数据把数据下刷到磁盘读流程 参考资料 前言 BlueFS具体是个什么东西呢&#xff1f; 如上图&#xff0c;在Ceph里&#xff0c;使用BlueStore作为默认的存储引擎。…

LLM(大语言模型)——大模型简介

目录 概述 发展历程 大语言模型的概念 LLM的应用和影响 大模型的能力、特点 大模型的能力 涌现能力&#xff08;energent abilities&#xff09; 作为基座模型支持多元应用的能力 支持对话作为统一入口的能力 大模型的特点 常见大模型 闭源LLM&#xff08;未公开源…

datax离线同步oracle表到clickhouse实践1

时间&#xff1a;2024.01 目录1、安装启动 oracle19c 容器 2、rpm包安装clickhouse 3、datax安装 4、datax同步 目标库根据要同步的表&#xff0c;按照clickhouse建表规范建表 编写json文件 编写增量同步shell脚本&#xff0c;加入 crond 定时任务 1、安装启动 oracle19c 容器…

关于如何在Mac上安装Windows,看这篇文章就差不多了

启动转换(Boot Camp)助手可以让你在一些Mac电脑上安装Windows并引导到其中,就像在普通电脑上一样。现代苹果硅型号(M1、M2等)不支持启动转换助手,必须使用其他方法来运行Windows。 可以在M1或M2 Mac上使用启动转换助手吗 只有当Mac上有Intel(x86)处理器时,才能使用启…

Arcgis使用过程中常见问题解决方法

Arcgis无法连接数据库/数据库连接或创建失败解决方法 最近在使用arcgis过程中出现无法连接数据库或者是无法创建数据库。连接到数据库失败&#xff1b;无法创建新的数据库&#xff0c;权限被拒绝&#xff08;如下图&#xff09;。 出现这个原因是你所用的电脑系统文件dao360.…

生存类游戏《幻兽帕鲁》从部署服务器到开始体验全过程

SteamDB数据显示&#xff0c;《幻兽帕鲁》上线24小时内&#xff0c;在线人数峰值便突破200万&#xff0c;跻身Steam历史排行榜第二位。随着热度进一步发酵&#xff0c;《幻兽帕鲁》官方发布推文称&#xff0c;游戏发售不到6天&#xff0c;销量已经突破了 800万份。欢迎大家在阿…

香港公司员工遭遇2亿港币Deepfake诈骗; 阿里巴巴Qwen1.5震撼发布;通义千问App推新功能迎佳节

&#x1f989; AI新闻 &#x1f680; 香港公司员工遭遇2亿港币Deepfake诈骗 摘要&#xff1a;近日&#xff0c;香港一家英国跨国企业的员工被Deepfake技术仿造的视频诈骗&#xff0c;损失2亿港币。骗子通过伪造高管的面貌和声音&#xff0c;用视频会议的形式命令其转账&#…

双非本科准备秋招(17.1)—— 力扣二叉树

1、257. 二叉树的所有路径 要求返回根节点到叶子节点的所有路径&#xff0c;这里用前序遍历就好。 每次递归前&#xff0c;都让字符串s加上当前节点的值和“->”&#xff0c;然后判断是否为叶子节点&#xff0c;如果是的话&#xff0c;说明这条路径是一个答案&#xff0c;因…

基于springboot实现二次元商品购物系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现二次元商品购物系统演示 摘要 时代的变化速度实在超出人类的所料&#xff0c;21世纪&#xff0c;计算机已经发展到各行各业&#xff0c;各个地区&#xff0c;它的载体媒介-计算机&#xff0c;大众称之为的电脑&#xff0c;是一种特高速的科学仪器&#xff0…

python + 蒙特卡罗 = 股市神器! 能用到A股吗?

蒙特卡罗模拟作为一种在金融领域广泛应用的强大统计技术&#xff0c;能够对金融资产&#xff08;例如股票&#xff09;的行为进行模拟建模。在本文中&#xff0c;我们将深入探讨如何在Python编程环境中实现蒙特卡罗模拟&#xff0c;以预测股票市场未来可能出现的情况。我们将利…

MySQL进阶45讲【12】为什么你的MySQL偶尔会卡一下

1 前言 平时的工作中&#xff0c;不知道大家有没有遇到过这样的场景&#xff0c;一条SQL语句&#xff0c;正常执行的时候特别快&#xff0c;但是有时也不知道怎么回事&#xff0c;它就会变得特别慢&#xff0c;并且这样的场景很难复现&#xff0c;它不只随机&#xff0c;而且持…

【Linux驱动】块设备驱动(二)—— 块设备读写(使用请求队列)

块设备的操作函数并没有类似于字符驱动中的read 和write函数&#xff0c;要实现读写操作&#xff0c;只能在请求处理函数中实现。这就分为两种&#xff0c;是否要使用请求队列&#xff0c;请求队列的主要作用是管理和调度IO请求。在以下情况中&#xff0c;一般需要用到请求队队…

Kafka 使用手册

kafka3.0 文章目录 kafka3.01. 什么是kafka&#xff1f;2. kafka基础架构3. kafka集群搭建4. kafka命令行操作主题命令行【topic】生产者命令行【producer】消费者命令行【consumer】 5. kafka生产者生产者消息发送流程Producer 发送原理普通的异步发送带回调函数的异步发送同步…

虚幻5源码版打包服务端

适用情况&#xff0c;windows系统&#xff0c;已经安装vs2022之类的&#xff0c;和UE5适配的版本 源码版使用 1.下载源码版&#xff0c;推荐下载 压缩包 tar.gz那个&#xff0c;zip和git clone我老是下载不下载来&#xff0c;只是这个压缩包要解压1个多小时… 2.点击 源码的…