Como é a linguagem assembly multicore?

243

Era uma vez, para escrever um montador x86, por exemplo, você teria instruções dizendo "carregar o registro EDX com o valor 5", "incrementar o registro EDX", etc.

Com CPUs modernas que possuem 4 núcleos (ou mais), no nível do código da máquina, parece que existem 4 CPUs separadas (ou seja, existem apenas 4 registros "EDX" distintos)? Se sim, quando você diz "incrementar o registro EDX", o que determina qual registro EDX da CPU é incrementado? Existe um conceito de "contexto de CPU" ou "thread" no assembler x86 agora?

Como a comunicação / sincronização entre os núcleos funciona?

Se você estava escrevendo um sistema operacional, qual mecanismo é exposto por hardware para permitir agendar a execução em diferentes núcleos? É alguma instrução especial privilegiada?

Se você estivesse escrevendo uma VM otimizada de compilador / bytecode para uma CPU multicore, o que você precisaria saber especificamente sobre, por exemplo, x86 para gerar um código que é executado de maneira eficiente em todos os núcleos?

Quais alterações foram feitas no código de máquina x86 para oferecer suporte à funcionalidade multinúcleo?

Paul Hollingsworth
fonte
2
Há uma pergunta semelhante (embora não idêntica) aqui: stackoverflow.com/questions/714905/...
Nathan Fellman

Respostas:

153

Esta não é uma resposta direta à pergunta, mas é uma resposta a uma pergunta que aparece nos comentários. Essencialmente, a questão é qual o suporte que o hardware oferece à operação multithread.

Nicholas Flynt estava certo , pelo menos em relação ao x86. Em um ambiente multithread (Hyper-threading, multi-core ou multiprocessador), o thread do Bootstrap (normalmente o thread 0 no núcleo 0 no processador 0) inicia a busca do código do endereço 0xfffffff0. Todos os outros encadeamentos iniciam em um estado de suspensão especial chamado Aguardar SIPI . Como parte de sua inicialização, o encadeamento primário envia uma IPI (interrupção entre processadores) especial sobre o APIC chamado SIPI (IPI de inicialização) para cada encadeamento que está no WFS. O SIPI contém o endereço do qual esse encadeamento deve começar a buscar código.

Esse mecanismo permite que cada thread execute o código de um endereço diferente. Tudo o que é necessário é suporte de software para cada thread para configurar suas próprias tabelas e filas de mensagens. O sistema operacional usa os a fazer a programação multi-thread real.

No que diz respeito à montagem real, como Nicholas escreveu, não há diferença entre as montagens para um único aplicativo com ou sem rosca. Cada encadeamento lógico possui seu próprio conjunto de registros, portanto, escrevendo:

mov edx, 0

será atualizado apenas EDXpara o segmento em execução no momento . Não há como modificar EDXem outro processador usando uma única instrução de montagem. Você precisa de algum tipo de chamada do sistema para solicitar ao sistema operacional que diga a outro thread para executar o código que será atualizado automaticamente EDX.

Nathan Fellman
fonte
2
Obrigado por preencher a lacuna na resposta de Nicholas. Marquei a sua como a resposta aceita agora .... fornece os detalhes específicos nos quais eu estava interessado ... embora fosse melhor se houvesse uma única resposta que tivesse suas informações e as de Nicholas combinadas.
22415 Paul Hollingsworth
3
Isso não responde à pergunta de onde os tópicos vêm. Núcleos e processadores são coisa de hardware, mas de alguma forma os threads devem ser criados no software. Como o encadeamento primário sabe para onde enviar o SIPI? Ou o próprio SIPI cria um novo encadeamento?
rica Remer
7
@richremer: Parece que você está confundindo threads HW e SW. O encadeamento HW sempre existe. Às vezes está dormindo. O próprio SIPI ativa o encadeamento HW e permite que ele execute SW. Cabe ao sistema operacional e ao BIOS decidir quais threads de HW são executados e quais processos e threads de SW são executados em cada thread de HW.
19714 Nathan Fellman
2
Muitas informações boas e concisas aqui, mas esse é um tópico importante - para que as perguntas possam persistir. Existem alguns exemplos de kernels completos "bare bones" em estado selvagem que inicializam a partir de unidades USB ou discos "flexíveis" - aqui está uma versão x86_32 escrita em assembler usando os antigos descritores TSS que podem realmente executar código C com vários threads ( github. com / duanev / oz-x86-32-asm-003 ), mas não há suporte à biblioteca padrão. Um pouco mais do que você pediu, mas talvez possa responder a algumas dessas perguntas remanescentes.
Duanev 15/12/19
87

