从横向和纵向了解 CPU Cache 缓存一致性

📣 大家好,我是Zhan,一名个人练习时长两年的大三后台练习生🏀

📣 这篇文章是 操作系统 第三篇笔记📙

📣 如果有不对的地方,欢迎各位指正🙏🏼

📣 Just do it! 🫵🏼🫵🏼🫵🏼


🔔 引言

在上篇文章程序员应该需要了解的CPU Cache知识和使用技巧 - 操作系统(二)中,我们了解到了 CPU 如何根据数据的地址进行映射 ,查找 Cache 中是否存在该数据,也就是 CPU Cache 的读取,而本篇我们将要介绍 Cache 的写入 ,对于缓存来说,写入的方式是我们需要重点去要了解的,就像 Redis 的缓存,我们如何保证缓存的一致性,是本文需要解决的问题。


1️⃣ 数据的写入方式

CPU Cache的数据写入通常有两种方式,分别是写回(Write-Back)和写直通(Write-Through)。这两种方式在处理缓存和主内存之间的数据写入时有不同的策略。

🚌 写直通

在这种方式下,当 CPU 需要写入数据要缓存的时候,数据会被同时写入缓存和内存中。 这样的话,确实可以保证缓存和内存的数据一致性,但是会增加主内存的写入频率,且花费大量的时间,影响性能。

🚈 写回

当发生写操作的时候,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Line 被替换的时候才需要写到内存中,听起来有些抽象,看看下面的流程图:

  1. 判断目前需要存储的数据是否存在于 Cache 中,对于不存在的数据,我们直接写入 Cache Line 中,同时标记该数据为脏数据(即内存和缓存不一致),不写入内存中,要注意写回这种方式,只有数据为脏数据且要被替换的时候才会写入内存中
  2. 如果通过映射得到的地址中有其他的缓存的数据的话,就需要检测该数据是否为脏数据:
    1. 如果是脏数据,现在需要取代它的位置,就要把这个脏数据写入内存,同时为了防止原本的数据在这个期间被 CPU 的其他核心修改,我们需要再次从内存中获取这个地址的值进行写入到 Cache LIne 中(即不是在同步脏数据后直接把原数据写入)
    2. 如果不是脏数据,就只需要从内存中再次读值然后写入 Cache LIne 中

综上,其实我们可以发现,CPU 直接操作的是 Cache,而只有在脏数据被取代的时候才会操作内存,不像写直达,每次都要操作内存,影响性能


2️⃣ 缓存数据一致性

🚩 写回策略的数据不一致性

尽管写回策略看上去比写直达策略更好,能够有更好的性能,更快的响应时间,不过写回策略无法保证数据的一致性 ,如果在缓存中的脏数据尚未同步到内存中的时候,系统崩溃或者断电,就可能会导致数据的不一致性。

而为了保证数据一致性,通常会在写回策略中引入额外的机制:

  1. 写缓冲区 :在处理器和缓存控制器之间引入写缓冲区,用于临时存储已经修改的数据。这样,即使数据尚未写回到内存,处理器也可以继续执行操作,从而提高性能。有点类似于一个异步线程
  2. 日志记录:在写回策略中,可以用日志记录下缓存和内存之间的数据操作,这样有利于在发生故障的时候进行数据一致性的恢复。

🏴 多核导致的数据不一致性

如果两个 CPU 核心都要修改一个变量 VAR,它在内存中的值为 10,而第一个核心让该变量进行自增操作,也就是变为 11,但是这个修改,按照写回的策略,仅仅是在缓存中,没有同步到内存中,那么另外一个 CPU 核心让这个变量翻倍,VAR 的值变为 20,在进行脏数据同步到内存的时候就会发生矛盾

其实这个问题让我想到了 volatile 这个轻量锁,用它修饰的变量可以让多线程可见,对呀,如果单个 CPU 在修改这个变量的时候,能把修改对其他的 CPU 可见

当然,除了要保证对其他的 CPU 可见,还要保证修改这个值的时候要加锁,保证事务的串行化

如果都是对变量做更改,需要保证其他核心对于更改的顺序是一致的,否则就会出现,其他的核心看到的变量的值并不一样,要做到这一步,那就不得不加锁,只有拿到了锁才能对数据对更改


3️⃣基于总线嗅探机制的 MESI 协议

对于 Java 这门高级语言来说,我们可以简单的用 volatile 实现可见性,但是对于 CPU 这个硬件来说,写传播 (更改让其他 CPU 核心可见) 和 事务串行化(加锁)的具体实现就不一样了:

🔗 总线嗅探

CPU多个核心之间有一条总线,这是它们可以相互通信的方法,那么 CPU 每时每刻监听总线上的一切活动,不管其他的核心是否缓存了这个数据,都会发出一个广播表明数据的修改,这样其他的 CPU 核心在监听到后,判断自己的 L1 Cache 和 L2 Cache 中有没有该数据,有则修改。

但是,第一,总线嗅探只能保证其他CPU核心能够意识到它的数据的变更,但是还是无法保证事务的串行化,第二,不管其他的CPU核心有没有这个数据都会发出广播,总线的带宽压力很大

📜 MESI 协议

MESI 协议解决了上面的问题,实现了事务串行化的同时,使用状态机降低了总线带宽的压力,至此基于总线嗅探机制的 MESI 协议就实现了 CPU 缓存一致性,那么下面就会讲解 MESI 协议是怎么做的:

MESI 协议其实是 4 个状态的开头字母缩写:Modified-已修改、Exclusive-独占,Shared-共享、Invalidated-已失效,依赖这四个标志,成功的实现了 CPU 缓存一致性:

  • 【已修改】 :就是上面说到的脏标志,代表这个 Cache Line 的数据是被修改过的
  • 【已失效】 :该 Cache Line 中的数据已经失效了,不能够被读取
  • 【独占】:代表 Cache Line 中的数据只是当前 CPU 独有的,即其他的核心中并不缓存这个数据,就可以自由的读写,而不广播到其他的核心
  • 【共享】 :与【独占】相反,它代表数据是被多个 CPU 核心所共享的,因此在我们需要进行更新数据的时候,应该先要向其他的核心广播,然后其他的核心把该 Cache Line 设置为【已失效】,然后更新当前 Cache 中的数据,并把数据的状态设置为【已修改】

注意:我们在把其他的核心设置为【已失效】后,是不会恢复它的状态的,因此对于【已修改】和【独占】的数据,可以直接修改更新数据而不需要广播给其他的 CPU 核心

这里还有一个 MESI 协议可视化网站,大家可以根据本文所学知识进行调试,与自己形成的知识体系做对比,看看是否能够得到验证


💬 总结

其实说到缓存,一致性这个问题就是我们躲不开的,本文主要从两个维度来解决缓存一致性的问题:

  • 纵向 :写回和写直达,讨论了如何让 Cache 和 内存 的数据一致性
  • 横向 :基于总线嗅探机制的 MESI 协议,讨论了 CPU 多核 之间的数据一致性

对于纵向的缓存一致性,我们讨论了写回和写直达两种方式:

  • 写直达 :在写入 Cache 的时候 同时写入主存中,这种方式对于性能的影响很大
  • 写回 :直接操作 Cache,而只有在脏数据被取代的时候才会操作内存,不像写直达,每次都要操作内存,影响性能,但是 断电、系统故障 也可能导致脏数据没有及时同步到内存,现代的 CPU 会使用 写缓冲区、日志记录 的方式尽力的去挽救

对于横向的缓存一致性,我们讨论了需要解决的两个问题:

  • 写传播:这点我们可以通过总线进行多核 CPU 之间的传播,需要每时每刻监听总线上的信息
  • 事务串行化:这点我们使用了 MESI 协议,通过对数据进行状态的标记,这样不仅能实现事务串行化,还有效的降低了主线监听的频率

🍁 友链


✒写在最后

都看到这里啦~,给个点赞再走呗~,也欢迎各位大佬指正以及补充,在评论区一起交流,共同进步!也欢迎加微信一起交流:Goldfish7710。咱们明天见~

相关推荐
码农小旋风1 小时前
详解K8S--声明式API
后端
Peter_chq1 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml41 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~1 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616881 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
睡觉谁叫~~~2 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
2401_865854885 小时前
iOS应用想要下载到手机上只能苹果签名吗?
后端·ios·iphone
AskHarries5 小时前
Spring Boot集成Access DB实现数据导入和解析
java·spring boot·后端
2401_857622665 小时前
SpringBoot健身房管理:敏捷与自动化
spring boot·后端·自动化
程序员阿龙5 小时前
基于SpringBoot的医疗陪护系统设计与实现(源码+定制+开发)
java·spring boot·后端·医疗陪护管理平台·患者护理服务平台·医疗信息管理系统·患者陪护服务平台