首页 > > > LINUX内核内存屏障
点击上方“蓝字”可以关注我们哦

|本文来自ChinaUnix博客
|作者:kouu
|原文链接:http://blog.chinaunix.net/uid-9918720-id-1640912.html
跨CPU的锁的屏障作用
在SMP系统中, 锁定原语给出了多种形式的屏障: 其中一种在一些特定的锁冲突的情况下, 会影响其他CPU上的内存访问顺序.
锁与内存访问
假设系统中有(M)和(Q)这一对spinlock, 有三个CPU; 那么可能发生如下操作序列:

那么对于CPU 3来说, 从*A到*H的访问顺序是没有保证的, 不像单独的锁对应单独的CPU有那样的限制. 例如, CPU 3可能看到的顺序是:
*E, LOCK M, LOCK Q, *G, *C, *F, *A, *B, UNLOCK Q, *D, *H, UNLOCK M
但是它不会看到如下情况:
*B, *C or *D 先于 LOCK M
*A, *B or *C 后于 UNLOCK M
*F, *G or *H 先于 LOCK Q
*E, *F or *G 后于 UNLOCK Q
但是, 如果是下面的情形:

CPU 3可能看到:
*E, LOCK M [1], *C, *B, *A, UNLOCK M [1],
LOCK M [2], *H, *F, *G, UNLOCK M [2], *D
但是如果CPU 1先得到锁, CPU 3不会看到下面的情况:
*B, *C, *D, *F, *G or *H 先于 LOCK M [1]
*A, *B or *C 后于 UNLOCK M [1]
*F, *G or *H 先于 LOCK M [2]
*A, *B, *C, *E, *F or *G 后于 UNLOCK M [2]
锁与IO访问
在某些情况下(特别是涉及到NUMA的情况), 两个CPU上发起的属于两个spinlock临界区的IO访问可能被PCI桥看成是交错发生的, 因为PCI桥并不一定参与cache一致性协议, 以至于无法响应读内存屏障.
例如:

PCI桥可能看到的是:
STORE *ADDR = 0, STORE *ADDR = 4, STORE *DATA = 1, STORE *DATA = 5
这可能会引起硬件操作的错误.
这里所需要的是, 在释放spinlock之前, 使用mmiowb()作为干预, 例如:

这样就能确保CPU 1的两次STORE操作先于CPU 2的STORE操作被PCI桥所看到.
此外, 对于同一硬件设备在进行STORE操作之后再进行LOAD操作, 可以省去mmiowb(), 因为LOAD操作将强制STORE操作在开始LOAD之前就完成:

什么地方需要内存屏障?
在正常操作下, 内存操作的乱序一般并不会成为问题, 即使是在SMP内核中, 一段单线程的线性代码也总是能够正确工作. 但是, 有四种情况, 乱序绝对可能是一个问题:
(*) 处理器间交互.
(*) 原子操作.
(*) 访问设备.
(*) 中断.
处理器间交互
当系统中拥有不止一个CPU时, 系统中的多个CPU可能在同一时间工作在同样的数据集上. 这将产生同步问题, 并且这样的问题通常要靠使用锁来解决. 但是, 锁是昂贵的, 所以不是万不得已的情况下最好不要使用锁. 在这种情况下, 为防止错误, 导致两个CPU相互影响的那些内存操作可能需要仔细协调好顺序.
比如, 考虑一下读写信号量的slow path. 信号量的等待队列里有一个进程正在等待, 这个等待进程栈空间上的一段内存(译注: 也就是栈上分配的waiter结构)被链到信号量的等待链表里:
struct rw_semaphore {
...
spinlock_t lock;
struct list_head waiters;
};
struct rwsem_waiter {
struct list_head list;
struct task_struct *task;
};
要唤醒这样一个等待进程, up_read()函数或up_write()函数需要这样做:
(1) 读取该等待进程所对应的waiter结构的next指针, 以记录下一个等待进程是谁;
(2) 读取waiter结构中的task指针, 以获取对应进程的进程控制块;
(3) 清空waiter结构中的task指针, 以表示这个进程正在获得信号量;
(4) 对这个进程调用wake_up_process()函数; 并且
(5) 释放waiter结构对进程控制块的引用计数.
换句话说, 这个过程会执行如下事件序列:
LOAD waiter->list.next;
LOAD waiter->task;
STORE waiter->task;
CALL wakeup
RELEASE task
而如果其中一些步骤发生了乱序, 那么整个过程可能会产生错误.
一旦等待进程将自己挂入等待队列, 并释放了信号量里的锁, 这个等待进程就不会再获得这个锁了(译注: 参阅信号量的代码, 它内部使用了一个spinlock来进行同步); 它要做的事情就是在继续工作之前, 等待waiter结构中的task指针被清空(译注: 然后自己会被唤醒). 而既然waiter结构存在于等待进程的栈上, 这就意味着, 如果在waiter结构中的next指针被读取之前, task指针先被清空了的话(译注: 等待进程先被唤醒了), 那么, 这个等待进程可能已经在另一个CPU上开始运行了(译注: 相对于唤醒进程所运行的CPU), 并且在up*()函数有机会读取到next指针之前, 栈空间上对应的waiter结构可能已经被复用了(译注: 被唤醒的进程从down*()函数返回, 然后可能进行新的函数调用, 导致栈空间被重复使用).
看看上面的事件序列可能会发生什么:

