当前位置: 首页 > 图灵资讯 > 技术篇> 【操作系统入门到成神系列 四】CPU缓存一致性

【操作系统入门到成神系列 四】CPU缓存一致性

来源:图灵教育
时间:2023-05-25 09:20:13

文章目录

  • CPU缓存一致性
  • 一、引言
  • 二、CPU Cache 的数据写入
  • 1. 写直达
  • 2. 写回
  • 三、缓存一致性问题
  • 1. 总线嗅探
  • 2. MESI 协议
  • 四、总结
CPU缓存一致性

【操作系统入门到成神系列 四】CPU缓存一致性_系统架构

一、引言

本文参考 小林coding 《图解操作系统》也是我非常喜欢的微信官方账号博主。 call

老读者知道我以前再写 Kafka 为什么博文突然开始写操作系统?

原因在于:

当我看到 Kafka 一些服务端 IO 操作时,我发现自己看不懂。理解后,我发现它在这里 Netty 的概念。

当我试着知道的时候 IO 当时发现了一些内存和磁盘的交换,让我焦头烂额,于是想从零开始静下心来。

当我把 小林coding 的 看完《图解操作系统》,发现对操作系统的理解更上一层楼。

用一段话作为今天的开场白:

阅读的根本目的不一定是解决实际问题,更像是心灵的安慰。 一个喜欢读书的人可能不记得自己读过什么书。 但是那些看过的故事,收获的感受,沉浸的气质,就像一颗种子,会在你的身体里慢慢发芽生长,不断提高你的认知,开阔你的视野。

二、CPU Cache 的数据写入

随着时间的推移,CPU和内存的访问性能越来越大,所以在 CPU 嵌入内部 CPU Cache(高速缓存)。

【操作系统入门到成神系列 四】CPU缓存一致性_面试_02

让我们简单介绍一下 CPU Cache 的结构:

CPU Cache 由很多的 CPU Line(缓存块)组成,CPU Line 是 CPU 从内存中读取数据的基本单位, CPU Line 由各种标志组成(Tag)+ 数据块(Data Block)组成。

【操作系统入门到成神系列 四】CPU缓存一致性_系统架构_03

我们之前介绍过 CPU 阅读数据时,尽可能多地阅读数据 CPU Cache 我们大大提高了中学的阅读能力 CPU 的性能。

当然,数据不仅有读取,还有写入。

如果我们在修改数据中写入修改数据 Cache 之后,Cache 我们需要将数据与内存数据不一致 Cache 数据与内存同步。

用什么方法? Cache 数据同步内存非常重要,提供两种方法:

  • 写直达(write through)
  • 写回(write back)
1. 写直达

我们上面说的是什么时候会? Cache 从数据同步到内存是非常重要的。

当数据写入Cache时,我们会将数据同步到内存。

【操作系统入门到成神系列 四】CPU缓存一致性_数据_04

事实上,一开始有一个问题,我不知道读者是否有这个问题。

为什么当我们的CPU写入数据时,这个数据会消失? CPU Cache 中呢?

按道理说,CPU 当需要获取某些数据时,数据将缓存到磁盘/内存中 CPU Cache 然后读入相应的寄存器计算数据,那么为什么数据不在呢? CPU Cache 的情况呢?

我们的 CPU Cache 比如大小有限制,比如 L1 Cache,一般数据和指令共占用一般数据和指令 64KB。如果我们的CPU Cache 满了,就会开始 CPU Cache 淘汰策略,一些CPU Cache 淘汰。注意:这里淘汰的是 CPU Cache 单位,也就是 CPU Line(缓存块)

这里介绍下 CPU Cache 淘汰策略:

  • LRU策略:老生常谈,淘汰最近最少使用的。
  • Random策略:随机淘汰

测试人员发现,一般情况下,LRU 淘汰策略比 Random 淘汰策略效果更好。

2. 写回

我们观察上面的 每次我们写直达,我们都能发现一个缺点。 CPU 在执行写入操作时,将操作写入内存。

我们以前谈过,CPU 写内存是浪费时间。如果你经常写,它会大大降低 CPU 的性能。

因此,出现了新的写回机制:**当写作操作发生时,新数据只被写入 CPU Block 中间,只有当修改时 CPU Block 只有在被替换时,才需要将其写入内存中。**大大降低了写回内存的频率,从而提高了系统的性能。

CPU Block:CPU Line 存储数据的位置

  • 当当当当前的写作操作发生时,首先检查我们当前的数据是否在 CPU Cache 中
  • 如果存在 CPU Cache 直接将当前数据更新到内部 CPU Blcok 中并将 CPU Blcok 标记为 脏
  • 如果不存在 CPU Cache 在中间,有必要分配当前的数据 CPU Block
  • 如果这个 CPU Block 如果不脏,需要从内存中读取当前要写的数据 CPU Block 中间,写入 CPU Block 中间,然后将此 CPU Block标记为 脏
  • 如果这个 CPU Block 如果是脏的,需要将之前的位置写回内存,然后从内存中读取当前要写的数据 CPU Block 中间,写入 CPU Block 中间,然后将此 CPU Block标记为 脏

这里的疑惑主要在于:为什么我们要去内存再读数据?

  • 这里我们先明确一下缓存块(Cache Line)大小为 64字节,CPU 写入高速缓存可以是不同的大小(1、2、4、8字节)
  • 如果我们的 CPU 一次性写入 4 字节,将这 4 所有字节都更新到 Cache Block 会导致什么问题?
  • 数据可能不一致,你目前的更新只是更新了这个 4 其余的字节数据 60 你不知道字节的数据是否更新了。
  • 因此,我们只需要在主内存中重新加载它 Cache Block,在重新写入 4 只有字节数据才能保持数据一致
  • 场景:主要发生在多CPU、在多核状态下,不同的核心会改变内存。