Exemplo de baremetal mínimo executável da Intel x86

Exemplo de metal nu executável com todos os padrões exigidos . Todas as principais partes são abordadas abaixo.

Testado no Ubuntu 15.10 QEMU 2.3.0 e no convidado de hardware real Lenovo ThinkPad T400 .

O Guia de programação do sistema Intel Manual Volume 3 - 325384-056BR setembro de 2015 aborda o SMP nos capítulos 8, 9 e 10.

Tabela 8-1. "Transmitir sequência INIT-SIPI-SIPI e escolha de tempos limite" contém um exemplo que basicamente funciona:

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

Nesse código:

  1. A maioria dos sistemas operacionais impossibilitará a maioria dessas operações do anel 3 (programas do usuário).

    Então você precisa escrever seu próprio kernel para brincar livremente com ele: um programa Linux do usuário não funcionará.

  2. Inicialmente, um único processador é executado, chamado de processador de bootstrap (BSP).

    Ele deve ativar os outros (chamados Application Processors (AP)) através de interrupções especiais chamadas Inter Processor Interrupts (IPI) .

    Essas interrupções podem ser feitas através da programação de Controlador de interrupção programável avançado (APIC) por meio do registro de comando de interrupção (ICR)

    O formato do ICR está documentado em: 10.6 "EMITIR INTERRUPTORES DE INTERPROCESSADORES"

    O IPI acontece assim que escrevemos para o ICR.

  3. ICR_LOW é definido em 8.4.4 "Exemplo de Inicialização MP" como:

    ICR_LOW EQU 0FEE00300H
    

    O valor mágico 0FEE00300é o endereço de memória do ICR, conforme documentado na Tabela 10-1 "Mapa local de endereços de registro APIC"

  4. O método mais simples possível é usado no exemplo: ele configura o ICR para enviar IPIs de difusão que são entregues a todos os outros processadores, exceto o atual.

    Mas também é possível, e recomendado por alguns , obter informações sobre os processadores por meio de estruturas de dados especiais configuradas pelo BIOS, como tabelas ACPI ou tabela de configuração MP da Intel, e apenas ativar os que você precisa um por um.

  5. XXin 000C46XXHcodifica o endereço da primeira instrução que o processador executará como:

    CS = XX * 0x100
    IP = 0
    

    Lembre-se de que o CS multiplica endereços por0x10 , portanto, o endereço de memória real da primeira instrução é:

    XX * 0x1000
    

    Portanto, se, por exemplo XX == 1, o processador iniciar às 0x1000.

    Devemos garantir que haja um código de modo real de 16 bits para ser executado nesse local de memória, por exemplo, com:

    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
    

    Usar um script vinculador é outra possibilidade.

  6. Os loops de atraso são uma parte chata para começar a trabalhar: não há uma maneira super simples de fazer essas dormidas com precisão.

    Os métodos possíveis incluem:

    • PIT (usado no meu exemplo)
    • HPET
    • calibrar o tempo de um loop ocupado com o descrito acima e usá-lo

    Relacionado: Como exibir um número na tela e dormir por um segundo com a montagem do DOS x86?

  7. Eu acho que o processador inicial precisa estar no modo protegido para que isso funcione enquanto escrevemos para o endereço 0FEE00300Hque é muito alto para 16 bits

  8. Para se comunicar entre processadores, podemos usar um spinlock no processo principal e modificar o bloqueio a partir do segundo núcleo.

    Devemos garantir que a gravação de memória seja feita, por exemplo, através wbinvd.

Estado compartilhado entre processadores

8.7.1 "Estado dos processadores lógicos" diz:

Os seguintes recursos fazem parte do estado arquitetural dos processadores lógicos nos processadores Intel 64 ou IA-32 que oferecem suporte à tecnologia Intel Hyper-Threading. Os recursos podem ser subdivididos em três grupos:

  • Duplicado para cada processador lógico
  • Compartilhado por processadores lógicos em um processador físico
  • Compartilhado ou duplicado, dependendo da implementação

