首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >互斥锁和解锁功能如何防止CPU重新排序?

互斥锁和解锁功能如何防止CPU重新排序?
EN

Stack Overflow用户
提问于 2018-06-20 14:47:49
回答 2查看 2.1K关注 0票数 2

据我所知,函数调用充当编译器屏障,而不是CPU屏障。

教程如下所示:

获取锁意味着获得语义,而释放锁则意味着发布语义!中间的所有内存操作都包含在一个漂亮的小屏障三明治中,防止任何不受欢迎的内存跨越边界重新排序。

我假设上面的引号是关于CPU重新排序的,而不是编译器的重新排序。

但我不明白互斥锁和解锁是如何导致CPU赋予这些函数获取和释放语义的。

例如,如果我们有以下C代码:

代码语言:javascript
复制
pthread_mutex_lock(&lock);
i = 10;
j = 20;
pthread_mutex_unlock(&lock);

上述C代码被翻译成以下(伪)汇编指令:

代码语言:javascript
复制
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 imov 20 into j重新排序到call pthread_mutex_lock()call pthread_mutex_unlock()以下?

如果是call指令阻止CPU进行重新排序,那么为什么我引用的教程让人觉得是互斥锁和解锁函数阻止了CPU的重新排序,为什么我引用的教程没有说任何函数调用都会阻止CPU重新排序?

我的问题是关于x86体系结构。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2018-06-20 16:05:50

简单地说,pthread_mutex_lockpthread_mutex_unlock调用的主体将包括必要的特定于平台的内存屏障,这将阻止CPU在关键部分之外移动内存访问。指令流将通过一个lock指令从调用代码移动到unlockcall函数中,为了重新排序的目的,必须考虑这个动态指令跟踪--而不是在程序集列表中看到的静态序列。

具体来说,在x86上,您可能不会在这些方法中找到显式的、独立的内存屏障,因为为了执行实际的锁定和原子解锁,您已经有了-prefixed指令,并且这些指令意味着一个完整的内存屏障,这会阻止您所关注的CPU重新排序。

例如,在我使用glibc 2.23的Ubuntu16.04系统上,pthread_mutex_lock是使用lock cmpxchg (比较和交换)实现的,pthread_mutex_unlock是使用lock dec (减量)实现的,两者都具有完全的屏障语义。

票数 9
EN

Stack Overflow用户

发布于 2018-06-20 14:56:53

如果ij是局部变量,则没有。如果编译器能够证明当前函数之外的任何内容都没有它们的地址,那么编译器可以在整个函数调用中将它们保存在寄存器中。

但是任何全局变量,或者其地址可能存储在全局中的局部变量, do 必须“同步”在内存中进行非内联函数调用。编译器必须假定它不能内联的任何函数调用都可以修改它可能具有引用的任何/每个变量。

例如,如果int i;是一个局部变量,那么在sscanf("0", "%d", &i);之后,它的地址将转义该函数和编译器将不得不围绕函数调用溢出/重新加载它,而不是将它保存在一个调用保存的寄存器中。

请参阅我在了解易失性asm与易失性变量上的答案,其中有一个例子,说明asm volatile("":::"memory")是一个局部变量的障碍,该变量的地址从函数(sscanf("0", "%d", &i);)中逃脱,但对于仍然是纯本地变量的局部变量则不是这样。这是完全相同的行为,完全相同的原因。

我假设上面的引号是关于CPU重新排序的,而不是编译器的重新排序。

这两者都是,因为两者都是正确的必要条件。

这就是为什么编译器不能用任何函数调用重新排序共享变量的更新。(这是非常重要的:弱C11内存模型允许大量编译时重新排序。强x86内存模型只允许StoreLoad重新排序和本地存储转发。)

pthread_mutex_lock 是一个非内联函数调用,它负责编译时重新排序,它执行locked操作,即原子RMW,这也意味着它在x86上包含了一个完整的运行时内存屏障。(不过,不是call指令本身,而是函数体中的代码。)这给了它获取语义。

解锁自旋锁只需要一个发布存储,而不是RMW,因此根据实现细节,解锁函数可能不是StoreLoad屏障。(这还是可以的:它使关键部分的每件事都不出去。没有必要在解锁之前停止以后的操作。见杰夫普莱辛的文章解释获取和发布语义)

在弱有序的ISA上,这些互斥函数将运行屏障指令,如ARM dmb (数据存储屏障)。正常函数不会,所以指南的作者正确地指出了这些函数是特殊的。

现在,是什么阻止 CPU将mov 10重新排序到i中,将mov 20重新排序到j到高于call pthread_mutex_lock()的位置?

这并不是重要的原因(因为在弱有序的ISA call instruction,上运行一个屏障指令),但实际上在x86上,存储甚至不能与重新排序,更不用说函数体在函数返回之前对互斥对象进行的实际锁定/解锁。

x86具有很强的内存排序语义(存储不与其他存储重新排序),而call是一个存储(推送返回地址)。

因此,mov [i], 10必须出现在call指令所做的存储之间的全局存储中。

当然,在一个正常的程序中,没有人观察到其他线程的调用堆栈,只有xchg来获取互斥对象,或者释放存储来在pthread_mutex_unlock中释放它。

票数 6
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/50951011

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档