学习系统编程No.28【多线程概念实战】

引言:

北京时间:2023/6/29/15:33,刚刚更新完博客,目前没什么状态,不好趁热打铁,需要去睡一会会,昨天睡的有点迟,然后忘记把7点到8点30之间的4个闹钟关掉了,恶心了我自己一早上,真的是罪过呀!极度没睡好加没睡够,由于上篇博客马上就可以完成,所以中午没有选择睡觉,而是想着更新完再睡,但是现在困意不是很重,所以趁着这个没什么状态期间,将该篇博客的引言写写,然后把git提交一下,并且重点是今天出成绩啦!在持续摆烂中,和预想的一样,考的不怎么样,不过好在都及格了,其中高数应该是老师捞起来的,低分飘过,哈哈哈!摆烂过程只有我自己知道,具体不好形容,所以只要没有挂科我就已经很满足了,不过对于我来说更重要的是,信号有关知识在上篇博客我们就全部学完了,接下来终于可以进军多线程的学习了,这点令我非常的激动,不然博客标题都不知道怎么命名了,哈哈哈!正式进入该篇博客的学习,有关Linux系统下多线程的知识!

在这里插入图片描述

什么是可重入函数

可重入函数是一个新的概念,伴随着可重入函数,那么自然就有不可重入函数,那么具体什么叫可重入函数,什么叫不可重入函数,需要我们进行一定的铺垫,才能搞清楚,首先由于该知识点是在信号相关知识,并且与内核态和用户态之间发生进程调度相关,所以此时我们明白第一点,就是当一个进程,时间片到了,就会发生进程调度,也就是保存当前进程的上下文,替换下一个等待进程,当然进程调度也就是让用户态进程切换为内核态进程,而如果是进行用户态到内核态进程的切换,那么同时就会导致操作系统对该进程进行信号检测,此时就会发生信号递达(默认动作,自定义动作,忽略),按照该场景,此时就会产生一个非常细节的问题,如下图所示:

在这里插入图片描述
同理,如图所示,此时就可以发现,如果在插入接口时,发生了进程调度,从而导致操作系统进行信号检测,执行对应信号的自定义动作,且刚好该动作,也是一个插入动作,那么就会导致上图所示问题,也就是内存泄露问题。显然,该问题的存在是不合理的,并且该不合理现象是由于同一个函数被重复进入导致,所以对于上述这种函数,一但被重入就会导致一系列的问题,那么这种函数就被称为不可重入函数,反之称为重入函数,也就是同一个函数被重入,不会出现问题的函数。同理,明白了这点之后,什么是可重入函数,什么是不可重入函数,我们就搞定啦!并且一般常见的不可重入接口都在各种容器中,如上述的链表,当然还有STL中各种容器的接口,还有malloc等!

理解volatile关键字

首先明白,这个关键字是C语言中的一个关键字,本质就是为了告诉编译器某个变量在程序执行过程中可能会被修改,要进行特殊处理,不能只是盲目的从CPU中的寄存器获取该变量,而是要重新从内存中获取,具体是什么意思呢?想要搞懂这个关键字的概念,需要从下述代码入手,如下:

在这里插入图片描述
从上图中可以看出,使用了gcc编译器中的优化级别对比没有使用gcc编译器的优化级别,两者在代码执行结果方面,有很大的不同,并且从运行结果来看,没有使用gcc优化,那么程序就会按照我们的预期,在收到了2号信号后,发生自定义动作捕捉,然后将quit置1,紧接着返回继续执行代码时,退出死循环,程序正常退出。而如果使用了gcc编译器的优化功能(-O2),此时从运行结果来看,无论是否收到2号信号,那么程序都一直处于死循环,本质也就是当该进程收到2号信号后,执行自定义动作捕捉时,quit全局变量并没有由0置1,导致循环不会退出,那么此时问题就来了,为什么执行了自定义动作捕捉,也就是执行了quit置1的代码,循环不会终止呢?还有就是为什么只有使用了gcc优化功能才会导致这个问题呢?具体如下图所示:

在这里插入图片描述
从上图有关代码在硬件上的执行过程,我们就发现,本质就是因为使用了O2的优化级别,从而导致CPU在进行计算时,不直接从物理内存中获取相关变量的值,而是直接从寄存器中获取先关变量的值,从而就会导致,因为信号执行自定义动作捕捉让quit全局变量由0置1,单单只是让物理内存上的quit变量置1,而没有让CPU寄存器中的quit变量置1,从而导致quit置不变,程序持续死循环,所以上述两个问题,就非常容易回答,本质就是因为使用了gcc的优化功能,会导致CPU在计算时,不直接从物理内存中获取数据,而是直接从寄存器中获取,所以也可以得出结论,gcc的优化功能本质就是在优化物理内存不断加载数据到CPU寄存器中的这个过程,从而导致意料中改变的值,CPU接收不到。当然注意:不是所有的代码都能像上述一样通过gcc的优化功能进行优化,从而导致寄存器中的数据不会被物理内存中的数据影响,只要像上述代码中while(quit != 0);这样频繁执行同一结果的代码,才有资格被优化,也就是减少频繁从物理内存加载数据到CPU寄存器中,从而提高效率。明白了上述知识之后,无论是上述的现象还是问题,我们就都搞定了,当然搞懂了上述问题和现象,volatile就不是什么重点了,同理上述所说,使用volatile就是在告诉编译器,每次进行计算时,都要去物理内存中获取数据而已(保证内存可见性)。所以当我们使用了volatile关键字,上述因为gcc优化功能导致死循环的问题就可以很好的被解决,如下图所示:
在这里插入图片描述

切记: 使用gcc优化的本质,还是在通过代码控制具体的执行方式,并且这个代码是在生成汇编指令的时候添加进去的,也就是我们的代码因为优化变成了一套更复杂和高级的代码,从而导致CPU在执行该代码时,变成优化形式执行(也就是不从物理内存中读取数据,而是从寄存器中读取),所以CPU具体执行代码如下图所示:
在这里插入图片描述
最终明白,在gcc中有许多的优化级别,具体如下图所示,这里不多过讲解:
在这里插入图片描述

SIGCHLD信号

搞定了上述相关知识,此时我们正式进入信号有关知识的最后一个知识点,与子进程退出相关的信号,在之前的学习中,我们学习了进程创建,进程等待等一系列知识,明白子进程需要被回收(父进程等待),不然就会导致僵尸进程等问题,并且父进程在等待子进程时,有两种方式,一种是阻塞式等待,一种是轮询式等待(非阻塞式),从而导致父进程想要成功的回收子进程就一定要牺牲一定的效率,那么如何可以避免这个问题呢? 首先明白父进程需要等待子进程的本质原因在于父进程并不知道子进程在干嘛,进而不知道对应子进程在什么时候会退出导致。明白了这点之后,我们就可以将问题转移为:子进程在退出时,是不是安安静静什么都不干,就默默的退出?答案肯定不是,那么子进程在退出时,会干什么呢?通过这个问题,我们就可以很好的引出该知识点的主角:SIGCHLD信号,明白子进程在退出时,它会发送一个SIGCHLD信号给父进程,但由于父进程对于该信号的默认处理动作是忽略,所以在我们看来,子进程退出时是默默无闻的退出,那么如何证明,子进程确实会发送SIGCHLD信号呢?如下代码所示:
在这里插入图片描述
整体代码非常简单,就是在一个程序中创建一个子进程,然后让该子进程退出的同时,父进程循环运行(防止孤儿进程),然后对SIGCHLD信号进行捕捉,看父进程是否会执行对应的自定义动作,当然,如果执行了,那么就表示父进程确实收到了子进程退出时,发送的SIGCHLD信号,反之没有。总之,目前从上图运行结果可以看出,父进程确实收到了SIGCHLD(17)信号。表明,子进程在退出时,确实不是默默无闻的退出,而是会发送SIGCHLD信号给父进程。搞清楚了这点之后,接下来就是顺水推舟,我们水到渠成的就可以搞定有关SIGCHLD相关的知识啦!还是从子进程退出时会发送一个信号给父进程出发,首先这样就可以解决我们上述父进程需要浪费效率等待子进程的问题,现在因为子进程会发送信号给父进程,那么父进程就不需要再浪费资源去等待子进程,而是等子进程退出,发送SIGCHLD信号给父进程时,父进程再去回收它,具体代码如下所示:
在这里插入图片描述
如上代码所示,我们在等子进程退出,父进程接收到SIGCHLD信号,执行自定义捕捉动作时,在该自定义动作中进行子进程回收,当然也就是使用waitpid接口等待子进程退出,并且明白如果等待成功,那么waitpid接口就会返回对应被等待进程的进程pid,所以使用该方法到底能不能成功等待子进程退出呢?如下图运行结果所示:
在这里插入图片描述
如图可以发现,最终waitpid的返回值和进程的pid值是相同的,并且代码执行了3秒之后,由于父进程没有立即回收,而是等待了一秒才回收,所以子进程在第四秒时,是处于僵尸状态,而在第五秒,父进程开始回收子进程时,子进程才从僵尸状态被父进程回收,父进程继续运行。

