멀티 코어 어셈블리 언어는 어떻게 생겼습니까?


243

예를 들어 x86 어셈블러를 작성하려면 "EDX 레지스터를 값 5로로드", "EDX 증가"레지스터 등을 지시하는 지침이 있습니다.

4 개의 코어 (또는 그 이상)가있는 최신 CPU를 사용하면 머신 코드 수준에서 4 개의 개별 CPU가있는 것처럼 보입니까 (즉, 4 개의 "EDX"레지스터 만 있음)? 그렇다면 "EDX 레지스터 증가"라고 말할 때 어떤 CPU의 EDX 레지스터가 증가하는지 결정하는 것은 무엇입니까? x86 어셈블러에 "CPU 컨텍스트"또는 "스레드"개념이 있습니까?

코어 간의 통신 / 동기화는 어떻게 작동합니까?

운영 체제를 작성하는 경우 하드웨어를 통해 어떤 메커니즘이 노출되어 다른 코어에서 실행을 예약 할 수 있습니까? 특별한 특권이 있습니까?

멀티 코어 CPU에 최적화 된 컴파일러 / 바이트 코드 VM을 작성하는 경우 모든 코어에서 효율적으로 실행되는 코드를 생성하려면 x86에 대해 구체적으로 알아야 할 사항은 무엇입니까?

멀티 코어 기능을 지원하기 위해 x86 기계 코드가 어떻게 변경 되었습니까?


2
비슷한 (하지만 동일하지) 질문은 여기있다 : stackoverflow.com/questions/714905/...은
나단 Fellman

답변:


153

이것은 질문에 대한 직접적인 답변은 아니지만 의견에 나타나는 질문에 대한 답변입니다. 본질적으로 문제는 하드웨어가 멀티 스레드 작업을 지원하는 것입니다.

니콜라스 플라이트는 적어도 x86에 대해서는 옳았다 . 다중 스레드 환경 (하이퍼 스레딩, 다중 코어 또는 다중 프로세서)에서 부트 스트랩 스레드 (일반적으로 프로세서 0의 코어 0의 스레드 0)는 address에서 코드 페치를 시작합니다 0xfffffff0. 다른 모든 스레드는 Wait-for-SIPI 라는 특수 절전 상태에서 시작합니다 . 기본 스레드는 초기화의 일부로 SIPI (Startup IPI)라는 APIC를 통해 WFS에있는 각 스레드에 특수한 프로세서 간 인터럽트 (IPI)를 보냅니다. SIPI에는 해당 스레드가 코드 페치를 시작해야하는 주소가 포함되어 있습니다.

이 메커니즘을 통해 각 스레드는 다른 주소에서 코드를 실행할 수 있습니다. 필요한 것은 각 스레드가 자체 테이블과 메시징 대기열을 설정하기위한 소프트웨어 지원입니다. OS는 를 사용 하여 실제 다중 스레드 스케줄링을 수행합니다.

실제 어셈블리에 관한 한, 니콜라스가 쓴 것처럼 단일 스레드 또는 다중 스레드 응용 프로그램의 어셈블리에는 차이가 없습니다. 각 논리 스레드에는 자체 레지스터 세트가 있으므로 다음과 같이 작성하십시오.

mov edx, 0

현재 실행중인 스레드에 대해서만 업데이트 EDX됩니다 . 단일 어셈블리 명령어를 사용하여 다른 프로세서 를 수정할 수있는 방법이 없습니다 . OS에 다른 스레드가 자체적으로 업데이트 할 코드를 실행하도록 지시하려면 일종의 시스템 호출이 필요합니다 .EDXEDX


2
Nicholas의 답변에 공백을 채워 주셔서 감사합니다. 귀하의 정보를 지금 허용 된 답변으로 표시했습니다. ... 관심있는 구체적인 세부 정보를 제공합니다 ... 귀하의 정보와 니콜라스가 모두 결합 된 단일 답변이 있으면 더 좋을 것입니다.
Paul Hollingsworth

3
스레드의 출처에 대한 질문에는 대답하지 않습니다. 코어와 프로세서는 하드웨어적인 것이지만 소프트웨어에서 스레드를 만들어야합니다. 기본 스레드는 SIPI를 보낼 위치를 어떻게 알 수 있습니까? 아니면 SIPI 자체가 새 스레드를 작성합니까?
rich remer

7
@ richremer : HW 스레드와 SW 스레드를 혼란스럽게하는 것 같습니다. HW 스레드는 항상 존재합니다. 때로는 잠이 듭니다. SIPI 자체는 HW 스레드를 활성화하고 SW를 실행할 수 있도록합니다. 어떤 HW 스레드를 실행할지, 각 HW 스레드에서 실행할 프로세스 및 SW 스레드를 결정하는 것은 OS 및 BIOS에 달려 있습니다.
Nathan Fellman

