Interrupções
PIC, vetores, hooks e notificações de hardware no ciclo de eventos Minix.
Uma interrupção permite ao dispositivo assinalar um evento sem o programa estar sempre a consultar o estado. No PC clássico, o pedido passa pelo controlador de interrupções antes de o processador o atender. No Minix, há ainda outra etapa: o kernel notifica o processo que subscreveu esse pedido.
IRQ, vetor e rotina
Uma IRQ é uma linha de pedido de interrupção. O PIC i8259 regista pedidos, aplica máscaras e escolhe qual apresentar ao processador. No PC clássico há dois PICs em cascata. A IRQ2 do primeiro liga ao segundo, pelo que não há 16 entradas independentes para dispositivos.
| Dispositivo | IRQ global |
|---|---|
| Timer 0 | 0 |
| Teclado | 1 |
| COM2 | 3 |
| COM1 | 4 |
| RTC | 8 |
| Rato PS/2 | 12 |
Na máscara do PIC, um bit a 1 desativa a linha correspondente. É uma máscara de bloqueio, ou seja, não a confundas com a máscara de eventos recebidos no Minix, onde um bit a 1 assinala uma notificação.
O PIC entrega um vetor, um número que o processador usa para escolher a entrada na tabela de descritores de interrupção, a IDT em IA-32. O vetor não é o número da IRQ, por isso não derives um vetor do número da IRQ no código da aplicação.
Em IA-32, o atendimento guarda o contexto exigido, executa a rotina e restaura o contexto para continuar o programa interrompido. A rotina precisa de reconhecer o pedido no controlador, com EOI, e terminar pelo mecanismo de retorno de interrupção apropriado. No modelo clássico dos slides, isso aparece como IRETD. A arquitetura ARM tem exceções e controladores próprios; não usa a IDT nem as instruções IN, OUT ou IRETD do x86.
Dois atendimentos diferentes
A rotina genérica do Minix, no kernel, recebe a interrupção real, mascara a linha, notifica os interessados, aplica a política de reativação, envia EOI e regressa. A função timer_ih ou kbc_ih que escreves no laboratório corre no teu processo, quando o teu ciclo lê a notificação e a chama.
Isto muda o raciocínio sobre concorrência, porque as tuas funções C não são chamadas arbitrariamente pela CPU enquanto outra instrução C tua está a correr. No modelo de um processo e um ciclo de receção, chamas cada handler de forma sequencial. Não acrescentes volatile como se isso resolvesse sincronização, nem envies EOI ao PIC por iniciativa própria: o kernel trata da interrupção física.
O handler do dispositivo recolhe estado e dados. A lógica da aplicação interpreta-os depois. Para teclado e rato, uma notificação permite recolher o byte disponível, por isso não esperes ali pelos restantes bytes de um scancode ou pacote. Podem chegar em notificações futuras.
Hook e máscara de notificação
sys_irqsetpolicy(irq, policy, &hook) usa hook em dois sentidos:
- À entrada, o valor escolhe o bit da notificação que o processo quer usar.
- À saída, a variável contém o identificador que deves passar a outras kernel calls sobre essa subscrição.
Guarda o número do bit pedido antes da chamada. A máscara a testar é 1u << bit_number; com IRQ 12 e bit pedido 2, a máscara de notificação é 0x04, não 1 << 12. Um enunciado do teste de abril de 2025 pedia o caso do bit 7: hook entra com o valor 7 e a máscara a testar é BIT(7), ou 0x80. Passar BIT(7), que vale 128, como valor de hook pediria um bit que não existe no campo de notificações.
Repara que a variável hook muda de sentido: entra com 2 e sai com o identificador do kernel. A máscara continua a usar o 2 inicial; o valor devolvido serve para as kernel calls seguintes. Os bits das várias subscrições do processo devem ser distintos e caber no campo de notificações.
IRQ_REENABLE autoriza a rotina genérica a reativar a linha. Sem essa política, o processo tem de chamar sys_irqenable quando estiver pronto. IRQ_EXCLUSIVE impede a partilha da subscrição com outro driver. É relevante para periféricos como o rato, onde não queres que o driver normal do Minix consuma os mesmos dados. No timer, a subscrição pode coexistir com a do sistema.
Ciclo completo com timer
Esta função completa destina-se a um módulo do ambiente Minix/LCF, com permissão para subscrever IRQ0. Chama wait_timer_ticks(60) depois de inicializares o programa de apoio. Se o timer estiver a 60 Hz, o efeito esperado é regressar após cerca de um segundo. Não configura a frequência e não é um programa de C portátil.
#include <lcom/lcf.h>
#include <stdint.h>
int wait_timer_ticks(uint32_t limit) {
if (limit == 0) return OK;
const int notification_bit = 0;
int hook = notification_bit;
const uint32_t mask = 1u << notification_bit;
int result = sys_irqsetpolicy(0, IRQ_REENABLE, &hook);
if (result != OK) return result;
result = sys_irqenable(&hook);
if (result != OK) {
sys_irqrmpolicy(&hook);
return result;
}
uint32_t ticks = 0;
while (ticks < limit) {
message msg;
int ipc_status;
result = driver_receive(ANY, &msg, &ipc_status);
if (result != OK) break;
if (!is_ipc_notify(ipc_status)) continue;
if (_ENDPOINT_P(msg.m_source) != HARDWARE) continue;
if (msg.m_notify.interrupts & mask) ++ticks;
}
int disable_result = sys_irqdisable(&hook);
int remove_result = sys_irqrmpolicy(&hook);
if (result != OK) return result;
if (disable_result != OK) return disable_result;
return remove_result;
}
Há três filtros: a mensagem é uma notificação, vem do hardware e contém o bit do timer. Uma mensagem normal não contém necessariamente o significado que esperas em m_notify.
driver_receive bloqueia enquanto não há mensagem. Depois da receção, o teu código chama o handler. Um ciclo a consultar uma variável à espera de que o handler corra sozinho impediria chegar à receção e ao despacho que o chamam.
Notificações podem agrupar pedidos. O bit significa que há um pedido assinalado; não contém um contador de quantas interrupções ocorreram enquanto o processo estava ocupado. Contar um tick por notificação é o modelo usado nos pequenos ciclos do laboratório, mas não garante um relógio sem perdas se bloqueares longamente o atendimento.
Vários dispositivos na mesma notificação
Suponhamos que pediste o bit 0 para o timer e o bit 3 para o teclado. Uma notificação com interrupts = 0x09 contém ambos. Os testes devem ser independentes:
if (pending & timer_mask) timer_ih();
if (pending & keyboard_mask) keyboard_ih();
Um else if descartaria um evento quando os dois bits estivessem ativos. Define também a ordem da lógica da aplicação. Se um timeout e uma tecla chegarem juntos, a política de aceitação da tecla no limite do tempo deve ser explícita.
Latência e limpeza
A interrupção não tem latência zero. O evento espera enquanto as interrupções estão bloqueadas, enquanto pedidos prioritários são atendidos e enquanto o processo ainda não recebeu a notificação. Um handler curto reduz o tempo ocupado, mas não elimina esses custos.
Ao terminar, repõe configurações alteradas e remove cada subscrição obtida. Trata também os caminhos de falha parcial. A ausência de dados pode resultar de uma linha não ativada, máscara errada, dispositivo que não gera o evento, leitura que não consome o buffer ou outro driver que o consome primeiro. Verifica estas hipóteses por observações, sem trocar várias configurações ao mesmo tempo.
Exercícios
Separar IRQ e bit de notificação
Subscreves o rato na IRQ12 e pedes inicialmente hook = 3. Que máscara de notificação deves conservar para testar o evento, em decimal?
Pista
Solução
A máscara é 1 << 3 = 8, ou 0x08. IRQ12 escolhe a linha física. O valor devolvido em hook serve nas kernel calls seguintes e não substitui a máscara guardada. A mesma distinção vale para o bit 7 do teste de abril de 2025: passa-se o número 7 em hook e testa-se com BIT(7), ou seja 0x80; passar BIT(7) como número pediria um bit inexistente.
Erros frequentes
Usar 1 << 12; usar diretamente o número 3 como máscara; ou testar com 7 em vez de BIT(7), o que verifica os bits 0 a 2 em conjunto em vez do bit 7.
Atender dois bits pendentes
Timer usa máscara 0x02 e teclado usa 0x10. A notificação contém 0x12. Um programa usa if para timer e else if para teclado. Que evento perde e como corriges o despacho?
Pista
Solução
O programa chama o timer e deixa de chamar o teclado. Deve usar dois if independentes, com pending & mask em cada um. Depois aplica a política definida para a ordem de eventos, sobretudo num limite de timeout.
Confere a tua resposta:
- Reconheci que 0x12 contém 0x02 e 0x10.
- Usei testes independentes.
- Defini a ordem da aplicação.
Erros frequentes
Supor que só pode existir uma IRQ em cada mensagem ou comparar pending == mask, o que falharia quando há vários bits.
Localizar a rotina que escreves
No modelo Minix do laboratório, a CPU chama diretamente
timer_ih do teu ficheiro C a qualquer instante? Explica o
caminho entre IRQ0 e essa função e quem envia EOI.
Pista
Solução
O kernel atende a interrupção física pela rotina genérica, notifica o
processo e trata EOI e retorno de interrupção. O processo recebe a
notificação com driver_receive, confere a máscara e chama a
sua timer_ih de forma sequencial no ciclo. Não é essa
função que recebe diretamente a entrada da IDT.
Confere a tua resposta:
- Distingui a rotina genérica do kernel.
- Expliquei quando corre timer_ih no processo.
- Atribuí EOI e retorno físico ao kernel.
Erros frequentes
Aplicar automaticamente o modelo de uma ISR de kernel ao handler C chamado pelo ciclo de mensagens.
Contar sem supor um por um
O timer gerou duas interrupções enquanto o processo atendia o teclado. O kernel entrega uma notificação com o bit do timer ativo uma vez. Quantos ticks podes garantir a partir desse bit?
Pista
Solução
Garantes pelo menos um, não exatamente dois. O bit significa que há um pedido assinalado. Nos pequenos ciclos do laboratório conta-se um tick por notificação, mas esse modelo pode perder ticks se bloqueares o atendimento.
Confere a tua resposta:
- Tratei o bit como pedido assinalado.
- Não converti um bit em vários ticks.
- Assinalei a possível perda sob carga.
Erros frequentes
Somar sempre um tick por interrupção física ou ler a notificação como contador.