程序员应该知道的操作系统基础知识

在多线程并发环境下,经常出现一些意想不到的错误,例如数值的累加,错的原因可能涉及到计算机原理以及JAVA方面的一些知识。

下面我们就先从CPU的多级缓存开始说起;

CPU缓存

缓存存在的意义:

CPU缓存存在的意义分两点(局部性原理):

  1. 时间局部性:如果某个数据被访问,那么在不久的将来它很可能被再次访问。
  2. 空间局限性:如果某个数据被访问,那么与它相邻的数据很快也可能被访问。

我们先来认识一下整体结构,看一张图片,如下:

image-20201219174053850

CPU Core : CPU核心

Cache : 高速缓存,数据的读取和存储都经过此高速缓存

Main Memory : 内存/主存

缓存的工作原理是当CPU要读取一个数据时,首先从缓存中查找,如果找到就立即读取并运送给CPU处理;如果没有找到,就用相对慢的速度内存中读取并运送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。

这里只涉及到了一个高速缓存Cache,那为什么后来有了缓存的分级呢?有了L1-CacheL2-CacheL3-Cache呢?要想弄懂这些首先我们要明白我们为什么需要缓存?

因为CPU的频率太快了,快到内存/主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源,这样会使CPU花费很长时间等待数据到来或把数据写入内存。所以Cache的出现,是为了解决CPU运行处理速度与内存读写速度不匹配的矛盾(结构:CPU - > CACHE - > Main Memory - > SSD)

我们先不说缓存为什么需要分级?可以想一下,如果是你你想要一个什么样的缓存器/存储器,或者说你想设计一个什么样子的缓存Cache;速度快、体积小、空间大、能耗低、散热好、断电数据不丢失等等都是我们想要的;但在现实中,我们往往无法把所有需求都实现。

  • 如果一个存储器的体积小,那它存储空间就会受到制约,因此命中率就会降低。
  • 如果一个存储器离 CPU 较远,那么在传输过程中必然会有延迟,因此传输速度也会下降。

你可能还会问,那干吗不把内存放到 CPU 里?

如果你这么做的话,除了整个电路散热和体积会出现问题,服务器也没有办法做定制内存了。也就是说 CPU 在出厂时就决定了它的内存大小,如果你想换更大的内存,就要换 CPU,而组装定制化是你非常重要的诉求,这肯定是不能接受的。

此外,在相同价格下,速度越快,那么它的能耗通常越高。能耗越高,发热量越大。

因此,我们上面提到的需求是不可能被全部满足的,除非将来哪天存储技术有颠覆性的突破。

CPU分级缓存

综上所诉,我们不能用一块存储器来解决所有的需求,那就必须把需求分级。

一种可行的方案,就是根据数据的使用频率使用不同的存储器:高频使用的数据,读写越快越好,因此用最贵的材料,放到离 CPU 最近的位置;使用频率越低的数据,我们放到离 CPU 越远的位置,用越便宜的材料。

image-20201219173825769

这里我们就增加了L1-Cache,L2-Cache,L3-Cache这三级缓存都是集成在CPU内的缓存;它们的作用都是作为CPU与主内存之间的高速数据缓冲区,L1最靠近CPU核心;L2其次;L3再次。运行速度方面:L1最快、L2次快、L3最慢;容量大小方面:L1最小、L2较大、L3最大。CPU会先在最快的L1中寻找需要的数据,找不到再去找次快的L2,还找不到再去找L3,L3都没有那就只能去内存找了。L1、L2、L3可以说是各有特点,下面我们就分开来讲一下。

在讲解之前我们先回顾一下在上一节中对寄存器大致讲解了一下,那寄存器的位置在哪里呢?其实寄存器是距离CPU核心是最近的;

image-20201219174701737

寄存器紧挨着 CPU 的控制单元和逻辑计算单元,它所使用的材料速度也是最快的。就像我们前面讲到的,存储器的速度越快、能耗越高、产热越大,而且花费也是最贵的,因此数量不能很多。

寄存机的访问速度非常快,一般要求在半个 CPU 时钟周期内完成读写。比如一条要在 4 个周期内完成的指令,除了读写寄存器,还需要解码指令、控制指令执行和计算。如果寄存器的速度太慢,那 4 个周期就可能无法完成这条指令了。

