Pular para o conteúdo

Troca de Contexto

Uma troca de contexto é o ato de salvar o estado de CPU de uma tarefa atualmente em execução e restaurar em seu lugar o estado previamente salvo de uma tarefa diferente, de modo que a execução seja retomada na nova tarefa exatamente de onde parou pela última vez, sem qualquer indicação, da perspectiva da própria tarefa, de que tempo passou ou outra tarefa rodou nesse meio-tempo. Tudo em multitarefa que não é política de escalonamento é, no fundo, a mecânica de realizar essa troca corretamente e de forma razoavelmente eficiente, já que ela acontece com frequência, comumente centenas de vezes por segundo em um sistema ocupado.

O estado de execução de uma tarefa consiste em tudo que determina o que ela faz em seguida: os registradores de propósito geral, o instruction pointer, o registrador de flags, e o stack pointer. Em vez de salvar cada um desses individualmente em uma estrutura separada, a abordagem convencional os salva empurrando-os para a própria stack da tarefa, significando que “restaurar” uma tarefa mais tarde é, em grande parte, apenas restaurar seu stack pointer salvo e desempilhar de volta os mesmos valores, de forma simétrica a como foram salvos.

; salva o estado da tarefa atualmente em execução na própria stack dela
push rax
push rbx
push rcx
; ... registradores de propósito geral restantes ...
push rbp
mov [current_task_rsp], rsp ; lembra onde paramos
mov rsp, [next_task_rsp] ; troca para a stack salva da próxima tarefa
pop rbp
; ... registradores de propósito geral restantes, em ordem reversa ...
pop rcx
pop rbx
pop rax
ret ; retorna para onde quer que o endereço de retorno
; salvo da próxima tarefa, em sua própria stack, aponte

O ret final é o que de fato retoma a execução da próxima tarefa: como a stack de cada tarefa, no momento em que foi trocada por último, tem um endereço de retorno sentado onde ret espera encontrar um, restaurar o stack pointer daquela tarefa e executar ret transfere o controle para exatamente onde aquela tarefa parou: seja isso no meio de código comum, ou, para uma tarefa que nunca rodou, um ponto de entrada designado colocado ali deliberadamente quando a tarefa foi criada pela primeira vez.

Se as duas tarefas pertencem a processos diferentes, a troca também recarrega CR3 com a raiz de tabela de página da nova tarefa, o que, como efeito colateral, descarta toda entrada não global do TLB, um custo real de desempenho, já que os próximos vários acessos de memória na nova tarefa sofrerão TLB misses que, de outra forma, seriam acertos de cache. Trocar entre duas threads do mesmo processo pode pular esse passo por completo, já que ambas as threads já compartilham um espaço de endereçamento idêntico, e recarregar CR3 com o valor que já contém não realizaria nada além de descartar o TLB desnecessariamente, um detalhe que vale a pena verificar explicitamente, em vez de recarregar CR3 incondicionalmente em toda troca.

Registradores pertencentes à unidade de ponto flutuante e às extensões SIMD (os registradores XMM/YMM/ZMM, e o estado legado da FPU x87) não estão incluídos na sequência de salvamento de propósito geral acima, e salvá-los e restaurá-los incondicionalmente em toda troca de contexto é comparativamente caro, já que esses bancos de registrador são consideravelmente maiores que o conjunto de propósito geral, e uma tarefa que só realiza aritmética de inteiros e ponteiros nunca os toca. Muitos kernels, em vez disso, implementam troca preguiçosa de FPU: o acesso a ponto flutuante é deixado desabilitado (via bit TS de CR0) para uma tarefa recém-trocada, as instruções exatas de salvamento/restauração e a exceção #NM que isso dispara cobertas por completo em FXSAVE/XSAVE e Estado de FPU/SIMD, e só na primeira instrução que de fato tenta usá-lo uma falha ocorre, momento em que o kernel salva o estado de FPU de qualquer tarefa que o possuía antes, restaura o da tarefa atual, e reabilita o acesso, significando que o salvamento/restauração mais caro só acontece para tarefas que de fato usam instruções de ponto flutuante ou SIMD, em vez de incondicionalmente para toda tarefa em toda troca, independentemente de precisar disso.

O conjunto exato de registradores salvos, e a ordem em que são salvos e restaurados, precisa ser perfeitamente simétrico: uma incompatibilidade entre a sequência de push e a sequência de pop correspondente carrega lixo silenciosamente nos registradores errados, em vez de produzir qualquer erro diagnosticável no ponto do engano, já que ambas as sequências executam sem falha independentemente de de fato corresponderem uma à outra. Interrupções também tipicamente precisam estar desabilitadas durante a troca em si: uma interrupção chegando na breve janela depois que rsp foi atualizado para apontar para a stack da próxima tarefa, mas antes que os registradores dessa tarefa tenham terminado de ser restaurados, empurraria seu próprio frame de interrupção para uma stack em um estado inconsistente e parcialmente restaurado.

  1. ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Capítulo 13: descreve o estado x87/SSE com o qual o bit TS e a técnica de troca preguiçosa acima interagem.