实操(不可重入函数、volatile、SIGCHLD、线程)Linux

news2025/4/7 19:37:15

1 不可重入函数

为什么会导致节点丢失内存泄露?main函数在执行insert,但是没执行完就被信号中断了,又进了这个函数里,所以这个insert函数在不同的执行流中,同一个函数被重复进入,如果没有问题,该函数被称为可重入函数,如果有问题,该函数被称为不可重入函数

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

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

2 volatile

一开始程序疯狂的卡在while循环,如果收到了2号信号(ctrl+c),quit由0变1,逻辑反条件就不满足,输出正常退出结果

makefile

mysignal:signal.c
	gcc -o $@ $^
.PHONY:clean
clean:
	rm -f mysignal

signal.c 

#include <stdio.h>
#include <signal.h>

int quit = 0;

void handler(int signo)
{
    printf("change quit from 0 to 1\n");
    quit = 1;
    printf("quit=%d\n", quit);
}
int main()
{
    signal(2, handler);

    while(!quit);  //注意这里故意没有携带while的代码块,故意让编译器认为在main中,quit只会被检测

    printf("main 正常退出\n");

    return 0;
}


 

  • 无论你在进程里面定义多少变量,最终想能够被访问,必须要加载到物理内存里,只不过是通过虚拟地址空间让我们找到它而已,定义的全局quit变量,当二进制程序开始执行,一定要在内存里把数据(代码)开辟好,所以quit毫无疑问在内存当中;循环判断的时候是一种计算,cpu匹配运算的种类,一种是算术运算(加减乘除取模、各种浮点数运算),一种是逻辑运算(真假判断、逻辑与、逻辑取反),while(!quit)是在CPU上跑,现在的问题是你要检测的数据开始的时候是在内存里,要计算的时候是在CPU内部,所以如果你想要计算,第一件事情是将内存中的数据通过某种方式load到CPU内,cpu中有对应的寄存器、pc指针,把quit=0放进寄存器,它才会对quit做真假判断,PC指针表征当前的进程执行到哪里,如果条件满足,pc指针继续执行while(!quit)代码,如果当前条件不满足,它会让PC指针向下移动,指向下一条语句,开始向后执行,整个逻辑就是这样子的,总结一下就三步,第一要把数据从内存加载到CPU里的寄存器,第二做判断运算,第三判断完成后更改PC指针(继续执行当前语句还是下一条语句)
  • 后面加了信号的捕捉,2号信号到来的时候调用handler方法,将quit改成1,后面的逻辑依旧是要做判断,CPU从内存里读取qiit=1,逻辑反之后判断条件满不满足,更改PC指针,这是我们之前退出的原因,数据一旦被修改就可以被while循环检测到,条件不满足就退出了

实际上c++在编译的时候是有优化级别的,查看man手册(man gcc),找到优化选项(/Optimization Options)

按了2号信号,quit改成1了却还不退出的现象,退出指令(ctrl+\)

makefile(在第二行加一个 O2选项)

mysignal:signal.c
	gcc -o $@ $^ -O2
.PHONY:clean
clean:
	rm -f mysignal 

显示 

  • 在刚刚的过程中,让计算机执行while循环,每一次都要尝试着把数据从内存加载到CPU,在main函数里,我发现quit是没有被修改的,只是在被检测,编译器编译的时候发现,quit变量没有被修改,为什么要重复做把数据加载到cpu里的动作呢,只需要在代码编译形成汇编代码时,把数据加载到CPU里的寄存器,往后循环检测时直接检测寄存器里的就行了,不用再去内存里面查quit,因为每次找结果都是一样的,好比你问某个人问了好几次不同的问题,他都说不知道,那你下次再想问他问题的时候,本来要张口的,心里想着说算了,反正他也不知道,那你对这个人的判断就是以后问他什么问题他都不知道,同样的,编译器就不需要在内存里找了,直接检测寄存器里的数据就可以了,编译器就这么认为的,它会将quit变量直接优化到CPU里的寄存器当中,只有第一次会将quit加载到寄存器里,往后判断时候只检测寄存器是否为真还是假,再决策对应的指针,相当于CPU遵循就近原则,只查寄存器的值而不查内存的值,叫做内存位置不可见了,这就是为什么加了优化,quit=1也执行了,最终这个循环却不退出的原因

