【Linux系统编程】第四十弹---深入理解操作系统:信号捕捉、可重入函数、volatile关键字与SIGCHLD信号解析

news2024/11/7 9:26:25

 ✨个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

目录

1、捕捉信号

1.1、内核如何实现信号的捕捉

1.2、内核态与用户态

1.3.1、用户态(User Space)

1.3.2、内核态(Kernel Space)

1.3.3、用户态与内核态的交互

1.3.4、再谈地址空间

1.3、键盘输入数据过程

1.4、OS如何正常的运行

1.5、sigaction 

2、可重入函数

3、volatile

4、SIGCHLD信号


1、捕捉信号

1.1、内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

用户程序注册了SIGQUIT信号的处理函数sighandler。

  • 1、当前正在执行main函数,这时发生中断或异常切换到内核态
  • 2、在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达
  • 3、内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。
  • 4、sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

信号可能不会被立即处理(信号被阻塞),而是在合适的时候处理。

进程从内核态返回到用户态的时候进行处理。

在内核态切回用户态时,进行信号的处理与检测。 

1.2、内核态与用户态

1.3.1、用户态(User Space)

  1. 定义:用户态是应用程序运行的空间。当用户进程运行时,它会在用户态执行大部分的操作。

  2. 权限:用户态的代码具有较低的权限,不能直接访问硬件资源,也不能执行特权指令(如修改内存保护、执行I/O操作等)。这些操作必须通过系统调用(System Call)来请求内核来完成。

  3. 稳定性与安全性:由于用户态的代码权限较低,即使发生错误(如段错误、缓冲区溢出等),也不会对整个系统造成致命的影响。

  4. 示例:常见的运行在用户态的程序包括文本编辑器、浏览器、数据库等。

1.3.2、内核态(Kernel Space)

  1. 定义:内核态是操作系统内核运行的空间。内核是操作系统的核心部分,负责管理硬件、内存、进程、文件系统、网络等系统资源。

  2. 权限:内核态的代码具有较高的权限,可以直接访问硬件资源,执行特权指令。这使得内核能够执行各种底层操作,如设备驱动、中断处理、内存管理等。

  3. 稳定性与安全性:由于内核态的代码权限较高,如果内核代码出现错误(如内核崩溃、漏洞等),可能会导致整个系统崩溃或受到攻击。因此,内核代码需要格外小心地进行编写和测试。

  4. 系统调用接口:内核通过提供系统调用接口(System Call Interface, SCI)来与用户态程序进行交互。用户态程序通过系统调用请求内核执行特权操作。

  5. 示例:内核态的主要组成部分包括进程调度器、内存管理器、设备驱动程序、网络堆栈等。

1.3.3、用户态与内核态的交互

  1. 系统调用:当用户态程序需要执行特权操作时,它会通过系统调用接口请求内核完成该操作。系统调用是一种从用户态切换到内核态的机制。

  2. 中断和异常:除了系统调用外,中断和异常也是用户态与内核态交互的重要方式。例如,硬件中断(如I/O完成中断)会触发内核代码的执行,而异常(如除零异常)则可能导致内核接管并处理错误。

  3. 上下文切换:当用户态程序执行系统调用时,CPU会从用户态切换到内核态,并保存用户态的上下文(如寄存器值、堆栈指针等)。当系统调用完成后,CPU会恢复用户态的上下文并继续执行用户态程序。

1.3.4、再谈地址空间

基本认知:

1、无论进程如何切换,我们总能找到OS

2、我们访问的OS,实际上还是在我们的地址空间中进行的,和我们访问库函数没区别

3、OS不相信任何用户,因此用户访问[3,4]G的地址空间(内核空间)时,要收到一定的约束,只能使用系统调用

4、内核级页表只需要维护一份

1.3、键盘输入数据过程

键盘输入的过程是一个涉及硬件、驱动程序、操作系统以及用户界面的复杂交互过程。

1.4、OS如何正常的运行

1.5、sigaction 

sigaction - 检查和改变信号行为

#include <signal.h>

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