Os seguintes recursos são duplicados para cada processador lógico:

  • Registradores de uso geral (EAX, EBX, ECX, EDX, ESI, EDI, ESP e EBP)
  • Registros de segmento (CS, DS, SS, ES, FS e GS)
  • Registros EFLAGS e EIP. Observe que os registros CS e EIP / RIP para cada processador lógico apontam para o fluxo de instruções do encadeamento que está sendo executado pelo processador lógico.
  • Registros FPU x87 (ST0 a ST7, palavra de status, palavra de controle, palavra de tag, ponteiro de operando de dados e ponteiro de instrução)
  • Registradores MMX (MM0 a MM7)
  • Registros XMM (XMM0 a XMM7) e o registro MXCSR
  • Registradores de controle e registradores de ponteiros da tabela do sistema (GDTR, LDTR, IDTR, registro de tarefas)
  • Registros de depuração (DR0, DR1, DR2, DR3, DR6, DR7) e os MSRs de controle de depuração
  • MSRs de status global de verificação de máquina (IA32_MCG_STATUS) e de capacidade de verificação de máquina (IA32_MCG_CAP)
  • Modulação de relógio térmico e controle de gerenciamento de energia ACPI MSRs
  • MSRs de contador de carimbo de data / hora
  • A maioria dos outros registros MSR, incluindo a tabela de atributos da página (PAT). Veja as exceções abaixo.
  • Registros APIC locais.
  • Registros de uso geral adicionais (R8-R15), registros XMM (XMM8-XMM15), registro de controle, IA32_EFER nos processadores Intel 64.

Os seguintes recursos são compartilhados por processadores lógicos:

  • Registradores de intervalo de tipo de memória (MTRRs)

Se os seguintes recursos são compartilhados ou duplicados, é específico da implementação:

  • IA32_MISC_ENABLE MSR (endereço 1A0H do MSR)
  • MSRs da arquitetura de verificação da máquina (MCA) (exceto para os MSRs IA32_MCG_STATUS e IA32_MCG_CAP)
  • Controle de monitoramento de desempenho e MSRs de contador

O compartilhamento de cache é discutido em:

Os hyperthreads da Intel têm maior compartilhamento de cache e pipeline do que núcleos separados: /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858

Kernel Linux 4.2

A principal ação de inicialização parece estar em arch/x86/kernel/smpboot.c.

Exemplo mínimo de baremetal executável do ARM

Aqui, forneço um exemplo mínimo de ARMv8 aarch64 executável para QEMU:

.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 .

Montar e executar:

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 \
;

Neste exemplo, colocamos a CPU 0 em um loop de spinlock, e ele só sai com a CPU 1 libera o spinlock.

Após o spinlock, a CPU 0 faz uma chamada de saída de semi - host que faz com que o QEMU saia.

Se você iniciar o QEMU com apenas uma CPU -smp 1, a simulação ficará suspensa para sempre no spinlock.

A CPU 1 é acordada com a interface PSCI, mais detalhes em: ARM: Iniciar / Ativar / Recuperar os outros núcleos / APs da CPU e passar o endereço inicial de execução?

A versão upstream também possui alguns ajustes para fazê-lo funcionar no gem5, para que você também possa experimentar as características de desempenho.

Eu não o testei em hardware real, então não tenho certeza de como isso é portátil. A seguinte bibliografia do Raspberry Pi pode ser interessante:

Este documento fornece algumas orientações sobre o uso de primitivas de sincronização ARM, que você pode usar para fazer coisas divertidas com vários núcleos: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf

Testado no Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.

Próximas etapas para uma programação mais conveniente

Os exemplos anteriores ativam a CPU secundária e fazem a sincronização básica da memória com instruções dedicadas, o que é um bom começo.

Mas, para facilitar a programação de sistemas multicore, por exemplo, como o POSIX pthreads , você também precisará entrar nos seguintes tópicos mais envolvidos:

  • A instalação interrompe e executa um cronômetro que decide periodicamente qual thread será executado agora. Isso é conhecido como multithreading preventivo .

    Esse sistema também precisa salvar e restaurar registros de encadeamento à medida que são iniciados e parados.

    Também é possível ter sistemas multitarefa não-preemptivos, mas isso pode exigir que você modifique seu código para que todos os encadeamentos produzam (por exemplo, com uma pthread_yieldimplementação), e fica mais difícil equilibrar as cargas de trabalho.

    Aqui estão alguns exemplos simplistas do temporizador bare metal:

  • lidar com conflitos de memória. Notavelmente, cada thread precisará de uma pilha exclusiva se você quiser codificar em C ou em outros idiomas de alto nível.

    Você pode limitar os encadeamentos para ter um tamanho máximo fixo de pilha, mas a melhor maneira de lidar com isso é com a paginação, que permite pilhas eficientes de "tamanho ilimitado".

    Aqui está um exemplo baremetal ingênuo do aarch64 que explodiria se a pilha crescesse muito fundo

