【Linux】25.进程信号(2)

news2025/2/5 14:22:27

文章目录

  • 4.捕捉信号
    • 4.1 重谈地址空间
    • 4.2 内核如何实现信号的捕捉
    • 4.3 sigaction
    • 4.4 可重入函数
    • 4.5 volatile
    • 4.6 SIGCHLD信号(了解)


4.捕捉信号

4.1 重谈地址空间

fdeb1e02c9a416380118e84ff616ef0a

  1. 用户页表有几份?

    有几个进程,就有几份用户级页表–进程具有独立性

  2. 内核页表有几份?

    1份

  3. 每一个进程看到的3~4GB的东西都是一样的。整个系统中进程再怎么切换,3,4GB的空间的内容是不变的。

  4. 进程视角:我们调用系统中的方法,就是在我自己的地址空间中进行执行的。

  5. 操作系统视角:任何一个时刻,都有有进程执行。我们想执行操作系统的代码,就可以随时执行。

  6. 操作系统的本质:基于时钟中断的一个死循环。

  7. 计算机硬件中,有一个时钟芯片,每个很短的时间,向计算机发送时钟中断


4.2 内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

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

92c6c6fb5d1b186954273d41583c30a1


4.3 sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。actoact指向sigaction结构体。

  • sa_handler赋值为常数SIG_IGN,传给sigaction表示忽略信号。赋值为常数SIG_DFL,表示执行系统默认动作。赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

sigaction()函数中的两个指针参数 *act*oldact 有不同的用途:

  1. *act (新动作):

    • 指向要设置的新的信号处理方式

    • 如果不为NULL,系统会按照这个结构体设置新的信号处理方式

    • 用于指定我们"想要"的信号处理方式

  2. *oldact (旧动作):

    • 用于保存信号的原有处理方式

    • 如果不为NULL,系统会将原来的信号处理方式保存在这个结构体中

    • 常用于之后恢复原有的信号处理方式

示例:

struct sigaction new_action, old_action;

// 设置新的处理方式
new_action.sa_handler = my_handler;    // 设置处理函数
sigemptyset(&new_action.sa_mask);      // 清空信号掩码
new_action.sa_flags = 0;               // 设置标志

// 设置SIGINT的处理方式,同时保存原有设置
sigaction(SIGINT, &new_action, &old_action);

// ... 一段时间后 ...

// 恢复原有的处理方式
sigaction(SIGINT, &old_action, NULL);

注意:

  • 如果只想设置新的处理方式,可以将oldact设为NULL
  • 如果只想查询当前的处理方式,可以将act设为NULLoldact指向一个结构体

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。

如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,信号处理函数返回时,这些额外屏蔽的信号也会自动解除屏蔽。

代码:

#include <iostream>
#include <cstring>
#include <ctime>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

// 信号处理函数(被注释掉的版本)
// 不是必须调用wait,但建议调用以避免僵尸进程
// void handler(int signo)
// {
//     sleep(5);  // 模拟信号处理需要一定时间
//     pid_t rid;
//     // WNOHANG: 非阻塞等待,如果没有子进程退出立即返回0
//     while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
//     {
//         cout << "I am proccess: " << getpid() << " catch a signo: " << signo 
//              << "child process quit: " << rid << endl;
//     }
// }