参数:
    signum:指定要设置的信号编号。这个参数可以是除SIGKILL及SIGSTOP
外的任何一个特定有效的信号。为这两个信号定义自己的处理函数,将导致信号安装错误。
    act:指向struct sigaction结构体的指针,该结构体中指定了对特定信号的处理方式。
如果为NULL,则进程会以缺省方式对信号处理。
    oldact:如果不为NULL,则保存原来对相应信号的处理方式。
如果不需要保存旧的处理方式,可以将其设置为NULL。

struct sigaction {  
    void (*sa_handler)(int);    // 或 union 中的 _sa_handler  
    void (*sa_sigaction)(int, siginfo_t *, void *); // 三参数信号处理函数  
    sigset_t sa_mask;           // 信号屏蔽字  
    int sa_flags;               // 标志位  
    // 以下成员已过时,POSIX不支持,不应再使用  
    void (*sa_restorer)(void);    
};

    sa_handler:这是一个指向信号处理函数的指针,与signal()函数的handler参数类似。
当接收到指定信号时,将调用此函数。但请注意,如果设置了SA_SIGINFO标志位,
则应使用sa_sigaction而不是sa_handler。
    sa_sigaction:这是一个三参数信号处理函数,当设置了SA_SIGINFO标志位时,
将使用此函数处理信号。它提供了关于信号的更多信息,如信号编号、信号来源等。
    sa_mask:定义了一组信号,在调用由sa_handler或sa_sigaction所定义的处理器程序时,
将阻塞这些信号,防止它们中断处理器程序的执行。
    sa_flags:位掩码,指定用于控制信号处理过程的各种选项。常用的标志位包括:
        SA_NODEFER:捕获该信号时,不会在执行处理器程序时将该信号自动添加到进程掩码中。
        SA_ONSTACK:针对此信号调用处理器函数时,使用了由sigaltstack()安装的备选栈。
        SA_RESETHAND:当捕获该信号时,会在调用处理器函数之前将信号处置重置为默认值(即SIG_IGN)。
        SA_SIGINFO:调用信号处理器程序时携带了额外参数,其中提供了关于信号的深入信息。
        SA_RESTART:执行信号处理后自动重启动先前中断的系统调用。

struct sigaction 结构体通常包含以下字段:

  • sa_handler 或 sa_sigaction:一个指向信号处理函数的指针,或者是 SIG_IGN(忽略信号)或 SIG_DFL(采用默认行为)。
  • sa_mask:一个信号集,指定在信号处理函数执行期间应该阻塞哪些信号。
  • sa_flags:一组标志,用于修改 sigaction 的行为。例如,SA_RESTART 标志指示被信号中断的系统调用应该自动重启。
  • sa_restorer:(已废弃)用于恢复旧的信号处理机制,现代代码中不应使用。

代码演示

打印pending表