Essas são algumas boas razões para usar o kernel do Linux ou algum outro sistema operacional :-)

Primitivas de sincronização de memória do Userland

Embora o início / parada / gerenciamento do encadeamento esteja geralmente fora do escopo da área do usuário, você pode, no entanto, usar instruções de montagem dos encadeamentos da área do usuário para sincronizar os acessos à memória sem chamadas de sistema potencialmente mais caras.

Obviamente, você deve preferir usar bibliotecas que agrupem essas primitivas de baixo nível. O padrão C ++ si fez grandes avanços nos <mutex>e <atomic>cabeçalhos, e em particular com std::memory_order. Não tenho certeza se ele cobre todas as semânticas de memória possíveis, mas apenas pode.

A semântica mais sutil é particularmente relevante no contexto de estruturas de dados sem bloqueio , que podem oferecer benefícios de desempenho em certos casos. Para implementá-las, você provavelmente precisará aprender um pouco sobre os diferentes tipos de barreiras de memória: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/

O Boost, por exemplo, tem algumas implementações de contêiner sem bloqueio em: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html

Essas instruções da terra do usuário também parecem ser usadas para implementar a futexchamada do sistema Linux , que é uma das principais primitivas de sincronização no Linux. man futex4,15 lê:

A chamada do sistema futex () fornece um método para aguardar até que uma determinada condição se torne verdadeira. É normalmente usado como uma construção de bloqueio no contexto da sincronização de memória compartilhada. Ao usar futexes, a maioria das operações de sincronização é realizada no espaço do usuário. Um programa de espaço do usuário emprega a chamada do sistema futex () somente quando é provável que o programa precise bloquear por um longo período de tempo até que a condição se torne verdadeira. Outras operações do futex () podem ser usadas para ativar quaisquer processos ou threads que aguardam uma condição específica.

O próprio nome do syscall significa "Fast Userspace XXX".

Aqui está um exemplo mínimo inútil de C ++ x86_64 / aarch64 com assembly embutido que ilustra o uso básico dessas instruções principalmente por diversão:

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 upstream .

Saída possível:

my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267

A partir disso, vemos que a LDADDinstrução x86 LOCK prefix / aarch64 tornou a adição atômica: sem ela, temos condições de corrida em muitas das adições, e a contagem total no final é menor que a 20000 sincronizada.

Veja também:

Testado no Ubuntu 19.04 amd64 e com o modo de usuário QEMU aarch64.

Ciro Santilli adicionou uma nova foto
fonte
Qual assembler você usa para compilar seu exemplo? O GAS não parece gostar do seu #include(toma como comentário), NASM, FASM, YASM não conhecem a sintaxe da AT&T, portanto não podem ser eles ... então o que é?
Ruslan
@Ruslan gcc, #includevem do pré-processador C. Use o Makefilefornecido conforme explicado na seção de introdução : github.com/cirosantilli/x86-bare-metal-examples/blob/… Se isso não funcionar, abra um problema do GitHub.
Ciro Santilli escreveu:
no x86, o que acontece se um núcleo perceber que não há mais processos prontos para serem executados na fila? (o que pode ocorrer de tempos em tempos em um sistema inativo). O núcleo gira na estrutura de memória compartilhada até que haja uma nova tarefa? (provavelmente não é bom, ele usará muita energia) chama algo como HLT para dormir até que haja uma interrupção? (nesse caso, que é responsável para acordar esse núcleo?)
tigrou
@ tigrou não tenho certeza, mas acho extremamente provável que a implementação do Linux o coloque em um estado de energia até a próxima interrupção (provável temporizador), especialmente no ARM, onde a energia é fundamental. Eu tentaria rapidamente ver se isso pode ser observado concretamente facilmente com o rastreamento de instruções de um simulador executando o Linux; pode ser: github.com/cirosantilli/linux-kernel-module-cheat/tree/…
Ciro Santilli 郝海东 冠状 病六四事件法轮功
1
Algumas informações (específicas para x86 / Windows) podem ser encontradas aqui (consulte "Tópico ocioso"). TL; DR: quando não existe um thread executável em uma CPU, a CPU é despachada para um thread inativo. Juntamente com algumas outras tarefas, finalmente chamará a rotina ociosa do processador de gerenciamento de energia registrado (por meio de um driver fornecido pelo fornecedor da CPU, por exemplo: Intel). Isso pode fazer a transição da CPU para um estado C mais profundo (por exemplo: C0 -> C3) para reduzir o consumo de energia.
tigrou
43

