操作系统-内存管理

一、总论

1.1 硬件术语

​ 为了不让读者懵逼(主要是我自己也懵逼),所以特地整理一下在后面会用到术语。

​ 我们电脑上有个东西叫做内存,他的大小比较小,像我的电脑就是 16 GB 的。它是由 ROM 和 RAM 组成的(“组成”可能不太严谨)。他也被叫做主存,我们为了访问他,所以需要给他的存储空间进行编号,这种编号被叫做物理地址

​ 我们电脑上还有个东西叫做外存,他的大小比较大,像我的电脑是 256 GB的。它可以是磁盘,也可以是固态硬盘,还可以是 U 盘。我们访问它,也是需要对它的存储空间进行编号的,拿磁盘举例,我们一般用**“分区-扇区”**的系统来描述我们在访问磁盘的哪个区域。总之它是一个不在这一章重点讲的东西。

1.2 盲人摸象

​ 内存管理一直是一个很困扰我的知识,我从上个学期学计组的时候开始接触,课上就很好好听了(主要是跟 cache 对照着理解),然后为了备考又复习了一遍,但是就说要把理解透彻的东西写下来,后来因为时间关系就鸽了。如今学操作系统,还没有进入这一章的时候,各种概念就铺面而来,也是手忙脚乱。

​ 我觉得现代的内存管理之所以难以理解,是因为它是一个整体的、多功能的管理系统。多功能就导致很多时候,我们往往抓着其中一个功能来谈,然后就根据这一个功能来定义内存管理了。其实这就是盲人摸象的一种现象,人们在描述内存管理的时候,往往只说他的一个功能,比如说

“我们为什么要内存管理呀?因为访问磁盘太慢了,我们将内存作为磁盘的缓存,就可以提高访问速率了。”

“我们为什么要内存管理呀?因为内存空间太小了,可能容纳不了一个进程,所以我们就需要内存管理。”

“我们为什么要内存管理呀?因为要给每个进程提供一个独自占有存储器的假象,所以我们就需要内存管理了。”

​ 这三句话是没有错误的。但是他们都没有看到内存管理系统的全貌。或者说他们明明看到了全貌,却只愿意用一种观点去解释。比如当我们认为内存管理是为了提高访问速率,那么我们看见什么页式的,就会认为页就是 Cache 中的 Cache line。也不能说不对,就是页式管理的本质不是为了干这个的,只能说它只是在完成它的主要功能后,捎带手符合了一种牵强的解释。

​ 这是一个很大的困境。反正我本人在听 Cache 的时候,老师就很喜欢多说一句内存管理也是这么回事。当然老师融会贯通了,他可以说“大象有大象腿啊”,而我作为学生听到的却是“这大象腿就是大象啊”。

​ 在后面的几章中,我会逐个提出内存管理需要解决的问题,然后阐述它的解决办法。最后这些办法堆叠起来,才是我们全部的内存管理系统。

1.3 抽象

​ 在这里记录一下我上操作系统以来看到的最牛逼的话:

操作系统提取“处理器”的概念建立了“进程”的抽象,提取“物理存储器(主存)”的概念建立了“进程(虚拟)地址空间”的抽象,我们提取“外存”的概念建立了”文件系统“的抽象。

​ 可以说,这句话好就好在它描述了操作系统在干啥,以及它是怎样实现他的目标的。操作系统希望在软件(普通软件)和硬件之间做一个中间层。至于为什么需要这个中间层,是因为如果让软件直接管理硬件,软件不仅需要考虑自身功能的实现,还需要考虑与硬件的沟通,与其他同时运行的软件的沟通,这无疑是很麻烦的。操作系统的出现,使得软件只需要考虑自己的事情,而不需要考虑其他的事情。相当于操作系统把原来本该由软件实现的沟通啊,协作啊之类的功能都转移到了自己身上。

​ 所谓的抽象,就是操作系统给软件提供的一种幻象(这个说法不本质),这种幻象可以让软件只操心自己的事情,比如说认为自己独占了处理器,认为独占了地址空间。我们研究操作系统,其实就是理清楚它是怎样制造出这种幻象的。


二、内存管理功能

2.1 实现进程的并行