int main()
{
    // 忽略SIGCHLD信号(17),避免产生僵尸进程
    // SIG_DFL是默认处理方式,SIG_IGN是忽略信号
    signal(17, SIG_IGN); 

    // 创建10个子进程
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)  // 子进程
        {
            while (true)
            {
                cout << "I am child process: " << getpid() 
                     << ", ppid: " << getppid() << endl;
                sleep(5);
                break;
            }
            cout << "child quit!!!" << endl;
            exit(0);  // 子进程退出
        }
        sleep(1);  // 父进程每隔1秒创建一个子进程
    }
    
    // 父进程循环
    while (true)
    {
        cout << "I am father process: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

// 以下是被注释的其他示例代码:

// volatile关键字示例
// volatile int flag = 0;  // volatile防止编译器优化

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

// int main()
// {
//     signal(2, handler);  // 设置SIGINT(Ctrl+C)的处理函数
//     // flag可能被优化到CPU寄存器中,volatile防止这种优化
//     while(!flag);  // flag为0时循环继续

//     cout << "process quit normal" << endl;
//     return 0;
// }

// 信号pending示例
// pending位图从1变为0的时机:在执行信号处理函数之前清零
// 处理信号时会将该信号添加到block表中,防止信号处理函数被重入

// 打印当前进程的pending信号集
// void PrintPending()
// {
//     sigset_t set;
//     sigpending(&set);  // 获取当前pending的信号集

//     // 打印1-31号信号的pending状态
//     for (int signo = 1; signo <= 31; signo++)
//     {
//         if (sigismember(&set, signo))
//             cout << "1";
//         else
//             cout << "0";
//     }
//     cout << "\n";
// }

// void handler(int signo)
// {
//     cout << "catch a signal, signal number : " << signo << endl;
//     while (true)
//     {
//         PrintPending();
//         sleep(1);
//     }
// }

// sigaction使用示例
// int main()
// {
//     // struct sigaction act, oact;
//     // memset(&act, 0, sizeof(act));
//     // memset(&oact, 0, sizeof(oact));

//     // sigemptyset(&act.sa_mask);  // 清空信号屏蔽字
//     // sigaddset(&act.sa_mask, 1); // 添加要屏蔽的信号
//     // sigaddset(&act.sa_mask, 3);
//     // sigaddset(&act.sa_mask, 4);
//     // act.sa_handler = handler;    // 设置信号处理函数
//     // sigaction(2, &act, &oact);   // 设置SIGINT的处理方式

//     // while (true)
//     // {
//     //     cout << "I am a process: " << getpid() << endl;
//     //     sleep(1);
//     // }

//     return 0;
// }

这段代码主要演示了:

  1. 信号处理和僵尸进程避免
  2. 进程创建和父子进程通信
  3. volatile关键字的使用
  4. 信号的pending机制
  5. sigaction的使用方法

4.4 可重入函数

2e6f9bb11fd6b3bc5cf95107300f3e9b

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

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

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

  • 调用了mallocfree,因为malloc也是用全局链表来管理堆的。

  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

不可重入函数特征:

  1. 使用静态或全局变量
  2. 返回指向静态变量的指针
  3. 调用malloc/free
  4. 调用不可重入的系统函数

可重入函数特征:

  1. 仅使用局部变量
  2. 数据通过参数传递
  3. 不调用不可重入函数
  4. 不依赖共享资源

4.5 volatile

  1. 基本作用:
// 没有volatile的问题
int flag = 0;
while (!flag) {
    // 编译器可能优化为死循环
    // 因为编译器认为没人修改flag
}

// 使用volatile解决
volatile int flag = 0;
while (!flag) {
    // 每次都会从内存重新读取flag
    // 而不是使用寄存器中的值
}

volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

  1. 常见场景:
// 1. 信号处理
volatile sig_atomic_t signal_flag = 0;

void signal_handler(int signo) {
    signal_flag = 1;  // 修改共享变量
}

int main() {
    signal(SIGINT, signal_handler);
    while (!signal_flag) {
        // 等待信号处理程序修改flag
    }
}

// 2. 硬件寄存器访问
volatile uint32_t* hardware_reg = (uint32_t*)0x20000000;
*hardware_reg = 0x1;  // 每次都直接写入硬件
  1. volatile的特性:
volatile int counter = 0;

void example() {
    // 1. 防止优化删除
    counter++;  // 不会被优化掉
    
    // 2. 保证读写顺序
    int temp = counter;  // 确保在counter++之后读取
    
    // 3. 每次都访问内存
    for (int i = 0; i < 10; i++) {
        counter++;  // 每次都读写内存
    }
}
  1. volatile的局限:
// volatile不能保证原子性
volatile int shared = 0;

// 多线程访问时仍需要互斥锁
mutex mtx;
void thread_func() {
    lock_guard<mutex> lock(mtx);
    shared++;
}

主要作用:

  1. 防止编译器优化
  2. 保证每次都从内存读取
  3. 保证代码执行顺序
  4. 用于多线程共享或硬件访问

不能做到:

  1. 不保证原子性
  2. 不保证线程安全
  3. 不是线程同步工具

4.6 SIGCHLD信号(了解)

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

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

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

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

相关文章

洛谷 P1387 最大正方形 C语言

题目描述 在一个 n m 的只包含 0 和 1 的矩阵里找出一个不包含 0 的最大正方形&#xff0c;输出边长。 输入格式 输入文件第一行为两个整数 n, m (1 ≤ n, m ≤ 100)&#xff0c;接下来 n 行&#xff0c;每行 m 个数字&#xff0c;用空格隔开&#xff0c;0 或 1。 输出格式 …

ChatGPT提问技巧:行业热门应用提示词案例--咨询法律知识

ChatGPT除了可以协助办公&#xff0c;写作文案和生成短视频脚本外&#xff0c;和还可以做为一个法律工具&#xff0c;当用户面临一些法律知识盲点时&#xff0c;可以向ChatGPT咨询获得解答。赋予ChatGPT专家的身份&#xff0c;用户能够得到较为满意的解答。 1.咨询法律知识 举…

[吾爱出品]CursorWorkshop V6.33 专业鼠标光标制作工具-简体中文汉化绿色版

CursorWorkshop V6.33 专业鼠标光标制作工具 链接&#xff1a;https://pan.xunlei.com/s/VOIFeq5DFB9FS56Al_mT2EfdA1?pwd7ij4# 产品概述 Axialis CursorWorkshop 是一个专业光标创作工具它在 Windows 下运行&#xff0c;让您轻松创建高质量的静态和动态光标适用于 Windows …

【C语言】自定义类型讲解

文章目录 一、前言二、结构体2.1 概念2.2 定义2.2.1 通常情况下的定义2.2.2 匿名结构体 2.3 结构体的自引用和嵌套2.4 结构体变量的定义与初始化2.5 结构体的内存对齐2.6 结构体传参2.7 结构体实现位段 三、枚举3.1 概念3.2 定义3.3 枚举的优点3.3.1 提高代码的可读性3.3.2 防止…

LabVIEW涡轮诊断系统

一、项目背景与行业痛点 涡轮机械是发电厂、航空发动机、石油化工等领域的核心动力设备&#xff0c;其运行状态直接关系到生产安全与经济效益。据统计&#xff0c;涡轮故障导致的非计划停机可造成每小时数十万元的经济损失&#xff0c;且突发故障可能引发严重安全事故。传统人…

Kubernetes 中 BGP 与二层网络的较量:究竟孰轻孰重?

如果你曾搭建过Kubernetes集群&#xff0c;就会知道网络配置是一个很容易让人深陷其中的领域。在负载均衡器、服务通告和IP管理之间&#xff0c;你要同时应对许多变动的因素。对于许多配置而言&#xff0c;使用二层&#xff08;L2&#xff09;网络就完全能满足需求。但边界网关…

大模型综述一镜到底(全文八万字) ——《Large Language Models: A Survey》

论文链接&#xff1a;https://arxiv.org/abs/2402.06196 摘要&#xff1a;自2022年11月ChatGPT发布以来&#xff0c;大语言模型&#xff08;LLMs&#xff09;因其在广泛的自然语言任务上的强大性能而备受关注。正如缩放定律所预测的那样&#xff0c;大语言模型通过在大量文本数…

物理群晖SA6400核显直通win10虚拟机(VMM)

写在前面&#xff1a;请先确保你的核显驱动支持开启SR-IOV 确保你的BIOS开启了以下选项&#xff1a; VT-D VMX IOMMU Above 4G ResizeBAR 自行通过以下命令确认支持情况&#xff1a; dmesg | grep -i iommudmesg | grep DMAR分配1个虚拟vGPU&#xff1a;echo 1 | sudo tee /sy…

【python】tkinter实现音乐播放器(源码+音频文件)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;专__注&#x1f448;&#xff1a;专注主流机器人、人工智能等相关领域的开发、测试技术。 【python】tkinter实现音乐播放器&#xff08;源码…

MyBatis-Plus速成指南:常用注解

Table Name: 概述&#xff1a; MyBatis-Plus 在确定操作的表时&#xff0c;由 BaseMapper的泛型决定&#xff0c;即实体类决定&#xff0c;且默认操作的表名和实体类的类名一致 问题&#xff1a; 如果实体类类型的类名和要操作表的表名不一致会出现什么问题&#xff1f;(把 us…

Linux 压缩打包

Linux压缩打包 文章目录 Linux压缩打包压缩的意义和原理压缩的意义压缩的原理压缩与解压缩的好处 压缩打包命令.zipzip 命令用法unzip 的用法 .gzgzip 的用法gunzip 的用法 .bz2bzip2 的用法bunzip2 的用法 .xzxz 命令用法 tar 04-Linux压缩打包课后习题 压缩的意义和原理 压缩…

RabbitMQ深度探索:前置知识

消息中间件&#xff1a; 消息中间件基于队列模式实现异步 / 同步传输数据作用&#xff1a;可以实现支撑高并发、异步解耦、流量削峰、降低耦合 传统的 HTTP 请求存在的缺点&#xff1a; HTTP 请求基于响应的模型&#xff0c;在高并发的情况下&#xff0c;客户端发送大量的请求…

智慧校园平台:构建现代化教育体系的技术支撑

在当今信息技术飞速发展的时代&#xff0c;智慧校园平台成为了现代教育领域中的重要组成部分。智慧校园平台不仅能够提升学校的管理水平&#xff0c;还能提供更为个性化和高效的教学服务&#xff0c;从而促进学生的全面发展。 数据分析是智慧校园平台的重要组成部分。通过对学生…

20250204将Ubuntu22.04的默认Dash的shell脚本更换为bash

20250204将Ubuntu22.04的默认Dash的shell脚本更换为bash 2025/2/4 23:45 百度&#xff1a;dash bash https://blog.csdn.net/2201_75772333/article/details/136955776 【Linux基础】dash和bash简介 Dash&#xff08;Debian Almquist Shell&#xff09;和 Bash&#xff08;Bou…

Golang 并发机制-3:通道(channels)机制详解

并发编程是一种创建性能优化且响应迅速的软件的强大方法。Golang&#xff08;也称为 Go&#xff09;通过通道&#xff08;channels&#xff09;这一特性&#xff0c;能够可靠且优雅地实现并发通信。本文将揭示通道的概念&#xff0c;解释其在并发编程中的作用&#xff0c;并提供…

可视化大屏在石油方面的应用。

可视化大屏通过整合石油工业全链条数据&#xff0c;构建数字孪生驱动的运营监控体系&#xff0c;显著提升油气勘探、开采、储运及炼化的管理效能。其技术架构依托工业物联网&#xff08;IIoT&#xff09;实时采集钻井参数、管道压力、储罐液位等数据&#xff0c;通过OPC UA协议…

【学术投稿-2025年计算机视觉研究进展与应用国际学术会议 (ACVRA 2025)】从计算机基础到HTML开发:Web开发的第一步

会议官网&#xff1a;www.acvra.org 简介 2025年计算机视觉研究进展与应用&#xff08;ACVRA 2025&#xff09;将于2025年2月28-3月2日在中国广州召开&#xff0c;将汇聚世界各地的顶尖学者、研究人员和行业专家&#xff0c;聚焦计算机视觉领域的最新研究动态与应用成就。本次…

Axure PR 9 旋转效果 设计交互

大家好&#xff0c;我是大明同学。 这期内容&#xff0c;我们将学习Axure中的旋转效果设计与交互技巧。 旋转 创建旋转效果所需的元件 1.打开一个新的 RP 文件并在画布上打开 Page 1。 2.在元件库中拖出一个按钮元件。 创建交互 创建按钮交互状态 1.选中按钮元件&#xf…

Docker 部署教程jenkins

Docker 部署 jenkins 教程 Jenkins 官方网站 Jenkins 是一个开源的自动化服务器&#xff0c;主要用于持续集成&#xff08;CI&#xff09;和持续交付&#xff08;CD&#xff09;过程。它帮助开发人员自动化构建、测试和部署应用程序&#xff0c;显著提高软件开发的效率和质量…

计算图 Compute Graph 和自动求导 Autograd | PyTorch 深度学习实战

前一篇文章&#xff0c;Tensor 基本操作5 device 管理&#xff0c;使用 GPU 设备 | PyTorch 深度学习实战 本系列文章 GitHub Repo: https://github.com/hailiang-wang/pytorch-get-started PyTorch 计算图和 Autograd 微积分之于机器学习Computational Graphs 计算图Autograd…