📣 大家好,我是Zhan,一名个人练习时长两年的大三后台练习生🏀
📣 这篇文章是 操作系统 第三篇笔记📙
📣 如果有不对的地方,欢迎各位指正🙏🏼
📣 Just do it! 🫵🏼🫵🏼🫵🏼
🔔 引言
在上篇文章程序员应该需要了解的CPU Cache知识和使用技巧 - 操作系统(二)中,我们了解到了 CPU 如何根据数据的地址进行映射 ,查找 Cache 中是否存在该数据,也就是 CPU Cache 的读取,而本篇我们将要介绍 Cache 的写入 ,对于缓存来说,写入的方式是我们需要重点去要了解的,就像 Redis 的缓存,我们如何保证缓存的一致性,是本文需要解决的问题。
1️⃣ 数据的写入方式
CPU Cache的数据写入通常有两种方式,分别是写回(Write-Back)和写直通(Write-Through)。这两种方式在处理缓存和主内存之间的数据写入时有不同的策略。
🚌 写直通
在这种方式下,当 CPU 需要写入数据要缓存的时候,数据会被同时写入缓存和内存中。 这样的话,确实可以保证缓存和内存的数据一致性,但是会增加主内存的写入频率,且花费大量的时间,影响性能。
🚈 写回
当发生写操作的时候,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Line 被替换的时候才需要写到内存中,听起来有些抽象,看看下面的流程图:
- 判断目前需要存储的数据是否存在于 Cache 中,对于不存在的数据,我们直接写入 Cache Line 中,同时标记该数据为脏数据(即内存和缓存不一致),不写入内存中,要注意写回这种方式,只有数据为脏数据且要被替换的时候才会写入内存中
- 如果通过映射得到的地址中有其他的缓存的数据的话,就需要检测该数据是否为脏数据:
- 如果是脏数据,现在需要取代它的位置,就要把这个脏数据写入内存,同时为了防止原本的数据在这个期间被 CPU 的其他核心修改,我们需要再次从内存中获取这个地址的值进行写入到 Cache LIne 中(即不是在同步脏数据后直接把原数据写入)
- 如果不是脏数据,就只需要从内存中再次读值然后写入 Cache LIne 中
综上,其实我们可以发现,CPU 直接操作的是 Cache,而只有在脏数据被取代的时候才会操作内存,不像写直达,每次都要操作内存,影响性能
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 协议,通过对数据进行状态的标记,这样不仅能实现事务串行化,还有效的降低了主线监听的频率
🍁 友链
- 无论你是科技爱好者还是程序猿,冯诺依曼体系结构你得知道! - 操作系统(一)
- 程序员应该需要了解的CPU Cache知识和使用技巧 - 操作系统(二)
- 计算机系统 #10 12 张图看懂 CPU 缓存一致性与 MESI 协议,真的一致吗?
- MESI 协议可视化网站
- 小林coding (xiaolincoding.com)
✒写在最后
都看到这里啦~,给个点赞再走呗~,也欢迎各位大佬指正以及补充,在评论区一起交流,共同进步!也欢迎加微信一起交流:Goldfish7710。咱们明天见~