对付这个问题可以使用信号量中的锁, 但是当进程被唤醒后, down_xxx()函数其实没必要重新获得这个spinlock.
实际的解决办法是插入一个通用SMP内存屏障:
LOAD waiter->list.next;
LOAD waiter->task;
smp_mb();
STORE waiter->task;
CALL wakeup
RELEASE task
这样, 对于系统中的其他CPU来说, 屏障将保证屏障之前的所有内存访问先于屏障之后的所有内存访问发生. 屏障并不保证屏障之前的所有内存访问都在屏障指令结束之前完成.
在UP系统中 - 这种情况将不是问题 - smp_mb()函数只是一个编译优化屏障, 这就确保了编译器生成顺序正确的指令, 而不需要干预CPU. 既然只有一个CPU, 该CPU的数据依赖逻辑将处理所有事情.
原子操作
虽然原子操作在技术上实现了处理器之间的交互, 然而特别注意一些原子操作隐含了完整的内存屏障, 而另外一些则没有, 但是它们却作为一个群体被整个内核严重依赖.
许多原子操作修改内存中的一些状态, 并且返回该状态相关的信息(旧状态或新状态), 就在其中实际操作内存的两边各隐含一个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(); /* 如果成功 (返回 1) */
test_and_set_bit();
test_and_clear_bit();
test_and_change_bit();
它们被用于作为类LOCK和类UNLOCK操作的实现, 和用于控制对象析构的引用计数, 这些情况下, 隐含内存屏障是有必要的.
以下操作由于没有隐含内存屏障, 会有潜在的问题, 但有可能被用于实现类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();
如果它们用于产生统计, 那么他们可能就不需要内存屏障, 除非统计数据之间存在耦合.
如果它们被用作控制对象生命周期的引用计数, 那么它们可能并不需要内存屏障, 因为要么引用计数需要在一个锁的临界区里面进行调整, 要么调用者已经持有足够的引用而相当于拥有了锁(译注: 一般在引用计数减为0的时候需要将对应的对象析构, 如果调用者知道引用计数在某些情况下不可能减为0, 那么这个对象也就不可能在这些情况下被析构, 也就不需要通过内存屏障来避免访存乱序导致的对象在析构之后还被访问的情况), 这样的情况下并不按需要内存屏障.
如果它们用于构成锁的一些描述信息, 那么他们可能就需要内存屏障, 因为锁原语一般需要按一定的顺序来操作.
基本上, 每个场景都需要仔细考虑是否需要使用内存屏障.
以下操作是特殊的锁原语:
test_and_set_bit_lock();
clear_bit_unlock();
__clear_bit_unlock();
它们都执行了类LOCK和类UNLOCK的操作. 相比其他操作, 它们应该优先被用于实现锁原语, 因为它们的实现可以在许多体系结构下得到优化.
[!] 注意, 这些特殊的内存屏障原语对一些情况也是有用的, 因为在一些体系结构的CPU上,使用的原子操作本身就隐含了完整的内存屏障功能, 所以屏障指令在这里是多余的, 在这样的情况下, 这些特殊的屏障原语将不使用额外的屏障操作.
访问设备
许多设备都可以被映射到内存, 因此在CPU看来, 它们只是一组内存地址. 为了控制这些设备, 驱动程序通常需要确保正确的内存访问按正确的顺序来执行.
但是, 聪明的CPU或者聪明的编译器却导致了潜在的问题, 如果CPU或编译器认为乱序, 或合并访问更有利于效率的话, 驱动程序代码中仔细安排的访存序列可能并不会按正确的顺序被送到设备上 - 从而可能导致设备的错误.
在Linux内核里面, IO访问应该使用适当的访问函数 - 例如inb()或writel() - 它们知道如何得到恰当的访问顺序. 大多数情况下, 在使用这些函数之后就不必再显式的使用内存屏障, 但是在两种情况下, 内存屏障可能还是需要的:
(1) 在一些系统中, IO存储操作对于所有CPU来说并不是严格有序的, 所以对于所有的通用驱动程序(译注: 通用驱动程序需要适应各种体系结构的系统), 需要使用锁, 并且一定要在解锁临界区之前执行mmiowb()函数.
(2) 如果访存函数访问松散属性的IO内存窗口, 那么需要使用强制内存屏障来确保执行顺序.
中断
驱动程序可能被它自己的中断处理程序所打断, 然后驱动程序中的这两个部分可能会相互干扰对方控制或访问设备的意图.
通过禁用本地中断(一种形式的锁)可能至少部分缓解这种情况, 这样的话, 驱动程序中的关键操作都将包含在禁用中断的区间中. 于是当驱动程序的中断处理程序正在执行时, 驱动程序的核心代码不可能在相同的CPU上运行, 并且在当前中断被处理完之前中断处理程序不允许再次被调用, 于是中断处理程序就不需要再对这种情况使用锁.
但是, 考虑一个驱动程序正通过一个地址寄存器和一个数据寄存器跟以太网卡交互的情况.假设驱动程序的核心代码在禁用中断的情况下操作了网卡, 然后驱动程序的中断处理程序被调用:
LOCAL IRQ DISABLE
writew(ADDR, 3);
writew(DATA, y);
LOCAL IRQ ENABLE
<进入中断>
writew(ADDR, 4);
q = readw(DATA);
<退出中断>
如果执行顺序的规则足够松散, 对数据寄存器的写操作可能发生在第二次对地址寄存器的写操作之后:
STORE *ADDR = 3, STORE *ADDR = 4, STORE *DATA = y, q = LOAD *DATA
如果执行顺序像这样松散, 就需要假定在禁用中断区间内应该完成的访问可能泄漏到区间之外, 并且可能漏到中断过程中进行访问 - 反之亦然 - 除非使用隐式或显式的屏障.
通常这并不是一个问题, 因为禁用中断区间内完成的IO访存将会包含严格有序的同步LOAD操作, 形成隐式的IO屏障. 如果这还不够, 那么需要显式的调用一下mmiowb().
在一个中断服务程序与两个运行在不同CPU的程序相互通信的情况下, 类似的情况也可能发生. 如果出现这样的情况, 那么禁用中断的锁操作需要用于确保执行顺序. (译注: 也就是类似于spinlock_irq这样的操作.)
内核中I/O屏障的作用
在对IO内存进行存取的时候, 驱动程序应该使用适当的存取函数:
(*) inX(), outX():
它们都是倾向于跟IO空间打交道, 而不是普通内存空间, 不过这主要取决于具体CPU的逻辑. i386和x86_64处理器确实有特殊的IO空间存取周期和指令, 但是许多系统结构的CPU却并没有这些概念.
包括PCI总线也可能会定义成IO空间 - 比如在i386和x86_64的CPU上 - 很容易将它映射到CPU的IO空间上. 但是, 它也可能作为虚拟的IO空间被映射到CPU的内存空间上, 特别对于那些不支持IO空间的CPU.
访问这些空间可能是完全同步的(比如在i386上), 但是对于桥设备(比如PCI主桥)可能并不完全是这样.
他们能保证完全遵守IO操作之间的访问顺序.
他们不能保证完全遵从IO操作与其他类型的内存操作之间的访问顺序.
(*) readX(), writeX():
在发起调用的CPU上, 这些函数是否保证完全遵从内存访问顺序而且不进行合并访问,取决于它们所访问的内存窗口上定义的属性. 例如, 较新的i386体系结构的机器, 可以通过MTRR寄存器来控制内存窗口的属性.
通常, 只要不是访问预取设备, 这些函数将保证完全有序并且不进行合并访问.
但是对于桥设备(比如PCI桥), 如果它们愿意的话, 可能会倾向于对内存操作进行延迟处理; 要冲刷一个STORE操作, 首选是对相同地址进行一次LOAD[*], 但是对于PCI来说 , 对相同设备或相同的配置的IO空间进行一次LOAD就足够了.
[*] 注意! 试图从刚写过的地址LOAD数据, 可能会导致错误 - 比如对于16550 Rx/Tx串口寄存器.
遇到带预取的IO内存, 可能需要使用mmiowb()屏障来强制让STORE操作有序.
关于PCI事务交互方面的更多信息, 请参阅PCI规范.
(*) readX_relaxed()
这些函数类似于readX(), 但是任何情况下都不保证有序. 请注意, 这里没有用到IO读屏障.
(*) ioreadX(), iowriteX()
这些函数在进行访存的时候会根据访存类型选择适当的操作, inX()/outX()或readX()/writeX().
最小限度有序的假想模型
从概念上说, 必须假定的CPU是弱有序的, 但是它会保持程序上下文逻辑关系的外观. 一些CPU(比如i386或x86_64)比另一些(比如powerpc或frv)更具有约束力, 而在体系结构无关的代码中, 必须假定为最松散的情况(也就是DEC Alpha).
也就是说, 必须考虑到CPU可能会按它喜欢的顺序来执行操作 - 甚至并行执行 - 只是当指令流中的一条指令依赖于之前的一条指令时, 之前的这条指定才必须在后面这条指令可能被处理之前完全结束; 换句话说: 保持程序的上下文逻辑关系.
[*] 一些指令会产生不止一处影响 - 比如会修改条件码, 修改寄存器或修改内存 - 不同的指令可能依赖于不同的影响.
CPU也可能丢弃那些最终不产生任何影响的操作序列. 比如, 如果两个相邻的指令都将一个立即数LOAD到寄存器, 那么第一个LOAD指令可能被丢弃.
类似的, 也需要假设编译器可能按它觉得舒服的顺序来调整指令流, 但同样也会保持程序的上下文逻辑关系.
CPU cache的影响
操作cache中缓存的内存之后, 相应的影响会在整个系统间得到传播. 位于CPU和内存之间的cache, 和保持系统状态一致的内存一致性机构, 在一定程度上影响了传播的方法.
自从CPU与系统中其他部分的交互通过使用cache来实现以来, 内存系统就包含了CPU的缓存,而内存屏障基本上就工作在CPU和其cache之间的界面上(逻辑上说, 内存屏障工作在下图中虚线所示的地方):

