linux信号 | 信号的补充知识

news2024/10/9 16:24:11

        前言:本节内容主要是一些linux信号的周边知识或者补充知识。 对于信号的学习, 学习了信号概念, 产生, 保存与捕捉就已经算是认识我们的信号了。 如果想要知道更多关于信号的知识也可以看一下本篇文章。 

        ps:本篇内容适合了解信号的友友们进行观看哦

目录

sigaction

信号递达后pending位图清零的时机

 信号处理时的屏蔽

sa_mask 

函数重入

volatile关键字

SIGCHLD


sigaction

        这里面第一个参数signum就类似于signal里面的signum, 都是用来重新设置某个信号的处理动作。 第二个参数我们会发现它的类型名称和我们的函数名称是一样的, 第三个参数也是这个类型。  那么这里的第二个参数其实是一个输入型参数。 第三个参数是一个输出型参数。其中第二个参数就是传送我们自己定义的自定义捕捉方法。 第三个参数就是将对应的信号的老的处理方法给我们传出来。

        然后我们看一下sigaction类型的定义:

        上面就是sigaction的定义, 但是里面有许多成员都是和实时信号相关的, 我们不关心。 我们只关心第一个和第三个成员变量。 其中第一个就是我们将来捕捉信号要执行的处理方法。

信号递达后pending位图清零的时机

        当我们在处理某个信号的时候, 内核会将当前信号的pending位图由1清零。问题是, 是我们处理之前清零呢, 还是处理之后清零呢 ? 这个我们就可以在handler函数里面打印一下我们的pending位图, 就能观察到到底是在哪里清零的, 下面的实验方法就是——我们ctrl + C, 这个时候给进程发送信号pending位图2号位被置为1. 此时我们处理这个信号, 我们在捕捉信号里面打印pending位图, 如果是处理之前清零, 那么就打印一串0, 但是如果是处理之后清零, 那么就打印一串1。下面为代码:

代码:

#include<iostream>
#include<cstring>
#include<signal.h>
#include<unistd.h>
using namespace std;

void PrintPending()  //打印位图结构
{
    sigset_t set;
    sigpending(&set);

    for (int signo = 1; signo <= 31; signo++)
    {
        if (sigismember(&set, signo)) cout << "1" << endl;
        else cout << "0";
    }
    cout << endl;
}
void handler(int signo)
{
    PrintPending();
    cout << "catch a signal: " << signo << endl;
}