Pelo que entendi, cada "núcleo" é um processador completo, com seu próprio conjunto de registros. Basicamente, o BIOS inicia você com um núcleo em execução e, em seguida, o sistema operacional pode "iniciar" outros núcleos, inicializando-os e apontando-os para o código a ser executado, etc.

A sincronização é feita pelo sistema operacional. Geralmente, cada processador está executando um processo diferente para o sistema operacional; portanto, a funcionalidade de multiencadeamento do sistema operacional é responsável por decidir qual processo tocará em qual memória e o que fazer no caso de uma colisão de memória.

Nicholas Flynt
fonte
28
o que sugere a pergunta: quais instruções estão disponíveis para o sistema operacional?
Paul Hollingsworth
4
Há um conjunto de instruções privilegiadas para isso, mas é o problema do sistema operacional, não o código do aplicativo. Se o código do aplicativo quiser ser multithread, ele deve chamar as funções do sistema operacional para executar a "mágica".
Sharptooth 11/06/09
2
O BIOS geralmente identifica quantos núcleos estão disponíveis e passa essas informações ao sistema operacional quando solicitado. Existem padrões com os quais o BIOS (e o hardware) devem estar em conformidade para acessar informações específicas de hardware (processadores, núcleos, barramento PCI, placas PCI, mouse, teclado, gráficos, ISA, PCI-E / X, memória etc.) para diferentes PCs parece o mesmo do ponto de vista do sistema operacional. Se o BIOS não relatar que existem quatro núcleos, o sistema operacional normalmente assumirá que existe apenas um. Pode até haver uma configuração de BIOS para experimentar.
Olof Forshell
1
Isso é legal e tudo mais, e se você estiver escrevendo um programa bare-metal?
Alexander Ryan Baggett
3
@AlexanderRyanBaggett,? O que é isso mesmo? Reiterando, quando dizemos "deixe para o sistema operacional", estamos evitando a pergunta, porque a questão é como o sistema operacional faz isso? Quais instruções de montagem ele usa?
Pacerier
39

Perguntas freqüentes sobre o SMP não oficial logotipo de estouro de pilha


Era uma vez, para escrever um montador x86, por exemplo, você teria instruções dizendo "carregar o registro EDX com o valor 5", "incrementar o registro EDX" etc. etc. Com CPUs modernas que possuem 4 núcleos (ou mais) , no nível do código da máquina, parece que existem 4 CPUs separadas (ou seja, existem apenas 4 registros "EDX" distintos)?

Exatamente. Existem 4 conjuntos de registros, incluindo 4 ponteiros de instruções separados.

Se sim, quando você diz "incrementar o registro EDX", o que determina qual registro EDX da CPU é incrementado?

A CPU que executou essa instrução, naturalmente. Pense nisso como 4 microprocessadores completamente diferentes que simplesmente compartilham a mesma memória.

Existe um conceito de "contexto de CPU" ou "thread" no assembler x86 agora?

Não. O montador apenas traduz instruções como sempre. Não há alterações lá.

Como a comunicação / sincronização entre os núcleos funciona?

Como eles compartilham a mesma memória, é principalmente uma questão de lógica do programa. Embora exista agora um mecanismo de interrupção entre processadores , não é necessário e não estava originalmente presente nos primeiros sistemas x86 de CPU dupla.

Se você estava escrevendo um sistema operacional, qual mecanismo é exposto por hardware para permitir agendar a execução em diferentes núcleos?

O agendador, na verdade, não muda, exceto que é um pouco mais cuidadoso sobre as seções críticas e os tipos de bloqueios usados. Antes do SMP, o código do kernel eventualmente chamava o agendador, que examinaria a fila de execução e escolheria um processo para executar como o próximo encadeamento. (Os processos no kernel se parecem muito com threads.) O kernel SMP executa exatamente o mesmo código, um thread de cada vez, mas agora o bloqueio de seção crítico precisa ser seguro para SMP para garantir que dois núcleos não possam escolher acidentalmente o mesmo PID.

É alguma instrução especial privilegiada?

Não. Os núcleos estão todos rodando na mesma memória com as mesmas instruções antigas.