2.1.1 问题描述

​ 进程是有同时执行的需要的,比如说谁都希望自己可以一边码代码一边听歌。为了让这两个进程都同时运行,我肯定得把他们的代码加载到主存中,不可以加载一份,然后运行一会儿,然后再换进来另一份。这是因为这样换来换去的太慢了,没有办法给用户营造一个同时干两件事的假象。也就是说,主存里必须有两份代码。

​ 主存里有两份代码就会出现很多个问题,比如说进程 A 说我要跳转到地址 100 所在的那条指令,进程 B 也说我要跳转到地址 100 所在的那条指令。这个说法在程序看来没毛病,我编程的时候想要跳转到第 25 条指令,这个指令的地址就是 100,那么我就说我要跳转到地址 100 怎么了。但是一旦主存里放了两个进程的指令,那么地址 100 只可能是一个进程的第 25 条指令,而另一个进程的第 25 条指令并不是地址 100。

​ 这个问题的实质是,当程序员在编程的时候,他可不知道自己写的程序被加载到主存的哪里,他是没办法确定自己跳转的时候要跳去哪里的。访问数据也是这个道理,因为内存中有多个进程的数据,谁能在编程的时候知道自己的数据到底被存到内存的哪个位置去了。这就好像一个人只知道自己住十楼,是没有办法找到自己的家的,因为他不知道自己是哪栋楼哪个单元的。

2.1.2 问题解决

​ 当然最朴素的办法就是让程序员在编写程序的时候,就让他自己注意自己要被内存加载到哪里。这件事情是不可能的,虽然我们经常在现实中这么干,比如我们记忆家地址,就会记住大概 8 号楼一单元 1202。是一个很正常的事情。运行的时候,不可能把每个进程的位置都给固定了,因为为了不发生冲突(就是两个进程加到到一片区域了),我们必须给每个进程都固定一个位置,那么主存的空间一定不够。我们只能这个进程进到内存,挑块地方给他,另一个进程来了,从剩下空着的地方挑一块给他,那个进程走了,那么就把他这块地方给清除了,好给其他进程腾地方。

​ 既然进程被加载到哪里在编写代码的时候不确定,那么我们就在他确定的时候给他记下来不就得了。当进程被加载的时候,我们记录一下他的首地址,比如说是 2000,那么等下次它再访问地址 100 的时候,我们就让他访问 2100 ,这就对了。这个技术被称为**“重定位”**。

​ 让我们再看一下重定位这个技术,我们发现程序编程的时候用的地址不再是真实的物理地址了,因为要是真实的物理地址,那么说访问 100,那就必须访问物理地址第 100 个存储单元。但是它实际访问的是物理地址 2100 。这个其实就是朴素的虚拟地址。也就是程序编程的时候用到的地址空间并不是真正的物理地址空间,而是一个假的,一个幻象,是需要通过操作系统进行转换才能变成物理地址的一个空间。这个虚假的空间是很好的,程序员编程的时候不再需要考虑加载的问题(本质是考虑与其他进程并行的问题)。

​ 当然这个重定位技术不是没有缺点,每次对虚拟地址的访问都需要经过转换,从这里我们可以看出采用的是加法,而加法是一个很耗时的运算,这导致采用这种技术的会让整个程序变得很慢,因为每次对访问内存都要经历一个加法。

2.2 主存放不下多个进程

2.2.1 问题描述

​ 因为前面我们解决了实现进程并行的问题,那么就会导致另一个问题,就是用户可以同时打开多个进程,那理论上就会出现运行的进程太多了,主存存不下了的情况。那当然不能让用户知道这件事情,多点了几个程序电脑就崩了,那我怎么敢多点程序呢。我们希望给用户营造一种无论打开多少个进程,这些进程都会并行的假象,顶多就是慢一点而已的假象。这就需要我们进一步完善我们的内存管理系统。

2.2.2 问题解决

​ 可以这样解决这个问题,就是如果把程序载入内存的时候,一看剩下的空间,发现好像这个进程放不下,那么就把当前内存中的一个没在工作的进程(理论上只有一个进程在工作)调往外存,给这个新来的进程腾出空间。等到了一定的时机,在把那个外存中的进程调回来(因为这个进程还没结束)。这个方案被叫做**“交换”**。