所以为了解决这个问题,要告诉编译器保证每次检测都要尝试着从内存中进行数据读取,不要用寄存器中的数据,让内存数据可见!就有了volatile关键字,他的作用就是杜绝对quit变量做寄存器级别的优化,保证内存可见性        

 加volatile关键字,让main正常退出

volatile int quit = 0;

显示 

3 SIGCHLD

子进程退出的时候会向父进程发SIGCHLD信号,但是父进程默认处理动作是忽略的,所以我们为了证明它会发,我们可以自定义捕捉SIGCHLD信号(现象是子进程被创建出来,先正常运行5秒,期间父进程持续做自己的事情,当信号到来的时候会等待5秒钟,但是在等期间这个子进程已经退了,且会处于僵尸状态,因为父进程还没回收它,5秒之后wait它的时候,最终我们就会看到打印等待成功,僵尸状态也会随着消失)

makefile

mysignal:signal.c
	gcc -o $@ $^ #-O2
.PHONY:clean
clean:
	rm -f mysignal 

查看waitpid第一个参数说明(man waipid)

pid_t waitpid(pid_t pid, int *wstatus, int options);

  • 大于0代表你传的参数就是你要等的进程的id,-1代表等待任意一个子进程

signal.c

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

//id暂时定义成全局的,一会方便做对比,因为信号和main函数是两个执行流
pid_t id; 

void handler(int signo)
{ 
    sleep(5);
    printf("捕捉到一个信号:%d,who:%d\n", signo, getpid());
    //-1表示等待任意一个子进程,退出结果不关心,阻塞式等待
    //虽然写了0,阻塞式等待,我们绝对不会被阻塞,因为我已经收到信号了
    //就证明当前的子进程肯定有退的,waitpid就可以返回了
    pid_t res = waitpid(-1, NULL, 0);
    if (res > 0)
    {
        //等待成功返回子进程的pid
        //id 在父进程中存储的是子进程的 PID
        printf("wait success, res: %d, id: %d\n", res, id);
    }
}