Se você estivesse escrevendo uma VM otimizada de compilador / bytecode para uma CPU multicore, o que você precisaria saber especificamente sobre, por exemplo, x86 para gerar um código que é executado de maneira eficiente em todos os núcleos?

Você executa o mesmo código que antes. É o kernel do Unix ou Windows que precisava mudar.

Você pode resumir minha pergunta como "Quais alterações foram feitas no código da máquina x86 para oferecer suporte à funcionalidade multinúcleo?"

Nada foi necessário. Os primeiros sistemas SMP usavam exatamente o mesmo conjunto de instruções dos uniprocessadores. Agora, houve uma grande evolução na arquitetura x86 e zilhões de novas instruções para acelerar as coisas, mas nenhuma era necessária para o SMP.

Para obter mais informações, consulte a especificação do multiprocessador Intel .


Atualização: todas as perguntas a seguir podem ser respondidas aceitando completamente que uma CPU multicore n- way é quase 1 exatamente a mesma coisa que n processadores separados que compartilham a mesma memória. 2 Havia uma pergunta importante não feita: como um programa é escrito para ser executado em mais de um núcleo para obter mais desempenho? E a resposta é: ela é escrita usando uma biblioteca de threads como Pthreads. Algumas bibliotecas de encadeamentos usam "encadeamentos verdes" que não são visíveis para o sistema operacional, e esses não terão núcleos separados, mas enquanto a biblioteca de encadeamentos usar recursos de encadeamento do kernel, seu programa encadeado será automaticamente multicore.
1. Para compatibilidade com versões anteriores, apenas o primeiro núcleo inicia na redefinição e algumas coisas do tipo driver precisam ser feitas para ativar os restantes.
2. Eles também compartilham todos os periféricos, naturalmente.

DigitalRoss
fonte
3
Eu sempre acho que "thread" é ​​um conceito de software, o que me dificulta a compreensão do processador multi-core, o problema é: como os códigos podem dizer ao núcleo "Vou criar um thread em execução no núcleo 2"? Existe algum código de montagem especial para fazer isso?
demonguy
2
@demonguy: Não, não há instruções especiais para algo assim. Você pede ao sistema operacional para executar seu encadeamento em um núcleo específico, definindo uma máscara de afinidade (que diz "esse encadeamento pode ser executado neste conjunto de núcleos lógicos"). É completamente uma questão de software. Cada núcleo da CPU (segmento de hardware) está executando independentemente o Linux (ou Windows). Para trabalhar em conjunto com os outros threads de hardware, eles usam estruturas de dados compartilhadas. Mas você nunca "diretamente" inicia um thread em uma CPU diferente. Você diz ao sistema operacional que gostaria de ter um novo encadeamento e ele faz uma anotação em uma estrutura de dados que o sistema operacional em outro núcleo vê.
31515 Peter Cordes
2
Posso dizer, mas como colocar códigos em um núcleo específico?
demonguy
4
@demonguy ... (simplificado) ... cada núcleo compartilha a imagem do sistema operacional e começa a executá-la no mesmo local. Portanto, para 8 núcleos, são 8 "processos de hardware" em execução no kernel. Cada um chama a mesma função do planejador que verifica a tabela de processos em busca de um processo ou encadeamento executável. (Essa é a fila de execução. ) Enquanto isso, os programas com threads funcionam sem o conhecimento da natureza subjacente do SMP. Eles apenas bifurcam (2) ou algo assim e informam ao kernel que desejam executar. Essencialmente, o núcleo encontra o processo, em vez de o processo encontrar o núcleo.
precisa saber é o seguinte
1
Na verdade, você não precisa interromper um núcleo de outro. Pense da seguinte maneira: tudo o que você precisava para se comunicar antes era bem comunicado com os mecanismos de software. Os mesmos mecanismos de software continuam funcionando. Então, pipes, chamadas de kernel, sleep / wakeup, tudo isso ... eles ainda funcionam como antes. Nem todo processo está sendo executado na mesma CPU, mas eles têm as mesmas estruturas de dados para comunicação que tinham antes. O esforço para tornar o SMP restringe-se principalmente a fazer os bloqueios antigos funcionarem em um ambiente mais paralelo.
DigitalRoss
10

Se você estivesse escrevendo uma VM otimizada de compilador / bytecode para uma CPU multicore, o que você precisaria saber especificamente sobre, por exemplo, x86 para gerar um código que é executado de maneira eficiente em todos os núcleos?

Como alguém que escreve otimizando VMs de compilador / bytecode, talvez eu possa ajudá-lo aqui.

