Pular para o conteúdo

TLB Shootdown

Um TLB shootdown é o mecanismo que um kernel usa para invalidar uma tradução desatualizada em cache na TLB de outra CPU, algo que nem invlpg nem um reload de CR3 conseguem fazer sozinhos, já que Paging já cobre os dois como operações estritamente locais: cada um só toca a TLB do núcleo que o executa.

Todo núcleo de CPU mantém sua própria TLB privada, preenchida independentemente conforme aquele núcleo percorre tabelas de página e armazena os resultados em cache. Quando um núcleo modifica uma entrada de tabela de página, talvez desmapeando uma página sendo liberada, sua própria invalidação local limpa essa entrada desatualizada de sua própria TLB, mas todo outro núcleo que por acaso tenha essa mesma tradução em cache continua usando-a, completamente alheio a que o mapeamento subjacente mudou. Num sistema de núcleo único isso nunca foi um problema que valesse a pena nomear: só havia uma TLB para manter consistente. Sob SMP, isso vira um dos riscos de corretude mais afiados que o código de gerenciamento de memória de um kernel precisa acertar, já que o modo de falha é exatamente do tipo difícil de reproduzir: um núcleo lê ou escreve através de um endereço físico que um núcleo diferente já reaproveitou para outra coisa, um bug intermitente que só aparece quando um par específico de núcleos entra em corrida numa ordem específica.

Um shootdown funciona fazendo a CPU que mudou o mapeamento enviar uma interrupção entre processadores para toda outra CPU que possa ter a tradução desatualizada em cache, o mesmo mecanismo de IPI do Local APIC já coberto para acordar um núcleo no boot, reaproveitado aqui para uma mensagem diferente: o handler de IPI de cada destinatário roda invlpg (ou recarrega CR3, se estiver invalidando o espaço de endereçamento inteiro em vez de uma página) contra sua própria TLB local, depois sinaliza de volta que terminou.

void tlb_shootdown(void *addr, cpu_mask_t targets) {
invlpg(addr); // invalida localmente primeiro
atomic_set(&shootdown_pending, targets);
send_ipi(targets, IPI_TLB_SHOOTDOWN, (uintptr_t)addr);
while (atomic_read(&shootdown_pending) != 0)
cpu_relax(); // espera todo alvo confirmar
}
// roda em cada CPU alvo quando a IPI chega
void tlb_shootdown_handler(void *addr) {
invlpg(addr);
atomic_and(&shootdown_pending, ~(1 << current_cpu_id()));
}
CPU0 invalidando localmente, enviando uma IPI para CPU1 e CPU2, depois esperando as duas invalidarem sua própria TLB e reconhecerem antes de liberar a páginaCPU0CPU1CPU2invlpg (local)IPIinvlpg + ackinvlpg + ackespera as duasconfirmaçõespágina livre p/ liberar

A CPU que iniciou o shootdown precisa esperar todo alvo de fato reconhecer conclusão antes de poder prosseguir com segurança, mais comumente sondando uma bitmask compartilhada da qual cada destinatário limpa seu próprio bit, já que liberar a página física subjacente (ou reaproveitá-la para outra coisa) antes de toda entrada de TLB desatualizada apontando para ela ter sumido permitiria que um acesso em andamento de outro núcleo lesse ou escrevesse através de uma tradução que não significa mais o que significava.

Enviar a IPI para toda CPU incondicionalmente é correto mas desperdiça recursos num sistema com muitos núcleos, a maioria dos quais nunca sequer tocou o espaço de endereçamento em questão; um kernel que rastreia, por espaço de endereçamento, quais CPUs de fato rodaram uma thread dele recentemente consegue restringir o conjunto de alvos apenas a esses núcleos, pulando a interrupção (e a espera) por completo para núcleos sem nada a invalidar. Esse rastreamento em si precisa ser conservador em vez de exato: uma CPU que rodou uma thread daquele espaço de endereçamento e depois trocou para outra coisa ainda precisa estar no conjunto de alvos a menos que suas entradas de TLB para aquele espaço de endereçamento já sejam sabidamente esvaziadas de alguma outra forma (um reload subsequente de CR3 para um espaço de endereçamento não relacionado, por exemplo, que esvazia toda entrada não global como efeito colateral independentemente de quais mapeamentos específicos mudaram).

Um shootdown não é de graça mesmo quando corretamente direcionado: toda CPU destinatária precisa parar o que está fazendo, receber a interrupção, e rodar o handler, uma interrupção real de qualquer trabalho útil em andamento ali, motivo pelo qual um kernel fazendo várias pequenas mudanças de mapeamento em sequência comumente as agrupa num único shootdown cobrindo uma faixa de endereços em vez de enviar uma IPI por invlpg individual. Interrupções precisam permanecer habilitadas na CPU que iniciou o shootdown enquanto ela sonda esperando confirmações, especificamente porque o próprio caminho de confirmação pode depender dessa CPU tratar suas próprias IPIs recebidas de outros shootdowns em andamento simultaneamente; desabilitar interrupções durante toda a espera arrisca um deadlock onde duas CPUs estão cada uma esperando uma confirmação que a outra não consegue entregar até que suas próprias interrupções sejam reabilitadas.

  1. ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Capítulo 4.10: comportamento de TLB e as instruções de invalidação das quais um shootdown é construído.