面试加油站——第五期(计算机底层基础知识扫盲)

计算机底层基础知识扫盲

重点知识点(大纲):

  • 计算机组成 —— 控制器、运算器、存储器、输入设备、输出设备;

    • 现代计算机:CPU(控制器+运算器)、主存+磁盘(存储器)、外围设备(输入输出设备)、主板(芯片组<司机>/总线<高速公路>)

  • CPU逻辑组成 —— 控制单元、运算单元、存储单元

    • CPU执行指令的过程:取指令 ——> 指令译码 ——>执行指令 ——> 访存数据 ——> 写回结果

  • DMA总线优势 —— 当要从磁盘将数据拷贝至主存时,可通过DMA总线,释放CPU;(总线控制权转移)

  • 存储器 —— 寄存器、3级缓存、主存、磁盘

  • 缓存一致性协议(MESI)

    • 程序局部性原理 ——> 缓存行(按块读取)——> Intel芯片64字节(实践经验折中值)

    • 缓存数据一致性 ——> 缓存一致性协议(轻量级MESI)——> 总线锁(重量级)

    • 伪共享 ——> 总线风暴 ——> 缓存行对齐编程模式 ——> JDK8中@Contented注解

  • NUMA(非统一内存访问)——> JVM内存分配TLAB机制 

  • 汇编语言就是机器语言,只是加了一些助记符

  • 内存和磁盘的实现差异


image.png

计算机底层的基础知识,我们不需要学习太深,但是应该有适当的理解,这样更有助于我们深刻地理解我们后面关于内核Kernel,乃至更后面的应用层的技术。


一、计算机组成

1、冯·诺依曼体系

计算机五大组件:运算器,控制器、存储器、输入设备、输出设备

具体到现代计算机,结合本文最顶部的计算机组成逻辑图

  • CPU = 运算器 + 控制器

  • 内存 + 磁盘 = 存储器

  • 鼠标 + 键盘 + 显示器等 = 输入输出设备

主板(芯片组 + IO总线等)负责:CPU、内存、磁盘、输入输出设备等之间的数据传输

2、什么是主板?

  • 存放在内存中的数据需要被CPU读取,CPU计算完成后,还需要把数据写回到内存中,然而CPU又不能直接插在内存中,这时候就需要主板出马了;

  • 主板上又很多个插槽,CPU和内存都是插在主板上的,主板上的芯片组与总线就解决了CPU与内存之间的通讯问题。

    • 芯片组控制着数据传输的流转方向,决定着数据从哪里流向哪里 —— 司机

    • 总线是实际数据传输的高速公路,总线速度决定了数据的传输速度 —— 高速公路

总结就是:主板上又很多插槽,负责整个计算机各部件之间的数据传输,由芯片组(司机:控制数据流向)+ 总线(高速公路)组成。

3、什么是总线?

中大型计算机大多采用三总线结构:系统总线 + 内存总线 + IO总线

另外还有一种是两总线结构:系统总线 + IO总线

依然可以通过本文顶部的计算器组成逻辑图很直观的看到:

  • 系统总线(CPU总线):负责CPU和各个I/O之间的信息传输,由CPU管理;(可以访问内存)

  • 内存总线(DMA总线):负责主存与告诉外设之间的信息交换,由DMA控制器管理;(可以访问内存)

  • IO总线(常见PCI总线):外围设备互联总线;

所以,现代的计算机整体看,又可以看成:CPU + 主存(内存) +  外围设备 + 总线

通过以上的列表,我们知道,正常情况下 CPU总线 和 内存总线 都可以访问内存,但是他们却不可以同时访问内存,即同一时刻,只能有一条总线访问内存。

在实现DMA内存访问时,是有DMA控制器直接掌管总线的,因此,就存在了一个总线控制权转移的问题:

  • 即当发生DMA传输前,CPU需要先将总线控制权交给DMA控制器,而在DMA传输完成后,DMA控制器应立即将总线控制权交回给CPU

4、那么到底什么是DMA总线?为什么要设计DMA总线?

参考文章:https://www.cnblogs.com/ttaall/p/13738562.html

DMA,全称Direct Memory Access,即直接内存访问,是一种不经过CPU而直接访问内存一种数据交互模式

  • 没有DMA技术之前,如果需要将数据从硬盘读取到内存中,必须要依赖于CPU,数据必须要先经过CPU寄存器,再从CPU寄存器写到内存中

  • 但是有了DMA技术后,数据就可以不经过CPU寄存器而是直接从硬盘中拷贝到内存中,很明显,这可以大大地提升效率。

