「 操作系统 」CPU缓存一致性协议MESI详解

news2024/12/26 10:34:38

「 操作系统 」CPU缓存一致性协议MESI详解

参考&鸣谢

缓存一致性协议MESI 小天

CPU缓存一致性协议MESI 枫飘雪落

CPU缓存一致性协议(MESI) 广秀

2.4 CPU 缓存一致性 xiaoLinCoding


文章目录

  • 「 操作系统 」CPU缓存一致性协议MESI详解
    • 一、计算机的缓存一致性
    • 二、CPU高速缓存(Cache Memory)
      • 存在的意义
      • 存储器层次结构
      • 缓存如何提高效率
      • 单核下高速缓存的CPU执行流程
      • 多级缓存结构
    • 三、多核CPU多级缓存下的MESI
      • MESI的缓存状态
      • MESI状态间的转换
      • 多核缓存示意图
      • 单核下的数据读取
      • 多核下的数据读取
      • 单核下的数据修改
      • 多核下的数据修改及数据同步
    • 四、MESI问题及优化
      • 伪共享(False Sharing)
        • 问题定义
        • 问题解决
      • CPU切换状态堵塞
        • 问题定义
        • 问题解决
    • 五、小结


一、计算机的缓存一致性

计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存(Cache Memory)。CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。
有了CPU高速缓存虽然解决了效率问题,但是它会带来一个新的问题:数据一致性在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时CPU不再与主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中。
解决缓存一致性方案有两种:

  1. 通过在总线加LOCK#锁的方式
  2. 通过缓存一致性协议

二、CPU高速缓存(Cache Memory)

存在的意义

CPU高速缓存是为了解决CPU速率和主存访问速率差距过大问题。

  • CPU:根据摩尔定律,CPU会以每18个月的时间将访问速度翻一番,相当于每年增长60%。
  • 内存:内存的访问速度虽然也在不断增长,却远没有这么快,每年只增长 7% 左右

到今天来看,一次内存的访问,大约需要 120 个 CPU Cycle,这也意味着,在今天,CPU 和内存的访问速度已经有了 120 倍的差距。

因此引入了“高速缓存”,CPU厂商在CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题。(高速缓存是插在CPU寄存器和主存之间的缓存存储器)

  • 高速缓存(CPU Cache):用于平衡 CPU 和内存的性能差异,分为 L1/L2/L3 Cache。其中 L1/L2 是 CPU 私有,L3 是所有 CPU 共享。
  • 缓存行(Cache Line):高速缓存的最小单元,一次从内存中读取的数据大小。常用的 Intel 服务器 Cache Line 的大小通常是 64 字节。

存储器层次结构

存储器在计算机内是有层次,就像一个金字塔,塔顶的存储器速度极高,但容量很小,越往下,速度越慢,但容量越大。

img

缓存如何提高效率

计算机程序运行遵循局部性原则。局部性原理是指程序在执行时呈现出局部性规律,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。

局部性原理又表现为:时间局部性和空间局部性。

  • 时间局部性(Temporal Locality):指如果某条指令一旦被执行,很有可能不久后还会再次被执行;如果某个数据一旦被访问了,很有可能不久之后还会再次被访问。如:循环、递归等。
  • 空间局部性(Spatial Locality):指如果某个存储单元一旦被访问了,很有可能不久后它附件的存储单元也会被访问。如连续创建多个对象、数组等。

具有良好局部性的程序比差的程序更多的倾向于从存储器层次结构较高层次处访问数据,因此运行的更快,尤其是执行大数据量的算术运算。

单核下高速缓存的CPU执行流程

  • 1.程序和数据都被加载到主内存中

  • 2.执行指令和数据被加载到CPU的高速缓存中,进行逻辑处理

  • 3.CPU执行指令再将处理后的结果写到CPU的高速缓存中

  • 4.CPU的高速缓存再将数据写回(更新)到主内存中

列举缓存结构图:

img

多级缓存结构

高速缓存是插在CPU寄存器和主存之间的缓存存储器,称为L1高速缓存,基本是由SRAM(static RAM)构成,访问时大约需要4个始终周期。刚开始只有L1高速缓存,后来CPU和主存访问速度差距不断增大,在L1和主存之间增加了L2高速缓存,可以在10个时钟周期内访问到。现代CPU又增加了一个更大的L3高速缓存,可以在大约50个时钟周期内访问到它。

列举多级缓存结构图:

img

三、多核CPU多级缓存下的MESI

MESI的缓存状态

CPU中每个缓存行(Caceh line)使用4种状态进行标记,使用2bit来表示:

状态描述监听任务状态转换
M 修改 (Modified)该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。
E 独享、互斥 (Exclusive)该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。当CPU修改该缓存行中内容时,该状态可以变成Modified状态
S 共享 (Shared)该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。当有一个CPU修改该缓存行时,其它CPU中该缓存行可以被作废(变成无效状态 Invalid)。
I 无效 (Invalid)该Cache line无效。

注意: 对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。

MESI状态间的转换

MESI状态转换图:

img

  • 本地读取(Local Read): 本地cache读取本地cache中的数据

  • 远端读取(Remote Read): 其它cache读取本地cache中的数据

  • 本地写入(Local Write): 本地cache将数据写入本地cache中

  • 远端写入(Remote Write): 其它cache将数据写入本地cache中

装换说明

第一:

某个CPU(CPU A)发起本地写请求(Local Write),比如对某个内存地址的变量赋值,如果此时所有CPU的Cache中都没加载此内存地址,即此内存地址对应的Cache Line为无效状态(Invalid),则CPU A中的Cache Line保存了最新内存变量值以后,其状态被修改为Modified

随后,如果CPU B发起对同一个变量的读操作(Remote Read),则CPU A在总线上嗅探到这个读请求以后,先将Cache Line里修改过的数据回写(Write Back)到Memory中,然后在内存总线上放一份Cache Line的拷贝作为应答,最后再将自身的Cache Line的状态修改为Shared,由此产生的结果是CPU A与CPU B里对应的Cache Line的状态都为Shared。

第二:

在第一点的基础上,CPU A发起本地写请求导致自身的Cache Line状态变为Modified以后,如果此时CPU B发起同一个内存地址的写请求(Remote Write),则我们看到状态图里此时CPU A的Cache Line状态为Invalid

其原因是如下:CPU B此时发出的是一个特殊的请求——“读并且打算修改数据”(read with intent to modify),当CPU A从总线上嗅探到这个请求后,会先阻止此请求并取得总线的控制权(Takes control of bus),随后将Cache Line里修改过的数据回写(Write Back)到Memory中,再将此Cache Line的状态修改为Invalid(这是因为其他CPU要改数据,所以没必要改为Shared了)。

与此同时,CPU B发现之前的请求并没有得到响应,于是重新再发起一次请求,此时由于所有CPU的Cache里都没有内存副本了,所以CPU B的Cache就从Memory中加载最新的数据到Cache Line中,随后修改数据,然后改变Cache Line的状态为Modified

下图表示了当一个缓存行(Cache line)的调整的状态的时候,另外一个缓存行(Cache line)需要调整的状态。

状态MESI
M×××
E×××
S××
I
举例:
比如有某个变量a=1;
1.cache line处于M(修改)状态,其它cache对此变量都应是I(无效)状态
2.cache line处于S(共享)状态,其它cache对此变量可以是I(无效)状态,也可以是S(共享)状态

多核缓存示意图

比如有多个线程(此处3个),共同读取主存中的某个变量int z=1;

img

单核下的数据读取

img

1.CPU A发出了一条读取数据的指令,需要从主存中读取变量z。

2.首先从主存中将数据读取到BUS总线中。

3.再通过BUS总线读取到CPU A的缓存中。也就是Remote Read,此时cache line的状态需修改为E(独享)。

多核下的数据读取

img

1.CPU A发出了一条读取数据的指令,需要从主存中读取变量z。

2.首先从主存中将数据读取到BUS总线中。

3.再通过BUS总线读取到CPU A的缓存中。也就是Remote Read,此时cache line的状态需修改为E(独享)。

4.CPU B也发出了一条读取数据的指令,需要从主存中读取变量z。

5.CPU B尝试从主存中读取变量z,但被CPU A嗅探到了有内存地址的冲突。此时CPU A对数据做出状态更改,为S(共享),根据上面表格得到其它cache line的此变量需要是S或者I,于此当变量被读取到CPU B时也是S状态。

单核下的数据修改

1.CPU A发出了一条修改数据的指令,需要从主存中修改变量z。(一开始没其它cache读取,状态为I)

2.首先从主存中将数据读取到BUS总线中。

3.再通过BUS总线读取到CPU A的缓存中,进行Local write,此时cache line的状态需修改为M(修改)。

4.修改完了,再将数据回写到主存中。

img

多核下的数据修改及数据同步

修改:(承接上面多核读取结束后,CPU A对数据进行了修改)

1.CPU A进行Local write,修改变量z=2,此时要将其cache line的状态修改为M(修改),并通知有缓存了z变量的CPU,此处即CPU B。