2
여기에 좋고 간결한 정보가 많이 있지만 이것은 큰 주제이므로 질문이 남아있을 수 있습니다. USB 드라이브 나 "플로피"디스크에서 부팅하는 완전한 "베어 본 (bare bone)"커널의 몇 가지 예가 있습니다. 실제로 멀티 스레드 C 코드 ( github)를 실행할 수있는 이전 TSS 디스크립터를 사용하여 어셈블러로 작성된 x86_32 버전이 있습니다 . com / duanev / oz-x86-32-asm-003 )이지만 표준 라이브러리 지원은 없습니다. 당신이 요청한 것보다 훨씬 더 많은 것이지만 그것은 아마도 남아있는 그 질문들에 대답 할 수 있습니다.
duanev

87

Intel x86 최소 실행 가능 베어 메탈 예

모든 필수 상용구가있는 실행 가능한 베어 메탈 예 . 모든 주요 부품은 아래에 설명되어 있습니다.

Ubuntu 15.10 QEMU 2.3.0 및 Lenovo ThinkPad T400 실제 하드웨어 게스트 에서 테스트되었습니다 .

325384-056US 2015년 9월 - 인텔 설명서 제 3 권 시스템 프로그래밍 가이드 장 8, 9, 10 커버 SMP.

표 8-1. "브로드 캐스트 INIT-SIPI-SIPI 시퀀스 및 시간 초과 선택"에는 기본적으로 작동하는 예제가 포함되어 있습니다.

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)라고하는 단일 프로세서가 실행됩니다.

    IPI (Inter Processor Interrupts) 라는 특수한 인터럽트를 통해 다른 것 (애플리케이션 프로세서 (AP))을 깨워 야합니다 .

    인터럽트는 IRC (Interrupt Command Register)를 통해 APIC (Advanced Programmable Interrupt Controller)를 프로그래밍하여 수행 할 수 있습니다.

    ICR의 형식은 다음과 같습니다. 10.6 "ISSUING INTERPROCESSOR INTERRUPTS"

    IPI는 ICR에 쓰는 즉시 발생합니다.

  3. ICR_LOW는 8.4.4 "MP 초기화 예"에서 다음과 같이 정의됩니다.

    ICR_LOW EQU 0FEE00300H
    

    매직 값 0FEE00300은 표 10-1 "로컬 APIC 레지스터 주소 맵"에 설명 된대로 ICR의 메모리 주소입니다.

  4. 예제에서 가장 간단한 방법이 사용됩니다. 현재 IPC를 제외한 다른 모든 프로세서로 전달되는 브로드 캐스트 IPI를 보내도록 ICR을 설정합니다.

    그러나 그것은 또한 가능하며, 일부에서 권장하는 등의 BIOS에서 특별한 데이터 구조 설정을 통해 프로세서에 대한 정보를 얻기 위해, ACPI 테이블이나 인텔의 MP 구성 테이블 만이 하나씩 필요로하는 사람을 깨워.

  5. XXin 000C46XXH은 프로세서가 실행할 첫 번째 명령어의 주소를 다음과 같이 인코딩합니다.

    CS = XX * 0x100
    IP = 0
    

    그 기억 에 의해 CS 배수 주소를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 (내 예제에서 사용)
    • HPET
    • 위의 방법으로 통화 중 루프 시간을 교정하고 대신 사용하십시오.

    관련 : 화면에 숫자를 표시하고 DOS x86 어셈블리로 1 초 동안 잠자기하는 방법?

  7. 0FEE00300H16 비트에 비해 너무 높은 주소에 쓸 때 초기 프로세서가 보호 모드에 있어야 작동합니다.

  8. 프로세서 간 통신을 위해 기본 프로세스에서 스핀 락을 사용하고 두 번째 코어에서 잠금을 수정할 수 있습니다.

    메모리 쓰기가 예를 들어 통해 이루어 지도록해야합니다 wbinvd.

프로세서 간 공유 상태

8.7.1 "논리 프로세서의 상태"는 다음과 같이 말합니다.

다음 기능은 Intel 하이퍼 스레딩 기술을 지원하는 Intel 64 또는 IA-32 프로세서 내 논리 프로세서의 아키텍처 상태의 일부입니다. 기능은 세 그룹으로 세분 될 수 있습니다.

  • 각 논리 프로세서마다 중복
  • 실제 프로세서에서 논리 프로세서가 공유
  • 구현에 따라 공유 또는 복제

