作者:POWER_WALKING_823 | 来源:互联网 | 2022-12-13 10:39
据我所知,函数调用充当编译器障碍,但不作为CPU障碍.
本教程说明如下:
获取锁意味着获取语义,而释放锁意味着释放语义!其间的所有内存操作都包含在一个漂亮的小屏障三明治中,防止任何不希望的内存重新排序跨越边界.
我假设上面的引用是关于CPU重新排序而不是编译器重新排序.
但我不明白互斥锁和解锁如何导致CPU赋予这些函数获取和释放语义.
例如,如果我们有以下C代码:
pthread_mutex_lock(&lock);
i = 10;
j = 20;
pthread_mutex_unlock(&lock);
上面的C代码被翻译成以下(伪)汇编指令:
push the address of lock into the stack
call pthread_mutex_lock()
mov 10 into i
mov 20 into j
push the address of lock into the stack
call pthread_mutex_unlock()
现在是什么阻止了CPU重新排序mov 10 into i
以及mov 20 into j
上方call pthread_mutex_lock()
或下方call pthread_mutex_unlock()
?
如果它是call
阻止CPU进行重新排序的指令,那么为什么我引用的教程使它看起来像是互斥锁和解锁函数来阻止CPU重新排序,为什么我引用的教程没有说任何函数调用会阻止CPU重新排序吗?
我的问题是关于x86架构.
1> Peter Cordes..:
如果i
和j
是局部变量,则什么也没有。如果编译器可以证明当前函数之外的任何内容都没有其地址,则编译器可以在函数调用期间将它们保留在寄存器中。
但是,任何全局变量,或当地人其地址可以被存储在一个全球性的,也必须是“同步”在内存中一个非内联函数调用。 编译器必须假定它无法内联的任何函数调用都会修改其可能引用的任何变量。
因此,例如,如果int i;
是局部变量,则sscanf("0", "%d", &i);
其地址将转义函数后,编译器将不得不在函数调用周围溢出/重新加载它,而不是将其保留在保留调用的寄存器中。
请参阅我的理解volatile asm vs volatile变量的答案,并举一个例子,说明asm volatile("":::"memory")
它的地址超出了函数(sscanf("0", "%d", &i);
)的局部变量的屏障,而不是仍然纯粹是局部的局部变量。出于完全相同的原因,这是完全相同的行为。
我假设以上引用是在谈论CPU重新排序,而不是在编译器重新排序。
谈论两者,因为两者对于正确性都是必需的。
这就是为什么编译器无法通过任何函数调用对共享变量的更新进行重新排序的原因。(这非常重要:弱的C11内存模型允许大量的编译时重新排序。强的x86内存模型仅允许StoreLoad重新排序和本地存储转发。)
pthread_mutex_lock
作为非内联函数调用需要进行编译时重新排序,并且它执行lock
ed操作(原子RMW)这一事实也意味着它在x86上包括完整的运行时内存屏障。(call
尽管不是指令本身,而只是函数体内的代码。)这使它获得了语义。
解锁自旋锁仅需要一个释放存储,而不需要RMW,因此根据实现细节,解锁功能可能不是StoreLoad障碍。(这仍然可以:它使关键部分中的所有内容都不会消失。不必在解锁之前停止以后的操作出现。请参阅Jeff Preshing的文章,解释Acquire和Release语义)
在弱序的ISA上,那些互斥函数将运行屏障指令,例如ARM dmb
(数据存储器屏障)。普通函数不会,所以该指南的作者正确地指出那些函数是特殊的。
现在是什么阻止CPU将 mov 10重新排序为i并将mov 20重新排序为j到上面call pthread_mutex_lock()
这不是重要的原因(因为在弱排序的ISA上pthread_mutex_unlock
会运行屏障指令),但实际上在x86上确实不能使用call
指令对存储进行重新排序,更不用说对这些存储进行实际的锁定/解锁了。函数返回之前由函数主体完成的互斥。
x86具有很强的内存排序语义(商店不会与其他商店重新排序),并且call
是商店(推送返回地址)。
因此mov [i], 10
必须在call
指令完成的存储之间出现在全局存储中。
当然,在普通程序中,没有人观察其他线程的调用堆栈,只是观察xchg
互斥量或释放存储以释放它pthread_mutex_unlock
。
2> BeeOnRope..:
简短的回答是是,本体pthread_mutex_lock
和pthread_mutex_unlock
呼叫将包括将在防止CPU移动存储器中的临界区内访问在其外部的必要特定于平台的存储器的障碍.指令流将通过指令从调用代码移动到lock
和unlock
函数中call
,为了重新排序,您必须考虑这个动态指令跟踪 - 而不是您在汇编列表中看到的静态序列.
特别是在x86上,你可能在这些方法中找不到明确的独立内存屏障,因为你已经有了lock
预先指定的指令,以便以原子方式执行实际的锁定和解锁,这些指令意味着一个完整的内存屏障,这可以防止你担心的CPU重新排序.
例如,在我的带有glibc 2.23的Ubuntu 16.04系统上,pthread_mutex_lock
使用lock cmpxchg
(比较和交换)pthread_mutex_unlock
实现并使用lock dec
(递减)实现,两者都具有完全屏障语义.
@ user8426277 - 因为它是_dynamic_指令流(大部分)对重新排序很重要.CPU不会将`call`作为单个指令执行,然后继续执行源/汇编顺序中的下一条指令,它将_into_作为`pthread`函数的主体.因此,为了分析的目的,您可以想象`lock`和`unlock`的整个主体在`call`指令出现的位置调用程序集中的"内联".如果不是这种情况,互斥体和许多其他同步机制将无法实现为普通函数调用!