总而言之,上述通过子进程退出,发送信号的方式,我们就可以让父进程不需要特意的去关心子进程是否退出,所以当我们的父进程在需要执行很多代码的情况下,此时就可以使用上述方法,通过信号来回收子进程。

但是当我们使用信号的方式来回收子进程,按照上述代码来看就会存在一定的问题,当然这个问题是存在于不同的情况下,也就是当我有多个子进程需要被回收的情况下,凭借以前学过有关信号处理的知识,我们知道一个进程的pending位图只能记录一次信号,当一个信号正在被执行时,该信号就会被添加到信号屏蔽字中(block位图),那么就会导致父进程不能同时处理多个子进程发送过来的SIGCHLD信号,那么此时就会导致某些子进程的SIGCHLD信号被遗漏,从而导致某些子进程不能被回收,最终造成僵尸进程问题。所以为了解决该问题,我们需要将上述代码进行一定的升级处理,如下代码所示:
在这里插入图片描述
注意:上述代码有两个知识点,一是waitpid的第一个参数使用-1就可以在不需要指定进程pid的情况下去等待任意进程,二是在使用waitpid接口时,第三个参数我们最好是使用WNOHANG参数,表示非阻塞等待,也就是只等待退出进程,不会等待未退出进程,这样可以使代码更加安全。

注意: 除了之前学习的回收子进程知识和今天学习的回收子进程知识,在Linux系统内部还存在一种回收子进程的方式,就是让父进程去调用sigaction或者signal接口将SIGCHLD信号的处理动作设置为SIG_IGN(忽略),这样fork出来的子进程在进程终止时,也会自动被操作系统清理,不会产生僵尸进程,也不会通知父进程。

线程基础知识学习

该篇博客来到这里,上述有关可重入函数,volatile和SIGCHLD信号相关知识就搞定啦!接下来正式进入该篇博客的主题,有关线程相关知识的学习,当然由于线程相关知识非常的繁杂,所以一篇博客肯定是搞不定的,并且由于我们是刚开始学习线程相关知识,所以肯定是由浅入深 ,先学习一下线程基础知识,大致了解一下线程的概念及其使用,具体如下所述:

Linux线程概念

什么是线程
首先明白操作系统相关的知识被称为是计算机里的哲学,就是那种读一遍过去,你感觉,嗯,很有道理,但是却不知道是什么意思,不知道怎么用,然后学了等于没学,哈哈哈!下述几个就是经典操作系统书籍中对线程的简单描述:

  • 线程是一个执行分支,执行粒度比进程更细,调度成本更低
  • 线程是进程内部的一个执行流
  • 线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体

明白了上述操作系统中对线程的描述,也就是那几句哲学一样的语句,此时如何理解呢?下面我们就将这几句话通过经典的场景来分析,进而搞懂这句话的深层含义,如下:

1.如何理解线程是一个执行分支,执行粒度比进程更细,调度成本更低
想要理解该知识点,首先需要明白CPU中有两类寄存器,一类是可见的,一类是不可见的,也就是有的寄存器是暴露给我们,允许我们使用的,有的寄存器是由CPU自己做管理,不提供给我们使用的,明白了这点之后,我们就可以来谈谈线程相关的知识了,每个线程都具有独立的执行上下文和栈空间,而在线程在运行时,寄存器就为线程的上下文切换提供存储环境,从而保证线程的运行环境,如下图所示:
在这里插入图片描述
从图中可以看出,寄存器为进程和线程的运行提供了存储环境,以便于CPU执行相应的代码,而线程是根据进程的pcb和操作系统中对应的代码创建出来的,并且对于进程来说,线程的特点就是只有根据pcb创建出来的TCB,没有对应的虚拟地址空间,它们的虚拟地址空间是和进程共用的。从而导致多个线程可以同时共享同一块地址空间上的栈空间和代码段,并且操作系统通过一些列的操作,可以将代码段上的代码分配给每一个线程去执行(复杂),让每一个线程都拥有自己的上下文和栈空间,所以可以将线程看做是进程的一个执行分支。按照上图所示的话,那么该进程此时就拥有了4个执行分支,从而导致代码的执行效率大大提高。当然执行效率的提高虽然和执行分支增多有一定关系,但是使用线程的好处远远不止于此,重点在于线程的执行是并发执行,具体如何并发执行以及并发执行等细节相关知识,需要等我们深入学习之后再来详谈,此时我们只要知道,由于线程是并发执行的,所以导致线程不仅可以共享同一块地址空间,而且也可以共享同一个时间片,此时就会导致线程在调度时,不需要像进程调度时一样,需要切换地址空间,更改映射关系等!而是直接使用同一地址空间,这样就可以让线程调度成本大大降低,当然有关线程调度成本方面的问题,并不止于此,此处还涉及到一个CPU获取数据时的局部性原理,下文慢慢谈到。

2.如何理解线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体
明白了上述有关线程的概念,那么进程到底是什么呢?进程也是一个线程吗?这么理解肯定是不对的,因为线程是通过进程创建的,它们之间的关系肯定不是对等的,而应该是上下级。所以当我们谈到一个进程时,那么该进程一定是需要包含对应的执行流(线程)、地址空间,页表、物理内存等,并且对于进程的概念我们就需要进行升级,从之前的单执行流,理解为包含一大推东西的一个实体,所以也就是将进程理解为是承担分配系统资源的基本实体(如上图一般)。并且注意:同理时间片的分配,操作系统在分配系统资源时(内存资源、CPU资源等),是以进程为基本单位进行分配,只有当有了进程之后,相当于就是有了系统资源之后,我们才能根据进程去创建线程,也就是让线程去向进程申请资源,当然可以理解成是分配它的资源。同理如上图所示,在CPU看来,它识别的要么就是一个单独的进程执行流,要么就是该进程中对应的一个线程分支,而单独的一个进程执行流在我们看来和一个线程没有区别,所以对于CPU来说,调度的基本单位就是线程。

如何理解局部性原理
同理,首先明白,在CPU中不仅包括了上述所说的寄存器和之前所说的MMU(内存管理单元),其中还包括了运算器、控制器、高速缓存(cache L1,L2,L3)等!因为操作系统为了提高代码的执行效率,会将某些热点数据先加载到缓存中,也就是当我们在执行某段代码的时候,操作系统会将该代码附近的代码加载到缓存中,这样就可以让CPU上对应PC指针等指向对应执行代码的概率增大,从而提高代码执行效率,这就叫局部性原理。

总而言之,可以将线程理解为在一个进程内部独立执行的子执行单元。一个进程可以包含多个线程,每线程都有自己的执行路径和执行上下文,和对应的进程共享系统资源,并且可以实现并发机制,极大提高代码执行效率和资源管理方式。