​ 有一点需要强调的,存储被换出进程的外存区域被叫做 swap,他不是文件系统的一部分,而是更像内存管理系统的一部分。我们在装 linux 的时候,常说要把 swap 分区搞大一点,就是因为这个大了,方便进程间的并行。

​ 我们审视交换这个技术。我们可以这样理解:就是把磁盘的一部分作为一个大的存储器,而主存作为他的高速缓存。就好像 cache 没必要太大,因为很多数据存在主存一样,按照这样处理的主存也不需要太大,因为很多数据是存在外存中一样。

​ 交换这个技术也是有缺点的,它调入调出内存的单位是一个进程,一个进程这么大,调入调出是很慢的。而且调出的后遗症也很大,当一个进程被调出后,它腾出的主存空间要是有点小,而等待调入的进程比他大,那么这次调出就没用啊。这两个问题的本质都是以进程为单位的颗粒度太大了,是一种粗糙的方法。

2.3 主存放不下一个进程

2.3.1 问题描述

​ 难道只有多个进程的时候,主存才捉襟见肘吗?不是的,有可能一个进程的大小就让主存吃不消了,比如 Photoshop,运行起来几个 GB 不在话下。交换策略是没办法解决的,因为交换的调度单位是进程,一个进程再怎么调度,也是没有办法变小的。

2.3.2 问题解决

​ 这个问题要求更细颗粒度的调度策略,其实虚拟内存几乎呼之欲出,但是为了历史的严谨性,我们得先介绍另一个技术,即**“覆盖”**。这个技术说的是我们在编程的时候就给程序分好片段,每个片段我们叫做一个“覆盖块(overlap)”,然后我们在运行的时候,是按照覆盖块进行调度的,覆盖块存放在外存上,在需要的时候由操作系统动态地换入换出。

​ 覆盖能解决问题,依靠的是覆盖块是比进程跟小的调度单位,我们可以只在主存中保留一部分进程,而将另一部分暂时用不上的调往外存,这种细颗粒度的划分是关键。

​ 但是我们也应该看出他的缺点,就是程序员很痛苦,他们在编程的时候必须把程序分割成小片段,而且似乎这些小片段的数据依赖关系还得比较弱。这个缺点本质是又让程序员开始思考与硬件的协作,而不能专注于程序自身的功能。

2.4 跟踪主存使用情况

2.4.1 问题描述

​ 每次运行一个进程,都需要在主存中寻找一个合适的空间分配个这个待运行的进程。当一个进程结束运行的时候,都需要释放这个进程占据的空间。这就要求内存管理系统必须具有跟踪内存使用情况的功能。

2.4.2 问题解决

​ 我们有两种数据结构进行内存跟踪,一种是位图,一种是链表。两种方法都是十分的好理解,所以就不赘述了。除了跟踪内存使用情况,还需要有内存分配策略。根据数据结构的不同,策略的具体实现也不同。有首次适配策略,下次适配策略,最佳适配策略,最差适配策略,快速适配策略。不在此赘述。


三、虚拟内存

3.1 虚拟内存机制

​ 虚拟内存就是我在总论里提到的一个完整的内存管理的实现方案,它可以很有效的解决我们上面提出的问题。我们首先看一下虚拟内存的机制:

每个程序拥有自己的地址空间,这个空间被分割成多个小块,每一块被称为一个页面,每一页有连续的地址范围,这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的虚拟地址空间的时候,由硬件立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间的时,由操作系统负责将确实的部分装入物理内存并重新执行失败的指令。

​ 我们来看一下虚拟内存有没有解决我们提出的三个问题。对于“进程的并行”,虚拟内存也是提供了虚拟地址的,程序员编程的时候是在虚拟地址空间中进行处理的,而转换虚拟地址为物理地址交由操作系统处理。对于主存的大小问题,可以看出的颗粒度是足够小的,当采用交换的思想后,不仅可以解决“主存放不下多个进程”的问题,也可以解决“主存放不下一个进程”的问题。至于“跟踪内存使用情况”的问题,采用的还是原来的机制。

​ 除此之外,在虚拟内存的实现中,还做了具体的优化,我们在具体实现的时候讲解。

