Núcleos de CPU ou o que é SMP e o que ele come

1. Introdução


Bom dia, hoje eu gostaria de abordar um tópico bastante simples que é quase desconhecido para programadores comuns, mas cada um de vocês provavelmente o usou.
Estamos falando de multiprocessamento simétrico (popularmente - SMP) - a arquitetura encontrada em todos os sistemas operacionais multitarefa e, é claro, é parte integrante deles. Todo mundo sabe que quanto mais núcleos um processador tiver, mais poderoso ele será, sim, mas como um sistema operacional pode usar vários núcleos ao mesmo tempo? Alguns programadores não chegam a esse nível de abstração - eles simplesmente não precisam, mas acho que todos estarão interessados ​​em como o SMP funciona.

Multitarefa e sua implementação


Quem já estudou arquitetura de computadores sabe que o próprio processador não é capaz de executar várias tarefas ao mesmo tempo; a multitarefa fornece apenas o sistema operacional, que alterna essas tarefas. Existem vários tipos de multitarefa, mas o mais apropriado, conveniente e amplamente utilizado é a exclusão de multitarefa (você pode ler seus principais aspectos na Wikipedia). É baseado no fato de que cada processo (tarefa) tem sua própria prioridade, o que afeta quanto tempo do processador será alocado para ele. Cada tarefa recebe um intervalo de tempo durante o qual o processo faz alguma coisa; após o intervalo expirar, o SO transfere o controle para outra tarefa. Surge a questão - como distribuir recursos do computador, como memória, dispositivos, etc. entre processos? Tudo é muito simples: o Windows faz isso sozinho, o Linux usa um sistema de semáforo. Mas um núcleo não é sério, seguimos em frente.

Interrupções e PIC


Talvez para alguns isso acabe sendo notícia, para outros não, mas a arquitetura i386 (falarei sobre a arquitetura x86, o ARM não conta, porque não estudei essa arquitetura e nunca a encontrei (mesmo no nível de gravação de um serviço ou programa residente)) usa interrupções (falaremos apenas sobre interrupções de hardware, IRQ) para notificar o SO ou programa sobre um evento. Por exemplo, há uma interrupção 0x8 (para os modos protegido e longo, por exemplo, 0x20, dependendo de como configurar o PIC, mais sobre isso posteriormente), que é chamado pelo PIT, que, por exemplo, pode gerar interrupções com qualquer frequência necessária. Em seguida, o trabalho do sistema operacional para a distribuição de intervalos de tempo é reduzido para 0, quando uma interrupção é chamada, o programa para e o controle é dado, por exemplo, ao kernel, que por sua vez salva os dados atuais do programa (registros, sinalizadores etc.) e dá controle ao próximo processo .

