01-缓存一致性问题

发布于:2022-12-07 ⋅ 阅读:(434) ⋅ 点赞:(0)

01-CPU缓存一直性问题1

CPU Cache 的数据写⼊

随着时间的推移,CPU 和内存的访问性能相差越来越⼤,于是就在 CPU 内部嵌⼊了 CPU Cache(⾼速缓 存),CPU Cache 离 CPU 核⼼相当近,因此它的访问速度是很快的,于是它充当了 CPU 与内存之间的 缓存⻆⾊。

CPU Cache 通常分为三级缓存:L1 Cache、L2 Cache、L3 Cache,级别越低的离 CPU 核⼼越近,访问 速度也快,但是存储容量相对就会越⼩。其中,在多核⼼的 CPU ⾥,每个核⼼都有各⾃的 L1/L2 Cache, ⽽ L3 Cache 是所有核⼼共享使⽤的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r438DD9Y-1664157558194)(C:\Users\24879\Desktop\笔记图片\OS -jpg\1664151830783.png)]

问题

数据不光是只有读操作,还有写操作,那么如果数据写⼊ Cache 之后,内存与 Cache 相对应的 数据将会不同,这种情况下 Cache 和内存数据都不⼀致了,于是我们肯定是要把 Cache 中的数据同步到 内存⾥的。

策略

  1. 写直达(Write Through)
  2. 写回(Write Back)

写直达

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

在这个⽅法⾥,写⼊前会先判断数据是否已经在 CPU Cache ⾥⾯了:

  • 如果数据已经在 Cache ⾥⾯,先将数据更新到 Cache ⾥⾯,再写⼊到内存⾥⾯;
  • 如果数据没有在 Cache ⾥⾯,就直接把数据更新到内存⾥⾯。

写直达法很直观,也很简单,但是问题明显,⽆论数据在不在 Cache ⾥⾯,每次写操作都会写回到内存, 这样写操作将会花费⼤量的时间,⽆疑性能会受到很⼤的影响

写回

在写回机制中,当发⽣写操作时,新的数据仅仅被写⼊ 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 的时候,只有在缓存不命中,同时数据对应的 Cache 中 的 Cache Block 为脏标记的情况下,才会将数据写到内存中,⽽在缓存命中的情况下,则在写⼊后 Cache 后,只需把该数据对应的 Cache Block 标记为脏即可,⽽不⽤写到内存⾥。

缓存一致性问题

现在 CPU 都是多核的,由于 L1/L2 Cache 是多个核⼼各⾃独有的,那么会带来多核⼼的缓存⼀致性 (Cache Coherence) 的问题,如果不能保证缓存⼀致性的问题,就可能造成结果错误

问题

这时如果 A 号核⼼执⾏了 i++ 语句的时候,为了考虑性能,使⽤了我们前⾯所说的写回策略,先把值为 1 的执⾏结果写⼊到 L1/L2 Cache 中,然后把 L1/L2 Cache 中对应的 Block 标记为脏的,这个时候数据 其实没有被同步到内存中的,因为写回策略,只有在 A 号核⼼中的这个 Cache Block 要被替换的时候,数据才会写⼊到内存⾥。

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

策略

那么,要解决这⼀问题,就需要⼀种机制,来同步两个不同核⼼⾥⾯的缓存数据。要实现的这个机制的 话,要保证做到下⾯这 2 点:

第⼀点,某个 CPU 核⼼⾥的 Cache 数据更新时,必须要传播到其他核⼼的 Cache,这个称为写传播 (Wreite Propagation)

第⼆点,某个 CPU 核⼼⾥对数据的操作顺序,必须在其他核⼼看起来顺序是⼀样的,这个称为事务 的串形化(Transaction Serialization)

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

CPU 核⼼对于 Cache 中数据的操作,需要同步给其他 CPU 核⼼;

要引⼊「锁」的概念,如果两个 CPU 核⼼⾥有相同数据的 Cache,那么对于这个 Cache 数据的更 新,只有拿到了「锁」,才能进⾏对应的数据更新。

总线嗅探

写传播的原则就是当某个 CPU 核⼼更新了 Cache 中的数据,要把该事件⼴播通知到其他核⼼。最常⻅实 现的⽅式是总线嗅探(Bus Snooping)

我还是以前⾯的 i 变量例⼦来说明总线嗅探的⼯作机制,当 A 号 CPU 核⼼修改了 L1 Cache 中 i 变量的 值,通过总线把这个事件⼴播通知给其他所有的核⼼,然后每个 CPU 核⼼都会监听总线上的⼴播事件,并 检查是否有相同的数据在⾃⼰的 L1 Cache ⾥⾯,如果 B 号 CPU 核⼼的 L1 Cache 中有该数据,那么也需 要把该数据更新到⾃⼰的 L1 Cache。

可以发现,总线嗅探⽅法很简单, CPU 需要每时每刻监听总线上的⼀切活动,但是不管别的核⼼的 Cache 是否缓存相同的数据,都需要发出⼀个⼴播事件,这⽆疑会加重总线的负载。

另外,总线嗅探只是保证了某个 CPU 核⼼的 Cache 更新数据这个事件能被其他 CPU 核⼼知道,但是并 不能保证事务串形化

于是,有⼀个协议基于总线嗅探机制实现了事务串形化,也⽤状态机机制降低了总线带宽压⼒,这个协议 就是 MESI 协议,这个协议就做到了 CPU 缓存⼀致性。

MESI协议

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

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

这四个状态来标记 Cache Line 四个不同的状态。

「已修改」状态就是我们前⾯提到的脏标记,代表该 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 状态 是「已修改」状态,就会在替换前先把数据同步到内存。

**
5. 如果 A 号 CPU 核⼼的 Cache ⾥的 i 变量对应的 Cache Line 要被「替换」,发现 Cache Line 状态 是「已修改」状态,就会在替换前先把数据同步到内存。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到