3.2 基本实现

​ 虚拟内存干的是这样的一个事情,它给进程提供一个虚拟地址空间,32 位的操作系统就是 4 GB 大小。然后它把这个虚拟地址空间按照虚拟页大小划分,一般一个虚拟页(Page)是 4 KB。所以可以划分成 2 20 2^{20} 220 这么多页(大约是十的六次方,一百万页)。它对于主存也做相同处理,按照页面大小把主存分割好,每个分出来的主存页面,被叫做**“页框(Page Frame)”**。我觉得这个名字超级形象,因为把虚拟内存中的页转换到物理内存,就像把一页照片粘到相框里的过程。

​ 虚拟内存中进行虚实转换的数据结构叫做页表。虚拟空间分成多少页,那么这个页表就有多少页表项。在这个页表项上面记录着这个虚拟页在不在主存中,如果在的话,还记录着此时的物理页号。当我们拿到一个虚拟地址,首先把前几位挑出来,那是他的虚拟页号,然后用这个页号去检索页表项,检索出页表项以后看看在不在,如果在的话,就把页表项中记录的物理页号和虚拟地址的页偏移量拼接在一起,就可以获得真实的物理内存地址,然后就可以访问了。

​ 可以看出,比起重定位技术的加法,页表进行虚实转换的时候,与 cache 的思路很像,像是一个全相连的 cache,这种策略避免了加法运算,而改成了位拼接运算,大大提高了虚实转换的效率。此外,因为固定了页面大小,调度的过程不需要程序员的参与,所以简化了编程的复杂度,这也是一种优化。

​ 但是在虚拟内存的实现中,也存在着诸多问题待解决:

  • 虚实转换速度不够快
  • 如果虚拟地址空间很大,那么页表也会很大

​ 还有诸多权衡

  • 置换页面的策略
  • 管理后备存储的策略(后备存储就是 swap 技术里的外存空间)

3.3 优化

5.3.1 TLB

首先我们需要弄懂 tlb 的结构,计组认为的 TLB,是长这样的:

在这里插入图片描述

也就是说,TLB 是一个全相连的 cache,既然是全相连,就不由 index 段了。我们用虚拟地址的前 22 位作为 TAG,并行的比较 64 个TLB 的 line,如果 TAG 相等,就说明找到了,反之,这说明没有找到。

不过这个模型还是有些粗糙的,很多细节并没有说明白。

在操作系统指导书里提到,tlb 构建了一个映射关系,我简化一下,就是 V P N   →   P P N VPN\space \rightarrow\space PPN VPN  PPN 。当然这是对的了,但是这种说法我就弄成了每个 VPN 都会对应一个 PPN,但是其实这种映射关系只有 64 对。而且叫映射似乎就是一下就射过去了,而不是一个并行的比较过程。

其次就是,我们没有了解具体硬件发生了啥,比如 VPN 是怎样被检索的,被检索到的 PPN 放到了哪里,tlb 缺失以后具体怎么填补。都是没有的。这其实跟协处理器有很大关系。

在了解协处理器之前,我们先来看一下 tlb 的表项,他比计组版本要复杂一些,我们以 MOS 中 64MB (也就是共有 2 14 2^{14} 214 页 )的物理内存为例

在这里插入图片描述

我们来说明一下这些差别:

  • 朴素版的 PPN 只有 14 位,是因为物理页框号可以最少用 14 位表示,但是真实版的 PPN 也与 VPN 相同,是 22 位。可能是考虑到不同电脑上内存不同吧,这估计也是 mips_detect_memory() 这个函数的设置。
  • 朴素版一个 entry 是 36 位,而真实版一个 entry 是 64 位。这是因为真实版的标志位更多,所以需要的位数就更多
  • 朴素版没有 ASID 段,而真实版有。ASID(address space identifier)应该是用于区分不同进程的一个标识符,因为操作系统可以同时运行多个进程,要是不用 ASID 的话,只要进程一切换,那么 TLB 里的所有内容都需要失效(因为进程切换就以为着虚拟地址和物理地址的映射关系切换),而这样是低效的,因为每次 TLB 中的内容清空,就意味着会发生 64 次的冷缺失。
  • 朴素版没有物理地址权限标志位(N,D,V,G),而真实版有。这四个标志位的解释见下表