一些LOAD和STORE可能不会实际出现在发起操作的CPU之外, 因为在CPU自己的cache上就能满足需要, 尽管如此, 如果其他CPU关心这些数据, 那么完整的内存访问还是会发生, 因为cache一致性机构将迁移相应的cache行到访问它的CPU, 使一致性得到传播.
在保持程序所期望的上下文逻辑的前提下, CPU核心可能会按它认为合适的顺序来执行指令.一些指令会产生LOAD和STORE操作, 并且将它们放到内存请求队列中, 等待被执行. CPU核心可能会按它喜欢的顺序来将这些操作放进队列, 然后继续运行, 直到它必须等待这些访存指令完成的时候为止.
内存屏障所需要关心的是访存操作从CPU一侧穿越到内存一侧的顺序, 和系统中的其他部件感知到的操作发生的顺序.
[!] 对于一个CPU自己的LOAD和STORE来说, 并不需要使用内存屏障, 因为CPU总是能按程序执行顺序看到它们所执行的LOAD和STORE操作.
[!] MMIO或其他设备存取可能绕开cache系统. 这取决于访问设备所经过的内存窗口的属性和/或是否使用了CPU所特有的与设备进行交互的指令.
CACHE一致性
但是, 事情并不是像上面所说的那样简单: 因为虽然可以期望cache是一致的, 但是一致性传播的顺序却是没有保证的. 也就是说, 虽然一个CPU所做出的更新将最终被其它CPU都看到, 但是却不保证其他CPU所看到的都是相同的顺序.
考虑这样一个系统, 它具有双CPU(1和2), 每个CPU有一对并行的数据cache(CPU 1对应A/B, CPU 2对应C/D):

