小林Coding阅读笔记:操作系统篇之硬件结构,CPU Cache一致性问题

前言

  1. 参考/导流:
    小林coding-2.4 CPU 缓存一致性
  2. 学习意义
  • 底层基础知识,了解CPU执行过程,让上层编码有效
  • 并发控制底层设计思维(对比 MySQL的并发控制)、更好地去理解JUC的锁、volatile以及JMM
  • 架构层面的一致性保证问题,由CPU协调单位上升至线程、进程、机器
  1. 相关说明
    该篇博文是个人阅读的重要梳理,仅做简单参考,详细请阅读小林coding的原文!

四、CPU缓存一致性

CPU Cache 和 内存不一致性

程序先加载至内存中,CPU执行程序需要先读取缓存Cache,若无该数据(内存地址)则读取内存。而计算机中使用 数据 是使用它的信息,可以存在不同地方,一份数据可存在于 Cache和内存,对于来说,未改变信息的状态【只需保证写的一致性,则读就会一致】,无论它在Cache,还是内存,它们是一致性的。而对于来说,则会改变数据的状态,此时则会导致 Cache的数据是新数据,内存由于距离远而是旧数据。此时,则产生了 缓存 和 内存 不一致的问题。对于CPU的缓存和内存不一致性的解决办法有:

  • 写直达:当缓存更新数据之后,直接写到内存,随时保证缓存和内存的一致性。问题则是加大了写的开销。如果单核情况下,CPU读数据优先从Cache读,本来该数据缓存已有该数据,无需从内存加载,随时保证缓存和内存一致性的必要性也不大。因此,只需考虑当Cache被替换,不再命中时,需要再从内存拿数据时,才更新cache的数据至内存即可。那么对于多核呢?多核则是后续Cache间来保证一致性的讨论。
  • 写回:在写直达的基础,写回则是当CacheBlock被替换,需要再从内存拿数据时才去更新至内存【而cacheblock被替换时也不一定是被更改过的,此时则需要去标记该数据是否为脏,若脏则需要写回内存】。这样就会减少由Cache写至内存的频率。增大效率。

Cache的读写策略

写直达

保持内存与 Cache 一致性最简单的方式是,把数据同时写入内存和 Cache 中,这种方法称为写直达(Write Through

在这里插入图片描述

写回

直达由于每次写操作都会把数据写回到内存,而导致影响性能,于是为了要减少数据写回内存的频率,就出现了写回(Write Back)的方法

在写回机制中,当发生写操作时,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Block「被替换」时才需要写到内存中,减少了数据写回内存的频率,这样便可以提高系统的性能。

在这里插入图片描述

  • 如果当发生写操作时,数据已经在 CPU Cache 里的话,则把数据更新到 CPU Cache 里,同时标记 CPU Cache 里的这个 Cache Block 为脏(Dirty)的,这个脏的标记代表这个时候,我们 CPU Cache 里面的这个 Cache Block 的数据和内存是不一致的,这种情况是不用把数据写到内存里的;
  • 如果当发生写操作时,数据所对应的 Cache Block 里存放的是「别的内存地址的数据」的话,就要检查这个 Cache Block 里的数据有没有被标记为脏的:
    • 如果是脏的话,我们就要把这个 Cache Block 里的数据写回到内存,然后再把当前要写入的数据,先从内存读入到 Cache Block 里。然后再把当前要写入的数据写入到 Cache Block,最后也把它标记为脏;
    • 如果不是脏的话,把当前要写入的数据先从内存读入到 Cache Block 里,接着将数据写入到这个 Cache Block 里,然后再把这个 Cache Block 标记为脏。
      在这里插入图片描述

总结一下Cache的读写策略

Cache写命中(Write Hit)时的策略

  • 写直达 (Write Through)
    CPU向Cache写数据时,Memory中的内容也将被同步更新。

    好处是可以保证Cache和Memory的一致性,坏处是影响运行速度。【对比CAP理论,程序设计总是 性能和可用之间做平衡】

  • 写回(Write Back)
    CPU向Cache写数据时,Memory中的内容将不立刻做同步更新,只做Dirty标记。当Cache块被换出时,才将改动同步到Memory中。

    好处是保证了程序运行速度,坏处是存在Cache和Memory的不一致性。

Cache写缺失(Write Miss)时的策略
写失效(write miss)即所要写的地址不在cache中。

  • Write allocate
    把要写的数据载入到Cache中,写Cache,然后再通过flush方式写入到内存中
  • No write allocate
    直接把要写的数据写入到内存中。

Cache读策略

  • Read through
    直接从内存中读取数据
  • Read allocate
    先把数据读取到Cache中,再从Cache中读数据。

多核心的缓存间不一致性

问题引入

现在 CPU 都是多核的,由于 L1/L2 Cache 是多个核心各自独有的,那么会带来多核心的缓存一致性(Cache Coherence 的问题,如果不能保证缓存一致性的问题,就可能造成结果错误。【对比 分布式架构,由核心 上升至 机器;内部通信 由 总线,到 网络通信】

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G5UseOB6-1671416925411)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9f148b27-7234-44c3-b2db-4f9439f0ac69/Untitled.png)]