标志位解释
N(Non-cachable)当该位置高时,后续的物理地址访存将不通过 cache
D(Dirty)事实上是可写位。当该位置高时,该地址可写;否则任何写操作都将引发 TLB 异常。
V(Valid)如果该位为低,则任何访问该地址的操作都将引发 TLB 异常。
G(Global)如果该位置高,那么允许不同的虚拟地址映射到相同的物理地址,可能类似于进程级别的共享

总结起来就是真实版的 tlb 建立了一个这样的映射 < V P N , A S I D >   →   < P P N , N , D , V , G > <VPN,ASID>\space\rightarrow\space<PPN,N,D,V,G> <VPN,ASID>  <PPN,N,D,V,G>

然后我们来解决下一个问题,就是 tlb 怎么用的问题。这是一个我之前忽略的点,因为其实我对于 tlb 的定位并不清楚,我本以为它就好像是一个 cache,是对于程序员是透明的,我就在编程的时候写虚拟地址,然后就有硬件(MMU)拿着这个地址去问 tlb,tb再做出相关反应,这一切都是我不需要了解的,但是实际上 tlb 的各种操作,都是需要软件协作的。之所以有这个错误认知,是因为似乎在 X86 架构下确实是由硬件干的,但是由于我们的 MIPS 架构,也就是 RISC 架构,所以似乎交由软件负责效率更高一些。

如果 tlb 是程序员可见的,那么我们必然要管理它,那么我们就需要思考怎样管理它?我们管理它的方式就是设置了专门的寄存器和专门的指令。指令用于读或者写 tlb 中的内容,而寄存器则用于作为 CPU 和 tlb 之间沟通的媒介,就好像我们需要用 hi 和 lo 寄存器与乘除单元沟通一样。这些寄存器,都位于 CP0 中

在协处理器里面与 tlb 有关的寄存器如下表:

寄存器编号作用
EntryHi10保存某个 tlb 表项的高 32 位,任何对 tlb 的读写,都需要通过 EntryHi 和 EntryLo
EntryLo2保存某个 tlb 表项的低 32 位
Index0决定索引号为某个值的 tlb 表项被读或者写
Random1提供一个随机的索引号用于 tlb 的读写

这里再说一下各个寄存器的域

  • EntryHi,EntryLo 的域与 tlb 表项完全相同

  • Index 的域:在这里插入图片描述

  • Random 的域:在这里插入图片描述

与 tlb 相关的指令

指令作用
tlbr以 Index 寄存器中的值为索引,读出 TLB 中对应的表项到 EntryHi 与 EntryLo。
tlbwi以 Index 寄存器中的值为索引,将此时 EntryHi 与 EntryLo 的值写到索引指定的 TLB 表项中。
tlbwr将 EntryHi 与 EntryLo 的数据随机写到一个 TLB 表项中(此处使用 Random 寄存器来“随机”指定表项,Random 寄存器本质上是一个不停运行的循环计数器)
tlbptlb probe。用于查看 tlb 是否可以转换虚拟地址(即命中与否)根据 EntryHi 中的 Key(包含 VPN 与 ASID),查找 TLB 中与之对应的表项。如果命中,并将表项的索引存入 Index 寄存器。若未找到匹配项,则 Index 最高位被置 1。

5.3.2 二级页表

在“MOS阅读”里讲了。

5.3.3 页面置换策略

鸽了

5.3.4 后备存储管理

当我们讨论完了如何将页面从物理内存中换出,但是到底换到外存(磁盘)的哪个部分,却没有讨论,当我们需要将其调入的时候,又是怎么找到这个页面在磁盘上的位置呢?这些问题就是后备存储管理的问题。

最简单的处理就是在磁盘上做一个交换分区(似乎只介绍了这一个方法),这个分区就不在文件系统的管理范围内了。我们用交换分区来存储我们置换出去的页面。

有一种方法叫做静态交换,当启动一个进程的时候,我们就在交换分区中开辟出一片跟进程映像(应该就是虚拟地址空间)相同大小的地区,然后当就进行对应的交换就可以了。

