锁与I/O访问在某些情况下(尤其是涉及NUMA),在两个不同CPU上的两个自旋锁区内的I / O访问,在PCI桥看来可能是交叉的,因为PCI桥不一定保证缓存一致性,此时内存屏障将失效。 例如: CPU 1 CPU 2 =============================== =============================== spin_lock(Q) writel(0, ADDR) writel(1, DATA); spin_unlock(Q); spin_lock(Q); writel(4, ADDR); writel(5, DATA); spin_unlock(Q); PCI桥可能看到的顺序如下所示: STORE *ADDR = 0, STORE *ADDR = 4, STORE *DATA = 1, STORE *DATA = 5 这可能会导致硬件故障。 这里有必要在释放自旋锁之前插入mmiowb()函数,例如: CPU 1 CPU 2 =============================== =============================== spin_lock(Q) writel(0, ADDR) writel(1, DATA); mmiowb(); spin_unlock(Q); spin_lock(Q); writel(4, ADDR); writel(5, DATA); mmiowb(); spin_unlock(Q); 这将确保在CPU 1上的两次store比CPU 2上的两次store操作先被PCI感知。 此外,相同的设备上如果store指令后跟随一个load指令,可以省去mmiowb()函数,因为load强制在load执行前store指令必须完成: CPU 1 CPU 2 =============================== =============================== spin_lock(Q) writel(0, ADDR) a = readl(DATA); spin_unlock(Q); spin_lock(Q); writel(4, ADDR); b = readl(DATA); spin_unlock(Q); 更多信息参见:Documentation/DocBook/deviceiobook.tmpl 什么地方需要内存障碍?在正常操作下,一个单线程代码片段中内存操作重排序一般不会产生问题,仍然可以正常工作,即使是在一个SMP内核系统中也是如此。但是,下面四种场景下,重新排序可能会引发问题:
多理器间的交互当系统具有一个以上的处理器,系统中多个CPU可能要访问同一数据集。这可能会导致同步问题,通常处理这种场景是使用锁。然而,锁是相当昂贵的,所以如果有其它的选择尽量不使用锁。在这种情况下,能影响到多个CPU的操作可能必须仔细排序,以防止出现故障。 例如,在R / W信号量慢路径的场景。这里有一个waiter进程在信号量上排队,并且它的堆栈上的一块空间链接到信号量上的等待进程列表: struct rw_semaphore { ... spinlock_t lock; struct list_head waiters; }; struct rwsem_waiter { struct list_head list; struct task_struct *task; }; 要唤醒一个特定的waiter进程,up_read()或up_write()函数必须做以下动作:
换句话说,它必须执行下面的事件: LOAD waiter->list.next; LOAD waiter->task; STORE waiter->task; CALL wakeup RELEASE task 如果这些步骤的顺序发生任何改变,那么就会出问题。 一旦进程将自己排队并且释放信号锁,waiter将不再获得锁,它只需要等待它的任务指针被清零,然后继续执行。由于记录是在waiter的堆栈上,这意味着如果在列表中的next指针被读取出之前,task指针被清零,另一个CPU可能会开始处理,up*()函数在有机会读取next指针之前waiter的堆栈就被修改。 考虑上述事件序列可能发生什么: CPU 1 CPU 2 =============================== =============================== down_xxx() Queue waiter Sleep up_yyy() LOAD waiter->task; STORE waiter->task; Woken up by other event <preempt> Resume processing down_xxx() returns call foo() foo() clobbers *waiter </preempt> LOAD waiter->list.next; --- OOPS --- 虽然这里可以使用信号锁来处理,但在唤醒后的down_xxx()函数不必要的再次获得自旋锁。 这个问题可以通过插入一个通用的SMP内存屏障来处理: LOAD waiter->list.next; LOAD waiter->task; smp_mb(); STORE waiter->task; CALL wakeup RELEASE task 在这种情况下,即使是在其它的CPU上,屏障确保所有在屏障之前的内存操作一定先于屏障之后的内存操作执行。但是它不能确保所有在屏障之前的内存操作一定先于屏障指令身执行完成时执行; 在一个UP系统中, 这种场景不会产生问题 , smp_mb()仅仅是一个编译屏障,可以确保编译器以正确的顺序发出指令,而不会实际干预到CPU。因为只有一个CPU,CPU的依赖顺序逻辑会管理好一切。 原子操作虽然它们在技术上考虑了处理器间的交互,但是特别注意,有一些原子操作暗含了完整的内存屏障,另外一些却没有包含,但是它们作为一个整体在内核中应用广泛。 任一原子操作,修改了内存中某一状态并返回有关状态(新的或旧的)的信息,这意味着在实际操作(明确的lock操作除外)的两侧暗含了一个SMP条件通用内存屏障(smp_mb()),包括; xchg(); cmpxchg(); atomic_cmpxchg(); atomic_inc_return(); atomic_dec_return(); atomic_add_return(); atomic_sub_return(); atomic_inc_and_test(); atomic_dec_and_test(); atomic_sub_and_test(); atomic_add_negative(); atomic_add_unless(); /* when succeeds (returns 1) */ test_and_set_bit(); test_and_clear_bit(); test_and_change_bit(); 它们都是用于实现诸如LOCK和UNLOCK的操作,以及判断引用计数器决定对象销毁,同样,隐式的内存屏障效果是必要的。 下面的操作存在潜在的问题,因为它们并没有包含内存障碍,但可能被用于执行诸如解锁的操作: atomic_set(); set_bit(); clear_bit(); change_bit(); 如果有必要,这些应使用恰当的显式内存屏障(例如:smp_mb__before_clear_bit())。 下面这些也没有包含内存屏障,因此在某些场景下可能需要明确的内存屏障(例如:smp_mb__before_atomic_dec()): atomic_add(); atomic_sub(); atomic_inc(); atomic_dec(); 如果将它们用于统计,那么可能并不需要内存屏障,除非统计数据之间有耦合。 如果将它们用于对象的引用计数器来控制生命周期,也许也不需要内存屏障,因为可能引用计数会在锁区域内修改,或调用方已经考虑了锁,因此内存屏障不是必须的。 如果将它们用于构建一个锁的描述,那么确实可能需要内存屏障,因为锁原语通常以特定的顺序来处理事情; 基本上,每一个使用场景都必须仔细考虑是否需要内存屏障。 以下操作是特殊的锁原语: test_and_set_bit_lock(); clear_bit_unlock(); __clear_bit_unlock(); 这些实现了诸如LOCK和UNLOCK的操作。在实现锁原语时应当优先考虑使用它们,因为它们的实现可以在很多架构中进行优化。 [!]注意:对于这些场景,也有特定的内存屏障原语可用,因为在某些CPU上原子指令暗含着完整的内存屏障,再使用内存屏障显得多余,在这种情况下,特殊屏障原语将是个空操作。 更多信息见 Documentation/atomic_ops.txt。 设备访问许多设备都可以映射到内存上,因此对CPU来说它们只是一组内存单元。为了控制这样的设备,驱动程序通常必须确保对应的内存访问顺序的正确性。 然而,聪明的CPU或者聪明的编译器可能为引发潜在的问题,如果CPU或者编译器认为重排、合并、联合访问更加高效,驱动程序精心编排的指令顺序可能在实际访问设备是并不是按照这个顺序访问的 —— 这会导致设备故障。 在Linux内核中,I / O通常需要适当的访问函数 —— 如inb() 或者 writel() —— 它们知道如何保持适当的顺序。虽然这在大多数情况下不需要明确的使用内存屏障,但是下面两个场景可能需要:
更多信息参见 Documentation/DocBook/deviceiobook.tmpl。 中断驱动可能会被自己的中断服务例程中断,因此,驱动程序两个部分可能会互相干扰,尝试控制或访问该设备。 通过禁用本地中断(一种锁的形式)可以缓和这种情况,这样,驱动程序中关键的操作都包含在中断禁止的区间中 。有时驱动的中断例程被执行,但是驱动程序的核心不是运行在相同的CPU上,并且直到当前的中断被处理结束之前不允许其它中断,因此,在中断处理器不需要再次加锁。 但是,考虑一个驱动使用地址寄存器和数据寄存器跟以太网卡交互,如果该驱动的核心在中断禁用下与网卡通信,然后驱动程序的中断处理程序被调用: LOCAL IRQ DISABLE writew(ADDR, 3); writew(DATA, y); LOCAL IRQ ENABLE <interrupt> writew(ADDR, 4); q = readw(DATA); </interrupt> 如果排序规则十分宽松,数据寄存器的存储可能发生在第二次地址寄存器之后: STORE *ADDR = 3, STORE *ADDR = 4, STORE *DATA = y, q = LOAD *DATA 如果是宽松的排序规则,它必须假设中断禁止部分的内存访问可能向外泄漏,可能会和中断部分交叉访问 – 反之亦然 – 除非使用了隐式或显式的屏障。 通常情况下,这不会产生问题,因为这种区域中的I / O访问将在严格有序的IO寄存器上包含同步load操作,形成隐式内存屏障。如果这还不够,可能需要显式地使用一个mmiowb()。 类似的情况可能发生在一个中断例程和运行在不同CPU上进行通信的两个例程的时候。这样的情况下,应该使用中断禁用锁来保证顺序。 内核I/O屏障效应访问I/O内存时,驱动应使用适当的存取函数:
它们都旨在跟I / O空间打交道,而不是内存空间,但这主要是一个特定于CPU的概念。在 i386和x86_64处理器中确实有特殊的I / O空间访问周期和指令,但许多CPU没有这样的概念。 包括PCI总线也定义了I / O空间,比如在i386和x86_64的CPU 上很容易将它映射到CPU的I / O空间上。然而,对于那些不支持IO空间的CPU,它也可能作为虚拟的IO空间被映射CPU的的内存空间。 访问这个空间可能是完全同步的(在i386),但桥设备(如PCI主桥)可能不完全履行这一点。 可以保证它们彼此之间的全序关系。 对于其他类型的内存和I / O操作,不保证它们的全序关系。 无论是保证完全有序还是不合并访问取决于他们访问时定义的访问窗口属性,例如,最新的i386架构的机器通过MTRR寄存器控制。 通常情况下,只要不是访问预取设备,就保证它们的全序关系且不合并。 然而,对于中间链接硬件(如PCI桥)可能会倾向延迟处理,当刷新一个store时,首选从同一位置load,但是对同一个设备或配置空间load时,对与PCI来说一次就足够了。 [*]注意:试图从刚写过的相同的位置load数据可能导致故障 – 考虑16550 RX / TX串行寄存器的例子。 对于可预取的I / O内存,可能需要一个mmiowb()屏障保证顺序; 请参阅PCI规范获得PCI事务间交互的更多信息; 这些类似readX(),但在任何时候都不保证顺序。因为没有I / O读屏障。 它们通过选择inX()/outX() or readX()/writeX()来实际操作。 假想的最小执行顺序模型首先假定概念上CPU是弱有序的,但它能维护程序因果关系。某些CPU(如i386或x86_64)比其它类型的CPU(如PowerPC的或FRV)受到更多的约束,所以,在考虑与具体体系结构无关的代码时,必须假设处在最宽松的场景(即DEC ALPHA)。 这意味着必须考虑CPU将以任何它喜欢的顺序执行它的指令流 —— 甚至是并行的 —— 如果流中的某个指令依赖前面较早的指令,则该较早的指令必须在后者执行之前完全结束[*],换句话说:保持因果关系。 [*]有些指令会产生多个结果 —— 如改变条件码,改变寄存器或修改内存 —— 不同的指令可能依赖于不同的结果。 CPU也可能会放弃那些最终不产生效果的指令。例如,如果两个相邻的指令加载一个直接值到同一个寄存器中,第一个可能被丢弃。 同样地,必须假定编译器可能以任何它认为合适的方式会重新排列指令流,但同样维护程序因果关系。 CPU缓存的影响缓存的内存操作被系统交叉感知的方式,在一定程度上,受到CPU和内存之间的缓存、以及保持系统一致状态的内存一致性系统的影响。 若CPU和系统其它部分的交互通过cache进行,内存系统就必须包括CPU缓存,以及CPU及其缓存之间的内存屏障(内存屏障逻辑上如下图中的虚线): <--- CPU ---> : <----------- Memory -----------> : +--------+ +--------+ : +--------+ +-----------+ | | | | : | | | | +--------+ | CPU | | Memory | : | CPU | | | | | | Core |--->| Access |----->| Cache |<-->| | | | | | | Queue | : | | | |--->| Memory | | | | | : | | | | | | +--------+ +--------+ : +--------+ | | | | : | Cache | +--------+ : | Coherency | : | Mechanism | +--------+ +--------+ +--------+ : +--------+ | | | | | | | | : | | | | | | | CPU | | Memory | : | CPU | | |--->| Device | | Core |--->| Access |----->| Cache |<-->| | | | | | | Queue | : | | | | | | | | | | : | | | | +--------+ +--------+ +--------+ : +--------+ +-----------+ : : 虽然一些特定的load或store实际上可能不出现在发出这些指令的CPU之外,因为在该CPU自己的缓存内已经满足,但是,如果其它CPU关心这些数据,那么还是会产生完整的内存访问,因为高速缓存一致性机制将迁移缓存行到需要访问的CPU,并传播冲突。 只要能维持程序的因果关系,CPU核心可以以任何顺序执行指令。有些指令生成load和store操作,并将他们放入内存请求队列等待执行。CPU内核可以以任意顺序放入到队列中,并继续执行,直到它被强制等待某一个指令完成。 内存屏障关心的是控制访问穿越CPU到内存一边的顺序,以及系统其他组建感知到的顺序。 [!]对于一个给定的CPU,并不需要内存屏障,因为CPU总是可以看到自己的load和store指令,好像发生的顺序就是程序顺序一样。 [!]MMIO或其它设备访问可能绕过缓存系统。这取决于访问设备时内存窗口属性,或者某些CPU支持的特殊指令。 缓存一致性但是事情并不像上面说的那么简单,虽然缓存被期望是一致的,但是没有保证这种一致性的顺序。这意味着在一个CPU上所做的更改最终可以被所有CPU可见,但是并不保证其它的CPU能以相同的顺序感知变化。 考虑一个系统,有一对CPU(1&2),每一个CPU有一组并行的数据缓存(CPU 1有A / B,CPU 2有C / D): : : +--------+ : +---------+ | | +--------+ : +--->| Cache A |<------->| | | | : | +---------+ | | | CPU 1 |<---+ | | | | : | +---------+ | | +--------+ : +--->| Cache B |<------->| | : +---------+ | | : | Memory | : +---------+ | System | +--------+ : +--->| Cache C |<------->| | | | : | +---------+ | | | CPU 2 |<---+ | | | | : | +---------+ | | +--------+ : +--->| Cache D |<------->| | : +---------+ | | : +--------+ : 假设该系统具有以下属性:
接下来,试想一下,第一个CPU上有两个写操作,并且它们之间有一个write屏障,来保证它们到达该CPU缓存的顺序: CPU 1 CPU 2 COMMENT =============== =============== ======================================= u == 0, v == 1 and p == &u, q == &u v = 2; smp_wmb(); Make sure change to v is visible before change to p <A:modify v=2> v is now in cache A exclusively p = &v; <B:modify p=&v> p is now in cache B exclusively write内存屏障强制系统中其它CPU能以正确的顺序感知本地CPU缓存的更改。现在假设第二个CPU要读取这些值: CPU 1 CPU 2 COMMENT =============== =============== ======================================= ... q = p; x = *q; 上述一对读操作可能不会按预期的顺序执行,持有P的缓存行可能被第二个CPU的某一个缓存更新,而持有V的缓存行在第一个CPU的另外一个缓存中因为其它事情被延迟更新了; CPU 1 CPU 2 COMMENT =============== =============== ======================================= u == 0, v == 1 and p == &u, q == &u v = 2; smp_wmb(); <A:modify v=2> <C:busy> <C:queue v=2> p = &v; q = p; <D:request p> <B:modify p=&v> <D:commit p=&v> <D:read p> x = *q; <C:read *q> Reads from v before v updated in cache <C:unbusy> <C:commit v=2> 基本上,虽然两个缓存行CPU 2在最终都会得到更新,但是在不进行干预的情况下不能保证更新的顺序与在CPU 1在提交的顺序一致。 所以我们需要在load之间插入一个数据依赖屏障或read屏障。这将迫使缓存在处理其他任务之前强制提交一致性队列; CPU 1 CPU 2 COMMENT =============== =============== ======================================= u == 0, v == 1 and p == &u, q == &u v = 2; smp_wmb(); <A:modify v=2> <C:busy> <C:queue v=2> p = &v; q = p; <D:request p> <B:modify p=&v> <D:commit p=&v> <D:read p> smp_read_barrier_depends() <C:unbusy> <C:commit v=2> x = *q; <C:read *q> Reads from v after v updated in cache DEC Alpha处理器上可能会遇到这类问题,因为他们有一个分列缓存,通过更好地利用数据总线以提高性能。虽然大部分的CPU在读操作需要读取内存的时候会使用数据依赖屏障,但并不都这样,所以不能依赖这些。 其它CPU也可能页有分列缓存,但是对于正常的内存访问,它们会协调各个缓存列。在缺乏内存屏障的时候,Alpha 的语义会移除这种协作。 缓存一致性与DMA对于DMA的设备,并不是所有的系统都维护缓存一致性。这时访问DMA的设备可能从RAM中得到脏数据,因为脏的缓存行可能驻留在各个CPU的缓存中,并且可能还没有被写入到RAM。为了处理这种情况,内核必须刷新每个CPU缓存上的重叠位(或是也可以直接废弃它们)。 此外,当设备以及加载自己的数据之后,可能被来自CPU缓存的脏缓存行写回RAM所覆盖,或者当前CPU缓存的缓存行可能直接忽略RAM已被更新,直到缓存行从CPU的缓存被丢弃和重载。为了处理这个问题,内核必须废弃每个CPU缓存的重叠位。 更多信息参见Documentation/cachetlb.txt。 缓存一致性与MMIO内存映射I / O通常作为CPU内存空间窗口中的一部分地址,它们与直接访问RAM的的窗口有不同的属性。 这些属性通常是,访问绕过缓存直接进入到设备总线。这意味着MMIO的访问可能先于早些时候发出的访问被缓存的内存的请求到达。在这样的情况下,一个内存屏障还不够,如果缓存的内存写和MMIO访问存在依赖,cache必须刷新。 CPU能做的事情程序员可能想当然的认为CPU会完全按照指定的顺序执行内存操作,如果确实如此的话,假设CPU执行下面这段代码: a = *A; *B = b; c = *C; d = *D; *E = e; 他们会期望CPU执行下一个指令之前上一个一定执行完成,然后在系统中可以观察到一个明确的执行顺序; LOAD *A, STORE *B, LOAD *C, LOAD *D, STORE *E. 当然,现实中是非常混乱的。对许多CPU和编译器来说,上述假设都不成立,因为:
所以对另一个CPU,上面的代码实际观测的结果可能是: LOAD *A, ..., LOAD {*C,*D}, STORE *E, STORE *B (Where "LOAD {*C,*D}" is a combined load) 但是,CPU保证自身的一致性:不需要内存屏障,也可以保证自己以正确的顺序访问内存,如下面的代码: U = *A; *A = V; *A = W; X = *A; *A = Y; Z = *A; 假设不受到外部的影响,最终的结果可能为: U == the original value of *A X == W Z == Y *A == Y 上面的代码CPU可能产生的全部的内存访问顺序如下: U=LOAD *A, STORE *A=V, STORE *A=W, X=LOAD *A, STORE *A=Y, Z=LOAD *A 对于这个顺序,如果没有干预,在保持一致的前提下,一些操作也可能被合并,丢弃。 在CPU感知这些操作之前,编译器也可能合并、丢弃、延迟加载这些元素。 例如: *A = V; *A = W; 可减少到: *A = W; 因为在没有write屏障的情况下,可以假定将V写入到*A的操作被丢弃了,同样: *A = Y; Z = *A; 若没有内存屏障,可被简化为: *A = Y; Z = Y; 在CPU之外根本看不到load操作。 ALPHA处理器DEC Alpha CPU是最松散的CPU之一。不仅如此,一些版本的Alpha CPU有一个分列的数据缓存,允许它们在不同的时间更新语义相关的缓存。在同步多个缓存,保证一致性的时候,数据依赖屏障是必须的,使CPU可以正确的顺序处理指针的变化和数据的获得。 Alpha定义了Linux内核的内存屏障模型。 参见上面的“缓存一致性”章节。 使用示例循环缓冲区内存屏障可以用来实现循环缓冲,不需要用锁来使得生产者与消费者串行。 更多详情参考“Documentation/circular-buffers.txt” 参考
Chapter 5.2: Physical Address Space Characteristics Chapter 5.4: Caches and Write Buffers Chapter 5.5: Data Sharing Chapter 5.6: Read/Write Ordering Chapter 7.1: Memory-Access Ordering Chapter 7.4: Buffering and Combining Memory Writes Chapter 7.1: Locked Atomic Operations Chapter 7.2: Memory Ordering Chapter 7.4: Serializing Instructions Chapter 8: Memory Models Appendix D: Formal Specification of the Memory Models Appendix J: Programming with the Memory Models Chapter 5: Memory Accesses and Cacheability Chapter 15: Sparc-V9 Memory Models Chapter 9: Memory Models Chapter 8: Memory Models Chapter 9: Memory Appendix D: Formal Specifications of the Memory Models Chapter 8: Memory Models Appendix F: Caches and Cache Coherency Chapter 3.3: Hardware Considerations for Locks and Chapter 13: Other Memory Models Section 2.6: Speculation Section 4.4: Memory Access (全文完)
|