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

 

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

                                              ♈️今日夜电波: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! 

                                       

                                                                        给个三连再走嘛~  

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SQL 表信息 | 统计 | 脚本

介绍 统计多个 SQL Server 实例上多个数据库的表大小、最后修改时间和行数&#xff0c;可以使用以下的 SQL 查询来获取这些信息。 脚本 示例脚本&#xff1a; DECLARE Query NVARCHAR(MAX)-- 创建一个临时表用于存储结果 CREATE TABLE #TableSizes (DatabaseName NVARCHAR…

游泳哪个牌子口碑最好?全球最好的游泳耳机排行榜

在水中畅游时&#xff0c;倾听悠扬的音乐或放松的声音是一种愉悦的体验&#xff0c;而蓝牙游泳耳机的出现更是为游泳者提供了更便利的选择。市场上涌现出多款不同品牌的蓝牙游泳耳机&#xff0c;为了满足游泳爱好者对音乐的需求&#xff0c;小编为大家精心整理了一些值得推荐的…

C++多线程学习[五]:RAII锁

一、什么是RAII 使用局部对象来控制资源的技术&#xff0c;即它的生命周期由操作系统来管理&#xff0c;无需人工的介入。 为什么要采用RAII技术呢&#xff1f; 主要是在开发过程中资源的销毁容易忘记&#xff0c;容易造成死锁或内存泄露。 {}为一个区域 &#xff0c;这里锁的…

JVM 性能调优 - 参数调优(3)