总结:由于时间关系,该篇博客就到这啦!线程相关知识我们算是开了个小头,下篇博客更精彩哦!详解页表映射和线程的相关使用等知识!反正都是干货,一起期待吧!

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

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

相关文章

基于单片机智能饮水机加热系统的设计与实现

功能介绍 以51单片机作为主控系统;LCD1602液晶显示当前水温,定时提醒,水量变化DS18B20检测当前水体温度;水位传感器检测当前水位;继电器驱动加热片进行水温加热;定时提醒喝水,蜂鸣器报警&#x…

Java-通过IP获取真实地址

文章目录 前言功能实现测试 前言 最近写了一个日志系统,需要通过访问的 IP 地址来获取真实的地址,并且存到数据库中,我也是在网上看了一些文章,遂即整理了一下供大家参考。 功能实现 这个是获取正确 IP 地址的方法,可…

【css】用css样式快速写右上角badge徽标,颜色设置为渐变色

先看效果展示&#xff0c;已公开显示在图片卡片的右上角。 首先是dom代码&#xff1a;需要两个view或者div&#xff0c;public-badge是“已公开”那个矩形&#xff0c;show-signal是右边那个下三角&#xff0c;也就是阴影部分&#xff0c;这样看起来比较有立体感。 <view…

LabVIEW-实现波形发生器

一、题目 用两种方法实现一种多类型信号波形发生器&#xff08;至少包括&#xff1a;正弦波、三角波、方波等&#xff09;&#xff0c;可以调节信号频率、幅度、相位等参数&#xff0c;可以图形化显示信号波形。 需要给出产生信号波形的基本方法、程序设计基本方法以及程序实现…

云计算的学习(二)

二、计算虚拟化 1.计算虚拟化的介绍 1.1虚拟化简介 a.什么是虚拟化 将物理设备逻辑化&#xff0c;转化成文件或者文件夹&#xff0c;这个文件或文件夹一定包含两个部分&#xff1a;一部分用于记录设备配置信息&#xff0c;另一部分记录用户数据。 虚拟机摆脱了服务器的禁锢…

性能测试工具 Jmeter 测试 JMS (Java Message Service)/ActiveMQ 性能

目录 前言 ActiveMQ 介绍 准备工作 编写jndi.properties添加到ApacheJMeter.jar 中 下载 ActiveMQ 配置 Jmeter 进行测试 点对点 (Queues 队列) 配置 Jmeter 进行测试 发布/订阅 (Topic 队列) 配置发布 Publisher 配置订阅 Subscriber 总结 前言 JMeter是一个功能强大…

全方位对比 Postgres 和 MySQL (2023 版)

根据 2023 年 Stack Overflow 调研&#xff0c;Postgres 已经取代 MySQL 成为最受敬仰和渴望的数据库。 随着 Postgres 的发展势头愈发强劲&#xff0c;在 Postgres 和 MySQL 之间做选择变得更难了。 如果看安装数量&#xff0c;MySQL 可能仍是全球最大的开源数据库。 Postgre…

Windows环境下安装Nacos

文章目录 一、什么是Nacos1. 主要特点&#xff1a;1.1 服务发现和注册&#xff1a;1.2 配置管理&#xff1a;1.3 服务管理&#xff1a;1.4 多语言支持&#xff1a;1.5 高可用性和扩展性&#xff1a; 二、Windows下安装单机版Nacos1. 安装包下载&#xff1a;2. 目录文件说明&…

冒泡排序模拟实现qsort()函数

冒泡排序模拟实现qsort函数 前言1. 分析2. 解决一&#xff0c;如何接受不同数据3. 解决二&#xff0c;如何实现不同数据的比较4. 解决三&#xff0c;如何实现不同数据交换5. 模拟bubble_sort&#xff08;&#xff09;函数排序整型所有代码实现6. 结构体排序实现7. 结尾 前言 要…

Android Studio无法打开问题解决记录