L1-Cache

L1- 缓存在 CPU 中,相比寄存器,虽然它的位置距离 CPU 核心更远,但造价更低。通常 L1-Cache 大小在几十 Kb 到几百 Kb 不等,读写速度在 2~4 个 CPU 时钟周期。

L2-Cache

L2- 缓存也在 CPU 中,位置比 L1- 缓存距离 CPU 核心更远。它的大小比 L1-Cache 更大,具体大小要看 CPU 型号,有 2M 的,也有更小或者更大的,速度在 10~20 个 CPU 周期。

L3-Cache

L3- 缓存同样在 CPU 中,位置比 L2- 缓存距离 CPU 核心更远。大小通常比L2-Cache 更大,读写速度在 20~60 个 CPU 周期。L3 缓存大小也是看型号的,比如 i9 CPU 有 512KB L1 Cache;有 2MB L2 Cache; 有16MB L3 Cache

内存

内存的主要材料是半导体硅,是插在主板上工作的。因为它的位置距离 CPU 有一段距离,所以需要用总线和 CPU 连接。因为内存有了独立的空间,所以体积更大,造价也比上面提到的存储器低得多。现在有的个人电脑上的内存是 16G,但有些服务器的内存可以到几个 T。内存速度大概在 200~300 个 CPU 周期之间。

SSD 和硬盘

SSD 也叫固态硬盘,结构和内存类似,但是它的优点在于断电后数据还在。内存、寄存器、缓存断电后数据就消失了。内存的读写速度比 SSD 大概快 10~1000 倍。以前还有一种物理读写的磁盘,我们也叫作硬盘,它的速度比内存慢 100W 倍左右。因为它的速度太慢,现在已经逐渐被 SSD 替代。

image-2020121922400449

当 CPU 需要内存中某个数据的时候,如果寄存器中有这个数据,我们可以直接使用;如果寄存器中没有这个数据,我们就要先查询 L1 缓存;L1 中没有,再查询 L2 缓存;L2 中没有再查询 L3 缓存;L3 中没有,再去内存中拿。

指令的预读

接下来我们讨论下指令预读的问题。

之前我们学过,CPU 顺序执行内存中的指令,CPU 执行指令的速度是非常快的,一般是 2-6 个 CPU 时钟周期;这里,我们学习了存储器分级策略,发现内存的读写速度其实是非常慢的,大概有 200-300 个时钟周期。

不知道你发现没有?这也产生了一个非常麻烦的问题:CPU 其实是不能从内存中一条条读取指令再执行的,如果是这样做,那每执行一条指令就需要 200-300 个时钟周期了。

那么,这个问题如何处理呢?

这里我再多说一句,你在做业务开发 RPC 调用的时候,其实也会经常碰到这种情况,远程调用拖慢了整体执行效率,下面我们一起讨论这类问题的解决方案。

一个解决办法就是 CPU 把内存中的指令预读几十条或者上百条到读写速度较快的 L1- 缓存中,因为 L1- 缓存的读写速度只有 2-4 个时钟周期,是可以跟上 CPU 的执行速度的。

这里又产生了另一个问题:如果数据和指令都存储在 L1- 缓存中,如果数据缓存覆盖了指令缓存,就会产生非常严重的后果。因此,L1- 缓存通常会分成两个区域,一个是指令区一个是数据区

与此同时,又出现了一个问题,L1- 缓存分成了指令区和数据区,那么 L2/L3 需不需要这样分呢?其实,是不需要的。因为 L2 和 L3,不需要协助处理指令预读的问题。

缓存的命中率

接下来,还有一个重要的问题需要解决。就是 L1/L2/L3 加起来,缓存的命中率有多少?

所谓命中就是指在缓存中找到需要的数据。和命中相反的是穿透,也叫 miss,就是一次读取操作没有从缓存中找到对应的数据。

据统计,L1 缓存的命中率在 80% 左右,L1/L2/L3 加起来的命中率在 95% 左右。因此,CPU 缓存的设计还是相当合理的。只有 5% 的内存读取会穿透到内存,95% 都能读取到缓存。 这也是为什么程序语言逐渐取消了让程序员操作寄存器的语法,因为缓存保证了很高的命中率,多余的优化意义不大,而且很容易出错。