1. 前言

最近阅读了一些UNIX系统书籍,对于多处理器系统和高速缓存的机制有了更多的理解,写下本文备忘。

2. 多处理器的高速缓存架构

2.1 SMP

最简单的多处理器架构是一种被称为对称多处理器结构(SMP:Symmetric Multi-Processor)的架构,SMP的逻辑架构如下图:

SMP和AMP架构_SMP和AMP架构

在上图中,我们可以清楚的看出SMP架构的特点:

  • 紧密耦合,所有的CPU、存储器和IO都是紧密耦合的,使用一条公共高速总线把所有的单元互连起来。
  • 共享内存,所有的CPU共用单独一个全局能够访问到存储模块,CPU本身没有局部存储器(不是高速缓存),它们把程序指令和数据都保存在全局共享存储器(global shared memory)中。一个CPU在存储器中保存的数据,对于其他CPU是可见的。
  • 对称,存储器的访问是对称的,所有CPU访问存储器的时间是相同的,内容是完全共享的。

同时,我们也可以看出,SMP架构支持的最大CPU核数受到共享总线和存储器带宽的限制。

2.2 带高速缓存的SMP架构

高速缓存是一种高速的存储系统,它保存有主存储器很小的一个子集,它位置根据层次的不同可以在CPU内部,也可以位于主板上。

高速缓存的逻辑架构如下图:

SMP和AMP架构_SMP和AMP架构_02

高速缓存通过引入局部性来改善系统性能,有两种类型的局部性:
时间局部性:程序有可能重复使用近期引用过的变量;
空间局部性:程序有可能重复使用前面附近引用过的变量。

带高速缓存的SMP逻辑架构如下图:

SMP和AMP架构_缓存_03


在这种逻辑架构中,每个CPU都有自己私有的高速缓存。当然在实际生产中,存在L1 L2 L3等多层次缓存的区别,有可能多个核共享高速缓存。

3. 多种硬件高速缓存一致性协议

由于不能让进程访问到过期数据,高速缓存中的数据必须和主存保持一致。在MP系统中,多个CPU有可能同时访问主存上的某条数据,因此需要通过软件在上下文切换时冲洗高速缓存或者利用具有总线监视功能的硬件来保证缓存一致性。

相对而已,硬件缓存一致性协议(cache consistency protocol)无需软件介入,就可以保持处理器之间共享数据的一致性,对于操作系统内核是透明的,是目前常用的方案。

3.1 写直通-使无效协议

当CPU对一个缓存行进行写操作时,除了执行写操作的CPU上副本外,其他高速缓存的副本全部失效。最简单的写-使无效协议就是写直通-使无效协议。MESI和MESIF也属于写-使无效协议。

写直通-使无效协议策略是,当一个处理器写共享数据时,它会立即被写入主存。同时执行此次写操作的总线事务被系统内其他高速缓存监听,而且如果写入的数据在这些高速缓存中命中的话,它们会让自己的缓存行副本无效。这迫使与他们自己关联的CPU在下次访问该行时,必须从主存中读取该行的新内容。

写直通-使无效协议修改数据的时序图如下:

SMP和AMP架构_主存_04

写直通-使无效协议的缺点也是非常明显的,对于任何共享数据的写操作都需要一次总线事务,效率非常低下。

3.2 MESI

MESI协议给缓存行引入了所有权的概念。一个缓存行有下列4种状态,协议名正是4种状态的首字母:
Modified 已修改:缓存行相对于主存已经被修改,其他高速缓存没有该行的副本。在此状态下,当前CPU可以继续修改该行而不触发总线事务。
Exclusive 独占:缓存行与主存一致,且其他高速缓存没有该行的副本;写该行时,状态转变为已修改,且不触发总线事务。
Share 共享:缓存行与主存一致,且其他高速缓存存在该行副本;修改该行之前,必须触发一次总线事务,使该行其他副本无效。
Invalid 无效:缓存行已失效,等待冲洗。

MESI状态转换如下图所示:

SMP和AMP架构_缓存_05

MESI在发生缓存缺失时,会发起一次总线事务读取,读取主存数据。当返回数据时,MESI协议提供了一个特殊的总线信号,指出其他高速缓存中是否存在该行副本。如有,缓存行状态为Share;如无,缓存行状态为Exclusive。

3.3 MESIF

MESIF是对MESI的优化,添加一种新状态F(Forward)。
当缓存行在多个高速缓存中存在副本时,如果某CPU加载了该行,在MESI协议中,其他S状态的缓存都将应答此加载请求,引起总线消息冗余。

为了避免该问题,将其中一个缓存行修改为F,由此副本负责应答。通常的最后一个加载的S将被置为F。

3.4 写-更新协议

除了写-使无效协议外,还存在一类写-更新协议。
当一个CPU修改一行时,更新它的全部缓存副本来保持一致性。这种协议与MESI的区别在于,针对处于共享状态的行进行保存操作会产生一次总线事务,更新系统别处的缓存副本,并且更新主存。

4. 对开发的影响

在开发实践中,对于竞争激烈的临界资源,我们应当对它们进行填充,让它们每个都占用自己的一行。这样做不但减少了初始访问数据结构时导致的缺失,而且也防止了伪共享现象。当两个或者两个以上的临界资源占用同一行,而且它们被多个CPU同时使用时,就会出现伪共享。

Java中可以使用sun.misc.Contended注解进行填充,该注解在原生并发API多处被使用。如LongAdder的Cell类:

@sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

5. 参考资料

《现代体系结构上的UNIX系统》
《深入探索LINUX操作系统》