int main()
{
    struct sigaction act, oact;  //这里可以省略struct,不省略是因为能够提醒我们是系统结构体。 
    memset(&act, 0, sizeof(act)); 
    memset(&oact, 0, sizeof(oact)); 
    act.sa_handler = handler;
    sigaction(2, &act, &oact);
    
    while (true)
    {
        cout << "I am a process: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

运行结果:

 信号处理时的屏蔽

        那么, 这里还有一个问题就是操作系统会将当前的信号加入到信号屏蔽字当中。 当信号处理函数返回的时候, 自动恢复原来的信号屏蔽字。也就是说, 当某个信号在被处理的时候, 是不能再次被递达的。 防止信号捕捉的时候, 发生各种嵌套调用。

        那么如何证明信号捕捉的时候, 这个信号确实被屏蔽了呢? 这里我们用一个程序来进行实验。 ——实验的流程就是我们的处理信号里面是一个死循环, 循环打印pending位图。 然后循环外会打印一句捕捉到信号。如果我们执行2号信号时没有屏蔽2号信号, 那么我们发送几次2号信号, 那么都应该被递达, 那么捕捉到信号就会频发的打印。 但是如果屏蔽的话就应该不会被递达, 也就只打印一句捕捉到信号:

#include<iostream>
#include<cstring>
#include<signal.h>
#include<unistd.h>
using namespace std;

void PrintPending()  //打印位图结构
{
    sigset_t set;
    sigpending(&set);

    for (int signo = 1; signo <= 31; signo++)
    {
        if (sigismember(&set, signo)) cout << "1" << endl;
        else cout << "0";
    }
    cout << endl;
}
void handler(int signo)
{
    cout << "catch a signal: " << signo << endl;
    while (true)
    {
        PrintPending();
    }
}

int main()
{
    struct sigaction act, oact;  //这里可以省略struct,不省略是因为能够提醒我们是系统结构体。 
    memset(&act, 0, sizeof(act)); 
    memset(&oact, 0, sizeof(oact)); 
    act.sa_handler = handler;
    sigaction(2, &act, &oact);
    
    while (true)
    {
        cout << "I am a process: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

运行结果:

可以看到只有第一次ctrl + C递达了信号, 其他时候的信号都没有被递达。 并且第二次ctrl + c后pending表2号为变成了1, 说明这个信号此时未决状态。 也可以推测被屏蔽了。 

sa_mask 

另外sigaction里面还有一个成员变量交sa_mask。 这个sa_mask是sigset_t类型, 如哦正在处理2号信号, 那么2号信号就会自动被系统屏蔽。 如果我还想屏蔽更多信号呢? 也就是说, 我们在捕捉2号期间, 如果sa_mask默认, 那么就只会屏蔽2号信号。 但是如果我们想要屏蔽更多的信号, 就要用sa_mask进行设置。 

代码:

#include<iostream>
#include<cstring>
#include<signal.h>
#include<unistd.h>
using namespace std;

void PrintPending()  //打印位图结构
{
    sigset_t set;
    sigpending(&set);

    for (int signo = 1; signo <= 31; signo++)
    {
        if (sigismember(&set, signo)) cout << "1";
        else cout << "0";
    }
    cout << endl;
}
void handler(int signo)
{
    cout << "catch a signal: " << signo << endl;
    while (true)
    {
        PrintPending();
        sleep(1);
    }
}

int main()
{
    struct sigaction act, oact;  //这里可以省略struct,不省略是因为能够提醒我们是系统结构体。 
    memset(&act, 0, sizeof(act)); 
    memset(&oact, 0, sizeof(oact)); 
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask); //先清空一下samask
    sigaddset(&act.sa_mask, 1); //添加1号比特位为1
    sigaddset(&act.sa_mask, 3); //添加1号比特位为1


    sigaction(2, &act, &oact);
    
    while (true)
    {
        cout << "I am a process: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

运行结果:

然后我们就会发现, 处理2号信号的时候, 1, 3号信号都被屏蔽了。 

函数重入

假如我们有下面的节点定义, 头插函数和自定义捕捉动作

现在我们有下面这个链表

现在我们执行insert。 

此时的链表结构是这样的:

因为信号捕捉又是插入一个新节点:

 所以, 就会再次进入insert函数插入新节点。那么此时的链表节点就是这样的:

最后, 信号捕捉完毕后, 再将原本的insert函数执行完, 就变成了最终结果, 如下:

        以上这种情况就是一个函数被重复进入了, 也叫做函数重入。 并且, 上面的listnode3我们称为节点发生了丢失。 即因为节点丢失导致的内存泄漏问题。 

        上面有两个问题, 这两个问题合起来就是, 函数重入导致的节点丢失问题。 而我们把这种重入后会有问题的函数称为不可重入函数即: 如果一个函数被重复进入的情况下, 出错了, 或者可能出错, 我们就把他叫做不可重入函数 否则叫做可重入函数。  可不可重入是特点, 不含褒贬。 目前我们学过的大部分函数, 都是不可重入的。 

volatile关键字

volatile在信号的角度怎么理解呢? 这里我们通过一串代码来进行理解。 首先写下下面的代码:


int flag = 0;

int main()
{
    while (!flag);
    
    cout << "process quit success" << endl;
    return 0;
}

 对于这串代码, 这串代码正常情况下是不能退出的, 会一直死循环。但是我们这里重新定义一下2号信号的处理方法:


int flag = 0;
void handler(int signo)
{
    cout << "catch a signal: " << signo << endl;
    flag = 1;
}


int main()
{
    signal(2, handler);

    while (!flag);
    
    cout << "process quit success" << endl;
    return 0;
}

这样, 如果我们捕捉2号后, flag被置为1. 那么就应该退出循环, 然后打印程序退出成功。 那么真实情况下是这样吗? 

        确实是我们预测的这样。 但是, 在极端情况下, handler和main是属于不同的执行流的。 编译器在编译的过程中会发现在main函数中, 没有任何地方去修改flag, 因为我们的while循环只是在对flag做检测, 并没有任何地方去修改。 所以编译器就可能会对flag做优化, 在优化条件下, flag变量就有可能被直接优化到cpu的寄存器当中, 为什么? 因为我们的flag在一个执行流中没有被修改; 而且, 我们也知道, while循环本身就能进行判断, 这种判断是一种计算, 计算本身都是在cpu中进行的。 而且cpu只会进行两种计算, 一种叫做算数计算, 一种叫做逻辑计算。 而这里的while判断就是逻辑运算, 所以就必须把flag放到cpu中进行计算。 那么flag既不会被修改, 又会到cpu中进行逻辑运算。那么flag就有可能会被优化到寄存器当中。 

        那么我们如何控制它进行优化呢?那么我们打开man g++, 然后/-O, 就能看到下面这个:

有-O1, -O2等等。 这里面的123是优化级别-O就是控制优化等级

这里我们直接控制优化等级3, 观察效果:

        我们就会发现程序退出不了了。 为什么呢? 正常来说, 我们的2号信号被捕捉后, flag置为1. 逻辑反就是0, 那么程序就应该被退出。 为什么没有退出呢? 

        那么我们看下面这张图:

        左边是cpu, 右边是物理内存。 我们需要知道的是, 不管怎么优化, 我们对应的变量, 一定会在内存里面开辟。 我们一开始cpu做检测, 就是将内存里面的变量加载到寄存器然后做逻辑检测, 检测之后有结果了再控制逻辑。 但是现在不做这个工作了, 只是在第一次的时候将flag直接放到寄存器里, 然后cpu从寄存器里面直接计算。 也就是说,我们的cpu计算flag不再从内存里面拿了, 而是先保存到寄存器中,以后做检测直接从寄存器里面拿。 问题是这样对于flag如果不做修改会有用, 但是我们的flag会做修改。 flag后续修改后cpu并不会检测到, 所以我们的程序就一直死循环了。 

        而volatile的核心作用就是防止编译器过度优化, 保持内存的可见性!!!凡是用volatile做修饰的变量, 就是在警告编译器不要做任何优化。 

SIGCHLD

        子进程退出并不是单纯的静悄悄的退出, 当它退出的时候, 它会向父进程发送信号, 这个信号就是SIGCHLD(17号。 怎么证明会发送这个信号呢? ——是不是我们父进程对17号信号进行捕捉, 然后创建一个子进程, 等到子进程退出的时候看看会不会捕捉到17号信号就可以了? 所以, 下面为代码:

void handler(int signo)
{
    cout << "catch a signal: " << signo << endl;
}


int main()
{
    signal(17, handler);

    pid_t id = fork();   //子进程返回0, 父进程返回子进程pid

    if (id == 0) 
    {
        while (true)
        {
            cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
            sleep(1);
            break;
        }
        exit(0); 
    }
    while (true)
    {
        cout << "I am a father process: " << getpid() << endl; 
        sleep(1); 
    }
    return 0;
}

然后我们就会看到运行结果, 一秒过后, 父进程就受到了一个signal信号! 

        所以, 我们就知道, 子进程在等待的时候, 我们可以采用基于信号的方式进行等待!等待的好处是什么?

  •         可以获取子进程的退出状态。 
  •         释放子进程的僵尸。
  •         通过阻塞或者非阻塞轮询, 我们虽然不知道父子进程谁先运行, 但是父进程一定是最后退出的!

        今天的基于信号方式进行等待还是要调用wait/waitpid这样的接口。 父进程一直得保证自己是一直在运行的(子进程不要孤儿) 那么如何做? ——就是把子进程等待写入信号的捕捉函数当中!!

        我们利用下面的代码进行验证,我们想要的现象是前五秒程序父进程在跑, 子进程也在跑。 然后子进程退出, 接下来的五秒子进程变成教室, 父进程还在跑。 最后子进程被回收!

void handler(int signo)
{
    sleep(5);
    pid_t rid = waitpid(-1, nullptr, 0);
    cout << "I am child process: " << getpid() << ", ppid: " << getppid() << "catch a signal: " << signo << endl;
}


int main()
{
    signal(17, handler);

    pid_t id = fork();   //子进程返回0, 父进程返回子进程pid

    if (id == 0) 
    {
        while (true)
        {
            cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
            sleep(5);
            break;
        }
        exit(0); 
    }
    while (true)
    {
        cout << "I am a father process: " << getpid() << endl; 
        sleep(1); 
    }
    return 0;
}

运行结果:

        我们就会发现中间正好五秒僵尸继承, 和我们预想的是一样的。  

        但是, 如果有多个子进程呢? 比如有10个子进程, 那么我们如果有一个子进程先退出。 然后捕捉信号, 然后其他子进程再一起退出。 但是这个时候17号信号屏蔽了。 所以其他子进程的信号阻塞了, 丢失了, 所以也就意味着我们最终只能够回收一两个进程。 其他的进程无法被回收。 遇到这种情况, 怎么才能正常处理呢? ——我们只要使用一个循环加非阻塞轮询, 就可以成功的解决这个问题,代码如下:

void handler(int signo)
{
    sleep(5);
    pid_t rid;
    while (pid_t rid = waitpid(-1, nullptr, WNOHANG) > 0)
    {
        cout << "I am child process: " << getpid() << ", ppid: " << getppid() << "catch a signal: " << signo << endl;
    }
}


int main()
{
    signal(17, handler);

    pid_t id = fork();   //子进程返回0, 父进程返回子进程pid

    if (id == 0) 
    {
        while (true)
        {
            cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
            sleep(5);
            break;
        }
        exit(0); 
    }
    while (true)
    {
        cout << "I am a father process: " << getpid() << endl; 
        sleep(1); 
    }
    return 0;
}

         这样, 我们的程序就能够将多个进程同时回收了!

——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!! 

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

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

相关文章

CSS——文字渐入效果

CSS——文字渐入效果 昨天制作了文字的打字机效果&#xff08;CSS——文字打字机效果&#xff09;&#xff0c;然后我想到有些网页的文字效果是平滑渐入的&#xff0c;我就去思考这样的实现方式&#xff0c;其实就把之前的steps()函数去掉即可&#xff0c;但是我想换种实现方式…

电脑无法无线投屏的解决办法

在前司的时候经常遇到电脑无法使用无线投屏器的情况&#xff0c;今天就来聊聊如何解决。 1.不会连接。这种情况&#xff0c;经常发生在WIN10升级WIN11之后&#xff0c;一般是两种办法&#xff0c;一种是同时按键盘上的WINDOWS和K键&#xff0c;右下角就会出来连接的图标&#…

Day8:返回倒数第k个节点

题目&#xff1a; 实现一种算法&#xff0c;找出单向链表中倒数第k个节点。返回该结点的值。 示例&#xff1a; 输入&#xff1a;1->2->3->4->5和k2 输出&#xff1a;4 说明&#xff1a; 给定的k保证是有效的。 public int kthToLast(ListNode head,int k){…

《动手学深度学习》Pytorch 版学习笔记一:从预备知识到现代卷积神经网络

前言 笔者有一定的机器学习和深度学习理论基础&#xff0c;对 Pytorch 的实战还不够熟悉&#xff0c;打算入职前专项突击一下 本文内容为笔者学习《动手学深度学习》一书的学习笔记 主要记录了代码的实现和实现过程遇到的问题&#xff0c;不完全包括其理论知识 引用&#x…

GRASP七大基本原则+纯虚构防变异

问题引出 软件开发过程中&#xff0c;需要设计大量的类&#xff0c;使他们交互以实现特定的功能性需求。但是不同的设计方式&#xff0c;对程序的非功能性需求&#xff08;可扩展性&#xff0c;稳定性&#xff0c;可维护性等&#xff09;的实现程度则完全不同。 有没有一种统一…

动态规划算法——三步问题

1.题目解析 2.算法原理 本题可以近似看做泰波那契数列&#xff0c;即小孩到第一个台阶需要一步&#xff0c;到第二个台阶则是到第一个台阶的步数加上第一阶到第二阶的步数&#xff0c;同理第三阶就是第二阶的步数加上第二阶到第三阶的步数&#xff0c;由于小孩只能走三步&#…

基于STM32的智能垃圾桶控制系统设计

引言 本项目设计了一个基于STM32微控制器的智能垃圾桶控制系统&#xff0c;能够通过超声波传感器检测手部动作&#xff0c;自动打开或关闭垃圾桶盖&#xff0c;提升用户的便利性和卫生性。该项目展示了STM32微控制器在传感器检测、伺服电机控制和嵌入式智能控制中的应用。 环…

在不支持WSL2的Windows环境下安装Redis并添加环境变量的方法

如果系统版本支持 WSL 2 可跳过本教程。使用官网提供的教程即可 官网教程 查看是否支持 WSL 2 如果不支持或者觉得麻烦可以按照下面的方式安装 下载 点击打开下载地址 下载 zip 文件即可 安装 将下载的 zip 文件解压到自己想要解压的地方即可。&#xff08;注意&#x…

毕业设计选题:基于ssm+vue+uniapp的模拟考试小程序

开发语言&#xff1a;Java框架&#xff1a;ssmuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;M…

2024最新 Navicat Premium 17 简体中文版安装图文详细教程

Navicat 17 引入了一系列新特性&#xff0c;旨在提升用户体验和工作效率。以下是一些值得关注的新功能&#xff1a; ‌模型工作区的全面重新设计‌&#xff1a;包含了增强的图表设计、更强大的同步工具、数据字典支持等多项功能。这有助于在一个工作区中创建多个模型&#xff0…

集合论基础 - 离散数学系列(一)

目录 1. 集合的基本概念 什么是集合&#xff1f; 集合的表示方法 常见的特殊集合 2. 子集与幂集 子集 幂集 3. 集合的运算 交集、并集与补集 集合运算规则 4. 笛卡尔积 5. 实际应用 6. 例题与练习 例题1 练习题 总结 引言 集合论是离散数学的基础之一&#xff…

HarmonyOS第一课 04 应用程序框架基础-习题分析

判断题 1.在基于Stage模型开发的应用项目中都存在一个app.json5配置文件、以及一个或多个module.json5配置文件。T 正确(True) 错误(False) 这个答案是T - AppScope > app.json5&#xff1a;app.json5配置文件&#xff0c;用于声明应用的全局配置信息&#xff0c;比如应用…

利用大规模语言模型提高生物医学 NER 性能的新方法

概述 论文地址&#xff1a;https://arxiv.org/pdf/2404.00152.pdf 大规模语言模型在零拍摄和四拍摄任务中表现出色&#xff0c;但在生物医学文本的独特表达识别&#xff08;NER&#xff09;方面仍有改进空间。例如&#xff0c;Gutirrez 等人&#xff08;2022 年&#xff09;的…

Chrome浏览器调用ActiveX控件--allWebOffice控件功能介绍

allWebOffice控件概述 allWebOffice控件能够实现在浏览器窗口中在线操作文档的应用&#xff08;阅读、编辑、保存等&#xff09;&#xff0c;支持编辑文档时保留修改痕迹&#xff0c;支持书签位置内容动态填充&#xff0c;支持公文套红&#xff0c;支持文档保护控制等诸多办公功…

医院伤病员食堂批量打印—未来之窗行业应用跨平台架构

一、订单后厨打印批量 在医院伤员管理中&#xff0c;预约订单现场打印的方式往往不太合适。现场打印可能会导致效率低下&#xff0c;尤其在伤员较多、情况紧急的时候&#xff0c;容易造成混乱和延误。 采用统一打印的方式具有诸多优势。首先&#xff0c;能够集中处理打印任务&…

猴子吃桃-C语言

1.问题&#xff1a; 猴子第一天摘下若干个桃子&#xff0c;当即吃了一半&#xff0c;还不过瘾&#xff0c;又多吃了一个。 第二天早上又将剩下的桃子吃掉一半&#xff0c;又多吃一个。以后每天早上都吃了前一天剩下的一半零一个。 到第N天早上想再吃时&#xff0c;见只剩下一个…

ctf.bugku - SOURCE

题目来源&#xff1a; source - Bugku CTF 首先&#xff0c;访问页面&#xff0c; 得到的是假的 flag &#xff0c; 查看前端页面、代码、response返回&#xff1b; 没有有用信息&#xff1b; 查后端&#xff1a; git泄露 下载git文件 # wget -r http://114.67.175.224:156…

SIE将使用AI和机器学习加速游戏开发

索尼在一份新的索尼公司报告中透露&#xff0c;PlayStation将利用人工智能和机器学习来加快游戏开发速度。在报告的第16页&#xff0c;索尼表示&#xff1a;“加强能够帮助创作者以高效、高质量的方式最大化其IP价值的技术&#xff0c;包括传感和捕捉以及实时3D处理、人工智能和…

IDM6.42免费安装破解注册(Internet Download Manager)

01 到官网或者网盘下载安装包 中文官网链接&#xff1a; https://souurl.cn/9nbWw2 优惠码&#xff1a;WMHRDIDM5 夸克网盘&#xff1a;https://pan.quark.cn/s/885c8a9e487e​​​​​​​ 02 Powershell脚本使用: 总的来说&#xff0c;这段代码的目的是通过管理员身份下…

Spring Boot助力医院数据管理

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常适…