Multithreading…

Não precisa de um processador especializado para multithreading funcionar… Você pode implementar até mesmo num antigo processador de 8 bits!

Uma thread é um “fio” (ou “novelo”) de execução de um processo. Podemos interromper esse fio, guardar todo o estado do processo (todos o conteúdo dos registradores no ponto em que a thread foi interrompida), recuperar o estado do outro processo e pular para ele. Isso sendo feito de tempos em tempos… Quem determina o “tempo de saltar”? Ora, um timer, o que mais?

Suponha que o circuito do computador possua um timer que envia um pedido de interrupção a cada \delta t, onde esse diferencial de tempo pode ser ajustada para, por exemplo, 20 ms. Suponha também que tenhamos uma lista contendo o contexto de cada processo em execução, mais ou menos assim:

struct task_context {
  reg_size_t registers[NUM_REGISTERS];
  size_t IP, SP;
  struct task_context *next;
};

Tudo o que o tratador da interrupção tem que fazer é obter todos os valores dos registradores, , salvá-los no “contexto” atual (no nó atual da lista), obter o próximo item da lista, recuperar todos o conteúdo dos registradores e saltar para o endereço contido no Instruction Pointer, ao retornar da interrupção do timer (pseudo código abaixo):

/* Aponta para o contexto da tarefa
   atual da lista. */
extern struct task_context *current_task;

__attribute__((interrupt))
void task_switch(void)
{
  salve_context(current_task);

  current_task = current_task->next;

  restore_context(current_task);

  change_ret_address(current_task->IP);

  /* Executará um IRETD aqui, saltando 
     para (R|E)IP. */
}

A lista, obviamente, é circular e conta com pelo menos 1 item. Assim, a cada 20 ms o processo atual será interrompido e o controle entregue ao próximo processo da lista.

Essencialmente é isso que os processadores Intel, no modo i386, fazem com o auxílio do próprio processador… Neste modo temos um registrador chamado TR (Task Register) que contém um seletor para um segmento de estado de tarefa (Task State Segment) que tem a estrutura necessária para armazenar todos os registradores da tarefa corrente.

A interrupção do timer, executada no ring 0, fará com que o processador armazene, automaticamente, o contexto da tarefa do userspace no TSS atual (apontado pelo TR) e fará apenas um salto para um Task Gate, que contém o seletor do TSS da próxima tarefa… E, voilà! Seus dois processos estão operando em time slice!

Task switching não é multitarefa simétrica!

O que descrevi acima, que pode ser feito com ou sem o auxílio do processador, é chamado de task switching. Em sistemas com apenas um processador temos que compartilhar o tempo de processamento entre threads, dando uma fatia \delta t para cada um… Nos sistemas atuais temos mais que um processador lógico (processadores dentro de um processador físico). Eles são capazes de executar código em paralelo, cada um com seu código particular… Eles são mesmo processadores separados que, no caso da Intel, podem compartilhar o espaço de memória.

O termo multitarefa simétrica existe em contraste a “assimetria” do time slicing ou task switching e funciona bem diferente do que foi descrito acima…

Se você tem um sistema com um processador Intei i5, pense nele como se tivesse 4 processadores separados. Cada um deles têm que ser inicializado, colocado em modo protegido, e 3 deles devem ser colocados para dormir… É colocado num estado de “parado” (halted). Apenas um deles, chamado de bootstrap processor, executa código o tempo todo…

A única maneira de sair dum estado de halt é através de uma interrupção. Enquanto o processador não receber uma requisição de interrupção ele ficará paradinho, sem consumir uma gota de energia, dormindo… esperando… E, na arquitetura de multiprocessamento da Intel (Hyperthreading) os processadores, desde o Pentium, tem um controlador de interrupções incorporado chamado Local APIC, que servem para tratar e distribuir interrupções que deverão ser tratadas por cada “núcleo”… Uma das funções dos Local APIC é enviar e receber interrupções específicas para “acordar” um processador… Outra das funções é gerar uma interrupção com base num timer para o próprio processador (para usar no task switching).

Os outros 3 processadores, além do bootstrap processor (BP), são chamados de application processors (AP). Quando o BP quer que um AP execute uma thread ele envia uma interrupção especial que o acorda e indica para onde ele deve saltar… Quando a thread termina o processador deverá executar uma instrução HLT e volta a dormir… simples assim.

Bem… não tão simples…

É claro que cada um dos APs podem efetuar task switching também! De fato, é tarefa de um código do sistema operacional chamado scheduler coordenar quais tarefas são “chaveadas”, quais são “simétricas” e em quais processadores elas são disparadas!

No caso das tarefas chaveadas temos outro problema: Algumas devem receber mais tempo de processamento do que outras. As threads que executam por mais tempo antes de serem chaveadas têm maior prioridade do que as que têm fatia de tempo disponível menor… Esse não é um problema, normalmente, em threads simétricas a não ser que os processadores sejam realmente isolados, inclusive com acessos à memória isoladas… Daí temos que lidar com um troço chamado NUMA (Non Uniform Memory Access) que é uma abstração usada para fazer parecer que todos os processadores têm acesso à toda memória, quando, na verdade, não têm… Há uma maneira maluca de comunicação entre eles que emula a simetria do acesso… Isso pode causar atrasos nas threads simétricas…

NUMA é algo que pretendo falar em outra oportunidade, já que não é a realidade da maioria das implementações de PCs, especialmente domésticos.

Resumo: Chavear tarefas se resume em salvar o “contexto” (registradores), recuperar o outro “contexto” e saltar para onde esse outro contexto te diz para saltar… E disparar threads simétricas é só uma questão de acordar o outro processador (AP) dizendo “salta pra lá!”…

Dois avisos: No modo x86-64 o chaveamento de tarefas assistido pelo processador não é “assistido”. O TSS continua existindo, mas não contém nenhum “contexto” útil (pelo menos, não no mesmo modelo do modo i386). Assim, salvar e recuperar contextos é tarefa do tratador de interrupção, não do processador em si.

O outro aviso é que o “contexto”, no modo i386, corresponde apenas aos registradores de uso geral, das pilhas dos 4 níveis de prioridade possíveis do processador (ring 0 até 3), do registrador de base de páginas (CR3) e, se presente, do mapa de permissão de I/O (falo disso depois)… O contexto não contém o estado do co-processador matemático 80×87 e dos registradores SIMD (MMX, SSE, AVX…). Esses outros “contextos” têm que ser salvos e recuperados pelo tratador de interrupção também!

Anúncios