2.CPU B需要将本地cache 中的z设置为I(无效)

3.CPU A对变量z进行赋值

img

同步:(涉及两种情况:其它CPU,如CPU B此时要读取z,或者CPU B此时要读取并修改z)

CPU B此时要读取z

1.CPU B发出读取z的指令(Remote read)

2.CPU A在总线上嗅探到这个读请求以后,先将Cache Line里修改过的数据回写(Write Back)到Memory中,然后在内存总线上放一份Cache Line的拷贝作为应答。

3.将自身的Cache Line的状态修改为Shared,由此产生的结果是CPU A与CPU B里对应的Cache Line的状态都为Shared。

img

CPU B此时要读取并修改z

1.CPU B发出读取z的指令(Remote Write)

2.CPU A在总线上嗅探到这个读请求以后,先阻止CPU B修改,然后将Cache Line里修改过的数据回写(Write Back)到Memory中,直接将自身cache line设置为I(无效)状态

3.CPU B再次获取修改请求,此时变量z在其它cache中没有缓存副本了,CPU B直接从主存中拿到最新的数据,进行修改操作,状态设置为M(修改)。

img

四、MESI问题及优化

伪共享(False Sharing)

问题定义

说回CPU缓存,缓存行(cache line)是CPU缓存的基本单位,缓存行通常是 32/64 字节,前面说了局部性原理。

当我们访问一个数据时,获取一个值后,其相邻的值也被缓存到就近的缓存行中。比如访问一个long类型数组,当数组中的一个值被加载到缓存中,它会额外加载另外7个,以致你能非常快地遍历这个数组。因此可以非常快速的遍历在连续的内存块中分配的任意数据结构。

但是没有任何是完美的存在,比如:当有多个线程操作不同的成员变量,但正好这多个变量处于相同的缓存行。如图:

img

注释:一个运行在处理器core1上的线程想要更新变量 X 的值,同时另外一个运行在处理器core2上的线程想要更新变量 Y 的值。
但是,这两个频繁改动的变量都处于同一条缓存行。两个线程就会轮番发送 RFO (Request For Owner) 消息,占得此缓存行的拥有权。
当 core1 取得了拥有权开始更新 X,则 core2 对应的缓存行需要设为 I 状态(失效态)。
当 core2 取得了拥有权开始更新 Y,则core1对应的缓存行需要设为 I 状态(失效态)。
轮番夺取拥有权不但带来大量的 RFO 消息,而且如果某个线程需要读此行数据时,L1 和 L2 缓存上都是失效数据,只有L3缓存上是同步好的数据。从前面的内容我们知道,读L3的数据会影响性能,更坏的情况是跨槽读取,L3 都出现缓存未命中,只能从主存上加载。

问题解决

1.padding
防止其他数据导致伪共享的问题常用增加padding,叫做缓存行填充的方式来解决,例如在前后加上无用的数据。

2.注解

在JDK1.8中,新增了一种注解**@sun.misc.Contended**,来使各个变量在Cache line中分隔开。

注意,jvm需要添加参数**-XX:-RestrictContended**才能开启此功能 。类前加上代表整个类的每个变量都会在单独的cache line中。属性前加代表该属性会在单独的cacheline中。


CPU切换状态堵塞

问题定义

众所周知,CPU的处理数据是非常快的,但MESI下,涉及到各个不同cache之间状态的转换通知(消息传递),这会耽误大量的时间(处理延迟)。而且CPU会一直等待消息传递和回应完成,其中的时间远大于一个指令的执行时间。

比如:CPU A需进行变量z的修改(Local Write),那必须通知其它CPU需要对缓存了z的缓存行置为I(无效)状态,并且要等所有CPU都响应确认。这等待期间会堵塞处理器,降低其性能等。

问题解决

存储缓存(Store Buffere)

为了解决等待太长时间避免资源浪费等,引入了store buffere。

处理器将想要写回到主存的数据写入到store buffere中,然后继续处理自己的事情。当发出去的所有设置无效状态的通知都响应了后,数据才会最终被同步到主存中去。

风险一:处理器会从store buffere中尝试加载数据,但其还没提交。称为store forwading,即当加载的时候,如果store buffere中有数据就进行返回;如果没有才能读取自己缓存里面的数据。

风险二:store buffere中的缓存什么时候能同步到主存中,没有任何保证。

内存屏障(Nenory Barriers)

  • 写屏障 Store Memory Barrier是一条告诉处理器在执行这之后的指令之前,应用所有已经在存储缓存(store buffer)中的保存的指令。
  • 读屏障Load Memory Barrier是一条告诉处理器在执行任何的加载前,先应用所有已经在失效队列中的失效操作的指令。