这时如果 A 号核心执行了 i++ 语句的时候,为了考虑性能,使用了我们前面所说的**写回策略【**若采用写直达,时刻保证内存的数据最新,是否也会存在该问题?因为在写至内存之前,另外的核心也可能去读内存?也需要锁去保证 顺序性】,先把值为 1 的执行结果写入到 L1/L2 Cache 中,然后把 L1/L2 Cache 中对应的 Block 标记为脏的,这个时候数据其实没有被同步到内存中的,因为写回策略,只有在 A 号核心中的这个 Cache Block 要被替换的时候,数据才会写入到内存里。

如果这时旁边的 B 号核心尝试从内存读取 i 变量的值,则读到的将会是错误的值,因为刚才 A 号核心更新 i 值还没写入到内存中,内存中的值还依然是 0。这个就是所谓的缓存一致性问题,A 号核心和 B 号核心的缓存,在这个时候是不一致,从而会导致执行结果的错误。

解决以上问题,则需要保证以下两点:

  • 写传播,当一个核心更新数据,能通知到其他核心。【手段方式,对比RPC中的心跳检测机制,】
  • 串行化,保证事务的串行化,让核心执行事务是顺序性的。【目标,手段通过锁—】

要实现事务串行化,要做到 2 点:

  • CPU 核心对于 Cache 中数据的操作,需要同步给其他 CPU 核心;
  • 要引入「」的概念,如果两个 CPU 核心里有相同数据的 Cache,那么对于这个 Cache 数据的更新,只有拿到了「锁」,才能进行对应的数据更新。

具体实现

  • 总线嗅探【类似心跳检测,rpc发消息通知,结合着 分布式事务\RPC通信对比】
  • MESI协议【对比共识算法、数据一致性算法以及MySQL的ACID特性】

总线嗅探

写传播的原则就是当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心【时刻监听广播事件】。最常见实现的方式是总线嗅探(Bus Snooping)。

  • 广播
  • 监听

问题点:

  • CPU 需要每时每刻监听总线上的一切活动,但是不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件。
  • 总线嗅探只是保证了某个 CPU 核心的 Cache 更新数据这个事件能被其他 CPU 核心知道,但是并不能保证事务串行化。

MESI协议

在总线嗅探时讨论,对于发广播事件的CPU核心无论别的核心是否用该数据都发生,对于其他CPU核心需要时刻监听广播通知。针对该两种问题,可以去细粒度标记数据 Cacheline的状态,来减少不必要的开销浪费。

MESI则通过以下四个状态来标记 Cache Line 四个不同的状态,来更好地进行广播传播,保证同步以及 实现 串行化。

  • Modified,已修改
  • Exclusive,独占
  • Shared,共享
  • Invalidated,已失效

已修改状态,代表该 Cache Block 上的数据已经被更新过(脏标记),但没有写到内存里。

已失效状态,表示的是这个 Cache Block 里的数据已经失效了,不可以读取该状态的数据。

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

「独占」和「共享」的差别在于,独占状态的时候,数据只存储在一个 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有该数据。这个时候,如果要向独占的 Cache 写数据,就可以直接自由地写入,而不需要通知其他 CPU 核心,因为只有你这有这个数据,就不存在缓存一致性的问题了,于是就可以随便操作该数据。

状态转换

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XwiE7751-1671416946964)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/990ffa50-8582-4158-a0f6-6ce62122fbf7/Untitled.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X3hKyULp-1671416925412)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5c97cb7d-a392-40ca-bdb5-34aa36e14f9f/Untitled.png)]

具体示例

  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 状态是「已修改」状态,就会在替换前先把数据同步到内存。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值