Como você provavelmente entendeu, interrupções são funções (ou procedimentos) chamadas em algum momento pelo equipamento ou pelo próprio programa. No total, o processador suporta 16 interrupções em dois PICs. O processador possui sinalizadores e um deles é o sinal "I" - Controle de interrupção. Ao definir esse sinalizador como 0, o processador não causará nenhuma interrupção de hardware. Mas também quero observar que existem as chamadas NMIs - interrupções não mascaráveis ​​- os dados de interrupção ainda serão chamados, mesmo que o bit I esteja definido como 0. Usando a programação PIC, você pode desativar os dados de interrupção, mas depois de retornar de qualquer interrupção com IRET - eles novamente não serão banidos. Observo que, em um programa regular, você não pode rastrear a chamada de interrupção - seu programa é interrompido e retomado apenas depois de um tempo, seu programa nem percebe (sim, você pode verificar se a interrupção foi chamada - mas por quê?

PIC - Controlador de interrupção programável

Do Wiki:
Como regra, é um dispositivo eletrônico, às vezes feito como parte do próprio processador ou chips complexos de seu quadro, cujas entradas são eletricamente conectadas às saídas correspondentes de vários dispositivos. O número de entrada do controlador de interrupção é indicado por "IRQ". Esse número deve ser diferenciado da prioridade de interrupção, bem como do número da entrada na tabela de vetores de interrupção (INT). Portanto, por exemplo, em um PC IBM no modo de operação real (o MS-DOS funciona nesse modo) do processador, a interrupção do teclado padrão usa IRQ 1 e INT 9.

A plataforma IBM PC original usa um esquema de interrupção muito simples. O controlador de interrupção é um contador simples que itera sequencialmente os sinais de dispositivos diferentes ou é redefinido para o início quando uma nova interrupção é encontrada. No primeiro caso, os dispositivos têm prioridade igual; no segundo, os dispositivos com um número de série mais baixo (ou mais alto na contagem) têm uma prioridade mais alta.

Como você entende, este é um circuito eletrônico que permite que os dispositivos enviem solicitações de interrupção, geralmente existem exatamente 2 deles.

Agora, vamos ao tópico do artigo.

SMP


Para implementar esse padrão, novos esquemas começaram a ser colocados nas placas-mãe: APIC e ACPI. Vamos falar sobre o primeiro.

APIC - Advanced Programmable Interrupt Controller, uma versão aprimorada do PIC. É usado em sistemas multiprocessadores e é parte integrante de todos os mais recentes processadores Intel (e compatíveis). O APIC é usado para encaminhamento de interrupções complexo e para o envio de interrupções entre processadores. Essas coisas não eram possíveis usando a especificação PIC mais antiga.

APIC local e IO APIC


Em um sistema baseado em APIC, cada processador consiste em um "núcleo" e um "APIC local". O APIC local é responsável por manipular a configuração de interrupção específica do processador. Entre outras coisas, ele contém uma tabela vetorial local (LVT), que converte eventos, como o “relógio interno” e outras fontes “locais” de interrupções, em um vetor de interrupção (por exemplo, o contato LocalINT1 pode gerar uma exceção de NMI, preservando " 2 ”para a entrada LVT correspondente).

Mais informações sobre o APIC local podem ser encontradas no "Guia de programação do sistema" dos modernos processadores Intel.

Além disso, há um APIC IO (por exemplo, intel 82093AA), que faz parte do chipset e fornece controle de interrupção de vários processadores, incluindo distribuição simétrica estática e dinâmica de interrupções para todos os processadores. Em sistemas com vários subsistemas de E / S, cada subsistema pode ter seu próprio conjunto de interrupções.

Cada pino de interrupção é programado individualmente como disparado por borda ou nível. O vetor de interrupção e as informações de controle de interrupção podem ser especificados para cada interrupção. O esquema de acesso indireto ao registro otimiza o espaço de memória necessário para acessar os registros internos de E / S da APIC. Para aumentar a flexibilidade do sistema ao alocar espaço de memória, os dois registros de E / S APIC são realocáveis, mas o padrão é 0xFEC00000.

Inicializando um APIC “local”


O APIC local é ativado no momento da inicialização e pode ser desativado redefinindo o bit 11 IA32_APIC_BASE (MSR) (isso funciona apenas com processadores com uma família> 5, uma vez que o Pentium não possui esse MSR), então o processador recebe suas interrupções diretamente do 8259 PIC compatível . No entanto, o guia de desenvolvimento de software da Intel afirma que após desativar o APIC local por meio do IA32_APIC_BASE, você não poderá ativá-lo até que ele seja completamente redefinido. O APO IO também pode ser configurado para operar no modo legado, de modo a emular um dispositivo 8259.

Os APICs locais são mapeados para a página física FEE00xxx (consulte a Tabela 8-1 Intel P4 SPG). Este endereço é o mesmo para cada APIC local que existe na configuração, o que significa que você pode acessar diretamente os registros do kernel APIC local no qual seu código está sendo executado no momento. Observe que há um MSR que define a base APIC real (disponível apenas para processadores com uma família> 5). O MADT contém uma base APIC local e, em sistemas de 64 bits, também pode conter um campo que especifica uma redefinição de 64 bits do endereço base, que você deve usar em seu lugar. Você pode deixar a base local da APIC apenas onde a encontrar ou movê-la para onde quiser. Nota: Não acho que você possa movê-lo além do quarto GB de RAM.

Para permitir que o APIC local receba interrupções, você deve configurar o Spurious Interrupt Vector Register. O valor correto para esse campo é o número do IRQ que você deseja mapear para interrupções falsas com os 8 bits inferiores e o 8º bit definido como 1 para realmente ativar o APIC (consulte a especificação para obter detalhes). Você deve selecionar um número de interrupção com os 4 bits inferiores definidos; A maneira mais fácil é usar 0xFF. Isso é importante para alguns processadores mais antigos, pois para esses valores os 4 bits inferiores devem ser definidos como 1.

Desative o 8259 PIC corretamente. Isso é quase tão importante quanto configurar o APIC. Você faz isso em duas etapas: mascarando todas as interrupções e reatribuindo o IRQ. Disfarçar todas as interrupções as desativa no PIC. Remapear interrupções é o que você provavelmente já fez quando usou o PIC: deseja que as solicitações de interrupção iniciem em 32 em vez de 0 para evitar conflitos com exceções (nos modos de processador protegido e longo (longo), porque As primeiras 32 interrupções são exceções). Então você deve evitar usar esses vetores de interrupção para outros fins. Isso é necessário porque, apesar de você mascarar todas as interrupções do PIC, ele ainda pode gerar interrupções falsas, que serão processadas incorretamente como exceções no seu kernel.
Vamos para o SMP.

Multitarefa simétrica: inicialização


A sequência de inicialização é diferente para diferentes CPUs. O Guia do programador da Intel (Seção 7.5.4) contém um protocolo de inicialização para processadores Intel Xeon e não abrange processadores antigos. Para um algoritmo geral de “todos os tipos de processadores”, consulte Especificação do multiprocessador Intel.

Para 80486 (com APIC 8249DX externo), você deve usar o IPIT INIT, seguido pelo IPI "INIT level an-assert" sem nenhum SIPI. Isso significa que você não pode dizer a eles por onde começar a executar seu código (a parte vetorial do SIPI) e eles sempre começam a executar o código do BIOS. Nesse caso, você define o valor de redefinição do BIOS do CMOS para "inicialização a quente com salto em distância" (ou seja, define CMOS 0x0F como 10) para que o BIOS execute jmp far ~ [0: 0x0469] e, em seguida, define o segmento e o deslocamento Pontos de entrada do ponto de acesso em 0x0469.

O IPI “INIT level an-assert” não é suportado em novos processadores (Pentium 4 e Intel Xeon), e o AFAIK é completamente ignorado nesses processadores.

Para processadores mais recentes (P6, Pentium 4), basta um SIPI, mas não tenho certeza se os processadores Intel mais antigos (Pentium) ou de outros fabricantes precisam de um segundo SIPI. Também é possível que exista um segundo SIPI no caso de uma falha na entrega do primeiro SIPI (ruído do barramento, etc.).

Normalmente, envio o primeiro SIPI e espero para ver se o AP aumenta o número de processadores em execução. Se não aumentar esse contador dentro de alguns milissegundos, enviarei um segundo SIPI. Isso é diferente do algoritmo geral da Intel (que possui um atraso de 200 microssegundos entre o SIPI), mas tentar encontrar uma fonte de tempo que possa medir com precisão o atraso de 200 microssegundos durante uma inicialização antecipada não é tão simples. Também descobri que no hardware real, se o atraso entre o SIPI for muito longo (e você não estiver usando o meu método), o AP principal poderá executar o código de inicialização do AP inicial para o SO duas vezes (o que, no meu caso, fará com que o SO pense que temos o dobro de processadores do que realmente somos).

Você pode transmitir esses sinais no barramento para iniciar cada dispositivo presente. No entanto, você também pode ativar os processadores que foram especialmente desativados (porque estavam "com defeito").

Procurando informações usando a tabela MT


Algumas informações (que podem não estar disponíveis em máquinas mais recentes) destinadas ao multiprocessamento. Primeiro você precisa encontrar a estrutura do ponteiro flutuante MP. Ele está alinhado em um limite de 16 bytes e contém uma assinatura no início de "_MP_" ou 0x5F504D5F. O sistema operacional deve procurar no EBDA, no espaço da ROM do BIOS e no último kilobyte de "memória base"; o tamanho da memória base é especificado em um valor de 2 bytes de 0x413 em kilobytes, menos 1 KB. É assim que a estrutura se parece:

struct mp_floating_pointer_structure { char signature[4]; uint32_t configuration_table; uint8_t length; // In 16 bytes (eg 1 = 16 bytes, 2 = 32 bytes) uint8_t mp_specification_revision; uint8_t checksum; // This value should make all bytes in the table equal 0 when added together uint8_t default_configuration; // If this is not zero then configuration_table should be // ignored and a default configuration should be loaded instead uint32_t features; // If bit 7 is then the IMCR is present and PIC mode is being used, otherwise // virtual wire mode is; all other bits are reserved } 

Aqui está a aparência da tabela de configuração que a estrutura flutuante do ponteiro aponta para:

 struct mp_configuration_table { char signature[4]; // "PCMP" uint16_t length; uint8_t mp_specification_revision; uint8_t checksum; // Again, the byte should be all bytes in the table add up to 0 char oem_id[8]; char product_id[12]; uint32_t oem_table; uint16_t oem_table_size; uint16_t entry_count; // This value represents how many entries are following this table uint32_t lapic_address; // This is the memory mapped address of the local APICs uint16_t extended_table_length; uint8_t extended_table_checksum; uint8_t reserved; } 

Após a tabela de configuração, estão as entradas entry_count, que contêm mais informações sobre o sistema, seguidas por uma tabela estendida. As entradas têm 20 bytes para representar o processador ou 8 bytes para outra coisa. Aqui está a aparência do processador APIC e dos registros de E / S.

 struct entry_processor { uint8_t type; // Always 0 uint8_t local_apic_id; uint8_t local_apic_version; uint8_t flags; // If bit 0 is clear then the processor must be ignored // If bit 1 is set then the processor is the bootstrap processor uint32_t signature; uint32_t feature_flags; uint64_t reserved; } 

Aqui está a entrada IO APIC.

 struct entry_io_apic { uint8_t type; // Always 2 uint8_t id; uint8_t version; uint8_t flags; // If bit 0 is set then the entry should be ignored uint32_t address; // The memory mapped address of the IO APIC is memory } 

Procurando informações com a APIC


Você pode encontrar a tabela MADT (APIC) na ACPI. A tabela lista os APICs locais, cujo número deve corresponder ao número de núcleos no seu processador. Os detalhes desta tabela não estão aqui, mas você pode encontrá-los na Internet.

Iniciar AP


Depois de coletar as informações, você precisa desativar o PIC e se preparar para a E / S da APIC. Você também precisa configurar o BSP do APIC local. Então inicie o AP usando SIPI.

Código para o lançamento de kernels:

Observo que o vetor especificado na inicialização indica o endereço inicial: vetor 0x8 - endereço 0x8000, vetor 0x9 - endereço 0x9000, etc.

 // ------------------------------------------------------------------------------------------------ static u32 LocalApicIn(uint reg) { return MmioRead32(*g_localApicAddr + reg); } // ------------------------------------------------------------------------------------------------ static void LocalApicOut(uint reg, u32 data) { MmioWrite32(*g_localApicAddr + reg, data); } // ------------------------------------------------------------------------------------------------ void LocalApicInit() { // Clear task priority to enable all interrupts LocalApicOut(LAPIC_TPR, 0); // Logical Destination Mode LocalApicOut(LAPIC_DFR, 0xffffffff); // Flat mode LocalApicOut(LAPIC_LDR, 0x01000000); // All cpus use logical id 1 // Configure Spurious Interrupt Vector Register LocalApicOut(LAPIC_SVR, 0x100 | 0xff); } // ------------------------------------------------------------------------------------------------ uint LocalApicGetId() { return LocalApicIn(LAPIC_ID) >> 24; } // ------------------------------------------------------------------------------------------------ void LocalApicSendInit(uint apic_id) { LocalApicOut(LAPIC_ICRHI, apic_id << ICR_DESTINATION_SHIFT); LocalApicOut(LAPIC_ICRLO, ICR_INIT | ICR_PHYSICAL | ICR_ASSERT | ICR_EDGE | ICR_NO_SHORTHAND); while (LocalApicIn(LAPIC_ICRLO) & ICR_SEND_PENDING) ; } // ------------------------------------------------------------------------------------------------ void LocalApicSendStartup(uint apic_id, uint vector) { LocalApicOut(LAPIC_ICRHI, apic_id << ICR_DESTINATION_SHIFT); LocalApicOut(LAPIC_ICRLO, vector | ICR_STARTUP | ICR_PHYSICAL | ICR_ASSERT | ICR_EDGE | ICR_NO_SHORTHAND); while (LocalApicIn(LAPIC_ICRLO) & ICR_SEND_PENDING) ; } void SmpInit() { kprintf("Waking up all CPUs\n"); *g_activeCpuCount = 1; uint localId = LocalApicGetId(); // Send Init to all cpus except self for (uint i = 0; i < g_acpiCpuCount; ++i) { uint apicId = g_acpiCpuIds[i]; if (apicId != localId) { LocalApicSendInit(apicId); } } // wait PitWait(200); // Send Startup to all cpus except self for (uint i = 0; i < g_acpiCpuCount; ++i) { uint apicId = g_acpiCpuIds[i]; if (apicId != localId) LocalApicSendStartup(apicId, 0x8); } // Wait for all cpus to be active PitWait(10); while (*g_activeCpuCount != g_acpiCpuCount) { kprintf("Waiting... %d\n", *g_activeCpuCount); PitWait(10); } kprintf("All CPUs activated\n"); } 

 [org 0x8000] AP: jmp short bsp ;     -   BSP xor ax,ax mov ss,ax mov sp, 0x7c00 xor ax,ax mov ds,ax ; Mark CPU as active lock inc byte [ds:g_activeCpuCount] ;   ,   jmp zop bsp: xor ax,ax mov ds,ax mov dword[ds:g_activeCpuCount],0 mov dword[ds:g_activeCpuCount],0 mov word [ds:0x8000], 0x9090 ;  JMP   2 NOP' ;   ,   

Agora, como você entende, para que o sistema operacional use muitos núcleos, você precisa configurar a pilha para cada núcleo, cada núcleo, suas interrupções etc., mas o mais importante é que, ao usar o multiprocessamento simétrico, todos os núcleos têm os mesmos recursos: uma memória, um PCI, etc., e o sistema operacional só pode paralelizar tarefas entre os núcleos.

Espero que o artigo não seja chato o suficiente e bastante informativo. Da próxima vez, acho, podemos falar sobre como eles costumavam desenhar na tela (e agora eles desenham), sem usar shaders e placas de vídeo legais.

Boa sorte

Source: https://habr.com/ru/post/pt426497/


All Articles