void Print(sigset_t& pending)
{
    for(int sig = 31;sig >= 1;sig--)
    {
        if(sigismember(&pending,sig))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << "\n";
}

自定义捕捉方法

void handler(int signum)
{
    std::cout << "get a sig: " << signum << std::endl;
    while(true)
    {
        sigset_t pending;
        sigpending(&pending);

        Print(pending);
        sleep(1);
        //break;
    }
}

主函数

int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask); // 初始化信号集
    //sigaddset(&act.sa_mask,3); // 将3号信号添加到阻塞信号集
    // 将2号信号添加到阻塞信号集,如果进程再次接收到2号信号,它将被阻塞
    // 直到handler函数执行完毕或信号被解除阻塞
    sigaddset(&act.sa_mask,2); 
    act.sa_flags = 0;

    // 使用sigaction为1到31号的每个信号设置了一个相同的处理函数handler
    for(int i=1;i<=31;i++)
         sigaction(i,&act,&oact);
    //sigaction(2,&act,&oact);

    while(true)
    {
        std::cout << "I am a process,pid: " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

2、可重入函数

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

由于函数调用栈的独立性、线程隔离、递归调用的独立性以及作用域和生命周期的限制,两个不同的控制流程调用同一个函数时,访问其同一个局部变量或参数不会造成错乱。

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

3、volatile

  • 该关键字在C当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下 

代码演示

int gflag = 0;

void changedata(int sig)
{
    std::cout << "get a sig: " << sig << ",change gflag 0->1 " << std::endl;
    gflag = 1;
}

int main()
{
    signal(2,changedata);
    while(!gflag); // 不需要其他代码
    std::cout << "process quit formal!" << std::endl;
    return 0;
}

分析代码 

从上面现象我们可以看到,如果对该程序编译进行优化,就会一直循环,为了解决该问题,可以使用volatile关键字(保持内存可见性)修饰全局变量 

  • volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作 
volatile int gflag = 0; // 保持内存可见性,一直从内存加载到CPU

运行结果

4、SIGCHLD信号

进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。


其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

代码演示 

void notice(int sig)
{
    std::cout << "get a sig: " << sig << ",pid: " << getpid() << std::endl;
}

void DoOtherThing()
{
    std::cout << "DoOtherThing()~" << std::endl;
}

int main()
{
    signal(SIGCHLD,notice);
    pid_t id = fork();
    if(id == 0)
    {
        // child
        std::cout << "I am child process,pid: " << getpid() << std::endl;
        sleep(3);
        exit(1);
    }
    // father
    while(true)
    {
        DoOtherThing();
        sleep(1);
    }
    return 0;
}

运行结果  

子进程变僵尸进程 

请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定 义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。

修改上面验证子进程在终止时会给父进程发SIGCHLD信号中的notice函数代码即可。

void notice(int sig)
{
    std::cout << "get a sig: " << sig << ",pid: " << getpid() << std::endl;
    pid_t rid = waitpid(-1,nullptr,0); // 阻塞
    if(rid > 0)
    {
        std::cout << "wait child success pid: " << getpid() << std::endl;
    }
    else if(rid < 0)
    {
        std::cout << "wait child failed!!!" << std::endl; 
    }
}

 waitpid(-1,nullptr,0);  -1表示等待任何子进程

 运行结果 

问题1: 如果一共有10个子进程,且同时退出呢? 

在回收子进程的时候,打一个死循环即可,有进程就回收!

代码演示 

void notice(int sig)
{
    std::cout << "get a sig: " << sig << ",pid: " << getpid() << std::endl;
    while(true)
    {
        pid_t rid = waitpid(-1,nullptr,0); // 阻塞
        if(rid > 0)
        {
            std::cout << "wait child success pid: " << getpid() << std::endl;
        }
        else if(rid < 0)
        {
            std::cout << "wait child failed!!!" << std::endl; 
            break;
        }
    }
   
}

void DoOtherThing()
{
    std::cout << "DoOtherThing()~" << std::endl;
}

int main()
{
    signal(SIGCHLD,notice);
    for(int i=0;i<10;i++)
    {
        pid_t id = fork();
        if(id == 0)
        {
            // child
            std::cout << "I am child process,pid: " << getpid() << std::endl;
            sleep(3);
            exit(1);
        }
    }
    
    // father
    while(true)
    {
        DoOtherThing();
        sleep(1);
    }
    return 0;
}

运行结果 

问题2: 如果一共有10个子进程, 5个退出,5个永远不退出呢? 

回收需要退出的子进程即可,使用非阻塞等待子进程。

代码演示 

void notice(int sig)
{
    std::cout << "get a sig: " << sig << ",pid: " << getpid() << std::endl;
    while(true)
    {
        pid_t rid = waitpid(-1,nullptr,WNOHANG); // 阻塞 -> 非阻塞
        if(rid > 0)
        {
            std::cout << "wait child success pid: " << getpid() << std::endl;
        }
        else if(rid < 0)
        {
            std::cout << "wait child failed!!!" << std::endl; 
            break;
        }
        else
        {
            std::cout << "wait child success done " << std::endl;
            break;
        }
    }
   
}

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可 用。

int main()
{
    signal(SIGCHLD, SIG_IGN); // 收到设置对SIGCHLD进行忽略即可
    pid_t id = fork();
    if (id == 0)
    {
        int cnt = 5;
        while (cnt)
        {
            std::cout << "child running" << std::endl;
            cnt--;
            sleep(1);
        }

        exit(1);
    }
    while (true)
    {
        std::cout << "father running" << std::endl;
        sleep(1);
    }
}

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

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

相关文章

【面试】数组中 Array.forEach()、Array.map() 遍历结束后是否改变原数组

forEach 、map 同理 数组内元素是 基本数据类型 时&#xff0c; 1.1. 直接给 item赋值&#xff0c;是 不会改变原数组 的&#xff08;如图中1&#xff09;&#xff0c; 1.2. 通过 原数组索引赋值 是会改变原数组的&#xff08;如图中2&#xff09;数组内元素是 复杂数据类型 时…

List<T>属性和方法使用

//author&#xff1a;shark_ddd using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;//使用函数来减少长度namespace List_T {class Student{public string Name { get; set; }public int Age { get; set; …

liunx CentOs7安装MQTT服务器(mosquitto)

查找 mosquitto 软件包 yum list all | grep mosquitto出现以上两个即可进行安装&#xff0c;如果没有出现则需要安装EPEL软件库。 yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm查看 mosquitto 信息 yum info mosquitto安装 mosquitt…

视频去水印软件哪个好?这些软件值得一试

无论是社交媒体上的短视频&#xff0c;还是专业网站上的长视频&#xff0c;去除视频中的水印成为了许多人的需求。 选择一款合适的视频去水印软件&#xff0c;可以帮助我们轻松去除视频中的不必要标记&#xff0c;保持视频的完整性和美观。 那么&#xff0c;视频去水印软件哪…

qt QDoubleSpinBox详解

1、概述 QDoubleSpinBox是Qt框架中的一个控件&#xff0c;专门用于浮点数&#xff08;即小数&#xff09;的输入和调节。它提供了一个用户界面元素&#xff0c;允许用户在预设的范围内通过拖动滑块、点击箭头或使用键盘来递增或递减浮点数值。QDoubleSpinBox通常用于需要精确数…

在基于AWS EC2的云端k8s环境中 搭建开发基础设施

中间件下载使用helm,这里部署的都是单机版的 aws-ebs-storageclass.yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata:name: aws-ebs-storageclass provisioner: kubernetes.io/aws-ebs parameters:type: gp2 # 选择合适的 EBS 类型&#xff0c;如 gp2、io1…

MATLAB与STK互联:仿真并获取低轨卫星与指定区域地面站的可见性数据

MATLAB控制STK实现&#xff1a;仿真并获取低轨卫星与指定区域地面站的可见性数据 本次仿真主要参考了多篇文献和网站&#xff0c;包括但不限于&#xff1a;《Using MATLAB for STK Automation》、CSDN博文&#xff1a; 拜火先知的博客_CSDN博客-笔记、AGI官网有关MATLAB的内容…

docker engine stopped

1&#xff09;环境&#xff1a;win 10 2&#xff09;docker安装时已经已经安装了虚拟机 3&#xff09;启用网络适配器 4&#xff09;启用docker服务&#xff08;依赖服务LanmanServer&#xff09; 5&#xff09;全都弄好了&#xff0c;docker还是打不开&#xff0c;没办法了&a…

天翼网关 3.0 兆能 ZNHG600 获取超级密码改桥接

本文首发于只抄博客&#xff0c;欢迎点击原文链接了解更多内容。 前言 前不久朋友家断网&#xff0c;喊了宽带师傅修完之后&#xff0c;光猫就从桥接模式变成了路由模式。虽然对于日常上网来说区别不大&#xff0c;但这条宽带有公网 IP&#xff0c;通过光猫拨号的话&#xff0…

C语言常见进制 (二进制、八进制、十进制、十六进制)详解

C语言常见进制的详解 放在最前面的前言&#xff1a;1、分类2、二进制&#xff08;2.1&#xff09;二进制的解释说明&#xff08;2.2&#xff09;关于二进制的计算&#xff08;2.3&#xff09; 二进制转换为八进制&#xff08;2.4&#xff09; 二进制转换为十进制 3、八进制&…

在 .NET 8 Web API 中实现 Entity Framework 的 Code First 方法

本次介绍分为3篇文章&#xff1a; 1&#xff1a;.Net 8 Web API CRUD 操作.Net 8 Web API CRUD 操作-CSDN博客 2&#xff1a;在 .Net 8 API 中实现 Entity Framework 的 Code First 方法https://blog.csdn.net/hefeng_aspnet/article/details/143229912 3&#xff1a;.NET …

初识动态规划(由浅入深)

&#x1f913; 动态规划入门与进阶指南 &#x1f4d8; 动态规划&#xff08;Dynamic Programming, DP&#xff09;是一种非常经典的&#x1f4d0;算法方法&#xff0c;特别适合用来解决那些有大量重复计算的问题&#x1f300;。它可以将复杂的问题拆分为小问题&#x1f9e9;&a…

【STM32】SD卡

(一)常用卡的认识 在学习这个内容之前&#xff0c;作为生活小白的我对于SD卡、TF卡、SIM卡毫无了解&#xff0c;晕头转向。 SD卡&#xff1a;Secure Digital Card的英文缩写&#xff0c;直译就是“安全数字卡”。一般用于大一些的电子设备比如&#xff1a;电脑、数码相机、AV…

《JVM第5课》虚拟机栈

无痛快速学习入门JVM&#xff0c;欢迎订阅本免费专栏 Java虚拟机栈&#xff08;Java Virtual Machine Stack&#xff0c;简称JVM栈&#xff0c;又称Java方法栈&#xff09;是 JVM 运行时数据区的一部分&#xff0c;主要用于支持Java方法的执行。每当一个新线程被创建时&#xf…

Java Executor RunnableScheduledFuture 总结

前言 相关系列 《Java & Executor & 目录》《Java & Executor & RunnableScheduledFuture & 源码》《Java & Executor & RunnableScheduledFuture & 总结》《Java & Executor & RunnableScheduledFuture & 问题》 涉及内容 《…

软考(中级-软件设计师)数据库篇(1101)

第6章 数据库系统基础知识 一、基本概念 1、数据库 数据库&#xff08;Database &#xff0c;DB&#xff09;是指长期存储在计算机内的、有组织的、可共享的数据集合。数据库中的数据按一定的数据模型组织、描述和存储&#xff0c;具有较小的冗余度、较高的数据独立性和扩展…

zynq PS端跑Linux响应中断

这篇文章主要是讲述如何在Zynq的PS上跑Linux启动IRQ&#xff0c;环境为vivado2019.1&#xff0c;petalinux2019.1 ubuntu20.04&#xff0c;本人初学者&#xff0c;欢迎批评指正 1. Vivado硬件设计 确保自定义IP的中断信号通过 IRQ_F2P 连接到PS端。在开始Petalinux配置之前&a…

R语言贝叶斯

原文链接&#xff1a;R语言贝叶斯进阶&#xff1a;INLA下的贝叶斯回归、生存分析、随机游走、广义可加模型、极端数据的贝叶斯分析https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247625527&idx8&snba4e50376befd94022519152609ee8d0&chksmfa8daad0cdfa…

qt QRadioButton详解

QRadioButton 是一个可以切换选中&#xff08;checked&#xff09;或未选中&#xff08;unchecked&#xff09;状态的选项按钮。单选按钮通常呈现给用户一个“多选一”的选择&#xff0c;即在一组单选按钮中&#xff0c;一次只能选中一个按钮。 重要方法 QRadioButton(QWidget…

webm格式怎么转换成mp4?这9种转换方法你肯定能够学会!

webm格式怎么转换成mp4&#xff1f;WebM&#xff0c;作为一种新兴的视频文件格式&#xff0c;虽然带着革新性的光芒&#xff0c;在视频压缩效率和播放流畅性上表现出色&#xff0c;却也面临着几个重要的挑战&#xff0c;这些问题直接影响了用户的体验&#xff0c;首先&#xff…