查看 JVM 内存的占用情况 编写代码 package com.test;public class PrintMemoryDemo {public static void main(String[] args) {// 堆内存总量long totalMemory Runtime.getRuntime().totalMemory();// jvm 试图使用的最大堆内存long maxMemory Runtime.getRuntime().maxM…

Matlab使用点云工具箱进行点云配准

一、代码 source_pc pcread(bun_zipper.ply); target_pc pcread(bun_zipper2.ply); % 下采样 gridStep 0.001; ptCloudA pcdownsample(source_pc,gridAverage,gridStep); ptCloudB pcdownsample(target_pc,gridAverage,gridStep); % 初始变换矩阵 tform_initial affine3…

AE2023 After Effects 2023

After Effects 2023是一款非常强大的视频编辑软件&#xff0c;提供了许多新功能和改进&#xff0c;使得视频编辑和合成更加高效和灵活。以下是一些After Effects 2023的特色功能&#xff1a; 新合成预设列表&#xff1a;After Effects 2023彻底修改了预设列表&#xff0c;使其…

Excel——有效性、二级菜单联动

一、录入规范数据 1.手动输入序列录入有效性信息 选择需要录入有效性的所有单元格 选择【数据】——【有效性】——【有效性】 在【允许】输入的值之间选择【序列】 在【序列】输入框中输入想要选择的值&#xff0c;中间用逗号&#xff08;必须是英文逗号&#xff09;隔开 。…

Java入门之JavaSe(韩顺平p1-p?)

学习背景&#xff1a; 本科搞过一段ACM、研究生搞了一篇B会后&#xff0c;本人在研二要学Java找工作啦~~&#xff08;宇宙尽头是Java&#xff1f;&#xff09;爪洼纯小白入门&#xff0c;C只会STL、python只会基础Pytorch、golang参与了一个Web后端项目&#xff0c;可以说项目小…

Linux 网络:PTP 简介

文章目录 1. 前言2. PTP(Precision Time Protocol​) IEEE 1588 协议简介2.1 PTP IEEE 1588 协议时间同步原理2.2 PTP IEEE 1588 协议时钟类型2.2.1 普通时钟(OC: Ordinary Clock)2.2.2 边界时钟(BC: Boundary Clock)2.2.3 透明时钟(TC: Transparent Clock)2.2.3.1 端对端透明时…

挂耳耳机哪个牌子好?推荐几款性价比超高的挂耳耳机

在寻求更轻便舒适的听音体验时&#xff0c;挂耳耳机逐渐成为众多用户的优先选择。市场上各式各样的耳挂耳机琳琅满目&#xff0c;种类繁多&#xff0c;挂耳耳机哪个牌子好&#xff1f;为了帮助大家更好地了解耳挂耳机的市场状况&#xff0c;我推荐几款性价比超高的挂耳耳机。 挂…

MongoDB的分片集群(二) :mongodb4.x分片集群离线搭建开启安全认证

转载说明&#xff1a;如果您喜欢这篇文章并打算转载它&#xff0c;请私信作者取得授权。感谢您喜爱本文&#xff0c;请文明转载&#xff0c;谢谢。 相关文章&#xff1a; MongoDB的分片集群(一) : 基础知识 在《MongoDB的分片集群(一) : 基础知识》中梳理了分片集群的基础知识…

知名开发工具RubyMine全新发布v2023.3——支持AI Assistant

RubyMine 是一个为Ruby 和 Rails开发者准备的 IDE&#xff0c;其带有所有开发者必须的功能&#xff0c;并将之紧密集成于便捷的开发环境中。 RubyMine v2023.3正式版下载 新版本改进AI Assistant支持、Rails应用程序和引擎的自定义路径、对Rails 7.1严格locals的代码洞察、RB…

页面模块向上渐变显示效果实现

ps: 先祝各位朋友新春快乐 ^o^/ 想要首页不那么枯燥无味吗&#xff1f;还在未首页过于单调而苦恼吧&#xff0c;来试试这个吧&#xff08;大佬请忽略上述语句o&#xff09; 今天要实现一个页面线上渐变显示的效果&#xff0c;用来丰富首页等页面&#xff1a; 这里先随机建立几…

jsp商场会员卡管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 商场会员卡管理系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql5.…

C遗漏知识(个人向)

之前C语言遗漏的一些。 数据在内存中的存储 原码、反码、补码 整数的2进制表⽰⽅法有三种&#xff0c;即 原码、反码和补码 正整数的原、反、补码都相同。 负整数的三种表⽰⽅法各不相同。 原码&#xff1a;直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。 反码&…

【芯片设计- RTL 数字逻辑设计入门 6 -- 带同步复位的D触发器 RTL实现及testbench 验证】

文章目录 带同步复位的D触发器Verilog 代码testbench 代码编译及仿真问题小结 带同步复位的D触发器 同步复位 &#xff1a;复位只能发生在在clk信号的上升沿&#xff0c;若clk信号出现问题&#xff0c;则无法进行复位。 Verilog 代码 // timescale ins/1nsmodule flopr (inpu…

45 漏洞发现-API接口服务之漏洞探针类型利用修复

目录 端口服务类安全测试API接口-webservice RESTful APT 演示案例:端口服务类-Tomcat弱口令安全问题端口服务类-Glassfish任意文件读取其他补充类-基于端口WEB站点又测试其他补充类-基于域名WEB站点又测试其他补充类-基于IP配合端口信息再收集口令安全脚本工具简要使用-Snetcr…

sql求解连续两个以上的空座位

Q&#xff1a;查找电影院所有连续可用的座位。 返回按 seat_id 升序排序 的结果表。 测试用例的生成使得两个以上的座位连续可用。 结果表格式如下所示。 A:我们首先找出所有的空座位&#xff1a;1&#xff0c;3&#xff0c;4&#xff0c;5 按照seat_id排序&#xff08;上面已…

vue3 elementplus DateTimePicker 日期时间设置默认时间为当天

DateTimePicker里面有个自带属性 可以实现这个需求&#xff0c;如图&#xff1a; // 设置当前当天时间范围 00: 00: 00 - 23:59:59 const currentDate [setDefaultDate(0), setDefaultDate(1)]const setDefaultDate (type:number ): string > {let t ;let date new Da…

代码随想录算法训练营第25天 | 216.组合总和III ,17.电话号码的字母组合

回溯章节理论基础&#xff1a; https://programmercarl.com/%E5%9B%9E%E6%BA%AF%E7%AE%97%E6%B3%95%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html 216.组合总和III 题目链接&#xff1a;https://leetcode.cn/problems/combination-sum-iii/ 思路: 本题就是在[1,2,3,4,5,6,7,…