汇编:xadd指令需要锁吗?

bec*_*cks 9 x86 assembly smp

我正在阅读allan cruse 代码的smphello.s 代码

在接下来的部分中,他试图为每个处理器设置堆栈段.

关键是他在xadd的描述中使用了xadd而没有使用锁定前缀,就像在这里一样.可能有一个锁定前缀.

这是一个错误还是没关系?为什么?

# setup an exclusive stack-area for this processor
mov  $0x1000, %ax   # paragraphs in segment
xadd %ax, newSS     # 'atomic' xchg-and-add
mov  %ax, %ss       # segment-address in SS
xor  %esp, %esp     # top-of-stack into ESP
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 6

xadd没有lock是原子的。在这个核心上中断,但不是wrt。在其他内核(或 DMA)上运行的代码。就像除xchg. 请参阅此答案,其中涵盖了相同的问题cmpxchg

如果此代码同时在多个内核上运行,则 2 个或更多内核可以读取相同的 值newSS,从而有效地丢失增量并将相同的值分配ss:esp给两个内核。或者,一个存储可能会被其他碰巧是连续的内核延迟到多个 xadd 之间,将计数器“倒带”到稍后加载看到的某个先前值。或任何问题的组合。 num++ 可以是“int num”的原子吗?

假设newSS对齐,加载和存储分别是原子的,但形成原子 RMW。

如果同时唤醒多个内核(是否可以广播 IPI?),这似乎很可能是一个真正的问题。如果没有,很可能每个xadd内核都会在下一个内核到达此代码之前完成。(包括提交到 L1d 缓存的存储;变得全局可见。)但这只是一种“碰巧工作”的行为,除非核心唤醒功能在唤醒另一个核心之前等待收到来自一个核心的回复。

lock xadd如果它想匹配关于原子增量的注释,它肯定需要。 原子性。如果线程从不并发运行,则中断很好,只能通过单个内核上的上下文切换。(例如,主线程和信号处理程序之间的原子性,或同一核心上的中断处理程序)。但由于标题是smphello.s,单处理器假设似乎不太可能。


Nat*_*dge 6

只是为这些理论论点提供一些经验证据:

这是一个测试用例,其中多个线程使用xadd递增共享计数器。在具有 4 个内核的 i7-8565U 上,它输出

unlocked: counter = 1633267, expected 4000000
locked: counter = 4000000, expected 4000000
Run Code Online (Sandbox Code Playgroud)

这清楚地表明xadd没有lock不是原子的。

编码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>

unsigned long counter = 0;

#define COUNTS_PER_THREAD 1000000UL
#define THREADS 4

void *unlocked_worker(void *unused) {
    (void)unused;
    for (unsigned long i = 0; i < COUNTS_PER_THREAD; i++) {
        unsigned long inc = 1;
        asm volatile("xaddq %0, %1" : "+r" (inc), "+m" (counter));
    }
    return NULL;
}

void *locked_worker(void *unused) {
    (void)unused;
    for (unsigned long i = 0; i < COUNTS_PER_THREAD; i++) {
        unsigned long inc = 1;
        asm volatile("lock; xaddq %0, %1" : "+r" (inc), "+m" (counter));
    }
    return NULL;
}

void run_threads(int lock) {
    void *(*worker)(void *) = lock ? locked_worker : unlocked_worker;
    counter = 0;
    pthread_t th[THREADS];
    for (int i = 0; i < THREADS; i++) {
        int err = pthread_create(&th[i], NULL, worker, NULL);
        if (err != 0) {
            fprintf(stderr, "pthread_create: %s\n", strerror(err));
            exit(1);
        }
    }
    for (int i = 0; i < THREADS; i++) {
        int err = pthread_join(th[i], NULL);
        if (err != 0) {
            fprintf(stderr, "pthread_join: %s\n", strerror(err));
            exit(1);
        }
    }
    printf("%s: counter = %lu, expected %lu\n",
           lock ? "locked" : "unlocked",
           counter, COUNTS_PER_THREAD * THREADS);
}

int main(void) {
    run_threads(0);
    run_threads(1);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)


Son*_*Sun 3

再想一想,我的脑海中浮现出了这个案件的另一种情景。

如果微码实现是xadd这样的:

temp = ax + newSS
newSS = ax 
ax = temp ; the last 2 are actual xchg
Run Code Online (Sandbox Code Playgroud)

那么在这种情况下我们就会遇到问题:

假设newSS在 2 个线程之间共享。

线程 No.0(等于 5t0ax加载并添加with并将其放入.newSSaxtemp register

假设此时我们有一个上下文切换。然后t1使用它ax等于5尝试加载newSS并将其添加到ax并将结果放入temp register. 然后上下文切换回t0...两个堆栈段寄存器将指向相同的地址。

显然我们这里有问题。除非微码实现是这样的:

tmp register = ax
xchg ax, newSS
ax = ax + tmpRegister
Run Code Online (Sandbox Code Playgroud)

以任何其他方式newSS多次读取变量或以不同的指令读取和写入变量,我们需要锁。