我们做一下对比:

传统硬盘——内存读取模式:

image.png

image.png

DMA技术下的磁盘——内存读取模式:

image.png

总结,很明显:

  • 有了DMA技术后,对于处理将数据从磁盘——>内存这样的操作的时候,CPU只需要刚开始介入下,通知DMA控制器(小弟)做事,之后自己就不用管了,直到DMA将任务完成后,才告知CPU任务已经完成

  • 在数据从磁盘——>内存的过程中,CPU完全被释放出来,可以去做其它事情(不涉及内存操作的任务),这样效率自然是可以大大提升的!


二、CPU的组成

参考文章:https://www.cnblogs.com/yilang/p/10993051.html

1、CPU的三大逻辑组成:

image.png

CPU从逻辑上可以分为三个模块,控制单元 + 运算单元 + 存储单元,而这三部分由CPU内部总线连接起来。

  • 控制单元:整个CPU的指挥控制中心,协调整个计算机有序工作至关重要!

    • 指令寄存器IR(Instruction Register)、指令译码器ID(Instruction Decoder)、操作控制器OC(Operation Controller)、时序发生器程序计数器等部件组成。

  • 运算单元运算器的核心,真正干活的单元,可以执行算术运算(包括加减乘除算术运算及其附加运算)逻辑运算(包括移位、逻辑测试或两值比较)

    • 相对于控制单元而言,运算单元接受控制单元的命令而执行动作,即运算单元所进行的全部操作,都是由控制单元发出的控制信号来指挥的,所以他属于执行部件

    • 算术/逻辑运算单元(ALU)、累加器、数据缓冲寄存器、状态寄存器、通用寄存器组组成,所以它也是数据加工处理部件

  • 存储单元:是CPU中暂时存放数据的地方,里面保存着那些将要被处理的数据(从主存读过来等待被处理的数据),或已经处理过的数据(即将写回到主存的中的数据)!

    • CPU访问寄存器中的数据要比访问内存中的数据快得多(近100倍),将数据批量地从内存读到寄存器中,以减少CPU读取内存得次数,从而大大提升CPU的工作速度;

    • CPU片内缓存(三级缓存)、寄存器组 组成。

经过上面的列表,我们还能够发现:

在CPU内部,存在大量的寄存器(上百个之多),在每一个单元中都存在着一些功能不同的寄存器,每个寄存器都有不同的职责

对于以上的部件,或那些大量的寄存器,我们没必要全部了解,毕竟咱不是做CPU核心开发的,只需要记住一些核心组成的即可。

image.png

2、几大需要记住的核心组成:

  • CU(Control Unit):控制单元

  • ALU(Arithmetic and Logic Unit):算术/逻辑运算单元

  • MMU(Memory Manage Unit):内存管理单元

  • Register:寄存器 —— 存在于任意单元中

  • PC(Program Counter):程序计数器 —— 属于控制单元

  • Cache:CPU片内缓存(三级缓存)—— 属于存储单元

3、CPU执行指令的过程:

CPU执行指令分为5个阶段:取指令——>指令译码——>指令执行——>访存数据——>结果写回

image.png

  • 取指令将一条指令从主存中读取到指令寄存器的过程;

    • 程序计数器PC中的数值,用来指示当前指令在主存中的位置。当一条指令被取出后,PC中的数值将根据指令长度而自动递增:

      • 若为单字长指令,则(PC)+1àPC;

      • 若为双字长指令,则(PC)+2àPC;

      以此类推!

  • 指令译码:指令译码器按照预定的指令格式,对取出的指令进行拆分和解释,识别区分不通的指令类别以及各种获取操作数的方法;

  • 执行指令:完成指令所要求的各种操作,具体实现指令的功能

  • 访存数据:根据指令译码阶段,得到的指令类别以及获取操作数的方法,包括操作数在主存中的地址,此步骤需要从主存中读取数据并放到数据寄存器中,以用于运算

  • 结果写回:作为最后一个阶段,结果写回(Write Back,WB)阶段把执行指令阶段的运行结果数据”写回“到某种形式的存储介质中:不一定就是内存,更多时候先是写到CPU内部的寄存器(暂存器)中,以便被后续的指令快速获取