각 논리 프로세서에 대해 다음 기능이 복제됩니다.

  • 범용 레지스터 (EAX, EBX, ECX, EDX, ESI, EDI, ESP 및 EBP)
  • 세그먼트 레지스터 (CS, DS, SS, ES, FS 및 GS)
  • EFLAGS 및 EIP 레지스터. 각 논리 프로세서에 대한 CS 및 EIP / RIP 레지스터는 논리 프로세서에 의해 실행되는 스레드에 대한 명령 스트림을 가리 킵니다.
  • x87 FPU 레지스터 (ST0 ~ ST7, 상태 워드, 제어 워드, 태그 워드, 데이터 피연산자 포인터 및 명령어 포인터)
  • MMX 레지스터 (MM0 ~ MM7)
  • XMM 레지스터 (XMM0 ~ XMM7) 및 MXCSR 레지스터
  • 제어 레지스터 및 시스템 테이블 포인터 레지스터 (GDTR, LDTR, IDTR, 작업 레지스터)
  • 디버그 레지스터 (DR0, DR1, DR2, DR3, DR6, DR7) 및 디버그 제어 MSR
  • 머신 체크 글로벌 상태 (IA32_MCG_STATUS) 및 머신 체크 기능 (IA32_MCG_CAP) MSR
  • 열 클록 변조 및 ACPI 전원 관리 제어 MSR
  • 타임 스탬프 카운터 MSR
  • 페이지 속성 테이블 (PAT)을 포함하여 대부분의 다른 MSR 레지스터. 아래 예외를 참조하십시오.
  • 로컬 APIC 레지스터.
  • 추가 범용 레지스터 (R8-R15), XMM 레지스터 (XMM8-XMM15), 제어 레지스터, Intel 64 프로세서의 IA32_EFER

논리 프로세서는 다음 기능을 공유합니다.

  • 메모리 유형 범위 레지스터 (MTRR)

다음 기능이 공유되는지 또는 복제되는지는 구현별로 다릅니다.

  • IA32_MISC_ENABLE MSR (MSR 주소 1A0H)
  • MCA (Machine Check Architecture) MSR (IA32_MCG_STATUS 및 IA32_MCG_CAP MSR 제외)
  • 성능 모니터링 제어 및 카운터 MSR

캐시 공유는 다음에서 논의됩니다.

인텔 하이퍼 스레드가 별도의 코어보다 캐시와 파이프 라인을 공유 할 수 있습니다 /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858

리눅스 커널 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의 상류 .

조립 및 실행 :

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을 스핀 록 루프에 넣고 CPU 1 만 스핀 락을 해제 한 상태에서 종료합니다.

스핀 록 후 CPU 0 은 QEMU를 종료 시키는 세미 호스트 종료 호출 을 수행합니다.

를 사용하여 하나의 CPU로 QEMU를 시작하면 -smp 1시뮬레이션이 스핀 락에서 영원히 중단됩니다.

CPU 1은 PSCI 인터페이스로 깨어났습니다. ARM : 다른 CPU 코어 / AP 시작 / 깨우기 / 연결 및 실행 시작 주소 전달?

또한 업스트림 버전 에는 gem5에서 작동하도록 약간의 조정이있어 성능 특성도 실험 할 수 있습니다.

실제 하드웨어에서 테스트하지 않았기 때문에 이것이 얼마나 휴대 가능한지 잘 모르겠습니다. 다음 Raspberry Pi 참고 문헌이 관심을 가질 수 있습니다.

이 문서는 다음 멀티 코어와 재미있는 일을하는 데 사용할 수있는 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하려면 다음과 같은 관련 주제로 넘어 가야합니다.

  • setup은 인터럽트를 수행하고 타이머를 실행하여 현재 실행할 스레드를 주기적으로 결정합니다. 이것을 선점 형 멀티 스레딩이라고 합니다.

    또한 이러한 시스템은 스레드 레지스터가 시작 및 중지 될 때 저장 및 복원해야합니다.

    비선 점형 멀티 태스킹 시스템을 사용하는 것도 가능하지만 모든 스레드가 (예 : pthread_yield구현으로) 생성되도록 워크로드의 균형을 맞추기가 더 어려워 지도록 코드를 수정해야 할 수도 있습니다 .

    간단한 베어 메탈 타이머 예제는 다음과 같습니다.

  • 메모리 충돌을 다룹니다. 특히 C 또는 다른 고급 언어로 코딩하려면 각 스레드마다 고유 스택 이 필요합니다 .

    스레드가 고정 된 최대 스택 크기를 갖도록 제한 할 수 있지만이를 처리하는 더 좋은 방법 은 효율적인 "무제한 크기"스택을 허용 하는 페이징 입니다.

    다음은 스택이 너무 깊이 커질 경우 날려 버릴 것이라고 순진 aarch64의 baremetal 예제

Linux 커널이나 다른 운영 체제를 사용해야하는 좋은 이유가 있습니다. :-)

유저 랜드 메모리 동기화 프리미티브

스레드 시작 / 중지 / 관리는 일반적으로 사용자 범위를 벗어나지 만 사용자 랜드 스레드의 어셈블리 명령어를 사용하여 잠재적으로 더 비싼 시스템 호출없이 메모리 액세스를 동기화 할 수 있습니다.

물론 이러한 저수준 프리미티브를 이식 할 수있는 라이브러리를 사용하는 것이 좋습니다. C ++ 표준 자체는 <mutex><atomic>헤더, 특히 로 크게 발전했습니다 std::memory_order. 가능한 모든 메모리 의미를 포함하는지 확실하지 않지만 단지 가능할 수 있습니다.

보다 미묘한 의미론은 잠금없는 데이터 구조 와 관련하여 특히 관련이 있으며 , 이는 특정 경우 성능 이점을 제공 할 수 있습니다. 이를 구현하려면 다양한 유형의 메모리 장벽에 대해 약간 배워야 할 것입니다. https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/