在相关代码前使用对应的读写屏障,保证数据的一致性。


五、小结

CPU 在读写数据的时候,都是在 CPU Cache 读写数据的,原因是 Cache 离 CPU 很近,读写性能相比内存高出很多。对于 Cache 里没有缓存 CPU 所需要读取的数据的这种情况,CPU 则会从内存读取数据,并将数据缓存到 Cache 里面,最后 CPU 再从 Cache 读取数据。

而对于数据的写入,CPU 都会先写入到 Cache 里面,然后再在找个合适的时机写入到内存,那就有「写直达」和「写回」这两种策略来保证 Cache 与内存的数据一致性:

  • 写直达,只要有数据写入,都会直接把数据写入到内存里面,这种方式简单直观,但是性能就会受限于内存的访问速度;
  • 写回,对于已经缓存在 Cache 的数据的写入,只需要更新其数据就可以,不用写入到内存,只有在需要把缓存里面的脏数据交换出去的时候,才把数据同步到内存里,这种方式在缓存命中率高的情况,性能会更好;

当今 CPU 都是多核的,每个核心都有各自独立的 L1/L2 Cache,只有 L3 Cache 是多个核心之间共享的。所以,我们要确保多核缓存是一致性的,否则会出现错误的结果。

要想实现缓存一致性,关键是要满足 2 点:

  • 第一点是写传播,也就是当某个 CPU 核心发生写入操作时,需要把该事件广播通知给其他核心;
  • 第二点是事物的串行化,这个很重要,只有保证了这个,才能保障我们的数据是真正一致的,我们的程序在各个不同的核心上运行的结果也是一致的;

基于总线嗅探机制的 MESI 协议,就满足上面了这两点,因此它是保障缓存一致性的协议。

MESI 协议,是已修改、独占、共享、已失效这四个状态的英文缩写的组合。整个 MSI 状态的变更,则是根据来自本地 CPU 核心的请求,或者来自其他 CPU 核心通过总线传输过来的请求,从而构成一个流动的状态机。另外,对于在「已修改」或者「独占」状态的 Cache Line,修改更新其数据不需要发送广播给其他 CPU 核心。


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

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

相关文章

100种思维模型之长远思考思维模型-63

古语有云:“人无远虑,必有近忧!” 任正非说:不谋长远者,不足以谋一时! 长远思考思维,一个提醒我们要运用长远眼光,树立宏大目标,关注长期利益的思维模型 01何谓长远思考…

深度学习架构的对比分析

深度学习的概念源于人工神经网络的研究,含有多个隐藏层的多层感知器是一种深度学习结构。深度学习通过组合低层特征形成更加抽象的高层表示,以表征数据的类别或特征。它能够发现数据的分布式特征表示。深度学习是机器学习的一种,而机器学习是…

浅谈数据资产测绘系统的作用和挑战

随着数据被定义为第五大生产要素,数据已经成为数字经济发展的核心驱动力。数据资源的充分利用和开放共享给政企单位带来便利的同时,也带来了相应的数据安全风险。因此,摸清并动态掌握数据资产情况,持续进行数据资产测绘就成为企业…

Golang每日一练(leetDay0066) 有效电话号码、转置文件

目录 193. 有效电话号码 Valid Phone Numbers 🌟 194. 转置文件 Transpose File 🌟🌟 🌟 每日一练刷题专栏 🌟 Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 193. 有效电话号…

IDEA常用配置和插件总结

文章目录 1\. 配置1.1 设置编译版本1.2 设置编码1.3 自动导包1.4 自动编译1.5 设置主题1.6 设置字体字号1.7 滚轮修改字体大小1.8 控制台字体1.9 行号与方法分隔符1.10 忽略大小写字母1.11 多行显示1.12 设置 Maven1.13 GitHub 账户1.14 配置 Git1.15 配置文件隐藏1.16 配置相同…

java中List与AbstractList

一、List 接口 List 接口继承了 Collection 接口,在 Collection 接口的基础上增加了一些方法。相对于 Collection 接口,我们可以很明显的看到,List 中增加了非常多根据下标操作集合的方法,我们可以简单粗暴的分辨一个方法的抽象方…

C++——动态管理(类和对象收尾)

作者:几冬雪来 时间:2023年5月14日 内容:C内存管理讲解 目录 前言: 1.类的对象(收尾): 1.友元函数: 2.内部类: 3.匿名对象: 4.优化: 2.…

常见基础算法