4、所谓的单核双线程(4核8线程)——超线程:

image.png

  • 在没有超线程技术时,当线程切换时,需要将前一个线程的内容踢出去,然后换成新线程的内容;

  • 有了超线程技术后,只需要转头到另一个线程的寄存器中取数据处理即可,非常方便,所以性能可以大大地被提升!


三、CPU中的存储器

我们都知道,在整个计算机中,存在着大量的存储形式,如各种寄存器、内存、磁盘;它们之间存在着很明显的层次结构。image.png

1、各种存储形式的速度对比:

Registers 寄存器 < 1 ns
L1 Cache 高速缓存 约1 ns
L2 Cache 高速缓存 约3 ns
L3 Cache 高速缓存 约15 ns
Main Memory 主存 约80 
磁盘 
远程文件存储

由此可见,在我们应用层开发者眼中一直很快的内存,在和CPU内部的存储单元比起来时,还是很慢的,差了将近100倍!

2、多核CPU结构图(L1/L2/L3存在的位置):image.png

由上图,我们可以很清晰的看到:

  • L1、L2属于每一个核,而L3属于CPU级别的,即同一个CPU中的多个核共用一个L3 Cache!

  • 主存时所有CPU公用的,整个计算机共用内存区域!

3、程序局部性原理,可以提高效率:

定义:

程序局部性原理:程序在执行时呈现出局部性规律,即在一段时间内,程序的执行仅限于程序的某一部分。相应的,程序所访问的存储空间也局限于内存的某一区域。

局部性通常有两种形式:时间局部性 + 空间局部性

时间局部性:被引用过一次的存储器位置,在未来大概率会被多次使用

空间局部性:如果一个存储器的位置被引用,那么将来它附近的位置也大概率会被引用到

基于程序局部性原理,按块读取的数据读取方案就可以大大地提升性能!

按块读取:如果我们读取数据的时候,只需要读取一个字节,但是基于程序局部性原理,我们就不会只读取需要的那一个字节,而是以缓存行(缓存块)为单位,每次从上一级缓存中多读一点数据放到下一级缓存中

充分发挥总线CPU针脚能一次性读取更多数据的能力。

image.png

4、缓存行(缓存块)的大小?

Intel芯片中,设计的缓存行大小为64字节,但是这不是计算出来的结果,而是个最优实践的总结值

  • 缓存行越大,局部性空间效率越高,但是读取时间慢;

  • 缓存行越小,局部性空间效率越低,但是读取时间快;

所以64字节大大小,只是Intel芯片经过长期实践,总结出来的折中经验值!

5、缓存一致性协议(MESI)——缓存锁:

通过上面的内容,我们知道了,数据是按缓存行(缓存块)被读取到下一级缓存的,那么如上图,当左边核的缓存行被修改过,哪怕一个字节,也必须得让右边得核知道,不然就出大问题了!

为了解决这一问题,Intel公司的CPU中使用的技术,就是缓存一致性协议(MESI)

缓存一致性协议定义了缓存行的4种状态:Modified(被修改过的)、Exclusive(独占的)、Shared(共享的)、Invalid(失效的),使用2bit即可表示此4种状态,而MESI英文简写也就是这四个单词的首字母!

大概总结下就是:

  • 所有的缓存行,通过类似监听器的方式,监听一些事件:本地读、本地写,远端读,远端写

  • 如果某个核中的变量x所在缓存块状态为(E,独占的),那么当他修改数据时,只需要将自己状态变为M,然后再写回主内存即可,当写回成功后,就再次变为(E,独占的);

  • 但是当监听到有其它核也在读取这个数据时,上面的核中的x所在缓存块的状态就会升级为(S,共享的)—— 不存在其它核没读取,直接修改的情况;

  • 当某个核修改了本地变量x,如果x所在缓存块状态为(S,共享的),那么会将自己缓存块的状态变为(M,代表本地修改过,并且与主内存不一致),然后将x新值写回到主内存中,同时发出信号通知其它核将它们的对应缓存块状态置为(I,失效状态)

image.png