还有一种方法是动态备份,就是当页面换出的时候,我们才为其分配磁盘空间,而且空间并不固定。这样我们就需要记录它的位置了。

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

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

相关文章

RK3588平台开发系列讲解(同步与互斥篇)信号量介绍

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、信号量介绍二、信号量API1、结构体2、API三、函数调用流程沉淀、分享、成长,让自己和他人都能有所收获!😄 📢上一章我们看了自旋锁的原理,本章我们一起学习下信号量的用法。 一、信号量介绍 和自旋锁一样,…

渗透测试综合实验(迂回渗透,入侵企业内网并将其控制为僵尸网络)

第1节 实验概述 1.1 实验背景概述 本实验为模拟真实企业环境搭建的漏洞靶场&#xff0c;通过网络入侵Web服务器&#xff0c;拿到控制权限后发现有内网网段&#xff0c;建立隧道做内网穿透&#xff0c;接着进一步扫描内网主机&#xff0c;并进行漏洞利用&#xff0c;最终通过域…

java登录页面验证码的生成以及展示

1、代码示例 后端返回前端一个字节数组。 2、gif图面展示 网页中一张图片可以这样显示&#xff1a; <img src“http://www.jwzzsw.com/images/log.gif”/>也可以这样显示&#xff1a; <img src“data:image/gif;base64,R0lGODlhAgACAIAAAP///wAAACwAAAAAAgACAAACAo…

聚会Party

前言 加油 原文 聚会常用会话 ❶ He spun his partner quickly. 他令他的舞伴快速旋转起来。 ❷ She danced without music. 她跳了没有伴乐的舞蹈。 ❸ The attendants of the ball are very polite. 舞会的服务员非常有礼貌。 ❶ Happy birthday to you! 祝你生日快乐!…

Linux--高级IO--poll--0326

1. poll #include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); poll只负责等。 参数介绍 fds 是一个结构体类型的地址&#xff0c;相比于select中的fd_set类型,pollfd结构体可以内部封装一些遍历&#xff0c;解决需要关系那些文件描述符&#…

ES6新特性保姆级别教程【建议收藏】

文章目录1、ECMAScript 6 简介1.1、ECMAScript 和 JavaScript 的关系1.2、ES6 与 ECMAScript 2015 的关系1.3、ECMAScript 的历史2、let 和 const 命令2.1、let 命令2.1.1、基本用法2.1.2、不存在变量提升2.1.3、不允许重复声明2.1.4、暂时性死区2.2、const 命令2.2.1、基本用法…

cuda学习4-6

4. Hardware Implementation NVIDIA GPU架构是围绕一系列可扩展的多线程流式多处理器&#xff08;SM&#xff09;构建的。当主机CPU上的CUDA程序调用内核网格时&#xff0c;网格的块将被枚举并分配给具有可用执行能力的多处理器。线程块的线程在一个多处理器上并发执行&#x…

C++内存管理详解

大家好&#xff0c;这里是bang_bang&#xff0c;今天来分享下内存管理的知识。 目录 1.C/C内存分布 2.C内存管理方式 2.1new/delete操作内置类型 2.2new/delete操作自定义类型 3.operator new与operator delete函数 3.1operator new 3.2operator delete 4.new和delete的实现…

367. 有效的完全平方数 ——【Leetcode每日一题】

367. 有效的完全平方数 给你一个正整数 num 。如果 num 是一个完全平方数&#xff0c;则返回 tru &#xff0c;否则返回 false 。 完全平方数 是一个可以写成某个整数的平方的整数。换句话说&#xff0c;它可以写成某个整数和自身的乘积。 不能使用任何内置的库函数&#xf…

Mybatis框架源码笔记(九)之反射工具类解析

1 反射工具类 Java中的反射功能虽然强大&#xff0c;但是代码编写起来比较复杂且容易出错。Mybatis框架提供了专门的反射包&#xff0c;对常用的反射操作进行了简化封装&#xff0c;提供了更简单方便的API给调用者进行使用&#xff0c;主要的反射包代码结果如下&#xff1a; …

React 组件的 children 数据使用

