Quer criar um OS? Tem que entender o hardware!

Estou escrevendo essas “notinhas” sobre funcionamento do processador e de conceitos elementares sobre threading, paginação etc, para simplificar para você, que me lê, como é que o seu computador de fato funciona. Isso é essencial para qualquer um que tenha a pretensão de desenvolver um sistema operacional, por mais simples que ele seja.

No último artigo (e em outros) eu falei sobre “interrupções”. Já citei o conceito aqui antes: O processador está, o tempo todo, executando instruções… Mas, um circuito, externo à CPU, pode pedir que o fluxo normal de processamento seja “interrompido” para que uma rotina específica seja executada. Trata-se de um dispositivo pedindo a atenção do processador e a maneira de “chamar a atenção” é através de um pedido de interrupção (interruption request ou IRQ).

A antiga placa de áudio SoundBlaster, por exemplo, tinha um strap que a colocava mapeada na IRQ 7. Isso quer dizer que, quando a placa pedir a atenção do processador ela informará que a requisição nº 7 deve ser atendida, se o processador assim quiser… Existem, no PC, 16 interrupções possíveis: Da IRQ 0 até a IRQ 15. Bem… de fato, só existem 15, a IRQ 2 é reservada (ou, mais precisamente, é “cascateada” para o segundo controlador de interrupções que existe no PC). A lista dos mapeamentos das IRQs, num PC sem dispositivos externos extras, é essa:

IRQ Nº Designada para:
0 Timer 0
1 Teclado
2 *** reservada (2º PIC) ***
3 Porta serial 2 (se presente)
4 Porta serial 1 (se presente)
5 Porta paralela 2 (se presente)
6 Controladora de diskette
7 Porta paralela 1 (se presente)
8 Relógio de tempo real (RTC)
9 Mapeada para a IRQ 2
10 *** não usada ***
11 *** não usada ***
12 Mouse
13 Usada pelo 80×87
14 Controladora de HD 1
15 Controladora de HD 2

O número da IRQ diz qual é a sua prioridade. Se dois dispositivos pedirem a atenção do processador, o com menor número (maior prioridade) é atendido primeiro. Mas há um porém. A IRQ 2 tem mais prioridade que as subsequentes, mas ela é mapeada para o segundo controlador de interrupções (PIC), que trata as interrupções de 8 até 15 e assim, portando, a lista de prioridades fica: 0, 1, 8, 9, 10, 11, 12, 13, 14, 15, 3, 4, 5, 6 e 7.

