Pular para o conteúdo

Bring-Up de Multiprocessador (SMP)

Todo artigo sobre processos e escalonamento nesta wiki, Multitasking, Context Switching, Schedulers, Synchronization, já presume mais de uma CPU rodando ao mesmo tempo; nenhum deles explica como as CPUs além da primeira, os Application Processors (APs), de fato começam a executar código em primeiro lugar. Um kernel inicializa em exatamente um núcleo, o Bootstrap Processor (BSP), escolhido pelo firmware antes de o kernel sequer rodar, e todo outro núcleo fica ocioso, esperando num estado de inicialização, até o BSP deliberadamente acordá-lo.

Acordar um AP significa o BSP enviar a ele uma sequência específica de interrupções entre processadores através do Local APIC: uma IPI de INIT reseta o estado interno do AP alvo, seguida de duas Startup IPIs (SIPI), enviadas com um pequeno atraso entre elas, cada uma direcionando o AP a começar a executar num endereço físico específico codificado na própria SIPI.

void start_ap(uint8_t apic_id, uint8_t trampoline_page) {
send_ipi(apic_id, ICR_INIT, 0);
delay_ms(10);
send_ipi(apic_id, ICR_STARTUP, trampoline_page);
delay_us(200);
send_ipi(apic_id, ICR_STARTUP, trampoline_page); // enviada duas vezes por especificação
}

Enviar a SIPI duas vezes é uma parte deliberada da especificação original, uma medida defensiva contra um AP que perdeu a primeira (hardware mais antigo em particular nem sempre tinha garantia de capturar uma única SIPI de forma confiável); um AP bem-comportado que já processou a primeira SIPI simplesmente ignora a segunda, então enviar as duas incondicionalmente é seguro em toda implementação em vez de ser uma solução de contorno necessária só em algumas.

O endereço que uma SIPI codifica é um número de página, não um endereço completo, o que restringe onde a primeiríssima instrução do AP pode viver a algum lugar no primeiro 1 MB de memória física, alinhado a 4 KB: um AP começa a execução em real mode de 16 bits, idêntico a como o próprio BSP começou ao ligar, independentemente de qual modo o BSP esteja rodando no momento. Isso significa que um kernel precisa de um trampolim pequeno e autocontido, um bloco de código de real mode de 16 bits montado para rodar num endereço físico fixo e baixo, que leva o AP através da mesma transição de protected mode (e, para um kernel de 64 bits, long mode) que o próprio BSP já realizou uma vez durante o boot.

[bits 16]
[org 0x8000] ; trampolim carregado num endereço físico baixo fixo
trampoline_start:
cli
lgdt [gdt_descriptor] ; a mesma GDT que o BSP já configurou
mov eax, cr0
or eax, 1
mov cr0, eax
jmp CODE_SEG:protected_entry

O trampolim é tipicamente montado como um pequeno blob binário separado e copiado para o lugar num endereço físico baixo fixo pelo BSP antes de qualquer SIPI ser enviada, já que o AP não tem forma alguma de carregar qualquer coisa do disco nem buscar código de onde quer que a imagem principal do kernel esteja mapeada nesse ponto inicial; todo byte que ele executa já precisa estar sentado no exato endereço para o qual a SIPI mandou saltar.

Uma vez passadas as próprias transições de modo do trampolim, cada AP ainda precisa de seus próprios recursos privados antes de poder rodar com segurança o mesmo código de kernel que o BSP roda: sua própria stack, já que compartilhar uma única stack entre múltiplos núcleos executando concorrentemente a corromperia quase imediatamente, e sua própria estrutura de dados por CPU (contendo, entre outras coisas, o próprio TSS daquele núcleo, já coberto como um requisito rígido de SMP, e qualquer estado de escalonador rastreado por núcleo em vez de globalmente).

mov esp, [ap_stack_ptr] ; cada AP carrega sua própria stack, pré-alocada
mov eax, [ap_id]
call kernel_ap_entry ; salta para código de kernel comum, agnóstico de modo

O BSP convencionalmente pré-aloca uma stack por AP antes de enviar qualquer SIPI, e comunica o endereço de cada uma ao AP correspondente através de um local fixo e acordado que o trampolim sabe ler (uma pequena tabela indexada por APIC ID, ou um valor corrigido no próprio blob do trampolim antes de ser copiado para o lugar), já que o AP não tem alocador algum próprio disponível ainda para requisitar memória dinamicamente nesse ponto. Só depois que um AP tem sua própria stack e estrutura por CPU completamente configuradas ele entra no caminho de código comum e agnóstico de modo do kernel, os mesmos primitivos de escalonador e sincronização que Synchronization já cobre, indistinguível a partir desse ponto de código rodando no BSP.

O BSP precisa esperar confirmação de que um AP de fato iniciou (uma flag que o AP define em memória compartilhada uma vez que alcança código comum de kernel, sondada com um timeout) antes de enviar a SIPI do próximo AP ou presumir que o bring-up está completo, já que nada sobre enviar uma SIPI em si garante que o núcleo alvo executou o trampolim com sucesso. Um trampolim construído para ser independente de posição dentro de seu próprio endereço de carga fixo, em vez de presumir que sempre será copiado para esse exato mesmo endereço em todo boot, evita ter que remontá-lo para um layout físico diferente se o local de memória baixa escolhido algum dia estiver indisponível (já reservado pelo firmware, por exemplo) em alguma máquina específica.

  1. ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Capítulo 9.4: o protocolo de inicialização MP, incluindo a sequência INIT-SIPI-SIPI descrita acima.
  • PIC & APIC: o Local APIC e seu mecanismo de IPI através do qual a sequência de despertar deste artigo é enviada.
  • Synchronization: por que trazer um segundo núcleo à tona importa para tudo que já presume mais de um núcleo ativo.
  • A Task State Segment: o requisito de TSS por CPU que todo AP precisa satisfazer antes de conseguir receber uma interrupção com segurança.
  • TLB Shootdown: o mecanismo de IPI coberto aqui, reaproveitado para uma mensagem completamente diferente uma vez que toda CPU está rodando.