【Linux-进程信号】可重入函数+volatile关键字+SIGCHLD信号+重谈系统调用

可重入函数

首先我们看一个例子,单链表的头插;

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

那么节点B就发生了节点丢失,造成内存泄漏

那么我们现在就能看懂可重入函数的定义了

可重入函数

•    定义:在多线程或多任务环境下,可以在不影响其他线程或任务的执行下安全地并行调用的函数。
•    特性:
    1.无全局或静态变量:可重入函数不依赖于共享的全局变量或静态变量,或者每次访问这些变量时能加锁或使用其他同步机制来保证线程安全。
    2.不使用不可重入的函数:如果一个函数调用了其他不可重入的函数,它也会变成不可重入的。
    3.使用局部变量:所有数据存储

在函数的局部变量中(在栈上),而不是使用全局或静态数据。
    4.不使用系统资源:不使用如文件句柄、设备等共享资源,或在使用时能保证同步访问

不可重入函数

• 定义:在多线程或多任务环境下不安全,可能在被并发调用时出现不一致或错误行为的函数。
• 特性:
  1.使用全局或静态变量:不可重入函数可能依赖于全局变量或静态变量,导致多个调用者同时修改这些数据,产生数据竞争。
  2.不安全的系统调用:如果函数依赖系统资源(如文件、网络句柄)或调用了不可重入的库函数,则会变得不可重入。
  3.无同步机制:没有合适的锁或其他同步机制来保证并发调用时数据一致性。

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

  • 调用了malloc和free,因为malloc也是用全局链表来管理堆的。(malloc每申请一个新空间,就会将该空间链入全局变量)

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

可重入函数适合在多线程环境中安全使用,而不可重入函数如果在多个线程中并发使用,会出现竞态条件或数据损坏等问题。

我们之前学习的STL都是不可重入函数,因为它使用了全局的数据结构

volatile关键字

我们都知道,编译器是会对我们的代码做优化,不信我们来观察下面代码

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

int gflag = 0;

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

int main() // 没有任何代码对gflag进行修改!!!
{
    signal(2, changedata);

    while(!gflag); // while不要其他代码
    std::cout << "process quit normal" << std::endl;
}

 当接受到2号信号时

如果我们这样编译代码呢

我们会发现当我们接受到2号信号的时候,进程并未退出;这是由于

原本CPU是对物理内存不断做检测,每次都要将物理内存的gflag加入到CPU,但这样就特别浪费时间,于是我们对编译作出优化,使得CPU与物理内存独立起来,即使物理内存已经将gflag的值发生变化,但是我们CPU内由于优化,并不会对物理内存做读取,因此进程阻塞在while循环中

我们刚刚使用的是 g++ -O1 test2.cc,这个-O 就是一个优化选项

gcc优化选项gcc优化选项
-O0这是默认的优化级别,表示不执行任何优化。编译器主要关注代码的正确性,而不是性能。
-O1这是第一个优化级别,开启了一些简单的优化,如常量折叠、死代码删除等。这个级别通常不会对代码进行大量的重排或转换。
-O2这是第二个优化级别,包含了-O1的所有优化,并增加了更多的优化,如函数内联、循环展开、死循环删除等。这个级别通常会提高代码的运行速度,但可能会增加代码大小。
-O3这是第三个优化级别,包含了-O2的所有优化,并增加了更多的高级优化,如循环向量化、更复杂的函数内联等。这个级别通常会进一步提高代码的运行速度,但可能会进一步增加代码大小,并可能导致代码更难调试。
-Os这个选项主要关注代码大小,而不是运行速度。它会尽量减小生成的代码大小,可能会牺牲一些运行速度。
-Og这个选项主要用于调试优化。它会尽量保持代码的原始结构,以便在调试时更容易理解。这个选项通常与-g(生成调试信息)一起使用。

为了避免这种优化,我们引进volatile关键字,这个就是防止优化的

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

SIGCHLD信号

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

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

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

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