一、排序 & 查找算法 1.1 冒泡排序 相邻的数据进行比较。每次遍历找到一个最大值。 public void sort(int[] nums) {if (nums null) {return;}for (int i 0; i < nums.length; i) {for (int j 0; j < nums.length - 1 - i; j) {if (nums[j] > nums[j 1]…

Python每日一练(20230515) 只出现一次的数字 I\II\III

目录 1. 只出现一次的数字 Single Number 2. 只出现一次的数字 II Single Number II 3. 只出现一次的数字 III Single Number III &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 leetcod…

开源项目ChatGPT-website再次更新,累计下载使用1600+

&#x1f4cb; 个人简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是阿牛&#xff0c;全栈领域优质创作者。&#x1f61c;&#x1f4dd; 个人主页&#xff1a;馆主阿牛&#x1f525;&#x1f389; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4d…

数据交换方式(电路,报文,虚电路分组交换,数据报分组交换)

电路交换&#xff1a; 电路交换是通信网中最早出现的一种交换方式&#xff0c;在进行数据传输前&#xff0c;两个结点之间必须先建立一条专用&#xff08;双方独占&#xff09;的物理通信链路。该线路在整个数据传输期间一直被独占&#xff0c;用户始终占用端到端的固定传输带…

python实现带有操作界面的计算器程序,实现基本的数值计算,支持负数、小数、加减乘除等运算。

一、程序要求 python实现带有操作界面的计算器程序,实现基本的数值计算,支持负数、小数、加减乘除等运算。 预期计算器界面如下: 二、代码实现 1、python3自带tkinter,可以用来做可视化界面: import tkinter as tk import re 2、新建窗口对象,设置高宽、设置标题和背景…

【分布族谱】正态分布和对数正态分布的关系

文章目录 正态分布对数正态分布的推导测试 正态分布 正态分布&#xff0c;最早由棣莫弗在二项分布的渐近公式中得到&#xff0c;而真正奠定其地位的&#xff0c;应是高斯对测量误差的研究&#xff0c;故而又称Gauss分布。。测量是人类定量认识自然界的基础&#xff0c;测量误差…

UEFI 界面实例解析

这篇文章主要记录一些setup界面的实例&#xff0c;这些实例都是EDK上的&#xff0c;我们可以看到如下图&#xff1a; 上面三个为banner&#xff0c;下面的都是通过LABLE动态加载的&#xff0c;代码如下&#xff1a; 我们可以看到 UiListThirdPartyDrivers (HiiHandle, &gEf…

Sentinel 熔断降级和黑白名单控制

一、熔断降级 1、概述 除了流量控制以外&#xff0c;对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块&#xff0c;可能是另外的一个远程服务、数据库&#xff0c;或者第三方 API 等。例如&#xff0c;支付的时候&#xff0c;…

【C++ 入坑指南】(06)运算符

文章目录 一、算术运算符二、赋值运算符三、比较运算符四、逻辑运算符五、算法题5.1、拆分位数 运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 内置了丰富的运算符&#xff0c;并提供了以下类型的运算符&#xff1a; 运算符类型作用算术运算符用于处理四则运算赋值…

交换机配置第十二讲(ACL访问控制)

1.实验介绍 设备规划 类型名称数量终端PC3路由器AR22403 IP规划 主机 ip链接交换机端口网关client1192.168.1.2AR1-g/0/0/0192.168.1.1client2192.168.2.2AR2-g/0/0/1192.168.2.1client3192.168.3.2AR3-g/0/0/1192.168.3.1 2. 连线图介绍 连线顺序 3. 基础配置介绍 我们首…

基于SSM的高校共享单车管理系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

〖大前端 - 基础入门三大核心之JS篇㉞〗- JavaScript 的「立即执行函数IIFE」

当前子专栏 基础入门三大核心篇 是免费开放阶段。推荐他人订阅&#xff0c;可获取扣除平台费用后的35%收益&#xff0c;文末名片加V&#xff01;说明&#xff1a;该文属于 大前端全栈架构白宝书专栏&#xff0c;目前阶段免费开放&#xff0c;购买任意白宝书体系化专栏可加入TFS…

Threejs进阶之十四:在uniapp中使用threejs创建三维图形

在uniapp中使用threejs 一、uni-app介绍二、新建uni-app项目三、安装three.js库四、在vue组件中引入three.js库五、创建场景(Scene)和相机(Camera)六、创建渲染器(Renderer)七、创建物体和灯光八、渲染场景(Scene)九、运行测试核心代码 一、uni-app介绍 uni-app是一个基于Vue.…