三、缓存一致性问题

目前,由于计算机的快速发展,我们 CPU 都是多核的。我们前面说过。 L1 Cache、L2 Cache 核心是独一无二的,会带来缓存不一致性。

以下是缓存不一致的例子:

【操作系统入门到成神系列 四】CPU缓存一致性_数据_06

 

如果我们的 A 号执行了 i++ 句子结束后,为了考虑性能,为了实施我们之前提到的写回策略,我们将 i = 1 的值写回到 L1/L2 Cache,然后对应 Block 标记为脏的,下次更换时再刷新到内存。

但在这个时候,我们可以清楚地发现,如果我们 B 号重新读取 i 事实上,读取的数据是 i = 0

这样,我们 B 数字读取的数据实际上是错误的,此时 i 该值已修改为 1 这就是所谓的 缓存一致性问题

【操作系统入门到成神系列 四】CPU缓存一致性_缓存_07

 

那么,如何解决这个问题呢?简单地说,我们需要实现能够同步两个核心数据的机制。

要实现这一机制,必须做到以下两点:

  • 第一点:在核心CPU中 Cache 当数据更新时,需要通知其他核心 Cache,这叫写传播
  • 第二点:CPU核心对数据的操作顺序必须在其他核心看起来相同,这被称为事务串行

第一点很容易理解,第二点如下:

要实现事务,必须做到两点:

【操作系统入门到成神系列 四】CPU缓存一致性_面试_08

  • CPU 核心对于 Cache 中数据的操作需要与其他数据同步 CPU 核心;
  • 要引入「锁」如果有两个概念 CPU 核心中有相同的数据 Cache,所以对此 Cache 只有数据更新才能获得「锁」,可以更新相应的数据。
1. 总线嗅探

对于我们来说,第一点是写作和传播,使用它 CPU 总线嗅探的方式。

总线嗅探的工作机制:当 A 号 CPU 核心修改了 L1 Cache 中 i 变量值,通过总线通知所有其他核心,然后每个 CPU 核心会监控总线上的广播事件,检查自己是否有相同的数据。 L1 Cache 里面,如果 B 号 CPU 核心的 L1 Cache 该数据包含在数据中,然后,您还需要将数据更新到您自己 L1 Cache。

我们的总线嗅探并不能保证事务的串行化,所以此时出现了解决事务串行化问题的协议。

该协议的名称为:MESI协议

2. MESI 协议

MESI 协议其实是 4 状态单词的开头字母缩写分别为:

  • Medified:已修改
  • Exclusive:独占
  • Share:共享
  • Invalidated:无效的

「已修改」状态是我们前面提到的脏标记,代表应该 Cache Block 上面的数据已经更新了,但还没有写在内存中。而且「已失效」这意味着状态 Cache Block 数据中的数据已经失效,无法读取该状态的数据。

「独占」和「共享」状态都代表 Cache Block 里面的数据是干净的,也就是说,这个时候 Cache Block 内存中的数据与内存中的数据一致。

「独占」和「共享」不同之处在于,当独占状态时,数据只存储在一个中 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有这样的数据。在这个时候,如果你想独占 Cache 写数据可以直接自由地写入,而无需通知其他数据 CPU 核心,因为只有你有这个数据,就没有缓存一致性的问题,所以你可以随意操作这个数据。

另外,在「独占」状态下的数据,如果其他核心从内存读取相同的数据到各自的数据 Cache ,此时,独占状态下的数据将成为共享状态。

那么,「共享」状态代表多个数据中相同的数据 CPU 核心的 Cache 里面有,所以当我们想更新的时候 Cache 当内部数据不能直接修改时,它应该首先向所有其他数据修改 CPU 要求核心广播先要求其他核心广播 Cache 中对应的 Cache Line 标记为「无效」状态,然后更新当前 Cache 里面的数据。

也许只看上面有点绕,我们举个例子:

  1. 当 A 号 CPU 核心从内存读取变量 i 数据缓存的值 A 号 CPU 核心自己的 Cache 里面,这个时候还有别的 CPU 核心的 Cache 数据没有缓存,所以标记 Cache Line 状态为「独占」,此时其 Cache 数据与内存一致;
  2. 然后 B 号 CPU 核心也从内存读取变量 i 此时,消息将发送给其他值 CPU 核心,由于 A 号 CPU 核心已经缓存了数据,所以它将返回数据 B 号 CPU 核心。此时, A 和 B 核心缓存了相同的数据,Cache Line 状态会变成「共享」,并且其 Cache 数据和内存也是一致的;
  3. 当 A 号 CPU 核心要修改 Cache 中 i 发现数据对应的变量值 Cache Line 如果状态是共享状态,则应将其转移到所有其他状态 CPU 要求核心广播先要求其他核心广播 Cache 中对应的 Cache Line 标记为「无效」状态,然后 A 号 CPU 核心才更新 Cache 里面的数据,同时标记 Cache Line 为「已修改」状态,此时 Cache 数据与内存不一致。
  4. 如果 A 号 CPU 核心「继续」修改 Cache 中 i 由于此时的变量值 Cache Line 是「已修改」状态,所以不需要给其他的 CPU 核心发送消息,直接更新数据。
  5. 如果 A 号 CPU 核心的 Cache 里的 i 变量对应的 Cache Line 要被「替换」,发现 Cache Line 状态是「已修改」在替换之前,状态将数据同步到内存。

【操作系统入门到成神系列 四】CPU缓存一致性_缓存_09

四、总结

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 核心。