int main()
{
    signal(SIGCHLD, handler);
    id = fork();
    if (id == 0)
    {
        int cnt = 5;
        while(cnt--)
        {
            printf("我是子进程,我的pid:%d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
        exit(1);
    }
    while(1)
    {
        sleep(1);
    }

    return 0;
}

显示(while :; do ps axj | head -1&&ps axj | grep mysignal | grep -v grep; echo "---------------"; sleep 1; done)

现在来了一大堆的信号,pending位图比特位只有一个,比如第一个子进程退了,pending位设置1,第二个子进程来了,无非就是把1再置为1,此时信号不就相当于丢失了吗,相当于子进程退出发了10个信号,可能导致最终只回收了两三个子进程
signal.c

int main()
{
    signal(SIGCHLD, handler);
    int i = 1;
    for(; i <= 10; ++i)
    {
        id = fork();
        if (id == 0)
        {
            int cnt = 5; 
            while(cnt--)
            {
                printf("我是子进程,我的pid:%d, ppid: %d\n", getpid(), getppid());
                sleep(1);
            }
            exit(1);
        }
    }

    while(1)
    {
        sleep(1);
    }

    return 0;
}

显示

  • 只回收了两个子进程

所以要while式的循环回收,你有几个进程我就回收几个进程

signal.c

void handler(int signo)
{ 
    printf("捕捉到一个信号:%d,who:%d\n", signo, getpid());
    sleep(5);

    while(1)
    {
        //-1表示等待任意一个子进程,退出结果不关心,阻塞式等待
        //虽然写了0,阻塞式等待,我们绝对不会被阻塞,因为我已经收到信号了
        //就证明当前的子进程肯定有退的,waitpid就可以返回了 
        pid_t res = waitpid(-1, NULL, 0);
        if (res > 0)
        {
            //等待成功返回子进程的pid, id 在父进程中存储的是子进程的 PID
            printf("wait success, res: %d, id: %d\n", res, id);
        }   
        else break; //如果没有子进程了
    }
    printf("handler done...\n");
}

显示

假如有五个子进程退出五个子进程没退出,waitpid一直循环回收,它总会碰到没有退出的子进程,就会一直处于阻塞状态,导致handler无法返回,代码没办法再向后运行,所以要将它改成非阻塞等待,pid_t res = waitpid(-1, NULL, WNOHANG);完整代码

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

//id暂时定义成全局的,一会方便做对比,因为信号和main函数是两个执行流
pid_t id; 

void waitProcess(int signo)
{ 
    printf("捕捉到一个信号:%d,who:%d\n", signo, getpid());
    sleep(5);

    while(1)
    {
        //-1表示等待任意一个子进程,退出结果不关心,阻塞式等待
        //虽然写了0,阻塞式等待,我们绝对不会被阻塞,因为我已经收到信号了
        //就证明当前的子进程肯定有退的,waitpid就可以返回了 
        //pid_t res = waitpid(-1, NULL, 0);
        //非阻塞等待,有就回收,没有就出错返回
        pid_t res = waitpid(-1, NULL, WNOHANG);
        if (res > 0)
        {
            //等待成功返回子进程的pid, id 在父进程中存储的是子进程的 PID
            printf("wait success, res: %d, id: %d\n", res, id);
        }   
        else break; //如果没有子进程了
    }
    printf("handler done...\n");
}

int main()
{
    signal(SIGCHLD, waitProcess);
    int i = 1;
    for(; i <= 10; ++i)
    {
        id = fork();
        if (id == 0)
        {
            int cnt = 5; 
            while(cnt--)
            {
                printf("我是子进程,我的pid:%d, ppid: %d\n", getpid(), getppid());
                sleep(1);
            }
            exit(1);
        }
    }

    while(1)
    {
        sleep(1);
    }

    return 0;
}
  • 一个基于信号版的回收子进程的代码

显示

要想不产生僵尸进程,还有另一种办法,父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,往后创建的子进程在退出时就会被操作系统自动清理,不会产生僵尸进程,也不会通知父进程

signal.c(signal(SIGCHLD, waitProcess);)改成

signal(SIGCHLD, SIG_IGN);

显示

 

子进程退了,会给父进程发信号,父进程默认对该信号的处理动作是忽略,为什么还要再调一下这个signal函数呢

这其实是一种特殊情况,可以理解成在signal方法里,父进程调用signal的时候,如果他检测到你是SIGCHLD, SIG_IGN这种组合,除了设置SIGCHLD信号为忽略之外,他还会修改父进程PCB的状态位,因为子进程的状态是按父进程来的,父进程相当于可以给自己打一个标签,假设我创建的子进程以后就不要僵尸,这个标志位就会在fork的时候被子进程继承下去,所当子进程退出的时候,操作系统发现子进程退了,检测标志时发现这个进程早就设置过了标志位,你退了就直接把你释放掉了,相当于在信号handler表当中,默认是SIG_IGN,他就执行默认动作,如果你自己手动调了signal或sigaction,它是系统调用,会修改未来创建子进程的一些状态位,方便操作系统识别进而直接回收它;也可以这样理解,以前对子进程退出信号的默认动作是SIG_DFL,什么都不做,现在设置成SIG_IGN,操作系统就把它回收掉了

4 线程(看看现象)

makefile

mysignal:signal.c
	gcc -o $@ $^ -lpthread
.PHONY:clean
clean:
	rm -f mysignal 

signal.c

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void *thread1_run(void *args)
{
    while(1)
    {
        printf("我是线程1,我正在运行\n");
        sleep(1);
    }
}

void *thread2_run(void *args)
{
    while(1)
    {
        printf("我是线程2,我正在运行\n");
        sleep(1);
    }
}

void *thread3_run(void *args)
{
    while(1)
    {
        printf("我是线程3,我正在运行\n");
        sleep(1);
    }
}

int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, NULL, thread1_run, NULL);
    pthread_create(&t2, NULL, thread2_run, NULL);
    pthread_create(&t3, NULL, thread3_run, NULL);

    while(1) 
    {
        printf("我是主线程,我正在运行\n");
        sleep(1);
    }
}

查看线程(ps -aL | head -1&&ps -aL | grep mysignal)

  • 可以看出他们是属于同一个进程的,因为他们的PID都是一样的,LWP叫做轻量级进程,第一行执行流他的PID和LWP数字是一样的,那操作系统调度的时候如何区分呢,所以真正意义上,操作系统调度的时候,看的是LWP,所以CPU调度的时候,根据LWP,调度不同的线程去运行,以前我们说根据PID也没有错,因为当你的进程当中只有一个执行流时,PID和LWP数字是一样的,看哪个都行 

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

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

