6.6 指令和内存访问排序
人们很自然地会想到程序是按照源代码语句出现的顺序执行的。然而,许多编译器和中央处理器为了获得更高的性能而不按顺序执行代码。如果一条指令需要许多周期才能完成,中央处理器可能会提前发出指令,这样它就可以与其他指令重叠,避免中央处理器停顿。例如,中央处理器可能会注意到在顺序指令序列A和B中彼此不存在依赖。CPU也许首先启动指令B,或者是因为它的输入先于A的输入准备就绪,或者是为了重叠执行A和B。编译器可以执行类似的重新排序,方法是在源代码中一条语句的指令发出之前,先发出另一条语句的指令。
编译器和CPU在重新排序时需要遵循一定规则,以确保它们不会改变正确编写的串行代码的结果。然而,规则确实允许重新排序后改变并发代码的结果,并且很容易导致多处理器上的不正确行为。CPU的排序规则称为内存模型(memory model)。
例如,在push
的代码中,如果编译器或CPU将对应于第4行的存储指令移动到第6行release
后的某个地方,那将是一场灾难:
l = malloc(sizeof *l);
l->data = data;
acquire(&listlock);
l->next = list;
list = l;
release(&listlock);
如果发生这样的重新排序,将会有一个窗口期,另一个CPU可以获取锁并查看更新后的list
,但却看到一个未初始化的list->next
。
为了告诉硬件和编译器不要执行这样的重新排序,xv6在acquire
(kernel/spinlock.c:22) 和release
(kernel/spinlock.c:47)中都使用了__sync_synchronize()
。__sync_synchronize()
是一个内存障碍:它告诉编译器和CPU不要跨障碍重新排序load
或store
指令。因为xv6在访问共享数据时使用了锁,xv6的acquire
和release
中的障碍在几乎所有重要的情况下都会强制顺序执行。第9章讨论了一些例外。