예를 들어 Boost는 https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html에 잠금 해제 컨테이너 구현이 있습니다.

이러한 사용자 지침은 Linux futex의 주요 동기화 기본 요소 중 하나 인 Linux 시스템 호출 을 구현하는 데에도 사용됩니다 . man futex4.15는 다음과 같이 읽습니다.

futex () 시스템 호출은 특정 조건이 충족 될 때까지 기다리는 메소드를 제공합니다. 일반적으로 공유 메모리 동기화 컨텍스트에서 블로킹 구성으로 사용됩니다. futex를 사용할 때 대부분의 동기화 작업은 사용자 공간에서 수행됩니다. 사용자 공간 프로그램은 futex () 시스템 호출을 사용합니다. 조건이 true가 될 때까지 프로그램이 더 오랜 시간 동안 차단해야 할 경우입니다. 다른 futex () 연산을 사용하여 특정 조건을 기다리는 프로세스 또는 스레드를 깨울 수 있습니다.

syscall 이름 자체는 "Fast Userspace XXX"를 의미합니다.

다음은 대부분 재미있게 이러한 명령어의 기본 사용법을 보여주는 인라인 어셈블리가있는 최소한의 쓸모없는 C ++ x86_64 / aarch64 예제입니다.

main.cpp

#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의 상류 .

가능한 출력 :

my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267

이것으로부터 우리는 x86 LOCK prefix / aarch64 LDADD명령이 추가를 원자화했다는 것을 알았 습니다.이를 사용하지 않으면 많은 추가에 대한 경쟁 조건이 있으며 끝의 총 개수는 동기화 된 20000보다 적습니다.

또한보십시오:

Ubuntu 19.04 amd64 및 QEMU aarch64 사용자 모드에서 테스트되었습니다.


예제를 컴파일하기 위해 어떤 어셈블러를 사용합니까? GAS는 당신 #include의 의견 을 좋아하지 않는 것 같습니다 (의견으로 생각합니다), NASM, FASM, YASM은 AT & T 구문을 모르기 때문에 그럴 수는 없습니다.
Ruslan

@Ruslan gcc, #includeC 전처리에서 온다. 사용하여 Makefile: '시작'장에 설명 된대로 제공 github.com/cirosantilli/x86-bare-metal-examples/blob/... 이는 GitHub의 문제를 공개 할 작업을하지 않습니다.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

x86에서 코어가 큐에서 실행할 준비가 더 이상 없다는 것을 인식하면 어떻게됩니까? (유휴 시스템에서 때때로 발생할 수 있음). 새로운 작업이있을 때까지 공유 메모리 구조에 코어 스핀 록이 있습니까? (아마도 좋지는 않지만 많은 전력을 소비 할 것입니다) 인터럽트가있을 때까지 HLT와 같은 것을 절전 모드로 호출합니까? (이 경우 누가 그 핵심을 깨울 책임이 있습니까?)
tigrou

@tigrou 확실하지 않지만, 리눅스 구현이 다음 번 (타이머와 같은) 인터럽트까지, 특히 전원이 핵심 인 ARM에서 전원 상태가 될 가능성이 큽니다. Linux를 실행하는 시뮬레이터의 명령 추적으로 구체적으로 쉽게 관찰 할 수 있는지 신속하게 확인하려고합니다. github.com/cirosantilli/linux-kernel-module-cheat/tree/…
Ciro Santilli 郝海东 冠状 病六四 事件 法轮功

1
일부 정보 (x86 / Windows에만 해당)는 여기 에서 찾을 수 있습니다 ( "유휴 스레드"참조). TL; DR : CPU에 실행 가능한 스레드가 없으면 CPU가 유휴 스레드로 디스패치됩니다. 다른 작업과 함께 등록 된 전원 관리 프로세서 유휴 루틴을 호출합니다 (CPU 공급 업체에서 제공 한 드라이버 (예 : 인텔)). 이는 전력 소비를 줄이기 위해 CPU를보다 깊은 C 상태 (예 : C0-> C3)로 전환 할 수 있습니다.
tigrou December

43

내가 알기로, 각 "코어"는 자체 레지스터 세트가있는 완전한 프로세서입니다. 기본적으로 BIOS는 하나의 코어를 실행하면서 시작한 다음 운영 체제는 다른 코어를 초기화하고 코드에서 실행 등을 수행하여 다른 코어를 "시작"할 수 있습니다.

OS에서 동기화를 수행합니다. 일반적으로 각 프로세서는 OS에 대해 서로 다른 프로세스를 실행하므로 운영 체제의 다중 스레딩 기능은 어떤 프로세스가 어떤 메모리에 닿아 야하는지, 메모리 충돌시 수행 할 작업을 결정해야합니다.


28
그렇다면 어떤 질문이 생길까요 : 운영 체제에서 어떤 지침을 사용할 수 있습니까?
Paul Hollingsworth

