C 通过内联程序集锁定内存操作

C 通过内联程序集锁定内存操作,c,assembly,x86,locking,spinlock,C,Assembly,X86,Locking,Spinlock,我对低层次的东西还不熟悉,所以我完全不知道你可能会面临什么样的问题,我甚至不确定我是否理解“原子”这个术语。现在,我正试图通过扩展汇编来实现简单的原子锁。为什么?出于好奇。我知道我在这里重新发明轮子,可能把整个过程过于简单化了 问题? 我在这里介绍的代码是否实现了使内存操作既线程安全又可重入的目标 如果有效,为什么 如果它不起作用,为什么 不够好?例如,我应该在C中使用register关键字吗 我只是想做的 在内存操作之前,锁定 内存操作后,解锁 代码: volatile int原子门

我对低层次的东西还不熟悉,所以我完全不知道你可能会面临什么样的问题,我甚至不确定我是否理解“原子”这个术语。现在,我正试图通过扩展汇编来实现简单的原子锁。为什么?出于好奇。我知道我在这里重新发明轮子,可能把整个过程过于简单化了

问题? 我在这里介绍的代码是否实现了使内存操作既线程安全又可重入的目标

  • 如果有效,为什么
  • 如果它不起作用,为什么
  • 不够好?例如,我应该在C中使用register关键字吗
我只是想做的

  • 在内存操作之前,锁定
  • 内存操作后,解锁
代码:

volatile int原子门存储器=0;
静态内联void原子_打开(volatile int*gate)
{
挥发性物质(
“等待:\n”
“cmp%[锁],%[门]\n”
“请稍候\n”
“mov%[锁],%[门]\n”
:[gate]“=m”(*gate)
:[lock]“r”(1)
);
}
静态内联void原子_关闭(volatile int*gate)
{
挥发性物质(
“mov%[锁],%[门]\n”
:[gate]“=m”(*gate)
:[lock]“r”(0)
);
}
然后是:

void*\u malloc(大小)
{
原子门打开(和原子门内存);
void*mem=malloc(尺寸);
原子门关闭(和原子门内存);
返回mem;
}
#定义malloc(大小)\ u malloc(大小)
。。对于calloc、realloc、free和fork(对于linux)也是如此

\ifdef\u UNISTD\u H
int_fork()
{
pid_t pid;
原子门打开(和原子门内存);
pid=fork();
原子门关闭(和原子门内存);
返回pid;
}
#定义fork()_fork()
#恩迪夫
加载原子_open的stackframe后,objdump生成:

0000000000 4009A7:
4009a7:39 10 cmp%edx,(%rax)
4009a9:74 fc je 4009a7
4009ab:89 10 mov%edx,(%rax)

此外,鉴于上述拆卸;我可以假设我正在进行一个原子操作,因为它只是一条指令吗?

我认为一个简单的自旋锁在x86上没有任何真正主要/明显的性能问题,是这样的。当然,一个真正的实现会在旋转一段时间后使用一个系统调用(比如Linux),解锁需要检查是否需要用另一个系统调用通知任何服务员。这很重要,;你不想永远旋转浪费CPU时间(和能量/热量)什么都不做。但是从概念上讲,这是回退路径之前自旋锁的自旋部分。这是如何实现的一个重要部分。(在调用内核之前只尝试获取一次锁将是一个有效的选择,而不是完全旋转。)

在内联asm中尽可能多地实现这些功能,或者最好使用C11
stdatomic
,如下所示。这是NASM语法。在GNUC中,请确保使用
“内存”
clobber停止编译时内存访问的重新排序()

普通存储具有发布语义,但不具有顺序一致性(可以从xchg或其他东西中获得)。足以保护关键部分(因此得名)


如果您使用的是原子标志的位字段,那么可以使用
lock bts
(test and set)作为xchg-with-1的等效项。您可以在
bt
test
上旋转。要解锁,您需要
lock btr
,而不仅仅是
btr
,因为它将是字节的非原子读-修改-写,甚至包含32位

对于通常应该使用的字节或整数大小的锁,您甚至不需要执行
lock
ed操作来解锁。glibc的解锁功能与我的解锁功能相同:一个简单的商店

lock bts
是不必要的;
xchg
lock cmpxchg
与普通锁一样好。)


第一个访问应该是原子RMW 请参阅讨论-如果第一次访问是只读的,CPU可能只发送该缓存线的共享请求。然后,如果它看到缓存线被解锁(希望是常见的低争用情况),它必须发送RFO(为所有权读取)才能真正写入缓存线。因此,这是两倍的非核心交易

缺点是这将独占该缓存线的所有权,但真正重要的是拥有锁的线程可以有效地存储
0
,这样我们就可以看到它被解锁了。无论是只读还是RMW,该内核都将失去对该行的独占所有权,并且必须在提交解锁存储之前进行RFO

我认为,当多个线程排队等待一个已经被占用的锁时,只读第一次访问只会优化核心之间的通信量。这将是一个愚蠢的事情优化

(还测试了一个大规模争用的自旋锁的想法,多个线程什么也不做,只是试图获取锁,但结果很差。这个链接的答案提出了一些关于
xchg
全局锁定总线对齐的
lock
s的错误主张。不要这样做,只有一个缓存锁(),每个内核都可以被锁定。)


但是,如果最初的尝试发现它被锁定,我们不希望继续使用原子RMW在缓存线上敲打。那就是我们回到只读的时候了。10个线程对同一个自旋锁进行所有垃圾邮件
xchg
,将使内存仲裁硬件非常繁忙。这可能会延迟解锁商店的可见性(因为该线程必须争夺该行的独占所有权),因此直接适得其反。它还可以存储其他核的一般内存


;;; UNTESTED ;;;;;;;;
;;; TODO: **IMPORTANT** fall back to OS-supported sleep/wakeup after spinning some
;;; e.g. Linux futex
    ; first arg in rdi as per AMD64 SysV ABI (Linux / Mac / etc)

;;;;;void spin_lock  (volatile char *lock)
global spin_unlock
spin_unlock:
       ; movzx  eax, byte [rdi]  ; debug check for double-unlocking.  Expect 1
    mov   byte [rdi], 0        ; lock.store(0, std::memory_order_release)
    ret

align 16
;;;;;void spin_unlock(volatile char *lock)
global spin_lock
spin_lock:
    mov   eax, 1                 ; only need to do this the first time, otherwise we know al is non-zero
.retry:
    xchg  al, [rdi]

    test  al,al                  ; check if we actually got the lock
    jnz   .spinloop
    ret                          ; no taken branches on the fast-path

align 8
.spinloop:                    ; do {
    pause
    cmp   byte [rdi], 0       ; C++11
    je    .retry              ; if (lock.load(std::memory_order_acquire) == 0)
    jmp   .spinloop