补充:为了解决缓存一致性问题,其实还存在一种比MESI更重的协议——总线锁(某个处理器为了对某个变量执行修改操作,直接给总线发了一个LOCK#信号,从而直接将总线占为己有,阻塞其它所有的处理器使用总线,这显然大大地降低了CPU的并行度!)

有些无法被缓存的数据,或者跨越多个缓存行的数据,就得要用到总线锁,才能够保证多个核之间得缓存数据一致性!

6、一种框架常见的编程模式——缓存行对齐:

伪共享:引入了缓存行,本来是为了提高效率,但是由于这些数据经常被并发访问,而又必须保证缓存数据一致性,这就导致左边核刚修改完就得立即通知右边核废弃之前的缓存,重新从主存种读取最新值,这样频繁左右通知,效率反而会大大降低,这就成了伪共享

总线风暴:如果频繁发生上述描述的左右通知的情况,JAVA中volatile关键字的嗅探机制,以及CAS操作底层都会频繁占用总线带宽,导致总线流量激增,这样就容器触发总线风暴

对于一些敏感的数据,会存在线程高竞争的访问,这时候,为了保证不发生伪共享,我们就可以使用缓存行对齐的编程模式;

可以看著名框架Disruptor并发框架中关于cursor指针的实现:

image.png

一个long类型变量占用8个字节,而一个缓存行为64字节,可以容纳8个long类型的变量!

这里的主角就是中间的cursor指针,Disruptor并发框架故意在cursor指针的前后各插入了7个long类型的变量,这样的话,无论怎么取,目标cursor指针都不会跟其它数据存在于同一个缓存行中,从而不会由于缓存一致性协议而频繁地被通知去主存中刷新数据。

其实:

  • 在JDK8之前的版本中,有时候为了保证缓存行对齐,我们就经常会使用long padding这样的编程方式;

  • 在JDK8之后,官方增加了一个注解@Contented:如果我们在某个变量上加上了这个注解,JDK就能够做到让这个变量永远不会跟其他变量位于同一个缓存行中,底层可能也是使用的这种方案!

  • (需要加上:JVM -XX:-RestrictContented 参数,实验证明,效率可以提高2-3倍)

7、NUMA(Non Uniform Memory Access)

首先什么是UMA(统一内存访问)

image.png

NUMA(非统一内存访问,对自己CPU同一插槽的内存有限访问)

image.png

ZGC垃圾回收器可以做到:NUMAAware(即会感知当前机器是否支持NUMA),分配内存时,会优先分配该线程所在CPU的同一插槽中的内存。

JVM虚拟机中堆内存中的伊甸园区中,在为新生对象分配内存时候,也存在一个类似的现象 —— TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区,细节就在JVM内存分配的地方细聊了


四、汇编语言及其它一些小知识点补充

1、汇编语言的产生:

在汇编之前,计算机编程就是00100100这样的01编码方式(很恐怖)!

后来人们就在想,可不可以把一些常用的有特定含义的操作对应的01串整理起来,为它们都起一个容易记住的英文名字,比如:mov=00100110,add=10010101。

这种mov、add、sub等英文单词,就是助记符。

所以,汇编语言的本质就是助记符,汇编语言就是机器语言,只是给一些01串起了个别名。

2、汇编语言执行的过程:

计算机通电 ——> CPU到提前约定好的固定的位置读取程序指令(电信号输入)——> 时钟发生器不断震荡通断电

     ——> 推动CPU内部一步步地向前执行(执行多少步,取决于程序指令需要的时钟周期)

     ——> 计算完成 ——> 写回特定的地方(电信号输出)——> 写给显卡输出(sout或者图形)

所以,时钟发生器就是整个计算机的心脏,每通断电一次就是一个时钟周期。(现代计算机的时钟发生器每秒能达到GHz的振荡频率,也就是我们常说的CPU主频!)

3、内存核硬盘的实现原理,为什么内存比磁盘快那么多?

  • 内存:特殊的材料,只有通电的时候才会有01的状态,以此来记录数据,但是只要一断电,就会立即归零,也就是所谓的数据丢失;

  • 磁盘:磁盘表面凹凸不平,凸起的地方代表数字1(被磁化),凹下去的地方代表数字0(没有被磁化),以此来记录数据,因此磁盘可以以二进制持久化存储文字、图片等信息。

    • 当通过磁头写数据时,磁头中的电流会导致磁粉极化,改变方向;

    • 当通过磁头读数据时,磁头经过有磁力的区域会产生电流,通过这种方式来完成01数据的读取。

上述原理的不同,也解释了为什么内存比磁盘快,因为电的速度,肯定远远高于磁头扫过磁盘的速度

jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