4
이에 대한 사전 지침이 있지만 응용 프로그램 코드가 아닌 운영 체제의 문제입니다. 응용 프로그램 코드를 멀티 스레딩하려면 "매직"을 수행하기 위해 운영 체제 기능을 호출해야합니다.
sharptooth 2016 년

2
BIOS는 일반적으로 사용 가능한 코어 수를 식별하고 요청시이 정보를 OS로 전달합니다. 다른 PC의 하드웨어 사양 (프로세서, 코어, PCI 버스, PCI 카드, 마우스, 키보드, 그래픽, ISA, PCI-E / X, 메모리 등)에 액세스 할 수 있도록 BIOS (및 하드웨어)가 준수해야하는 표준이 있습니다. OS의 관점에서 동일하게 보입니다. BIOS에서 4 개의 코어가 있다고보고하지 않으면 OS는 일반적으로 하나만 있다고 가정합니다. 실험 할 BIOS 설정이있을 수도 있습니다.
Olof Forshell

1
멋지다.하지만 베어 메탈 프로그램을 작성한다면 어떨까?
Alexander Ryan Baggett 2016 년

3
@AlexanderRyanBaggett,? 그게 뭐야? "운영체제에 맡겨라"라고 말하면 OS는 어떻게 하는가하는 문제이기 때문에 질문을 피하고 있습니다. 어떤 조립 지침을 사용합니까?
Pacerier

39

비공식 SMP FAQ 스택 오버플로 로고


예를 들어 x86 어셈블러를 작성하려면 예를 들어 "값 5로 EDX 레지스터로드", "EDX 증가"레지스터 등의 명령이 표시됩니다. 4 코어 (또는 그 이상)의 최신 CPU 사용 머신 코드 레벨에서 4 개의 개별 CPU가있는 것처럼 보입니까 (즉, 4 개의 "EDX"레지스터 만 있음)?

바로 그거죠. 4 개의 개별 명령 포인터를 포함하여 4 개의 레지스터 세트가 있습니다.

그렇다면 "EDX 레지스터 증가"라고 말할 때 어떤 CPU의 EDX 레지스터가 증가하는지 결정하는 것은 무엇입니까?

그 명령을 자연스럽게 실행 한 CPU. 단순히 동일한 메모리를 공유하는 완전히 다른 4 개의 마이크로 프로세서로 생각하십시오.

x86 어셈블러에 "CPU 컨텍스트"또는 "스레드"개념이 있습니까?

아닙니다. 어셈블러는 항상했던 것처럼 명령어를 번역합니다. 거기에 변화가 없습니다.

코어 간의 통신 / 동기화는 어떻게 작동합니까?

그것들은 동일한 메모리를 공유하기 때문에 프로그램 논리의 문제입니다. 현재 프로세서 간 인터럽트 메커니즘이 있지만 필요하지 않으며 최초의 이중 CPU x86 시스템에는 원래 없었습니다.

운영 체제를 작성하는 경우 하드웨어를 통해 어떤 메커니즘이 노출되어 다른 코어에서 실행을 예약 할 수 있습니까?

스케줄러는 실제로 중요한 섹션과 사용 된 잠금 유형에 대해 약간 더 신중하다는 점을 제외하고는 변경되지 않습니다. SMP 이전에 커널 코드는 결국 스케줄러를 호출합니다. 스케줄러는 실행 큐를보고 다음 스레드로 실행할 프로세스를 선택합니다. (커널 프로세스는 스레드와 매우 비슷해 보입니다.) SMP 커널은 한 번에 하나의 스레드와 똑같은 코드를 실행합니다. 두 코어가 실수로 선택되지 않도록하려면 중요한 섹션 잠금이 SMP 안전해야합니다. 동일한 PID.

그것은 특별한 특권 교육인가?

아니요. 코어는 모두 이전 명령과 동일한 메모리에서 모두 실행 중입니다.

멀티 코어 CPU에 최적화 된 컴파일러 / 바이트 코드 VM을 작성하는 경우 모든 코어에서 효율적으로 실행되는 코드를 생성하려면 x86에 대해 구체적으로 알아야 할 사항은 무엇입니까?

이전과 동일한 코드를 실행합니다. 변경해야하는 것은 유닉스 나 Windows 커널입니다.

내 질문을 "멀티 코어 기능을 지원하기 위해 x86 기계 코드가 어떻게 변경 되었습니까?"라고 요약 할 수 있습니다.

아무것도 필요하지 않았습니다. 첫 번째 SMP 시스템은 단일 프로세서와 동일한 명령어 세트를 사용했습니다. 이제는 x86 아키텍처의 진화와 수십억 개의 새로운 명령이있어 더 빠르게 진행할 수 있었지만 SMP 에는 필요 하지 않았습니다 .

자세한 내용은 인텔 멀티 프로세서 사양을 참조하십시오 .


