多核程序设计——存储模型

最近在看《现代体系结构上的UNIX系统——内核程序员的SMP和Caching技术》,这里抄点东西作为笔记吧!
顺序存储模型强制存储器操作(load和store)都按照程序次序来执行,即这些指令是按照在随程序执行的指令流中出现的顺序次序来执行的。它也指定了,由不同处理器完成的load和store操作也要以某种顺序、但又是非确定性的方式排序。这种存储模型应该是大家最容易理解的,甚至都认为实际MP也是这样工作的。但是,这种存储结构是非常落后的,现代处理器应该已经淘汰了这种结构。
书中讲到了一个例子,如下;

        处理器一           处理器二
        store %r1, A        store %r1, B
        load %r2, B        load %r2, A

在强定序(strong order)的系统中,一共有四种可能的存储排序。但是可以肯定的是最后一条指令一定是load指令,也就是说至少有一条load指令会读取由别的处理器保存的新值。Dekker算法就是利用了这样的性质。Dekker算法是一种用在硬件中没有原子的读-改-写存储器操作(test-and-set或者xchg类似的指令)的情况下实现的临界区的技术。

enum state {
    UNLOCKED,
    LOCKED
};
typedef struct {
    char status[2];
    char turn;
}
lock_t;
void initlock(lock_t * lock) {
    lock - >status[0] = UNLOCK;
    lock - >status[1] = UNLOCK;
    lock - >turn = 0;
}

void lock(volatile lock_t * lock) {
    lock - >status[CPUID()] = LOCKED;

    while (lock - >status[othercpu()] == LOCKED) {
        if (lock - >turn != cpuid()) {
            lock - status[cpuid()] = UNLOCKED;
            while (lock - >turn == othercpu());
            lock - >status[cpuid()] = LOCKED;
        }
    }
}

void unlock(loct_t * lock) {
    lock - >status[cpuid()] = UNLOCKED;
    lock - >turn = othercpu();
}

TSO(完全存储定序):

完全存储定序相对强定序来说,对内存操作的顺序规律弱化了。首先,一个来自存储器的load指令的任意序列,假定它们都使用独立的寄存器,就能以任何次序来执行。所有保存指令都要确定次序,因此命名为完全存储定序(total store ordering)。完全存储定序MP中,如SPARC提供一种atomic-swap指令,它把单个位置的加载和保存操作当做一个不可分割的整体操作。
当一条atomic-swap指令发出时,他就被放到保存缓存区,相对于以前的store指令按FIFO次序进行处理。但是它会屏蔽更多的存储器操作,直到保存缓冲区为空、原子交换操作完成为止。Dekker算法在TSO下是不能工作的,这是因为第一张表中的指令的可能顺序变多了,没有了前面分析的规律。在TSO结构下,lock指令就需要以下面的方式实现。

void lock(void lock_t * lock_status) {
    while (swap_atomic(lock - status, 1) == 1);
}

PSO(部分存储定序):

PSO则相对更加宽松,store指令的顺序也不一定以FIFO的顺序执行了。但是可以保证store缓存中对相同位置的store操作以程序顺序执行。同时load指令仍然会查看store缓冲,看是否有命中,如果有的话则返回最近向那个位置执行的store指令所带的数据。在PSO中也需要atomic_swap指令来实现锁,其行和TSO时介绍的是一样的。原子交换操作阻止了再进一步发出存储器操作,但是在原子交换操作执行的时候,store缓存不保证为空,也就是说原子操作会在前面的store指令完成前结束。但是由于指令是顺序发射的(这个保证现在看来比较勉强),临界段内的store和load指令都还没有发射。所以,PSO的上锁实现和TSO是基本一样的。
那么,解锁的时候呢?考虑下面一段临界区指令流:

atomic-swap              /*获得锁*/
load    %r1, counter       /*获得原来的值*/
add     %r2, %r1, 1        /*累加计数器*/
store %r2, counter       /*保存新值*/
clear %r3
store %r3, lock          /*释放自旋锁*/

考虑倒数第一条指令和倒数第一条指令,他们都是store指令。在PSO结构中,对不同位置的store指令的顺序是不确定的。倘若,释放自旋锁指令在保存新值指令之前完成,则整个临界区就被破坏了。因为绝对不允许这种情况出现,所以SPARC体系结构包括了一种称为store-barrier的指令,强制造成一定程度的顺序。用《POSIX多线程程序设计》中的一句话,内存屏障就是一睹移动的“墙”。墙壁之间的指令顺序是不一定的,但是墙壁两侧的指令是有相对顺序规律的。
所以,PSO结构下释放自旋锁的方式如下:

void
unlock(volatile lock_t *lock_status)
{
    store_barrier();
    *lock_status();
}

其实我看本书的目的是为了看懂《Is Parallel Programming Hard, And, if so, What Can You Do About It?》。这本书貌似只有电子版的,并发编程网有一个翻译版本叫《深入理解并行编程V1.0》,个人觉得这两本书配合起来看非常爽,因为他们侧重点不同,可以互补理解。

发表评论