Você não precisa saber nada especificamente sobre o x86 para gerar um código que seja executado com eficiência em todos os núcleos.

No entanto, talvez você precise conhecer cmpxchg e amigos para escrever um código que seja executado corretamente em todos os núcleos. A programação multicore requer o uso de sincronização e comunicação entre os threads de execução.

Pode ser necessário saber algo sobre o x86 para gerar um código que seja executado com eficiência no x86 em geral.

Há outras coisas que seriam úteis para você aprender:

Você deve aprender sobre os recursos que o SO (Linux ou Windows ou OSX) fornece para permitir a execução de vários threads. Você deve aprender sobre APIs de paralelização, como OpenMP e Threading Building Blocks, ou o próximo "Grand Central" do OSX 10.6 "Snow Leopard".

Você deve considerar se o compilador deve ser paralelizado automaticamente ou se o autor dos aplicativos compilados pelo compilador precisa adicionar sintaxe especial ou chamadas de API ao programa para tirar proveito dos vários núcleos.

Alex Brown
fonte
Não existem várias VMs populares, como .NET e Java, que seu principal processo de GC é coberto por bloqueios e fundamentalmente único?
Marco van de Voort
9

Cada núcleo é executado a partir de uma área de memória diferente. Seu sistema operacional apontará um núcleo para seu programa e o núcleo executará seu programa. Seu programa não estará ciente de que há mais de um núcleo ou em qual núcleo ele está executando.

Também não há instruções adicionais disponíveis apenas para o sistema operacional. Esses núcleos são idênticos aos chips de núcleo único. Cada núcleo executa uma parte do sistema operacional que manipulará a comunicação com áreas de memória comuns usadas para o intercâmbio de informações para encontrar a próxima área de memória a ser executada.

Isso é uma simplificação, mas fornece a idéia básica de como isso é feito. Mais sobre multicores e multiprocessadores no Embedded.com tem muitas informações sobre este tópico ... Este tópico fica complicado muito rapidamente!

Gerhard
fonte
Acho que devemos distinguir um pouco mais cuidadosamente aqui como o multicore funciona em geral e o quanto o sistema operacional influencia. "Cada núcleo é executado a partir de uma área de memória diferente" é muito enganador na minha opinião. Em primeiro lugar, o uso de múltiplos núcleos em princípios não precisa disso, e você pode ver facilmente que, para um programa encadeado, você QUERIA dois núcleos, dois trabalhariam nos mesmos segmentos de texto e dados (enquanto cada núcleo também precisa de recursos individuais, como pilha) .
Volker Stolz
@ShiDoiSi É por isso que minha resposta contém o texto "Isso é uma simplificação" .
Gerhard
5

O código de montagem será convertido em código de máquina que será executado em um núcleo. Se você deseja que ele seja multithread, você precisará usar as primitivas do sistema operacional para iniciar esse código em diferentes processadores várias vezes ou diferentes partes de código em núcleos diferentes - cada núcleo executará um encadeamento separado. Cada thread verá apenas um núcleo no qual está sendo executado atualmente.

dente afiado
fonte
4
Eu ia dizer algo assim, mas como o sistema operacional aloca os threads nos núcleos? Eu imagino que existem algumas instruções de montagem privilegiadas que realizam isso. Nesse caso, acho que essa é a resposta que o autor está procurando.
A. Levy
Não há instruções para isso, esse é o dever do agendador do sistema operacional. Existem funções do sistema operacional como SetThreadAffinityMask no Win32 e o código pode chamá-las, mas são coisas do sistema operacional e afetam o agendador, não é uma instrução do processador.
Sharptooth 11/06/09
2
Deve haver um OpCode ou o sistema operacional também não seria capaz de fazê-lo.
Matthew Whited
1
Não é realmente um código de operação para agendamento - é mais como se você tivesse uma cópia do sistema operacional por processador, compartilhando um espaço de memória; sempre que um núcleo entra novamente no kernel (syscall ou interrupt), ele analisa as mesmas estruturas de dados na memória para decidir qual encadeamento será executado a seguir.
Pjc50 27/10/09
1
@ A.Levy: Quando você inicia um thread com uma afinidade que só permite que ele seja executado em um núcleo diferente, ele não se move imediatamente para o outro núcleo. Ele tem seu contexto salvo na memória, assim como uma troca de contexto normal. Os outros threads de hardware veem sua entrada nas estruturas de dados do planejador e um deles acabará por decidir que executará o thread. Portanto, da perspectiva do primeiro núcleo: você escreve em uma estrutura de dados compartilhada e, eventualmente, o código do SO em outro núcleo (segmento de hardware) perceberá e executará.
Peter Cordes
3