업데이트 : 모든 후속 질문이 단지 완전히 것을 수락하여 답변을 얻을 수 있습니다 N 웨이 멀티 코어 CPU는 거의이다 1 과 정확히 같은 일 N 그냥 같은 메모리를 공유 별도의 프로세서. 2 중요한 질문 이 있습니다. 더 많은 성능을 위해 프로그램이 둘 이상의 코어에서 실행되도록 어떻게 작성 되었습니까? 대답은 Pthreads 와 같은 스레드 라이브러리를 사용하여 작성되었습니다 . 일부 스레드 라이브러리는 OS에 표시되지 않는 "그린 스레드"를 사용하며 별도의 코어를 얻지 못하지만 스레드 라이브러리가 커널 스레드 기능을 사용하는 경우 스레드 프로그램은 자동으로 멀티 코어가됩니다.
1. 이전 버전과의 호환성을 위해 재설정시 첫 번째 코어 만 시작하고 나머지는 실행하기 위해 몇 가지 드라이버 유형 작업을 수행해야합니다.
2. 또한 모든 주변 장치를 자연스럽게 공유합니다.


3
저는 항상 "스레드"가 멀티 코어 프로세서를 이해하기 어렵게 만드는 소프트웨어 개념이라고 생각합니다. 문제는 코드가 어떻게 코어에 "코어 2에서 실행되는 스레드를 생성 할 것"인지 알 수 있습니까? 특별한 어셈블리 코드가 있습니까?
demonguy

2
@ demonguy : 아니요, 그런 것에 대한 특별한 지시는 없습니다. 선호도 마스크 ( "이 스레드는이 논리 코어 세트에서 실행될 수 있습니다")를 설정하여 특정 코어에서 스레드를 실행하도록 OS에 요청합니다. 완전히 소프트웨어 문제입니다. 각 CPU 코어 (하드웨어 스레드)는 독립적으로 Linux (또는 Windows)를 실행합니다. 다른 하드웨어 스레드와 함께 작동하기 위해 공유 데이터 구조를 사용합니다. 그러나 다른 CPU에서 스레드를 "직접"시작하지 마십시오. OS에 새로운 스레드를 원한다고 말하면 다른 코어의 OS가 보는 데이터 구조에 메모합니다.
Peter Cordes 2016

2
나는 그것을 말할 수 있지만, 어떻게 OS가 특정 코어에 코드를 넣는가?
demonguy

4
@demonguy ... (간체) ... 각 코어는 OS 이미지를 공유하고 동일한 위치에서 실행하기 시작합니다. 따라서 8 코어의 경우 커널에서 실행되는 8 개의 "하드웨어 프로세스"입니다. 각각은 실행 가능한 프로세스 또는 스레드에 대한 프로세스 테이블을 확인하는 동일한 스케줄러 함수를 호출합니다. (이는 실행 대기열입니다. ) 한편 스레드가있는 프로그램은 기본 SMP 특성에 대한 인식없이 작동합니다. 그들은 단지 포크 (2) 또는 무언가를 커널에게 그들이 실행하고 싶다고 알립니다. 기본적으로 코어는 프로세스를 찾는 프로세스가 아니라 프로세스를 찾습니다.
DigitalRoss

1
실제로 한 코어를 다른 코어에서 중단 할 필요는 없습니다. 이런 식으로 생각하십시오. 이전 통신 해야했던 모든 것이 소프트웨어 메커니즘으로 잘 전달 되었습니다 . 동일한 소프트웨어 메커니즘이 계속 작동합니다. 파이프, 커널 호출, 슬립 / 웨이크 업, 그 모든 것들은 여전히 ​​이전처럼 작동합니다. 모든 프로세스가 동일한 CPU에서 실행되는 것은 아니지만 이전과 동일한 통신을 위해 동일한 데이터 구조를 갖습니다. SMP를 사용하려는 노력은 대부분 이전 잠금을보다 병렬 환경에서 작동시키는 데 국한됩니다.
DigitalRoss

10

멀티 코어 CPU에 최적화 된 컴파일러 / 바이트 코드 VM을 작성하는 경우 모든 코어에서 효율적으로 실행되는 코드를 생성하려면 x86에 대해 구체적으로 알아야 할 사항은 무엇입니까?

최적화 컴파일러 / 바이트 코드 VM을 작성하는 사람으로서 여기에서 당신을 도울 수 있습니다.

모든 코어에서 효율적으로 실행되는 코드를 생성하기 위해 x86에 대해 특별히 알 필요는 없습니다.

그러나 모든 코어에서 올바르게 실행되는 코드를 작성하려면 cmpxchg 및 친구에 대해 알아야 할 수도 있습니다 . 멀티 코어 프로그래밍에는 실행 스레드 간 동기화 및 통신을 사용해야합니다.

x86에서 일반적으로 x86에서 효율적으로 실행되는 코드를 생성하려면 x86에 대해 알아야합니다.

배우는 데 도움이 될 다른 것들이 있습니다 :

여러 스레드를 실행할 수 있도록 OS (Linux 또는 Windows 또는 OSX)가 제공하는 기능에 대해 학습해야합니다. OpenMP 및 Threading Building Blocks 또는 OSX 10.6 "Snow Leopard"의 "Grand Central"과 같은 병렬화 API에 대해 배워야합니다.

