代码之家  ›  专栏  ›  技术社区  ›  Tony Lin

高级编程语言中的线程如何转换成CPU可读的内容?[副本]

  •  0
  • Tony Lin  · 技术社区  · 6 年前

    对于拥有4核(甚至更多)的现代CPU,在机器代码级别,它是否只是看起来有4个独立的CPU(即,是否只有4个不同的“EDX”寄存器)?如果是这样,当你说“增加EDX寄存器”时,是什么决定了哪个CPU的EDX寄存器是递增的?现在x86汇编程序中有“CPU上下文”或“线程”的概念吗?

    核心之间的通信/同步是如何工作的?

    如果您正在为多核CPU编写一个优化编译器/字节码VM,那么您需要特别了解什么,比如说x86,才能使它生成在所有核心上高效运行的代码?

    0 回复  |  直到 7 年前
        1
  •  142
  •   Community CDub    8 年前

    这不是对问题的直接回答,而是对出现在评论中的问题的回答。本质上,问题是硬件对多线程操作提供了什么支持。

    Nicholas Flynt had it right ,至少关于x86。在多线程环境(超线程、多核或多处理器)中 引导线程 0xfffffff0 . 所有其他线程都以一种称为 等待SIPI . 作为初始化的一部分,主线程通过名为SIPI(Startup IPI)的APIC向WFS中的每个线程发送一个特殊的处理器间中断(IPI)。SIPI包含该线程开始获取代码的地址。

    此机制允许每个线程从不同的地址执行代码。所需要的只是软件支持每个线程设置自己的表和消息队列。操作系统使用 那些

    就实际的程序集而言,正如Nicholas所写,单线程或多线程应用程序的程序集之间没有区别。每个逻辑线程都有自己的寄存器集,因此写入:

    mov edx, 0
    

    只会更新 EDX 对于 . 没办法修改 EDX公司 EDX公司 .

        2
  •  75
  •   Ciro Santilli OurBigBook.com    5 年前

    Intel x86最小可运行裸机示例

    Runnable bare metal example with all required boilerplate . 所有主要部分如下。

    在Ubuntu15.10QEMU2.3.0和Lenovo ThinkPad T400上测试 real hardware guest

    这个 Intel Manual Volume 3 System Programming Guide - 325384-056US September 2015

    MOV ESI, ICR_LOW    ; Load address of ICR low dword into ESI.
    MOV EAX, 000C4500H  ; Load ICR encoding for broadcast INIT IPI
                        ; to all APs into EAX.
    MOV [ESI], EAX      ; Broadcast INIT IPI to all APs
    ; 10-millisecond delay loop.
    MOV EAX, 000C46XXH  ; Load ICR encoding for broadcast SIPI IP
                        ; to all APs into EAX, where xx is the vector computed in step 10.
    MOV [ESI], EAX      ; Broadcast SIPI IPI to all APs
    ; 200-microsecond delay loop
    MOV [ESI], EAX      ; Broadcast second SIPI IPI to all APs
                        ; Waits for the timer interrupt until the timer expires
    

    关于那个密码:

    1. 大多数操作系统都会使环3(用户程序)中的大部分操作变得不可能。

      因此,您需要编写自己的内核来自由地使用它:userland Linux程序将无法工作。

    2. 首先,一个单独的处理器运行,称为引导处理器(BSP)。

      它必须通过称为 Inter Processor Interrupts (IPI)

      这些中断可以通过通过中断命令寄存器(ICR)编程高级可编程中断控制器(APIC)来实现

      ICR的格式记录在:10.6“发出处理器间中断”

    3. ICR_LOW在8.4.4“MP初始化示例”中定义为:

      ICR_LOW EQU 0FEE00300H
      

      神奇的价值 0FEE00300 是ICR的内存地址,如表10-1“本地APIC寄存器地址映射”所示

    4. 但也有可能, and recommended by some ,通过BIOS设置的特殊数据结构获取有关处理器的信息,如 ACPI tables or Intel's MP configuration table 把你需要的人一个一个地叫醒。

    5. XX 000C46XXH 将处理器将执行的第一条指令的地址编码为:

      CS = XX * 0x100
      IP = 0
      

      记住这一点 CS multiples addresses by 0x10 ,因此第一条指令的实际内存地址是:

      XX * 0x1000
      

      比如说如果 XX == 1 ,处理器将在 0x1000

      然后,我们必须确保在该内存位置运行16位实模式代码,例如:

      cld
      mov $init_len, %ecx
      mov $init, %esi
      mov 0x1000, %edi
      rep movsb
      
      .code16
      init:
          xor %ax, %ax
          mov %ax, %ds
          /* Do stuff. */
          hlt
      .equ init_len, . - init
      

      使用链接器脚本是另一种可能。

    6. 延迟循环是一个烦人的工作部分:没有超简单的方法来做这样的睡眠精确。

      可能的方法包括:

      • PIT(在我的示例中使用)
      • 用上面的方法校准繁忙循环的时间,并用它代替

      How to display a number on the screen and and sleep for one second with DOS x86 assembly?

    7. 我认为初始处理器需要处于保护模式才能在写入地址时工作 0FEE00300H

    8. 为了在处理器之间进行通信,我们可以在主进程上使用自旋锁,并从第二个核心修改锁。

      我们应该确保完成内存写回,例如通过 wbinvd

    处理器之间的共享状态

    8.7.1“逻辑处理器的状态”表示:

    支持英特尔超线程技术。这些功能可细分为三组:

    • 为每个逻辑处理器复制
    • 由物理处理器中的逻辑处理器共享

    每个逻辑处理器都具有以下功能:

    • 段寄存器(CS、DS、SS、ES、FS和GS)
    • EFLAGS和EIP寄存器。注意,每个逻辑处理器的CS和EIP/RIP寄存器指向
    • x87 FPU寄存器(ST0到ST7、状态字、控制字、标记字、数据操作数指针和指令 指针)
    • XMM寄存器(XMM0到XMM7)和mxsr寄存器
    • 调试寄存器(DR0、DR1、DR2、DR3、DR6、DR7)和调试控制MSR
    • 机器检查全局状态(IA32_MCG_status)和机器检查能力(IA32_MCG_CAP)MSRs
    • 时间戳计数器
    • 大多数其他MSR寄存器,包括页属性表(PAT)。见以下例外情况。
    • 附加通用寄存器(R8-R15)、XMM寄存器(XMM8-XMM15)、控制寄存器、IA32

    逻辑处理器共享以下功能:

    以下功能是共享的还是复制的是特定于实现的:

    • IA32_MISC_启用MSR(MSR地址10h)
    • 机器检查架构(MCA)MSRs(IA32_MCG_状态和IA32_MCG_CAP MSRs除外)
    • 性能监控控制与计数器

    Intel超线程比单独的内核具有更大的缓存和管道共享: https://superuser.com/questions/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858

    Linux内核4.2

    主初始化操作似乎位于 arch/x86/kernel/smpboot.c

    ARM最小可运行裸金属示例

    在这里,我为QEMU提供了一个最小的可运行ARMv8 aarch64示例:

    .global mystart
    mystart:
        /* Reset spinlock. */
        mov x0, #0
        ldr x1, =spinlock
        str x0, [x1]
    
        /* Read cpu id into x1.
         * TODO: cores beyond 4th?
         * Mnemonic: Main Processor ID Register
         */
        mrs x1, mpidr_el1
        ands x1, x1, 3
        beq cpu0_only
    cpu1_only:
        /* Only CPU 1 reaches this point and sets the spinlock. */
        mov x0, 1
        ldr x1, =spinlock
        str x0, [x1]
        /* Ensure that CPU 0 sees the write right now.
         * Optional, but could save some useless CPU 1 loops.
         */
        dmb sy
        /* Wake up CPU 0 if it is sleeping on wfe.
         * Optional, but could save power on a real system.
         */
        sev
    cpu1_sleep_forever:
        /* Hint CPU 1 to enter low power mode.
         * Optional, but could save power on a real system.
         */
        wfe
        b cpu1_sleep_forever
    cpu0_only:
        /* Only CPU 0 reaches this point. */
    
        /* Wake up CPU 1 from initial sleep!
         * See:https://github.com/cirosantilli/linux-kernel-module-cheat#psci
         */
        /* PCSI function identifier: CPU_ON. */
        ldr w0, =0xc4000003
        /* Argument 1: target_cpu */
        mov x1, 1
        /* Argument 2: entry_point_address */
        ldr x2, =cpu1_only
        /* Argument 3: context_id */
        mov x3, 0
        /* Unused hvc args: the Linux kernel zeroes them,
         * but I don't think it is required.
         */
        hvc 0
    
    spinlock_start:
        ldr x0, spinlock
        /* Hint CPU 0 to enter low power mode. */
        wfe
        cbz x0, spinlock_start
    
        /* Semihost exit. */
        mov x1, 0x26
        movk x1, 2, lsl 16
        str x1, [sp, 0]
        mov x0, 0
        str x0, [sp, 8]
        mov x1, sp
        mov w0, 0x18
        hlt 0xf000
    
    spinlock:
        .skip 8
    

    GitHub upstream .

    aarch64-linux-gnu-gcc \
      -mcpu=cortex-a57 \
      -nostdlib \
      -nostartfiles \
      -Wl,--section-start=.text=0x40000000 \
      -Wl,-N \
      -o aarch64.elf \
      -T link.ld \
      aarch64.S \
    ;
    qemu-system-aarch64 \
      -machine virt \
      -cpu cortex-a57 \
      -d in_asm \
      -kernel aarch64.elf \
      -nographic \
      -semihosting \
      -smp 2 \
    ;
    

    在这个例子中,我们将CPU 0放在一个spinlock循环中,它只在CPU 1释放spinlock时退出。

    在自旋锁之后,CPU 0然后执行 semihost exit call 这让QEMU辞职了。

    -smp 1 ,然后模拟就永远挂在自旋锁上。

    ARM: Start/Wakeup/Bringup the other CPU cores/APs and pass execution start address?

    这个 upstream version 也有一些调整,使其在gem5上工作,所以您也可以试验性能特性。

    本文档提供了一些关于使用ARM同步原语的指导,然后您可以使用这些原语在多个核心上做有趣的事情: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf

    在Ubuntu 18.10,GCC 8.2.0,Binutils 2.31.1,QEMU 2.12.0上测试。

    前面的例子唤醒了辅助CPU,并使用专用指令执行基本的内存同步,这是一个很好的开始。

    POSIX pthreads

    • 安装程序中断并运行一个计时器,该计时器周期性地决定现在运行哪个线程。这被称为 preemptive multithreading

      这样的系统还需要在启动和停止时保存和恢复线程寄存器。

      也可以使用非抢占式多任务系统,但这些系统可能需要修改代码,以便每个线程都能产生(例如 pthread_yield

      以下是一些简单的裸机计时器示例:

    • 处理内存冲突。值得注意的是,每个线程都需要 unique stack 如果你想用C或其他高级语言编写代码。

      您可以将线程限制为固定的最大堆栈大小,但处理此问题的更好方法是 paging 它允许有效的“无限大小”堆栈。

      a naive aarch64 baremetal example that would blow up if the stack grows too deep

    这些是使用Linux内核或其他操作系统的一些好理由:-)

    Userland内存同步原语

    尽管线程启动/停止/管理通常超出了userland的范围,但是您可以使用userland线程的汇编指令来同步内存访问,而无需潜在的更昂贵的系统调用。

    <mutex> <atomic> 标题,尤其是 std::memory_order

    更微妙的语义在 lock free data structures ,在某些情况下可以提供性能优势。要实现这些,您可能需要了解不同类型的内存障碍: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/

    https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html

    这样的userland指令似乎也用于实现Linux futex 系统调用,它是Linux中的主要同步原语之一。 man futex 4.15改为:

    futex()系统调用提供了一个方法,用于等待某个条件变为真。它通常用作 共享内存同步上下文中的阻塞构造。当使用futex时,大多数的同步 操作在用户空间中执行。用户空间程序只有在 等待特定条件的进程或线程。

    系统调用名称本身意味着“快速用户空间XXX”。

    这里是一个最小的无用的C++ X86Y64/AARCH64示例,内联汇编说明了这些指令的基本用法,主要用于娱乐:

    #include <atomic>
    #include <cassert>
    #include <iostream>
    #include <thread>
    #include <vector>
    
    std::atomic_ulong my_atomic_ulong(0);
    unsigned long my_non_atomic_ulong = 0;
    #if defined(__x86_64__) || defined(__aarch64__)
    unsigned long my_arch_atomic_ulong = 0;
    unsigned long my_arch_non_atomic_ulong = 0;
    #endif
    size_t niters;
    
    void threadMain() {
        for (size_t i = 0; i < niters; ++i) {
            my_atomic_ulong++;
            my_non_atomic_ulong++;
    #if defined(__x86_64__)
            __asm__ __volatile__ (
                "incq %0;"
                : "+m" (my_arch_non_atomic_ulong)
                :
                :
            );
            // https://github.com/cirosantilli/linux-kernel-module-cheat#x86-lock-prefix
            __asm__ __volatile__ (
                "lock;"
                "incq %0;"
                : "+m" (my_arch_atomic_ulong)
                :
                :
            );
    #elif defined(__aarch64__)
            __asm__ __volatile__ (
                "add %0, %0, 1;"
                : "+r" (my_arch_non_atomic_ulong)
                :
                :
            );
            // https://github.com/cirosantilli/linux-kernel-module-cheat#arm-lse
            __asm__ __volatile__ (
                "ldadd %[inc], xzr, [%[addr]];"
                : "=m" (my_arch_atomic_ulong)
                : [inc] "r" (1),
                  [addr] "r" (&my_arch_atomic_ulong)
                :
            );
    #endif
        }
    }
    
    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);
        // We can also use the atomics direclty through `operator T` conversion.
        assert(my_atomic_ulong == my_atomic_ulong.load());
        std::cout << "my_non_atomic_ulong " << my_non_atomic_ulong << std::endl;
    #if defined(__x86_64__) || defined(__aarch64__)
        assert(my_arch_atomic_ulong == nthreads * niters);
        std::cout << "my_arch_non_atomic_ulong " << my_arch_non_atomic_ulong << std::endl;
    #endif
    }
    
    

    GitHub upstream .

    my_non_atomic_ulong 15264
    my_arch_non_atomic_ulong 15267
    

    由此我们可以看到x86锁前缀/aarch64 LDADD 指令使加法成为原子的:没有它,我们对许多加法都有竞争条件,最后的总数小于同步的20000。

    在Ubuntu19.04AMD64和QEMUaarch64用户模式下测试。

        3
  •  42
  •   Blank    16 年前

    据我所知,每个“核心”都是一个完整的处理器,有自己的寄存器集。基本上,BIOS在一个内核运行时启动,然后操作系统可以通过初始化其他内核并将它们指向要运行的代码等方式“启动”其他内核。

    同步是由操作系统完成的。通常,每个处理器为操作系统运行不同的进程,因此操作系统的多线程功能负责决定哪个进程可以接触哪个内存,以及在内存冲突的情况下应该做什么。

        4
  •  38
  •   DigitalRoss    9 年前

    stack overflow logo


    例如,以前,要编写x86汇编程序,您可能会有指令指出“使用值5加载EDX寄存器”、“增加EDX寄存器”等。对于具有4个内核(甚至更多)的现代CPU,在机器代码级别,它看起来像是有4个独立的CPU(即,是否只有4个不同的“EDX”寄存器)?

    确切地。有4组寄存器,包括4个独立的指令指针。

    自然地,执行该指令的CPU。把它想象成4个完全不同的微处理器,它们只是共享同一个内存。

    现在x86汇编程序中有“CPU上下文”或“线程”的概念吗?

    核心之间的通信/同步是如何工作的?

    由于它们共享相同的内存,这主要是程序逻辑的问题。尽管现在 inter-processor interrupt

    如果您正在编写一个操作系统,那么通过硬件公开什么机制允许您在不同的内核上调度执行?

    调度程序实际上并没有改变,只是它对关键部分和所使用的锁的类型稍微谨慎一些。在SMP之前,内核代码最终会调用调度器,调度器将查看运行队列并选择一个进程作为下一个线程运行。(内核的进程看起来很像线程)SMP内核运行完全相同的代码,一次运行一个线程,只是现在关键的部分锁定需要SMP安全,以确保两个内核不会意外地选择相同的PID。

    它是特殊特权指令吗?

    不。内核只是运行在同一个内存中,使用相同的旧指令。

    您运行的代码与以前相同。需要更改的是Unix或Windows内核。

    您可以将我的问题概括为“对x86机器代码进行了哪些更改以支持多核功能?”

    没什么必要。第一个SMP系统使用与单处理器完全相同的指令集。现在,已经有了大量的x86架构的发展和无数的新指令来加快速度,但是没有一个是 必要的

    有关详细信息,请参见 Intel Multiprocessor Specification


    更新: n个 -多核CPU几乎 1个 n个 2个 如何编写程序以在多个内核上运行以获得更高的性能? 答案是:它是使用类似于 Pthreads. 有些线程库使用操作系统看不到的“绿色线程”,这些线程库不会获得单独的内核,但是只要线程库使用内核线程功能,那么您的线程程序将自动成为多核程序。

    2。当然,它们也共享所有的外围设备。
        5
  •  10
  •   Alex Brown    16 年前

    如果你在写一个优化 CPU,你需要知道什么 它生成高效运行的代码 所有的核心?

    您不需要知道任何关于x86的具体信息,就可以使它生成在所有核心上高效运行的代码。

    正确地

    您可能需要了解一些关于x86的知识,以使它生成在x86上高效运行的代码。

    你还可以学习其他一些东西:

    您应该考虑编译器是否应该自动并行,或者编译器编译的应用程序的作者是否需要在其程序中添加特殊语法或API调用以利用多个内核。

        6
  •  9
  •   Gerhard    16 年前

    每个核心从不同的内存区域执行。你的操作系统将在你的程序中指向一个核心,核心将执行你的程序。您的程序将不知道有多个内核或它正在哪个内核上执行。

    也没有仅对操作系统可用的附加指令。这些核与单核芯片相同。每个核心运行操作系统的一部分,该部分将处理与用于信息交换的公共内存区域的通信,以找到下一个要执行的内存区域。

    More about multicores and multiprocessors 在Embedded.com上有很多关于这个话题的信息。。。这个话题很快就复杂了!

        7
  •  5
  •   sharptooth    16 年前

        8
  •  3
  •   pjc50    15 年前

    这根本不是在机器指令中完成的;核心假装是不同的CPU,并且没有任何特殊的能力相互交谈。他们有两种交流方式:

    • 它们共享物理地址空间。硬件处理缓存一致性,因此一个CPU写入另一个CPU读取的内存地址。

    http://www.cheesecake.org/sac/smp.html 是一个很好的参考与愚蠢的网址。

        9
  •  1
  •   Olof Forshell    14 年前

    单线程应用程序和多线程应用程序之间的主要区别在于前者有一个堆栈,后者每个线程有一个堆栈。由于编译器将假定数据和堆栈段寄存器(ds和ss)不相等,因此生成的代码有些不同。这意味着,通过默认为ss寄存器的ebp和esp寄存器的间接寻址也不会默认为ds(因为ds!=不锈钢)。相反,通过默认为ds的其他寄存器的间接寻址不会默认为ss。

    其他多线程代码可能涉及在程序的不同部分运行的不同线程。这种类型的编程不需要同样的调整,因此更容易学习。

        10
  •  0
  •   jakobengblom2    16 年前

    与之前的单处理器变体相比,在每个支持多处理的体系结构中添加的是内核之间同步的指令。此外,您还有处理缓存一致性、刷新缓冲区和操作系统必须处理的类似低级操作的说明。在同步多线程体系结构(如IBM POWER6、IBM Cell、Sun Niagara和Intel“超线程”)的情况下,您还倾向于看到在线程之间设置优先级的新指令(如设置优先级和在无事可做时显式生成处理器)。

    但是基本的单线程语义是相同的,您只需添加额外的工具来处理与其他内核的同步和通信。

    推荐文章