相关文章

【Flask开发】嘿马文学web完整flask项目第2篇:2.用户认证,Json Web Token(JWT)【附代码文档】

教程总体简介&#xff1a;2. 目标 1.1产品与开发 1.2环境配置 1.3 运行方式 1.4目录说明 1.5数据库设计 2.用户认证 Json Web Token(JWT) 3.书架 4.1分类列表 5.搜索 5.3搜索-精准&高匹配&推荐 6.小说 6.4推荐-同类热门推荐 7.浏览记录 8.1配置-阅读偏好 8.配置 9.1项目…

Ubuntu 下搭建 MCU 开发环境全流程指南(以 STM32 为例)

在嵌入式开发中,许多工程师都习惯于在 Windows 平台使用 Keil、IAR 等 IDE。然而,随着对自动化、可定制性以及开放工具链的需求增长,越来越多的开发者开始尝试在 Linux 环境下进行 MCU 开发。 本篇文章将以 STM32F1 系列 为例,手把手带你在 Ubuntu 下搭建一个完整的 MCU 开…

Python----计算机视觉处理(Opencv:道路检测之车道线拟合)

完整版&#xff1a; Python----计算机视觉处理&#xff08;Opencv:道路检测完整版&#xff1a;透视变换&#xff0c;提取车道线&#xff0c;车道线拟合&#xff0c;车道线显示&#xff09; 一、获取左右车道线的原始位置 导入模块 import cv2 import numpy as np from matplot…

如何理解神经网络中的“分段线性单元”,优雅解析前向和反向传播

什么是非线性 非线性本质上指的是一个系统或函数中输入与输出之间的关系不呈现简单的比例关系&#xff0c;也就是说&#xff0c;输出不只是输入的线性组合 ( 比如 y k 1 x 1 k 2 x 2 b ) (比如yk1x1k2x2b) (比如yk1x1k2x2b)。下面详细解释这个概念&#xff1a; 缺乏叠加性…

WVP-GB28181摄像头管理平台存在弱口令

免责声明&#xff1a;本号提供的网络安全信息仅供参考&#xff0c;不构成专业建议。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权&#xff0c;请及时与我联系&#xff0c;我将尽快处理并删除相关内容。 漏洞描述 攻击者可利用漏洞获取当前系统管…

开源身份和访问管理方案之keycloak(三)keycloak健康检查(k8s)

文章目录 开源身份和访问管理方案之keycloak&#xff08;三&#xff09;keycloak健康检查启用运行状况检查 健康检查使用Kubernetes下健康检查Dockerfile 中 HEALTHCHECK 指令 健康检查Docker HEALTHCHECK 和 Kubernetes 探针 开源身份和访问管理方案之keycloak&#xff08;三&…

Android学习总结之service篇

引言 在 Android 开发里&#xff0c;Service 与 IntentService 是非常关键的组件&#xff0c;它们能够让应用在后台开展长时间运行的操作。不过&#xff0c;很多开发者仅仅停留在使用这两个组件的层面&#xff0c;对其内部的源码实现了解甚少。本文将深入剖析 Service 和 Inte…

spring mvc异步请求 sse 大文件下载 断点续传下载Range

学习连接 异步Servlet3.0 Spring Boot 处理异步请求&#xff08;DeferredResult 基础案例、DeferredResult 超时案例、DeferredResult 扩展案例、DeferredResult 方法汇总&#xff09; spring.io mvc Asynchronous Requests 官网文档 spring.io webflux&webclient官网文…

Opencv计算机视觉编程攻略-第十节 估算图像之间的投影关系

目录 1. 计算图像对的基础矩阵 2. 用RANSAC 算法匹配图像 3. 计算两幅图像之间的单应矩阵 4. 检测图像中的平面目标 图像通常是由数码相机拍摄的&#xff0c;它通过透镜投射光线成像&#xff0c;是三维场景在二维平面上的投影&#xff0c;这表明场景和它的图像之间以及同一…

14.流程自动化工具:n8n和家庭自动化工具:node-red