컴파일러가 자동 병렬화되어야하는지 또는 컴파일러가 컴파일 한 애플리케이션 작성자가 다중 코어를 활용하기 위해 프로그램에 특수 구문 또는 API 호출을 추가해야 하는지를 고려해야합니다.


.NET 및 Java와 같이 널리 사용되는 여러 VM이 기본 GC 프로세스가 잠금 및 기본적으로 단일 스레드로 처리된다는 문제가 있습니까?
Marco van de Voort 2018 년

9

각 코어는 다른 메모리 영역에서 실행됩니다. 운영 체제는 프로그램의 핵심을 가리키고 핵심은 프로그램을 실행합니다. 프로그램은 하나 이상의 코어가 있거나 어떤 코어에서 실행되고 있는지 인식하지 못합니다.

운영 체제에서만 사용 가능한 추가 지침도 없습니다. 이 코어는 단일 코어 칩과 동일합니다. 각 코어는 운영 체제의 일부를 실행하여 정보 교환에 사용되는 공통 메모리 영역과의 통신을 처리하여 다음에 실행할 메모리 영역을 찾습니다.

이것은 단순화이지만 어떻게 수행되는지에 대한 기본 아이디어를 제공합니다. Embedded.com의 멀티 코어 및 멀티 프로세서 에 대한 자세한 내용은이 항목에 대한 많은 정보를 제공합니다.이 항목은 매우 복잡합니다!


멀티 코어가 일반적으로 어떻게 작동하고 OS가 얼마나 많은 영향을 미치는지 좀 더 신중하게 구별해야한다고 생각합니다. "각 코어는 다른 메모리 영역에서 실행된다"는 제 생각에는 너무 오도됩니다. 무엇보다도 원칙적으로 다중 코어를 사용하는 데는 이것이 필요하지 않으며 스레드 프로그램의 경우 두 개의 코어가 동일한 텍스트 및 데이터 세그먼트에서 작동한다는 것을 쉽게 알 수 있습니다 (각 코어에는 스택과 같은 개별 리소스가 필요함) .
Volker Stolz

@ShiDoiSi 그렇기 때문에 제 대답에는 "이것은 단순화입니다"라는 텍스트가 포함되어 있습니다 .
Gerhard

5

어셈블리 코드는 하나의 코어에서 실행될 기계 코드로 변환됩니다. 멀티 스레드 방식으로 운영하려면 운영 체제 프리미티브를 사용하여 다른 프로세서에서이 코드를 여러 번 시작하거나 다른 코어에서 다른 코드 조각을 시작해야합니다. 각 코어는 별도의 스레드를 실행합니다. 각 스레드는 현재 실행중인 코어를 하나만 볼 수 있습니다.


4
나는 이런 식으로 말할 것이지만 OS는 어떻게 스레드를 코어에 할당합니까? 나는 이것을 달성하는 특권 어셈블리 지침이 있다고 상상한다. 그렇다면 저자가 찾고있는 답이라고 생각합니다.
A. Levy 2016 년

이에 대한 지시는 없습니다. 이것이 운영 체제 스케줄러의 의무입니다. Win32에는 SetThreadAffinityMask와 같은 운영 체제 기능이 있으며 코드는이를 호출 할 수 있지만 운영 체제에 영향을 미치고 스케줄러에 영향을 미칩니다. 프로세서 명령이 아닙니다.
sharptooth 2016 년

2
OpCode가 있어야합니다. 그렇지 않으면 운영 체제에서도이를 수행 할 수 없습니다.
Matthew Whited

1
실제로 스케줄링을위한 opcode는 아닙니다. 프로세서 당 하나의 OS 사본을 확보하여 메모리 공간을 공유하는 것과 비슷합니다. 코어가 커널에 다시 들어갈 때마다 (syscall 또는 인터럽트) 메모리에서 동일한 데이터 구조를보고 다음에 실행할 스레드를 결정합니다.
pjc50

1
@ A.Levy : 다른 코어에서만 실행할 수있는 선호도로 스레드를 시작하면 다른 코어로 즉시 이동 하지 않습니다 . 일반 컨텍스트 전환과 마찬가지로 컨텍스트가 메모리에 저장됩니다. 다른 하드웨어 스레드는 스케줄러 데이터 구조에서 해당 항목을보고 그 중 하나가 결국 스레드를 실행할지 여부를 결정합니다. 따라서 첫 번째 코어의 관점에서 : 공유 데이터 구조에 쓰고 결국 다른 코어 (하드웨어 스레드)의 OS 코드가이를 인식하여 실행합니다.
Peter Cordes 2016

3