想象一下该系统有如下属性:
(*) 一个奇数号的cache行可能被缓存在cache A, cache C, 或者可能依然驻留在内存中;
(*) 一个偶数号的cache行可能被缓存在cache B, cache D, 或者可能依然驻留在内存中;
(*) 而当CPU核心访问一个cache时, 另一个cache可以同时利用总线来访问系统中的其他部分 - 可能是替换一个脏的cache行或者进行预取;
(*) 每个cache都有一个操作队列, 被用于保持cache与系统中的其他部分的一致性;
(*) 当LOAD命中了已经存在于cache中的行时, 该一致性队列并不会得到冲刷, 尽管队列中的内容可能会影响这些LOAD操作. (译注: 也就是说, 队列中有针对某一cache行的更新操作正在等待被执行, 而这时LOAD操作需要读这个cache行. 这种情况下, LOAD并不会等待队列中的这个更新完成, 而是直接获取了更新前的值.)
接下来, 想象一下在第一个CPU上执行两个写操作, 并在它们之间使用一个写屏障, 以保证它们按要求的顺序到达该CPU的cache:

写内存屏障保证系统中的其他CPU会按正确的顺序看到本地CPU cache的更新. 但是设想一下第二个CPU要去读取这些值的情形:

上面这一对读操作可能不会在预期的顺序下执行, 比如持有p的cache行可能被更新到另一个CPU的cache, 而持有v的cache行因为其他一些cache事件的影响而延迟了对那个CPU的cache的更新:

基本上, 虽然最终CPU 2的两个cache行都将得到更新, 但是在没有干预的情况下, 并不能保证更新的顺序跟CPU 1提交的顺序一致.
我们需要在两次LOAD之间插入一个数据依赖屏障或读屏障, 以作为干预. 这将强制cache在处理后续的请求之前, 先让它的一致性队列得到提交:

这些问题会在DEC Alpha处理器上遇到, 这些处理器使用了分列cache, 通过提高数据总线的利用率以提升性能. 虽然大部分的CPU在读操作依赖于读操作的时候, 会在第二个读操作中隐含一个数依赖屏障, 但是并不是所有CPU都这样, 因此不能依赖这一点.
其他的CPU也可能使用分列cache, 但对于普通的内存访问, 他们会协调各个cache列. 而Alpha处理器的处理逻辑则取消了这样的协调动作, 除非使用内存屏障.
cache一致性与DMA
对于进行DMA操作的设备, 并不是所有系统都保持它们的cache一致性. 在这种情况下, 准备进行DMA的设备可能从RAM得到陈旧的数据, 因为脏的cache行可能还驻留在各个CPU的cache中, 而尚未写回到RAM. 为了解决这个问题, 内核的相应部分必须将cache中重叠的数据冲刷
掉(或者使它们失效)(译注: 冲刷掉cache中的相应内容, 以保持cache与RAM的一致).
此外, 在设备已经通过DMA将数据写入RAM之后, 这些数据可能被cache写回RAM的脏的cache行所覆盖, 或者CPU已缓存的cache行可能直接掩盖了RAM被更新的事实(译注: 使得对应的LOAD操作只能获得cache中的旧值, 而无法得到RAM中的新值), 直到cache行被从CPU cache中丢弃并且重新由RAM载入. 为解决这个问题, 内核的相应部分必须将cache中重叠的数据失效.
cache一致性与MMIO
内存映射IO通常通过内存地址来触发, 这些地址是CPU内存空间的某个窗口中的一部分, 而这个窗口相比于普通RAM对应的窗口会有着不同的属性.
这些属性通常包含这样的情况: 访存会完全绕过cache, 而直接到达设备总线. 这意味着在效果上, MMIO可能超越先前发出的对被缓存内存的访问(译注: 意思是, MMIO后执行, 但是先到达内存; 而先执行的写内存操作则可能被缓存在cache上, 之后才能冲刷到内存). 这种情况下, 如果这两者有某种依赖的话, 使用一个内存屏障并不足够, 而需要在写被缓存内存和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和编译器来说, 上述假设不成立, 因为:
(*) LOAD操作可能更加需要立即完成以确保程序的执行速度(译注: 因为往往会有后续指令需要等待LOAD的结果), 而STORE操作推迟一下往往并不会有问题;
(*) LOAD操作可以通过预取来完成, 并且在确认数据已经不需要之后, 预取结果可以丢弃;
(*) LOAD操作可以通过预取来完成, 导致结果被获取的时机可能并不符合期望的执行顺序;
(*) 内存访问的顺序可能被重新排列, 以促进更好的使用CPU总线和cache;
(*) 有一些内存或IO设备支持对相邻地址的批量访问, 在跟它们打交道的时候, LOAD和STORE操作可能被合并, 从而削减访存事务建立的成本, 以提高性能(内存和PCI设备可能都可以这样做); 并且
(*) CPU的数据cache可能影响访问顺序, 尽管cache一致性机构可以缓解这个问题 - 一旦STORE操作命中了cache - 但并不能保证一致性将按顺序传播到其他CPU(译注: 如果STORE操作命中了cache, 那么被更新过的脏数据可能会在cache中停留一段时间, 而不会立刻冲刷到内存中);
所以说, 另一个CPU可能将上面的代码看作是:
LOAD *A, ..., LOAD {*C,*D}, STORE *E, STORE *B
("LOAD {*C,*D}"是一个合并的LOAD)
但是, CPU将保证自身的一致性: 它将按正确的顺序看到自己的内存操作, 而不需要使用内存屏障. 以下面的代码为例:
U = *A;
*A = V;
*A = W;
X = *A;
*A = Y;
Z = *A;
假设不存在外部的干扰, 那么可以肯定最终的结果一定是:
U == the original value of *A
U == *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;
于是, 在没有使用写屏障的情况下, 可以认为将V写入*A的STORE操作丢失了. 类似的:
*A = Y;
Z = *A;
在没有内存屏障的情况下, 可能削减为:
*A = Y;
Z = Y;
于是在该CPU之外, 根本就看不到有LOAD操作存在.
特别值得一提的Alpha处理器
DEC Alpha是现有的最为松散的CPU之一. 不仅如此, 许多版本的Alpha CPU拥有分列的数据cache, 允许他们在不同的时间更新两个语义相关的缓存. 因为内存一致性系统需要同步更新系统的两个cache, 数据依赖屏障在这里就真正成为了必要, 以使得CPU能够按正确的顺序来处理指针的更新和新数据的获取.
Alpha处理器定义了Linux内核的内存屏障模型. (译注: 体系结构无关的代码需要以最坏情况为基准来考虑.)

我知道一种学习
○
于坚




下载我们APP
Android和IOS均可使用

这里有最新鲜的干货
ITPUB|ChinaUnix|IT168


原文作者:;如有侵权,请联系管理员删除。