n8n 安装 docker方式 https://docs.n8n.io/hosting/installation/docker/ #https://hub.docker.com/r/n8nio/n8n docker pull n8nio/n8n:latest docker rm -f n8n; docker run -it \ --network macvlan --hostname n8n \ -e TZ"Asia/Shanghai" \ -e GENERIC_TIME…

图形渲染: tinyrenderer 实现笔记(Lesson 1 - 4)

目录 项目介绍环境搭建Lesson 1: Bresenham’s Line Drawing Algorithm&#xff08;画线算法&#xff09;Lesson 2: Triangle rasterization 三角形光栅化Scanline rendering 线性扫描Modern rasterization approach 现代栅格化方法back-face culling 背面剔除 Lesson 3: Hidde…

大规模硬件仿真系统的编译挑战

引言&#xff1a; 随着集成电路设计复杂度的不断提升&#xff0c;硬件仿真系统在现代芯片设计流程中扮演着越来越重要的角色。基于FPGA&#xff08;现场可编程门阵列&#xff09;的商用硬件仿真系统因其灵活性、全自动化、高性能和可重构性&#xff0c;成为验证大规模集成电路设…

记一次常规的网络安全渗透测试

目录&#xff1a; 前言 互联网突破 第一层内网 第二层内网 总结 前言 上个月根据领导安排&#xff0c;需要到本市一家电视台进行网络安全评估测试。通过对内外网进行渗透测试&#xff0c;网络和安全设备的使用和部署情况&#xff0c;以及网络安全规章流程出具安全评估报告。本…

【8】搭建k8s集群系列(二进制部署)之安装work-node节点组件(kubelet)

一、下载k8s二进制文件 下载地址&#xff1a; https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG -1.20.md 注&#xff1a;打开链接你会发现里面有很多包&#xff0c;下载一个 server 包就够了&#xff0c;包含了 Master 和 Worker Node 二进制文件。…

使用 VIM 编辑器对文件进行编辑

一、VIM 的两种状态 VIM&#xff08;vimsual&#xff09;是 Linux/UNIX 系列 OS 中通用的全屏编辑器。vim 分为两种状态&#xff0c;即命令状态和编辑状态&#xff0c;在命令状态下&#xff0c;所键入的字符系统均作命令来处理&#xff1b;而编辑状态则是用来编辑文本资料&…

visual studio 2022的windows驱动开发

在visual studio2022中&#xff0c;若在单个组件中找不到Windows Driver Kit (WDK)选项&#xff0c;可通过提升vs版本解决&#xff0c;在首次选择时选择WDM 创建好项目在Source Files文件夹中创建一个test.c文件&#xff0c;并输入以下测试代码&#xff1a; #include <ntdd…

基于大数据的美团外卖数据可视化分析系统

【大数据】基于大数据的美团外卖数据可视化分析系统 &#xff08;完整系统源码开发笔记详细部署教程&#xff09;✅ 目录 一、项目简介二、项目界面展示三、项目视频展示 一、项目简介 该系统通过对海量外卖数据的深度挖掘与分析&#xff0c;能够为美团外卖平台提供运营决策支…

vue2打包部署到nginx,解决路由history模式下页面空白问题

项目使用的是vue2&#xff0c;脚手架vue-cli 4。 需求&#xff1a;之前项目路由使用的是hash&#xff0c;现在要求调整为history模式&#xff0c;但是整个过程非常坎坷&#xff0c;遇到了页面空白问题。现在就具体讲一下这个问题。 首先&#xff0c;直接讲路由模式由hash改为…

【数据结构】排序算法(中篇)·处理大数据的精妙

前引&#xff1a;在进入本篇文章之前&#xff0c;我们经常在使用某个应用时&#xff0c;会出现【商品名称、最受欢迎、购买量】等等这些榜单&#xff0c;这里面就运用了我们的排序算法&#xff0c;作为刚学习数据结构的初学者&#xff0c;小编为各位完善了以下几种排序算法&…

AI随身翻译设备:从翻译工具到智能生活伴侣

文章目录 AI随身翻译设备的核心功能1. 实时翻译2. 翻译策略3. 翻译流程4. 输出格式 二、AI随身翻译设备的扩展功能1. 语言学习助手2. 旅行助手3. 商务助手4. 教育助手5. 健康助手6. 社交助手7. 技术助手8. 生活助手9. 娱乐助手10. 应急助手 三、总结四、未来发展趋势&#xff0…