Isso não é feito nas instruções da máquina; os núcleos fingem ser CPUs distintas e não possuem recursos especiais para conversar entre si. Existem duas maneiras de se comunicar:

  • eles compartilham o espaço de endereço físico. O hardware lida com a coerência do cache; portanto, uma CPU grava em um endereço de memória que outro lê.

  • eles compartilham um APIC (controlador de interrupção programável). É a memória mapeada no espaço de endereço físico e pode ser usada por um processador para controlar os outros, ativá-los ou desativá-los, enviar interrupções etc.

http://www.cheesecake.org/sac/smp.html é uma boa referência com um URL bobo.

pjc50
fonte
2
Na verdade, eles não compartilham um APIC. Cada CPU lógica tem sua própria. Os APICs se comunicam entre si, mas são separados.
197 Nathan Fellman
Eles sincronizam (em vez de se comunicar) de uma maneira básica e isso ocorre através do prefixo LOCK (a instrução "xchg mem, reg" contém uma solicitação implícita de bloqueio) que é executada no pino de trava que é executado em todos os barramentos, informando efetivamente que a CPU (na verdade, qualquer dispositivo de controle de barramento) deseja acesso exclusivo ao barramento. Eventualmente, um sinal retornará ao pino LOCKA (reconhecer) informando à CPU que agora possui acesso exclusivo ao barramento. Como os dispositivos externos são muito mais lentos que os trabalhos internos da CPU, uma sequência LOCK / LOCKA pode exigir muitas centenas de ciclos da CPU para ser concluída.
Olof Forshell
1

A principal diferença entre um aplicativo único e um multiencadeado é que o primeiro possui uma pilha e o último possui um para cada encadeamento. O código é gerado de maneira um pouco diferente, pois o compilador assumirá que os registros de segmento de dados e pilha (ds e ss) não são iguais. Isso significa que a indireção através dos registros ebp e esp que padrão para o registro ss também não será padrão para ds (porque ds! = Ss). Por outro lado, a indireção através dos outros registradores que padrão para ds não será padrão para ss.

Os threads compartilham tudo o mais, incluindo áreas de dados e código. Eles também compartilham rotinas de lib, portanto, certifique-se de que sejam seguros para threads. Um procedimento que classifica uma área na RAM pode ser multiencadeado para acelerar as coisas. Os encadeamentos estarão acessando, comparando e ordenando dados na mesma área de memória física e executando o mesmo código, mas usando diferentes variáveis ​​locais para controlar sua respectiva parte da classificação. É claro que isso ocorre porque os threads têm pilhas diferentes onde as variáveis ​​locais estão contidas. Esse tipo de programação requer um ajuste cuidadoso do código para reduzir as colisões de dados entre os núcleos (em caches e RAM), o que resulta em um código mais rápido com dois ou mais threads do que com apenas um. Obviamente, um código não sintonizado geralmente será mais rápido com um processador do que com dois ou mais. Depurar é mais desafiador, porque o ponto de interrupção "int 3" padrão não será aplicável, pois você deseja interromper um segmento específico e não todos. Os pontos de interrupção do registro de depuração também não resolvem esse problema, a menos que você possa configurá-los no processador específico que está executando o encadeamento específico que deseja interromper.

Outro código multithread pode envolver diferentes threads sendo executados em diferentes partes do programa. Esse tipo de programação não requer o mesmo tipo de ajuste e, portanto, é muito mais fácil de aprender.

Olof Forshell
fonte
0

O que foi adicionado em toda arquitetura com capacidade de multiprocessamento em comparação com as variantes de processador único que vieram antes delas são instruções para sincronizar entre núcleos. Além disso, você tem instruções para lidar com a coerência do cache, buffers de liberação e operações semelhantes de baixo nível com as quais um sistema operacional precisa lidar. No caso de arquiteturas multithread simultâneas como IBM POWER6, IBM Cell, Sun Niagara e Intel "Hyperthreading", você também tende a ver novas instruções para priorizar entre threads (como definir prioridades e fornecer explicitamente o processador quando não há nada a fazer) .

Mas a semântica básica de thread único é a mesma, basta adicionar recursos extras para lidar com a sincronização e a comunicação com outros núcleos.

jakobengblom2
fonte