children 属性表示该组件的子节点&#xff0c;只要组件内部有子节点&#xff0c;props 就有该属性&#xff0c;是自动带上的&#xff0c;不需要开发者添加。 children 可以是 普通文本、普通标签元素、函数、JSX … 案例一&#xff1a;普通文本 import React from "rea…

奇异值分解(SVD)和图像压缩

在本文中&#xff0c;我将尝试解释 SVD 背后的数学及其几何意义&#xff0c;还有它在数据科学中的最常见的用法&#xff0c;图像压缩。 奇异值分解是一种常见的线性代数技术&#xff0c;可以将任意形状的矩阵分解成三个部分的乘积&#xff1a;U、S、V。原矩阵A可以表示为&#…

数据结构与算法基础(王卓)(21):哈夫曼编码(1):过程

逻辑雏形 根据老师讲解的思路&#xff0c;梳理出程序运行的逻辑雏形如下&#xff1a; 搞一个多维数组HC&#xff0c;用来存储我们这里 n(每) 个节点的哈夫曼编码搞一个数组cd&#xff0c;用来存储我们这里每个节点是前面一位的左子树&#xff08;0&#xff09;还是右子树&…

Spark 基本知识介绍

文章目录1. Spark是什么2. Spark与Hadoop区别3. Spark四大特点3.1 速度快3.2 易于使用3.3 通用性强3.4 运行方式4. Spark整体框架5. Spark运行模式6. Spark架构角色6.1 YARN角色6.2 Spark 角色1. Spark是什么 Spark是用于大规模数据处理的统一分析引擎。 Spark 最早源于一篇论…

Qt5.15.2 for WebAssembly 环境搭建 - Windows篇

Qt系列文章目录 文章目录Qt系列文章目录前言一、准备工作二、操作步骤1.使用cmd工具2.安装Qt for WebAssembly3.创建工程WebAssembly3.创建Qt Assembly工程参考前言 由于前端Canvas绘制图像效率问题&#xff0c;而且实现三维特效也有性能瓶颈&#xff0c;虽然Web 技术突飞猛进…

基于Arm Cortex-M4核心MK60DN256VMD10、MK60DX256VMD10嵌入式微控制器芯片介绍

MK60DN256VMD10&#xff08;MK60DX256VMD10&#xff09;是Kinetis K6x系列32位微控制器&#xff0c;基于ArmCortex-M4核心&#xff0c;是与Kinetis Kx MCU家族兼容的引脚&#xff0c;外设和软件。K6x mcu还集成了10/100Mbps以太网与IEEE1588精确时间协议(PTP)收发器和USB 2.0 (…

【Linux-计算机网络】-TCP协议通信流程

1.TCP协议通信流程图 1.1TCP协议的通讯流程可以分为以下步骤&#xff1a; 应用层&#xff1a;应用程序通过系统调用API&#xff08;如socket&#xff09;创建一个TCP套接字&#xff08;socket&#xff09;&#xff0c;并设置好相关的选项。 传输层&#xff1a;当应用程序调用c…

再学C语言49:C库中的字符串函数

C库提供了很多处理字符串的函数&#xff1b;ANSI C用头文件 string.h 给出这些函数的原型 一、strlen()函数 功能&#xff1a;计算并返回字符串长度 示例代码&#xff1a; /* test strlen() function */ #include <stdio.h> #include <string.h>int main(void)…

数据的存储--->【大小端字节序】(Big Endian)(Little Endian)

⛩️博主主页&#xff1a;威化小餅干&#x1f4dd;系列专栏&#xff1a;【C语言】藏宝图&#x1f38f; ✨绳锯⽊断&#xff0c;⽔滴⽯穿&#xff01;一个编程爱好者的学习记录!✨前言计算机硬件有两种存储数据的方式&#xff1a;大端字节序——Big Endian小端字节序——Little …

【Python】【进阶篇】十、Python爬虫的Requests库

目录十、Python爬虫的Requests库10.1 常用请求方法10.1.1 requests.get()10.1.2 requests.post()10.2 对象属性10.3 Requests库应用十、Python爬虫的Requests库 Python 提供了多个用来编写爬虫程序的库&#xff0c;除了前面已经介绍的 urllib 库之外&#xff0c;还有一个很重的…