기계 지침에서는 전혀 수행되지 않습니다. 코어는 별개의 CPU 인 척하며 서로 대화 할 수있는 특별한 기능이 없습니다. 그들이 의사 소통하는 두 가지 방법이 있습니다 :

  • 물리적 주소 공간을 공유합니다. 하드웨어는 캐시 일관성을 처리하므로 한 CPU는 다른 CPU가 읽는 메모리 주소에 씁니다.

  • 이들은 APIC (프로그래밍 가능한 인터럽트 컨트롤러)를 공유합니다. 이것은 실제 주소 공간에 매핑 된 메모리이며, 한 프로세서에서 다른 프로세서를 제어하고, 켜거나 끄고, 인터럽트를 보내는 등의 용도로 사용할 수 있습니다.

http://www.cheesecake.org/sac/smp.html 은 바보 같은 URL에 대한 좋은 참고 자료입니다.


2
실제로 APIC를 공유하지 않습니다. 각 논리적 CPU에는 고유 한 CPU가 있습니다. APIC는 서로간에 통신하지만 별도입니다.
Nathan Fellman

그것들은 하나의 기본 방식으로 동기화 (통화하지 않고)합니다. 즉, 모든 버스에 대해 효과적으로 실행되는 CPU에 대한 잠금 핀으로 실행되는 LOCK 접두사 (명령 "xchg mem, reg"는 암시 적 잠금 요청을 포함 함)를 통해 이루어집니다. (실제로 모든 버스 마스터 링 장치)는 버스에 독점적으로 액세스하기를 원합니다. 결국 신호는 LOCKA (승인) 핀으로 돌아가 CPU에 버스에 독점적으로 액세스 할 수 있음을 알려줍니다. 외부 장치는 CPU의 내부 작동보다 훨씬 느리기 때문에 LOCK / LOCKA 시퀀스는 완료하는 데 수백 개의 CPU주기가 필요할 수 있습니다.
Olof Forshell

1

단일 스레드 응용 프로그램과 다중 스레드 응용 프로그램의 주요 차이점은 전자는 하나의 스택을 갖고 후자는 각 스레드마다 하나의 스택을 가지고 있다는 것입니다. 컴파일러는 데이터와 스택 세그먼트 레지스터 (ds와 ss)가 동일하지 않다고 가정하기 때문에 코드가 약간 다르게 생성됩니다. 이는 기본적으로 ss 레지스터로 기본 설정되는 ebp 및 esp 레지스터를 통한 간접 설정도 ds로 기본 설정되지 않음을 의미합니다 (ds! = ss 때문). 반대로, ds로 기본 설정되는 다른 레지스터를 통한 간접 설정은 기본적으로 ss로 설정되지 않습니다.

스레드는 데이터 및 코드 영역을 포함하여 다른 모든 것을 공유합니다. 또한 lib 루틴을 공유하므로 스레드로부터 안전해야합니다. RAM에서 영역을 정렬하는 절차는 작업 속도를 높이기 위해 멀티 스레드 될 수 있습니다. 그런 다음 스레드는 동일한 실제 메모리 영역에서 데이터를 액세스, 비교 및 ​​정렬하고 동일한 코드를 실행하지만 다른 로컬 변수를 사용하여 정렬의 각 부분을 제어합니다. 물론 스레드에는 로컬 변수가 포함 된 스택이 서로 다르기 때문입니다. 이러한 유형의 프로그래밍에는 코드 간 신중한 조정이 필요하므로 (코어 및 RAM에서) 코어 간 데이터 충돌이 줄어 코드 하나만 사용하는 것보다 2 개 이상의 스레드를 사용하는 코드가 더 빠릅니다. 물론, 조정되지 않은 코드는 종종 하나 이상의 프로세서에서 두 개 이상의 프로세서보다 빠릅니다. 표준 "int 3"중단 점은 특정 스레드를 중단하려는 것이 아니라 적용 할 수 없기 때문에 디버그하기가 더 어려워집니다. 디버그 레지스터 중단 점은 인터럽트하려는 특정 스레드를 실행하는 특정 프로세서에서 설정할 수 없으면이 문제를 해결하지 못합니다.

다른 멀티 스레드 코드는 프로그램의 다른 부분에서 실행되는 다른 스레드를 포함 할 수 있습니다. 이 유형의 프로그래밍에는 동일한 종류의 튜닝이 필요하지 않으므로 배우기가 훨씬 쉽습니다.


0

모든 멀티 프로세싱 가능 아키텍처에 추가 된 것은 이전의 단일 프로세서 변형과 비교하여 코어 간 동기화 지침입니다. 또한 캐시 일관성, 플러시 버퍼 및 OS가 처리해야하는 유사한 저수준 작업을 처리하기위한 지침이 있습니다. IBM POWER6, IBM Cell, Sun Niagara 및 Intel "Hyperthreading"과 같은 동시 멀티 스레드 아키텍처의 경우 스레드간에 우선 순위를 지정하는 새로운 지시 사항이 표시되는 경향이 있습니다 (우선 순위 설정 및 수행 할 작업이 없을 때 프로세서를 명시 적으로 생성하는 등). .

그러나 기본 단일 스레드 의미론은 동일합니다. 다른 코어와의 동기화 및 통신을 처리하기 위해 추가 기능을 추가하기 만하면됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.