• x86 汇编中的 “lock“ 指令详解


    在深入理解 “lock” 指令之前,我们先来看一下 Qt 源代码中的一段 x86 汇编代码:

    q_atomic_increment:
        movl 4(%esp), %ecx
        lock 
        incl (%ecx)
        mov $0,%eax
        setne %al
        ret
    
        .align 4,0x90
        .type q_atomic_increment,@function
        .size   q_atomic_increment,.-q_atomic_increment
    

    通过 Google 搜索,我知道 “lock” 指令会导致 CPU 锁住总线,但我不清楚 CPU 什么时候释放总线?另外,我不明白这段代码是如何实现“加法”的?


    什么是 “lock” 指令?

    “lock” 并不是一条独立的指令,而是一个指令前缀,它作用于随后的那条指令。被作用的指令通常是对内存进行读-修改-写操作的指令,比如 INC、XCHG、CMPXCHG 等。在我们示例代码中,它作用于 incl (%ecx) 指令,该指令会原子性地将 ECX 寄存器所指向内存中的值加 1。

    使用 “lock” 前缀的原因

    当我们使用 “lock” 前缀时,CPU 会确保该操作期间对相关缓存线的独占所有权,并提供某些额外的顺序保证。CPU 会尽量避免锁住整个总线,如果不得已需要锁住总线,那么会在该指令执行期间内完成。

    在上述代码中,首先将要增加的变量地址从栈中拷贝到 ECX 寄存器,接下来用 lock incl (%ecx) 来原子性地增加该变量。随后的两条指令设置 EAX 寄存器(函数的返回值)为 0,如果该变量的新值为 0,则设置 AL 寄存器为 1,即返回值为 1,这是一种增量操作而不是加法。

    深入解析代码逻辑

    让我们逐行解析代码:

    movl 4(%esp), %ecx  ; 将要增加的变量地址从栈中加载到 ECX 寄存器
    lock                ; "lock" 前缀,确保以下操作的原子性
    incl (%ecx)         ; 将 ECX 寄存器指向的内存变量加 1
    mov $0,%eax         ; 将 EAX 寄存器设置为 0
    setne %al           ; 如果加法结果不等于 0,将 AL 寄存器(EAX 的低字节)设置为 1
    ret                 ; 返回
    

    为什么需要 mov $0,%eax 指令?

    mov $0,%eax 不是冗余指令,它将整个 EAX 寄存器设置为 0,而随后的 setne %al 只会修改 EAX 的低字节(即 AL)。如果没有 mov $0,%eax,EAX 的高三字节可能还包含以前操作的随机值,从而导致返回值不正确。

    “lock” 前缀的实际作用

    尽管某些文献(例如 “Assembler for DOS, Windows и Linux, 2000. Sergei Zukkov”)指出 “lock” 前缀会锁定数据总线,现代 CPU 通常使用更高效的方法。如果数据不跨越缓存行,CPU 核心可以内部锁定缓存行,从而避免阻塞所有其他核的读/写访问。这种机制利用了 MESI 缓存一致性协议。

    一个完整示例

    以下是一个使用 C++ 和内嵌汇编的例子,演示了 “lock” 前缀的实际作用:

    #include 
    #include 
    #include 
    #include 
    #include 
    
    std::atomic_ulong my_atomic_ulong(0);
    unsigned long my_non_atomic_ulong = 0;
    unsigned long my_arch_atomic_ulong = 0;
    unsigned long my_arch_non_atomic_ulong = 0;
    size_t niters;
    
    void threadMain() {
        for (size_t i = 0; i < niters; ++i) {
            my_atomic_ulong++;
            my_non_atomic_ulong++;
            __asm__ __volatile__ (
                "incq %0;"
                : "+m" (my_arch_non_atomic_ulong)
                :
                :
            );
            __asm__ __volatile__ (
                "lock;"
                "incq %0;"
                : "+m" (my_arch_atomic_ulong)
                :
                :
            );
        }
    }
    
    int main(int argc, char **argv) {
        size_t nthreads;
        if (argc > 1) {
            nthreads = std::stoull(argv[1], NULL, 0);
        } else {
            nthreads = 2;
        }
        if (argc > 2) {
            niters = std::stoull(argv[2], NULL, 0);
        } else {
            niters = 10000;
        }
        std::vector<std::thread> threads(nthreads);
        for (size_t i = 0; i < nthreads; ++i)
            threads[i] = std::thread(threadMain);
        for (size_t i = 0; i < nthreads; ++i)
            threads[i].join();
        assert(my_atomic_ulong.load() == nthreads * niters);
        assert(my_atomic_ulong == my_atomic_ulong.load());
        std::cout << "my_non_atomic_ulong " << my_non_atomic_ulong << std::endl;
        assert(my_arch_atomic_ulong == nthreads * niters);
        std::cout << "my_arch_non_atomic_ulong " << my_arch_non_atomic_ulong << std::endl;
    }
    

    在 Ubuntu 19.04 amd64 上编译并运行:

    g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp -pthread
    ./main.out 2 10000
    

    可能输出结果:

    my_non_atomic_ulong 15264
    my_arch_non_atomic_ulong 15267
    

    从输出结果可以看出,加了 “lock” 前缀后,增加操作是原子性的:否则,我们会在许多加法操作上出现竞争条件,最终计数结果会小于预期的 20000。

    “lock” 前缀被广泛用于实现 C++11 中的 std::atomic 和 C11 中的 atomic_int,确保线程安全的增量和其他修改操作。

  • 相关阅读:
    Vue3 引入使用 vant组件详解
    ESP8266-Arduino编程实例-SHT21温度湿度传感器驱动
    深入理解Java消息中间件-RabbitMQ
    贴地气的安卓UI自动化工具4399AT全面更新了~
    postgresql Window Functions
    移动攻防-检测非标准调用和如何防止反射
    css基本样式之背景样式
    Java - JDBC批量插入原理
    virtualbox 菜单栏控制
    迅为IMX6开发板QT系统创建AP热点基于RTL8723交叉编译hostapd
  • 原文地址:https://blog.csdn.net/xxzhaoming/article/details/139871392