目录 1 问题起因2 发现问题3 解决问题 1 问题起因 问题的起因是我为了运行一个Kotlin项目&#xff0c;但是报了一个错误&#xff1a; Kotlin报错The binary version of its metadata is 1.5.1, expected version is 1.1.16 然后我就上百度去搜了以下&#xff0c;一篇博客让禁用…

http-server 的安装与使用

文章目录 问题背景http-server简介安装nodejs安装http-server开启http服务http-server参数 问题背景 打开一个文档默认使用file协议打开&#xff0c;不能发送ajax请求&#xff0c;只能使用http协议才能请求资源&#xff0c;所以此时我们需要在本地建立一个http服务&#xff0c…

低代码在边缘计算工业软件中的应用

近年来&#xff0c;边缘计算给工业现场带来了许多新的变化。由于计算、储存能力的大幅提升&#xff0c;边缘计算时代的新设备往往能够胜任多个复杂任务。另外&#xff0c;随着网络能力的提升&#xff0c;边缘设备与设备之间、边缘设备与工业互联网云平台之间的通讯延迟与带宽都…

Android手写占位式插件化框架之Activity通信、Service通信和BroadcastReceiver通信

前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下"通俗易懂&#xff0c;风趣幽默"&#xff0c;感觉非常有意思,忍不住分享一下给大家。 &#x1f449;点击跳转到教程 前言&#xff1a; 1、什么是插件化&#xff1f; 能运行的宿主APP去加载没有下载的APK文件…

SAP从放弃到入门系列之生产订单拆分

文章目录 一、概述二、订单拆分功能前世今生三、订单拆分不同版本的差异3.1 版本 603 以下的订单拆分3.2 自604 起版本的订单拆分 四、订单拆分实例4.1 数据准备4.2 拆分操作-到仓库的分解&#xff08;SPLT_OS&#xff09;4.2 拆分操作-到其他物料的分解&#xff08;SPLT_DP&am…

【STM32MP135】修改10.1寸屏1280x800分辨率配置,解决fb_size过小导致运行崩溃

文件路径&#xff1a;u-boot-stm32mp-v2021.10-stm32mp1-r1/configs/stm32mp13_defconfig

基于深度学习的高精度工人安全帽检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度工人安全帽检测识别系统可用于日常生活中或野外来检测与定位工人安全帽目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的工人安全帽目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果的导出。本系统采用…

使用Dreambooth LoRA微调SDXL 0.9

本文将介绍如何通过LoRA对Stable Diffusion XL 0.9进行Dreambooth微调。DreamBooth是一种仅使用几张图像(大约3-5张)来个性化文本到图像模型的方法。 本教程基于通过LoRA进行Unet微调&#xff0c;而不是进行全部的训练。LoRA是在LoRA: Low-Rank Adaptation of Large Language …

2023届网络安全岗秋招面试题及面试经验分享

Hello&#xff0c;各位小伙伴&#xff0c;我作为一名网络安全工程师曾经在秋招中斩获&#x1f51f;个offer&#x1f33c;&#xff0c;并在国内知名互联网公司任职过的职场老油条&#xff0c;希望可以将我的面试的网络安全大厂面试题和好运分享给大家~ 转眼2023年秋招已经到了金…

Python应用实例(一)外星人入侵(八)

外星人入侵&#xff08;八&#xff09; 1.添加Play按钮1.1 创建Button类1.2 在屏幕上绘制按钮1.3 开始游戏1.4 重置游戏1.5 将play按钮切换到非活动状态1.6 隐藏鼠标光标 我们添加一个Play按钮&#xff0c;用于根据需要启动游戏以及在游戏结束后重启游戏&#xff0c;还会修改这…

剖析C语言字符串函数(超全)

目录 前言&#xff1a; 一、strlen函数 功能&#xff1a; 参数和返回值&#xff1a; 注意事项&#xff1a; 返回值是无符号的易错点&#xff1a; strlen函数的模拟实现 1、计数器算法 2、递归算法 3、指针减去指针 二、strcpy函数 功能&#xff1a; 参数和返回值 …