void notice(int signo)
{
    std::cout << "get a signal: " << signo << " pid: " << getpid() << std::endl;
    while (true)
    {
        pid_t rid = waitpid(-1, nullptr, WNOHANG); // 阻塞啦!!--> 非阻塞方式
        if (rid > 0)
        {
            std::cout << "wait child success, rid: " << rid << std::endl;
        }
        else if (rid < 0)
        {
            std::cout << "wait child success done " << std::endl;
            break;
        }
        else
        {
            std::cout << "wait child success done " << 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)
        {
            std::cout << "I am child process, pid: " << getpid() << std::endl;
            sleep(3);
            exit(1);
        }
    }
    // father
    while (true)
    {
        DoOtherThing();
        sleep(1);
    }

    return 0;
}

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

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

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);
    }
}

重谈系统调用

系统调用的概念

系统调用(system call)提供了操作系统提供的有效服务界面。可以理解为一种可供应用程序调用的特殊函数,应用程序可以通过系统调用来请求获得操作系统内核的服务

 

操作系统如何使其系统调用可用之前,首先用一个例子来解释如何使用系统调用: 编写一个从一个文件读取数据并复制到另一个文件的简单程序。

一般应用程序开发人员根据应用程序接口(API)设计程序。

API是一系列适用于应用程序员的函数,包括传递给每个函数的参数及其返回的程序员想得到的值。

调用者不需要知道如何执行系统调用或者执行过程,它只需遵循API并了解执行系统调用后,系统给到的结果;

对于程序员,通过API操作系统接口的绝大多数细节被隐藏起来,并被执行支持库所管理。

 API、系统调用接口和操作系统之间的关系如图所示,它表现了操作系统如何处理一个调用open()系统调用的用户应用。

系统调用过程

1️⃣ 应用程序通过调用API,发起系统调用(请注意,此时程序处于用户态)。