Obs: Reparou que o mouse está na lista das IRQs? Entendeu porque, quando sua máquina “trava”, às vezes, o mouse continua “funcionando”? Outra coisa: É meio estranho que a prioridade da IRQ do teclado seja maior do que as demais. Teclado é um dispositivo lento (não há necessidade de gerar interrupções com \delta t menor que 20 ms, por exemplo (um humano normal não é tão rápido e, muito menos, uma tecla – que é uma chave mecânica!)… Só posso supor que a IBM fez isso para que, em caso de eventos catastróficos, o usuário pudesse usar alguma sequência (como Ctrl+Alt+Del) para resetar o sistema. Repare que os HDs também são lentos, em relação ao processador e têm suas IRQs mapeadas com a menor prioridade possível. Isso pode contradizer o mapeamento da IRQ 6 (diskettes), mas é que esses dispositivos exigem temporização mais “refinada”.

O processador não entende esse negócio de prioridade de IRQs muito bem… Ele depende de um cicuito integrado chamado PIC (Programmable Interrupt Controller). O PIC recebe os pedidos de interrupção dos dispositivos, os enfileira de acordo com as prioridades e envia um pedido para o processador. Este receberá o “vetor” de interrupção (o índice da tabela de interrupções) do PIC, interromperá o processamento normal e chamará a rotina apontada na tabela de interrupção…

O processador pode escolher não aceitar essas requisições de interrupções. Para isso base zerar o flag IF (Interrupt Flag). O PIC continuará enfileirando os pedidos, mas o processador não responderá que aceitou um pedido! É claro, podemos “mascarar” uma interrupção no próprio PIC, dizendo a ele para não aceitar interrupções de uma ou mais IRQs.

Além das IRQs existe outra interrupção de hardware que não pode ser ignorada. Trata-se da NMI (Non Maskable Interrupt). Sempre que o processador receber essa requisição ele é obrigado a aceitá-la. Na arquitetura dos PCs a NMI é usada quase que exclusivamente para indicar algum erro de hardware (ele pode ter outros usos, dependendo do fabricante do PC).

Interrupções por software:

Alguns já tiveram contato com a instrução INT. Mas essa não é a única interrupção de software que existe… De fato, a instrução INT (e suas irmãs INT3 e INTO) são uma emulação de uma interrupção de hardware… O operando imediato que segue INT é o índice do vetor de interrupção contido na tabela de interrupções.

As outras 3 interrupções de software são Faltas, Exceções ou Abortos. A famigerada GPF (General Protection Fault)  é uma delas. Essas interrupções acontecem em circunstâncias específicas, sendo que “Abortos” são “exceções” ou “faltas” que não podem ser “recuperados”. Uma falta é um aviso de que algo está errado… Uma exceção é um aviso mais grave e o aborto é, necessariamente um “ih! deu merda!”. No último caso o processador encontra-se num estado “instável”.

Uma interrupção pode ser interrompida!

Se o processador estiver tratando uma IRQ 3, por exemplo, e uma IRQ 0 acontece, o tratador da IRQ 3 pode ser interrompido. Para evitar isso, se for necessário, é prudente zerar o flag IF (é para isso que ele existe!).

É interessante notar que uma interrupção é como se fosse uma chamada de função comum, exceto pelo fato de que alguns dados adicionais são colocados na pilha… Para IRQs e NMIs, o endereço e o seletor CS da rotina interrompida são colocados lá. Para faltas, exceções e abortos um código de erro também é colocado… Algumas falhas colocam dados extras, como é o caso da Page Fault, que coloca o endereço linear onde ocorreu a falta.

A maneira de retornar dessa rotina é através da instrução IRET ou suas variantes (IRETD, no caso do modo i386, ou IRETQ, no caso do modo x86-64).

Exemplo de uso de IRQ 14:

E comum que dispositivos usem IRQs para avisar para o processador que um evento ocorreu, ao invés de pedirem que esse faça algo por eles. É o caso de alguns dispositivos ATA (HDs). Quando você pede para o HD ler ou gravar um bloco de dados, pode fazê-lo via uma técnica chamada DMA (Direct Memory Access). Essencialmente você informa a um controlador DMA o início e o tamanho do bloco de dados, na memória, que contém os setores que serão lidos e gravados em disco, daí, você informa à controladora de disco que leia/grave n setores, à partir do cilindro/cabeça/setor especificados, mas o faça via DMA.

O controlador de DMA colocará o barramento de endereços e dados do processador em “aberto”, de tempos em tempos, lendo/escrevendo os dados que o controlador de disco receber/enviar e, quando chegar ao fim o controlador de disco enviará uma IRQ 14 para o processador, dizendo: “terminei!”.

Isso faz com que as rotinas fiquem bem mais simples: Só precisamos disparar o processo, ir fazer outra coisa até que a IRQ 14 seja atendida, onde podemos setar um flag informando ao sistema operacional que o último pedido de leitura/escrita foi feito… Caso contrário, já que HDs são bem mais lentos do que o processador, precisaríamos ficar num loop, testando pelo término da operação (polling), o que pode tornar o sistema bem mais complexo e lento.

O fato do DMA pedir a liberação dos barramentos de tempos em tempos não é lá grande coisa, já que nos processadores modernos os dados e código estarão, quase sempre, nos caches (L1 até L3).

Aliás… esse é o mesmo funcionamento de placas de áudio como a SoundBlaster e, acredito, chips on-board modernos… A interrupção ocorre como aviso do término de um evento (transferência de um buffer, por exemplo).

Controladores DMA têm um pequeno problema:

Ninguém avisou para a IBM que poderíamos ter, algum dia, mais que 16 MiB de memória RAM! O controlador de DMA padrão, usado na arquitetura ISA dos PCs suporta endereçamento de até 24 bits. Mesmo assim, em blocos de 64 KiB (16 bits). Ou seja, os 8 bits superiores do endereço físico corresponde a uma “página” de DMA, os 16 bits inferiores ao offset no buffer onde os dados serão lidos/gravados…

A página não é incrementada ou decrementada (pode-se fazer um dos dois a cada leitura/escrita de bytes) junto com o offset. Isso quer dizer que, no caso de um overflow (o offset passar de 0xffff para 0x0000, ao ser incrementado) ou underflow (passar de 0x0000 para 0xffff, ao ser decrementado), a página não será incrementada/decrementada junto.

Eu ainda não lidei com outros modos de DMA em chipsets mais recentes, como os ICH7 ou ICH9 da Intel. Acredito que existam modos de DMA que lidem com 32 bits, mas não estou certo disso.

Anúncios