【发布时间】:2018-06-02 14:14:45
【问题描述】:
我正在研究我的 Haswell CPU 上的 Intel CPU 原子特性 (4/8 核 2.3-3.9ghz i7-4790M),我发现它真的很难 构建例如。可靠的 mutex_lock() 和 mutex_unlock() 例如 GCC 手册所建议的操作:
针对事务内存的 6.53 x86 特定内存模型扩展
x86 架构支持额外的内存排序标志来标记 锁定硬件锁省略的关键部分。这些必须 除了现有的内存模型之外,还指定了原子内在函数。
'__ATOMIC_HLE_ACQUIRE'
Start lock elision on a lock variable. Memory model must be
'__ATOMIC_ACQUIRE' or stronger.
'__ATOMIC_HLE_RELEASE'
End lock elision on a lock variable. Memory model must be
'__ATOMIC_RELEASE' or stronger.
当锁获取失败时,需要中止良好的性能 交易迅速。这可以通过'_mm_pause'来完成
#include <immintrin.h> // For _mm_pause
int lockvar;
/* Acquire lock with lock elision */
while (__atomic_exchange_n(&lockvar, 1,
__ATOMIC_ACQUIRE|__ATOMIC_HLE_ACQUIRE))
_mm_pause(); /* Abort failed transaction */
...
/* Free lock with lock elision */
__atomic_store_n(&lockvar, 0, __ATOMIC_RELEASE|__ATOMIC_HLE_RELEASE);
因此,请阅读该内容和英特尔软件开发人员手册第 3 卷部分 8.1,“锁定原子操作”,特别是第 8.1.4 节, “LOCK 操作对内部处理器缓存的影响”, 首先让我实现了我的测试 mutex_lock() mutex_unlock() 喜欢:
...
static inline attribute((always_inline,const))
bool ia64_has_clflush(void)
{ register unsigned int
ebx=0;
asm volatile
( "MOV $7, %%eax\n\t"
"MOV $0, %%ecx\n\t"
"CPUID\n\t"
"MOV %%ebx, %0\n\t"
: "=r" (ebx) :
: "%eax", "%ecx", "%ebx"
);
return ((ebx & (1U<<23)) ? true : false);
}
#define _LD_SEQ_CST_ __ATOMIC_SEQ_CST
#define _ST_SEQ_CST_ __ATOMIC_SEQ_CST
#define _ACQ_SEQ_CST_ (__ATOMIC_SEQ_CST|__ATOMIC_HLE_ACQUIRE)
#define _REL_SEQ_CST_ (__ATOMIC_SEQ_CST|__ATOMIC_HLE_RELEASE)
static bool has_clflush=false;
static
void init_has_clflush(void)
{ has_clflush = ia64_has_clflush();
}
static
void init_has_clflush(void) __attribute__((constructor));
static inline __attribute__((always_inline))
void mutex_lock( register _Atomic int *ua )
{ // the SDM states that memory to be used as semaphores
// should not be in the WB cache memory, but nearest we
// can get to uncached memory is to explicitly un-cache it:
if(has_clflush)
asm volatile
( "CLFLUSHOPT (%0)"
:: "r" (ua)
);
// why isn't the cache flush enough?
else
asm volatile
( "LFENCE" :: );
register unsigned int x;
x = __atomic_sub_fetch( ua, 1, _ACQ_SEQ_CST_);
_mm_pause();
if(has_clflush)
asm volatile
( "CLFLUSHOPT (%0)"
:: "r" (ua)
);
else
asm volatile
( "SFENCE" :: );
while((x = __atomic_load_n(ua,_LD_SEQ_CST_)) != 0)
switch(syscall( SYS_futex, ua, FUTEX_WAIT, x, nullptr,nullptr,0))
{case 0:
break;
case -1:
switch( errno )
{ case EINTR:
case EAGAIN:
continue;
default:
fprintf(stderr,"Unexpected futex error: %d : '%s'.", errno,
strerror(errno));
return;
}
}
}
static inline __attribute__((always_inline))
void mutex_unlock( register _Atomic int *ua )
{ if(has_clflush)
asm volatile
( "CLFLUSHOPT (%0)"
:: "r" (ua)
);
else
asm volatile( "LFENCE" :: );
register unsigned int x;
x = __atomic_add_fetch( ua, 1, _REL_SEQ_CST_);
_mm_pause();
if(has_clflush)
asm volatile
( "CLFLUSHOPT (%0)"
:: "r" (ua)
);
else
asm volatile ( "SFENCE" :: );
if(x == 0)
while( (1 < syscall( SYS_futex, ua, FUTEX_WAKE, 1,
nullptr,nullptr,0)) && (errno == EINTR));
}
现在,有趣的是关键的 mutex_lock() 减法和 mutex_unlock() 加法操作以指令结束:
互斥锁:
# 61 "intel_lock1.c" 1
CLFLUSHOPT (%rbx)
# 0 "" 2
#NO_APP
.L7:
lock xacquire subl $1, lck(%rip)
rep nop
cmpb $0, has_clflush(%rip)
je .L8
#APP
# 72 "intel_lock1.c" 1
CLFLUSHOPT (%rbx)
# 0 "" 2
互斥锁:
#APP
# 98 "intel_lock1.c" 1
CLFLUSHOPT (%rbx)
# 0 "" 2
#NO_APP
.L24:
movl $1, %eax
lock xacquire xaddl %eax, lck(%rip)
rep nop
addl $1, %eax
cmpb $0, has_clflush(%rip)
je .L25
#APP
# 109 "intel_lock1.c" 1
CLFLUSHOPT (%rbx)
# 0 "" 2
#NO_APP
但是这个实现似乎需要 LFENCE / SFENCE 可靠地运行(CLFLUSHOPT 是不够的),否则 两个线程最终都可能在 futex() 中死锁 锁定值是相同的 -1 。
我无法通过阅读英特尔文档看到它是如何 可能会发生两个线程进入指令 顺序:
# %rbx == $lck
CLFLUSHOPT (%rbx)
lock xacquire subl $1, lck(%rip)
rep nop
如果 *lck 为 0 ,则在 *lck 中都可以得到结果 '-1' ; 肯定一个线程必须得到-1,另一个线程必须得到-2?
但 strace 说不是:
strace: Process 11978 attached with 2 threads
[pid 11979] futex(0x60209c, FUTEX_WAIT, 4294967295, NULL <unfinished ...>
[pid 11978] futex(0x60209c, FUTEX_WAIT, 4294967295, NULL^C
这是死锁的情况。我哪里做错了?
请任何英特尔 CPU 锁定和缓存专家解释一下 同一个未缓存位置 *lck 的两个原子如何递减或递增 这两者 断言#LOCK 总线信号(独占总线访问)和 XACQUIRE 最终能在 *lck 中得到相同的结果吗?
我认为这就是#LOCK 前缀(和 HLE)的目的? 我尝试过不使用 HLE,而只使用 __ATOMIC_SEQ_CST 进行所有访问, (这只是添加了 LOCK 前缀,而不是 XACQUIRE)但它没有区别 - 没有 {L,S}FENCE-es 仍然会导致死锁。
我读过 Ulrich Drepper 的优秀论文 [ Futexes are Tricky ] :http://www.akkadia.org/drepper/futex.pdf ,但他提出了 仅将硬编码常量写入的互斥体实现 锁内存。我明白为什么了。很难 让互斥锁与服务员数量或任何数量可靠地工作 对锁定值进行的一种算术运算。 有没有人找到方法来做可靠的锁定算术 这样结果适合锁/信号量 x86_64 Linux 上的价值?最有兴趣讨论它们...
所以在调查了 HLE 和 CLFLUSH 几条死胡同之后, 我能够做到的唯一有效的锁定/解锁版本 到达使用硬编码常量和 __atomic_compare_exchange_n - 测试程序的完整源代码,它增加了一个计数器 (无锁定)直到收到 + / 退出信号, 位于:
工作示例:intel_lock3.c
[]:https://drive.google.com/open?id=1ElB0qmwcDMxy9NBYkSXVxljj5djITYxa
enum LockStatus
{ LOCKED_ONE_WAITER = -1
, LOCKED_NO_WAITERS = 0
, UNLOCKED=1
};
static inline __attribute__((always_inline))
bool mutex_lock( register _Atomic int *ua )
{ register int x;
int cx;
lock_superceded:
x = __atomic_load_n( ua, _LD_SEQ_CST_ );
cx = x;
x = (x == UNLOCKED)
? LOCKED_NO_WAITERS
: LOCKED_ONE_WAITER;
if (! __atomic_compare_exchange_n
( ua, &cx, x, false, _ACQ_SEQ_CST_, _ACQ_SEQ_CST_) )
goto lock_superceded;
if( x == LOCKED_ONE_WAITER )
{ do{
switch(syscall( SYS_futex, ua, FUTEX_WAIT, x, nullptr,nullptr,0))
{case 0:
break;
case -1:
switch( errno )
{ case EINTR:
return false;
case EAGAIN:
break;
default:
fprintf(stderr,"Unexpected futex WAIT error: %d : '%s'.",
errno, strerror(errno));
return false;
}
}
x = __atomic_load_n(ua,_LD_SEQ_CST_);
} while(x < 0);
}
return true;
}
static inline __attribute__((always_inline))
bool mutex_unlock( register _Atomic int *ua )
{ register int x;
int cx;
unlock_superceded:
x = __atomic_load_n( ua, _LD_SEQ_CST_ );
cx = x;
x = (x == LOCKED_ONE_WAITER)
? LOCKED_NO_WAITERS
: UNLOCKED;
if (! __atomic_compare_exchange_n
( ua, &cx, x, false, _ACQ_SEQ_CST_, _ACQ_SEQ_CST_) )
goto unlock_superceded;
if(x == LOCKED_NO_WAITERS)
{ while((1 <
syscall( SYS_futex, ua, FUTEX_WAKE, 1, nullptr,nullptr,0))
||( UNLOCKED != __atomic_load_n( ua, _LD_SEQ_CST_ ))
) // we were a waiter, so wait for locker to unlock !
{ if( errno != 0 )
switch(errno)
{case EINTR:
return false;
case EAGAIN:
break;
default:
fprintf(stderr,
"Unexpected futex WAKE error: %d : '%s'.",
errno, strerror(errno));
return false;
}
}
}
return true;
}
Build & Test (GCC 7.3.1 & 6.4.1 & 5.4.0) used:
$ gcc -std=gnu11 -march=x86-64 -mtune=native -D_REENTRANT \
-pthread -Wall -Wextra -O3 -o intel_lock3 intel_lock3.c
$ ./intel_lock3
# wait a couple of seconds and press ^C
^C59362558
使用算术破解的版本:
https://drive.google.com/open?id=10yLrohdKLZT4p3G1icFHdjF5eHY68Yws
用例如编译:
$ gcc -std=gnu11 -march=x86_64 -mtune=native -O3 -Wall -Wextra
-o intel_lock2 intel_lock2.c
$ ./intel_lock2
# wait a couple of seconds and press ^C
$ ./intel_lock2
^Cwas locked!
446
它不应该打印“被锁定!”并且在 几秒钟应该超过一个计数,打印 最后,@ 5e8 : 5x10^8 ,而不是 446。
用strace运行显示有两个线程阻塞 等待-1的锁值变为0:
$ strace -f -e trace=futex ./intel_lock2
strace: Process 14481 attached
[pid 14480] futex(0x602098, FUTEX_WAIT, 4294967295, NULL <unfinished ...>
[pid 14481] futex(0x602098, FUTEX_WAKE, 1 <unfinished ...>
[pid 14480] <... futex resumed> ) = -1 EAGAIN (Resource temporarily
unavailable)
[pid 14481] <... futex resumed> ) = 0
[pid 14480] futex(0x602098, FUTEX_WAKE, 1 <unfinished ...>
[pid 14481] futex(0x602098, FUTEX_WAIT, 4294967295, NULL <unfinished ...>
[pid 14480] <... futex resumed> ) = 0
[pid 14481] <... futex resumed> ) = -1 EAGAIN (Resource temporarily
unavailable)
[pid 14480] futex(0x602098, FUTEX_WAIT, 4294967295, NULL <unfinished ...>
[pid 14481] futex(0x602098, FUTEX_WAIT, 4294967295, NULL^C <unfinished
...>
[pid 14480] <... futex resumed> ) = ? ERESTARTSYS (To be restarted
if SA_RESTART is set)
strace: Process 14480 detached
strace: Process 14481 detached
was locked!
7086
$
通常,WAIT 应该安排在 WAKE 之前,但不知何故 GCC 将内存排序语义解释为 WAKE 总是在任何 WAIT 之前被安排;但即使那样 发生,代码应该只是延迟,并且永远不会结束 两个线程在进入 futex(...FUTEX_WAIT..) 时获得 -1 lck 值。
几乎相同的算法在锁定值上使用算术总是 当两个线程都获得 (-1,-1) 时出现死锁 - 注意,从未见过 -2 值 通过任何线程:
static inline __attribute__((always_inline))
bool mutex_lock( register _Atomic volatile int *ua )
{ register int x;
x = __atomic_add_fetch( ua, -1, _ACQ_SEQ_);
if( x < 0 )
{ do{
// here you can put:
// if( x == -2) { .. NEVER REACHED! }
switch(syscall( SYS_futex, ua, FUTEX_WAIT, x, nullptr,nullptr,0))
{case 0:
break;
case -1:
switch( errno )
{ case EINTR:
return false; // interrupted - user wants to exit?
case EAGAIN:
break;
default:
fprintf(stderr,"Unexpected futex WAIT error: %d : '%s'.",
errno, strerror(errno));
return false;
}
}
x = __atomic_load_n(ua,_LD_SEQ_);
} while(x < 0);
}
return true;
}
static inline __attribute__((always_inline))
bool mutex_unlock( register _Atomic volatile int *ua )
{ register int x;
x = __atomic_add_fetch( ua, 1, _REL_SEQ_);
if(x == 0) // there was ONE waiter
while( (1 <
syscall( SYS_futex, ua, FUTEX_WAKE, 1, nullptr,nullptr,0)
)
||(1 < __atomic_load_n(ua, _LD_SEQ_)
) // wait for first locker to unlock
)
{ if( errno != 0 )
switch(errno)
{case EINTR:
return false;
case EAGAIN:
break;
default:
fprintf(stderr,"Unexpected futex WAKE error: %d : '%s'.",
errno, strerror(errno));
return false;
}
}
return true;
}
所以,我想如果算术运算是 预期,即。被序列化和原子化,那么上面 代码不会死锁;算术应该生成 与中使用的 LockStatus 枚举值相同的数字 工作示例。
但是算术出了点问题,现在产生 说明:
互斥锁:
movl $-1, %eax
lock xaddl %eax, (%rdx)
互斥锁:
movl $1, %eax
lock xaddl %eax, (%rdx)
代码本身没有插入栅栏,但每个 __atomic_store_n(ua,...) 都会生成一个。
AFAICS,没有产生该代码的有效时间表 在两个线程中获得相同的 -1 值。
所以我的结论是在算术上使用 intel LOCK 前缀 指令不安全并在用户模式下引入错误行为 Linux x86_64 gcc 编译程序 - 仅限 将常量值从文本存储器写入数据存储器是 Intel Haswell i7-4790M 平台上的原子顺序排序 使用 gcc 和 Linux, 并且此类平台上的算术不能通过使用以下任何组合来实现原子和顺序排序 HLE / XACQUIRE、锁定前缀或 FENCE 指令。
我的预感是分支预测在某种程度上失败了,并且 添加额外的算术运算/未能执行 此平台上的算术运算,带有 LOCK 前缀断言 以及不同物理内核上的多个线程。 因此,所有带有 LOCK 前缀的算术运算都被断言 是可疑的,应该避免。
【问题讨论】:
-
asm("lfence")没有"memory"破坏器来阻止编译器重新排序内存操作是不安全的。此外,如果您不使用 NT 存储或 WC 内存,lfence和sfence对正确性没有影响。如果它恰好使您的代码工作,那只是因为额外的延迟。顺便说一句,对齐地址上的lock前缀不会导致总线锁定,只会导致该行的缓存锁定。 IDK 为什么要在锁上使用clflushopt。这将使它变慢而不会获得正确性。存储缓冲区已经使操作尽快可见。 -
ia64_has_clflush(void)名称错误:IA64 是安腾。 64 位 x86 称为 x86-64。或者就叫它x86_has_clflush。或者最好不要使用它。 -
WTF?不,原子操作在 WB 内存上工作得非常好,并且以这种方式最有效。缓存是连贯的,因此原子更新 L1d 中的一行会使更新对系统中的所有其他观察者来说是原子的。 (即所有其他核心)。 Can num++ be atomic for 'int num'?。有关使用 C11 原子的计数信号量,请参阅 C & low-level semaphore implementation。 (不使用 futex 系统调用,只是一个纯用户空间实现,没有回退到操作系统睡眠/等待,但展示了原子如何工作)
-
我使用 'ia64' 来表示“英特尔酷睿 64 位架构”,而不是安腾。那你就错了;不要那样做:P IA64 在 Intel CPU 的上下文中已经具有特定的技术含义。有效术语为 Intel64、x86-64 和 amd64。或者只是 x86,因为您的函数也可以在 32 位 x86 上编译和工作。
-
Re: 映射 WC 内存:显然在 Linux 下的用户空间是可能的:how to map memory as USWC under windows/linux?。但就像我说的,你绝对不想要这个。正常的
locked 操作在 WB 内存上高效工作,根本无需回写到 DRAM,而且 HLE 还被设计为在 WB 内存中的用户空间互斥体上高效工作。
标签: linux gcc x86-64 atomic futex