2️⃣ 当系统调用发生时,处理器通过一种特殊的机制(此处的中断编号是int0x80),通常是中断或者异常处理,把控制流程转移到内核态的一些特定的位置((请注意,通过中断,已经从用户态切换到了内核态)。处理器模式转变成特权模式。

3️⃣ 由内核程序执行被请求的功能代码。这个功能代码代表着对一段标准程序段的执行,用以完成所请求的功能。

4️⃣ 处理结束之后,程序状态恢复系统调用之前的现场;把运行模式从特权模式恢复成为用户模式。

5️⃣ 最后通过API将控制权转移回原来的用户程序。

传递系统调用参数 -> 执行陷入指令(用户态)-> 执行相应的内请求核程序处理系统调用(核心态)-> 返回应用程序

系统调用的执行过程如下:

(1)应用程序 在 用户态 准备好调用参数,执行 int 指令触发 软中断,中断号为 0x80 ;

(2)CPU 被软中断打断后,执行对应的 中断处理函数 ,这时便已进入内核态 ;

(3)系统调用处理函数进入内核执行栈 ,并保存所有寄存器 (一般用汇编语言实现);

(4)系统调用处理函数根据系统调用号调用对应的 C 函数—— 系统调用服务例程 ;

(5)系统调用处理函数准备返回值并从内核栈中恢复寄存器 ;

(6)系统调用处理函数 执行 return 指令切换回用户态 。

请注意

(1)陷入指令是在用户态执行的,执行陷入指令之后立即引发一个内中断,使CPU进入核心态。

(2)发出系统调用请求是在用户态,而对系统调用的相应处理在核心态下进行。

常见事件的发生和处理位置

发生位置处理位置
中断用户态/内核态内核态
异常用户态/内核态内核态
缺页用户态/内核态内核态
系统调用用户态内核态
进程创建用户态/内核态内核态
进程切换内核态内核态
进程调度内核态内核态

系统调用类型

 采用系统调用有诸多好处,应用程序通过系统调用请求操作系统的服务。

 系统中的各种共享资源都由操作系统内核统一掌管,因此凡是与共享资源有关的操作(如存储分配、I/O操作、文件管理等),都必须通过系统调用的方式向操作系统内核提出服务请求,由操作系统内核代为完成。

 这样可以保证系统的稳定性和安全性,防止用户进行非法操作;

进程控制文件管理设备管理进程通信
进程结束或者放弃创建文件请求设备,释放设备创建,删除通信连接
进程装入和执行删除文件读、写、重定位发送,接受消息
进程创建打开文件取得设备属性,设置设备属性传递状态消息
进程终止关闭文件逻辑连接或断开设备信息维护连接或断开远程设备
获取或者设置进程属性读、写、重定位文件
等待事假,唤醒时间设置或者获取文件属性
分配和释放内存

 系统调用获得是操作系统提供的通用服务。

 每一个系统调用都有独一无二的系统调用号

 应用程序通过系统调用号调用。

系统调用小结

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

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

相关文章

以AI算力助推转型升级,暴雨亮相CCF中国存储大会

2024年11月29日-12月1日&#xff0c;CCF中国存储大会&#xff08;CCF ChinaStorage 2024&#xff09;在广州市长隆国际会展中心召开。本次会议以“存力、算力、智力”为主题&#xff0c;由中国计算机学会&#xff08;CCF&#xff09;主办&#xff0c;中山大学计算机学院、CCF信…

vulnhub靶场【哈利波特】三部曲之Aragog

前言 使用virtual box虚拟机 靶机&#xff1a;Aragog : 192.168.1.101 攻击&#xff1a;kali : 192.168.1.16 主机发现 使用arp-scan -l扫描&#xff0c;在同一虚拟网卡下 信息收集 使用nmap扫描 发现22端口SSH服务&#xff0c;openssh 80端口HTTP服务&#xff0c;Apach…

【Leetcode】26.删除有序数组中的重复项

题目链接&#xff1a; https://leetcode.cn/problems/remove-duplicates-from-sorted-array/description/?envTypestudy-plan-v2&envIdtop-interview-150 题目描述&#xff1a; 解题思路&#xff1a; 使用双指针算法&#xff08;快慢指针&#xff09;&#xff0c;p1与p2…

深度学习开端知识

深度学习概述 什么是深度学习 人工智能、机器学习和深度学习之间的关系&#xff1a; 机器学习是实现人工智能的一种途径&#xff0c;深度学习是机器学习的子集&#xff0c;区别如下&#xff1a; 传统机器学习算法依赖人工设计特征、提取特征&#xff0c;而深度学习依赖算法自…

Redis自学之路—高级特性(实现消息队列)(七)

目录 简介 Redis的Key和Value的数据结构组织 全局哈希表 渐进式rehash 发布和订阅 操作命令 publish 发布消息 subscribe 订阅消息 psubscribe订阅频道 unsubscribe 取消订阅一个或多个频道 punsubscribe 取消订阅一个或多个模式 查询订阅情况-查看活跃的频道 查询…

高效集成:将聚水潭数据导入MySQL的实战案例

聚水潭数据集成到MySQL&#xff1a;店铺信息查询案例分享 在数据驱动的业务环境中&#xff0c;如何高效、准确地实现跨平台的数据集成是每个企业面临的重要挑战。本文将聚焦于一个具体的系统对接集成案例——将聚水潭的店铺信息查询结果集成到MySQL数据库中&#xff0c;以供BI…

LeetCode-430. 扁平化多级双向链表-题解

题目链接 430. 扁平化多级双向链表 - 力扣&#xff08;LeetCode&#xff09; 题目介绍 你将得到一个双链表&#xff0c;节点包含一个“下一个”指针、一个“前一个”指针和一个额外的“子指针”。这个子指针可能指向一个单独的双向链表&#xff0c;并且这些链表也包含类似的特殊…

arkTS:持久化储存UI状态的基本用法(PersistentStorage)

arkUI&#xff1a;持久化储存UI状态的基本用法&#xff08;PersistentStorage&#xff09; 1 主要内容说明2 例子2.1 持久化储存UI状态的基本用法&#xff08;PersistentStorage&#xff09;2.1.1 源码1的相关说明2.1.1.1 数据存储2.1.1.2 数据读取2.1.1.3 动态更新2.1.1.4 显示…

AI 助力开发新篇章:云开发 Copilot 深度体验与技术解析

本文 一、引言&#xff1a;技术浪潮中的个人视角1.1 AI 和低代码的崛起1.2 为什么选择云开发 Copilot&#xff1f; 二、云开发 Copilot 的核心功能解析2.1 自然语言驱动的低代码开发2.1.1 自然语言输入示例2.1.2 代码生成的模块化支持 2.2 实时预览与调整2.2.1 实时预览窗口功能…

AI高中数学教学视频生成技术:利用通义千问、MathGPT、视频多模态大模型,语音大模型,将4个模型融合 ,生成高中数学教学视频,并给出实施方案。

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下AI高中数学教学视频生成技术&#xff1a;利用通义千问、MathGPT、视频多模态大模型&#xff0c;语音大模型&#xff0c;将4个模型融合 &#xff0c;生成高中数学教学视频&#xff0c;并给出实施方案。本文利用专家模…

【前端Vue】day04

一、学习目标 1.组件的三大组成部分&#xff08;结构/样式/逻辑&#xff09; ​ scoped解决样式冲突/data是一个函数 2.组件通信 组件通信语法父传子子传父非父子通信&#xff08;扩展&#xff09; 3.综合案例&#xff1a;小黑记事本&#xff08;组件版&#xff09; 拆分…

嵌入式系统应用-LVGL的应用-平衡球游戏 part2

平衡球游戏 part2 4 mpu60504.1 mpu6050 介绍4.2 电路图4.3 驱动代码编写 5 游戏界面移植5.1 移植源文件5.2 添加头文件 6 参数移植6.1 4 mpu6050 4.1 mpu6050 介绍 MPU6050是一款由InvenSense公司生产的加速度计和陀螺仪传感器&#xff0c;广泛应用于消费电子、机器人等领域…

每日十题八股-2024年12月2日

1.你知道有哪个框架用到NIO了吗&#xff1f; 2.有一个学生类&#xff0c;想按照分数排序&#xff0c;再按学号排序&#xff0c;应该怎么做&#xff1f; 3.Native方法解释一下 4.数组与集合区别&#xff0c;用过哪些&#xff1f; 5.说说Java中的集合&#xff1f; 6.Java中的线程…

git 常用命令及问题

一、常用命令 git add filename git add . git commit -m "messge" git commit --amend 修改最近一次的提交 git push origin HEAD:refs/for/master git clone url git checkout branchname 切换分支 git branch -r 查看远程仓库分支列表 git branch br…

DSD-DA

adversarial loss L a d v _{adv} adv​ g() denotes the project function&#xff0c;Gradient Reverse Layer(GRL). ROI features F ( r ) (r) (r) 补充信息 作者未提供代码

医院管理系统

私信我获取源码和万字论文&#xff0c;制作不易&#xff0c;感谢点赞支持。 医院管理系统 摘要 随着信息互联网信息的飞速发展&#xff0c;医院也在创建着属于自己的管理系统。本文介绍了医院管理系统的开发全过程。通过分析企业对于医院管理系统的需求&#xff0c;创建了一个计…

SpringBoot 整合 Avro 与 Kafka

优质博文&#xff1a;IT-BLOG-CN 【需求】&#xff1a;生产者发送数据至 kafka 序列化使用 Avro&#xff0c;消费者通过 Avro 进行反序列化&#xff0c;并将数据通过 MyBatisPlus 存入数据库。 一、环境介绍 【1】Apache Avro 1.8&#xff1b;【2】Spring Kafka 1.2&#xf…

Win10+Ubuntu20.04双系统重装Ubuntu22.04单系统

从去年 8 月美化 Ubuntu 系统后一直存在内核错误问题&#xff0c;但因为大部分功能还能正常使用&#xff0c;只是在 apt 时报错&#xff0c;所以一直逃避不想重装&#xff0c;直到最近 12 月新的开始&#xff0c;恰好设置的界面打不开得重装 gnome &#xff0c;所以下定决心重装…

Linux:进程间通信之system V

一、共享内存 进程间通信的本质是让不同的进程看到同一份代码。 1.1 原理 第一步&#xff1a;申请公共内存 为了让不同的进程看到同一份资源&#xff0c;首先我们需要由操作系统为我们提供一个公共的内存块。 第二步&#xff1a;挂接到要通信进程的地址空间中 &#xff…

Vue进阶之单组件开发与组件通信

书接上篇&#xff0c;我们了解了如何快速创建一个脚手架&#xff0c;现在我们来学习如何基于vite创建属于自己的脚手架。在创建一个新的组件时&#xff0c;要在新建文件夹中打开终端创建一个基本的脚手架&#xff0c;可在脚手架中原有的文件中修改或在相应路径重新创建&#xf…