Subsistema de Entrada/Saída
Vídeo da aula estará disponível em breve
Hardware de E/S
Dispositivos de E/S são extremamente diversos: teclados, discos, GPUs, placas de rede, sensores. O SO precisa de uma camada de abstração que torne essa diversidade gerenciável. Tudo começa pelo hardware.
┌──────────────────────────────────────────────────────────┐
│ CPU │
└────────────────────┬─────────────────────────────────────┘
│ Barramento do sistema (PCIe, etc.)
┌───────────────┼───────────────┬──────────────┐
│ │ │ │
┌────┴────┐ ┌──────┴──────┐ ┌─────┴─────┐ ┌────┴────┐
│ Memória │ │Controlador │ │Controlador│ │Controlador│
│ RAM │ │ de Disco │ │ de Rede │ │ USB │
└─────────┘ │ (AHCI/NVMe)│ │ (NIC) │ │ (xHCI) │
└──────┬──────┘ └─────┬─────┘ └────┬────┘
│ │ │
┌──────┴──────┐ ┌─────┴─────┐ ┌────┴────┐
│ SSD/HDD │ │ Ethernet │ │Teclado, │
│ │ │ Cable │ │Mouse,etc│
└─────────────┘ └───────────┘ └─────────┘
Cada dispositivo tem um CONTROLADOR (chip dedicado):
- Possui registradores de status, comando e dados
- Interface padronizada (AHCI para SATA, NVMe para SSDs)
- CPU se comunica via portas de I/O ou memory-mapped I/O
Portas de I/O vs Memory-Mapped I/O
1. PORT-MAPPED I/O (x86 tradicional):
Espaço de endereços SEPARADO para I/O.
Instruções especiais: IN / OUT
Exemplo (x86):
outb(0x3F8, 'A'); // escreve 'A' na porta serial COM1
char c = inb(0x3F8); // lê da porta serial COM1
Portas clássicas:
0x60 = teclado PS/2
0x1F0 = controlador IDE primário
0x3F8 = porta serial COM1
0xCF8 = PCI config
2. MEMORY-MAPPED I/O (moderno, ARM, PCIe):
Registradores do dispositivo mapeados no espaço de
endereços da memória. Acesso com load/store normais.
Exemplo:
volatile uint32_t* gpu_reg = (uint32_t*)0xFE200000;
*gpu_reg = 0x01; // escreve no registrador da GPU
// (na verdade, acessa hardware, não RAM)
Vantagens:
- Não precisa de instruções especiais
- Funciona com ponteiros C normais
- MMU pode controlar acesso (proteção)
GPUs modernas: mapeiam GBs de VRAM no espaço de endereços
PCIe BARs: cada dispositivo recebe uma faixa de endereços
E/S Programada (Polling)
Na E/S programada, a CPU verifica repetidamente o status do dispositivo em um loop (busy waiting ou polling). É a técnica mais simples mas também a mais desperdiçadora de CPU.
#define SERIAL_PORT 0x3F8
#define STATUS_REG (SERIAL_PORT + 5)
#define TX_READY 0x20 // bit 5 = transmitter empty
void serial_putchar(char c) {
// Polling: espera o transmissor ficar pronto
while (!(inb(STATUS_REG) & TX_READY))
; // CPU fica presa aqui! (busy wait)
outb(SERIAL_PORT, c); // envia o byte
}
void serial_print(const char* s) {
while (*s)
serial_putchar(*s++);
}
/*
Problema: a CPU não faz NADA útil enquanto espera.
Uma porta serial a 9600 baud transmite ~960 bytes/segundo.
CPU moderna: ~4 GHz = ~4 bilhões de ciclos/segundo.
Para cada byte: ~4 milhões de ciclos desperdiçados no loop!
*/
E/S por Interrupção
Em vez de polling, o dispositivo interrompe a CPU quando precisa de atenção. A CPU executa outro trabalho e responde à interrupção quando ela chega.
CPU executando processo P1
│
│ ← dispositivo gera IRQ (interrupt request)
│
┌────┴────┐
│ Hardware │ 1. Salva contexto de P1 (registradores, PC)
│ da CPU │ 2. Consulta o vetor de interrupções
└────┬────┘ 3. Salta para o handler (ISR)
│
┌────┴──────────────────────┐
│ Interrupt Service Routine │ 4. Trata a interrupção
│ (handler do driver) │ (lê dados, atualiza buffer)
└────┬──────────────────────┘
│
┌────┴────┐
│ Hardware │ 5. Restaura contexto de P1
│ da CPU │ 6. Retorna à execução normal
└────┬────┘
│
CPU continua executando P1 (ou outro processo)
Vetor de interrupções (x86 IDT):
IRQ 0: Timer (tick do escalonador)
IRQ 1: Teclado
IRQ 14: Disco IDE primário
IRQ 16+: dispositivos PCI (MSI-X)
Latência de interrupção:
Hardware: ~100ns para salvar contexto
Software: ~1-10μs para handler
Muito mais eficiente que polling para I/O lenta!
// No kernel Linux, handlers são registrados com request_irq()
static irqreturn_t teclado_handler(int irq, void* dev) {
unsigned char scancode = inb(0x60); // lê tecla
// Coloca no buffer circular do teclado
keyboard_buffer_push(scancode);
// Acorda processos esperando input (read() bloqueante)
wake_up_interruptible(&keyboard_wait_queue);
return IRQ_HANDLED;
}
// Registro durante inicialização do driver:
request_irq(1, teclado_handler, 0, "keyboard", NULL);
/*
Handler deve ser RÁPIDO:
- Interrupções desabilitadas durante execução (em muitas archs)
- Trabalho pesado vai para bottom half (tasklet, workqueue)
- Top half: lê dados do hardware, agenda bottom half
- Bottom half: processa dados, acorda processos
*/
DMA (Direct Memory Access)
Para transferências grandes (disco, rede), nem polling nem interrupção por byte são eficientes. O DMA permite que o dispositivo transfira dados diretamente para a memória RAM, sem intervenção da CPU.
SEM DMA (E/S programada):
CPU lê byte do disco → escreve na RAM → repete 4096 vezes
CPU ocupada durante toda a transferência
COM DMA:
1. CPU programa o controlador DMA:
- Endereço de origem (dispositivo/porta)
- Endereço de destino (buffer na RAM)
- Quantidade de bytes
- Direção (device→memory ou memory→device)
2. DMA controller executa a transferência:
CPU ←──────────────── livre para outras tarefas!
│
DMA ════════════════════════════════════════
controller → [byte][byte][byte]... → RAM
│
Dispositivo (disco, NIC, etc.)
3. Quando termina, DMA gera UMA interrupção para a CPU:
"Transferência completa, dados estão na RAM"
Ganho:
Sem DMA: CPU processa cada byte (4096 interrupções para 4KB)
Com DMA: CPU programa 1 vez, recebe 1 interrupção no final
CPU livre para executar processos durante a transferência!
Todos os dispositivos modernos usam DMA:
NVMe SSDs: DMA via PCIe (gigabytes/segundo)
NICs: DMA para zero-copy networking
GPUs: DMA para transferir texturas/dados
Camadas de Software de E/S
O subsistema de E/S é organizado em camadas, cada uma com responsabilidade específica. Essa arquitetura isola a complexidade do hardware da simplicidade da interface para o programador.
┌──────────────────────────────────────┐
│ Processo de usuário │ read(fd, buf, n)
│ (user space) │ write(fd, buf, n)
├──────────────────────────────────────┤
│ Interface de system calls │ sys_read(), sys_write()
│ (independente de dispositivo) │ open(), close(), ioctl()
├──────────────────────────────────────┤
│ Subsistema de E/S do kernel │ Buffering, caching,
│ (independente de dispositivo) │ escalonamento de I/O,
│ │ error handling
├──────────────────────────────────────┤
│ Device drivers │ Cada driver conhece
│ (dependente de dispositivo) │ o hardware específico
│ NVMe driver, e1000 NIC driver, etc. │ Registradores, protocolos
├──────────────────────────────────────┤
│ Interrupt handlers │ Top/bottom halves
│ (dependente de hardware) │ Tratamento de IRQs
├──────────────────────────────────────┤
│ Hardware │ Controladores, DMA,
│ │ barramentos
└──────────────────────────────────────┘
Princípio: cada camada só conhece a interface da camada abaixo.
O processo chama read() sem saber se é SSD, HD, NFS ou /dev/null.
Device Drivers
Um device driver é o módulo de software que sabe como conversar com um dispositivo específico. No Linux, drivers são módulos de kernel que implementam uma interface padronizada.
#include <linux/module.h>
#include <linux/fs.h>
// Operações que o driver implementa
static ssize_t meu_read(struct file* f, char __user* buf,
size_t len, loff_t* off) {
// Lê dados do hardware e copia para userspace
char dado = inb(PORTA_DO_DISPOSITIVO);
copy_to_user(buf, &dado, 1);
return 1;
}
static ssize_t meu_write(struct file* f, const char __user* buf,
size_t len, loff_t* off) {
char dado;
copy_from_user(&dado, buf, 1);
outb(PORTA_DO_DISPOSITIVO, dado);
return 1;
}
// Tabela de operações (interface padronizada)
static struct file_operations fops = {
.owner = THIS_MODULE,
.read = meu_read,
.write = meu_write,
.open = meu_open,
.release = meu_close,
};
// Registro do driver
static int __init driver_init(void) {
register_chrdev(240, "meudispositivo", &fops);
return 0;
}
/*
Em user space:
echo "A" > /dev/meudispositivo → chama meu_write()
cat /dev/meudispositivo → chama meu_read()
"Everything is a file" no Unix:
/dev/sda → driver de bloco (disco)
/dev/tty0 → driver de caractere (terminal)
/dev/null → driver especial (descarta tudo)
/dev/random → driver especial (gera entropia)
*/
# Listar dispositivos de bloco
$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 500G 0 disk
├─sda1 8:1 0 450G 0 part /
└─sda2 8:2 0 50G 0 part [SWAP]
nvme0n1 259:0 0 1T 0 disk
└─nvme0n1p1 259:1 0 1T 0 part /home
# Ver interrupções em uso
$ cat /proc/interrupts | head -15
CPU0 CPU1
0: 46 0 IO-APIC timer
1: 2834 0 IO-APIC i8042 (keyboard)
8: 0 0 IO-APIC rtc0
14: 0 0 IO-APIC ata_piix
16: 45231 0 IO-APIC ehci_hcd
# Ver módulos (drivers) carregados
$ lsmod | head -5
Module Size Used by
nvidia 2345678 3
e1000e 98765 0 # driver Intel NIC
nvme 54321 0 # driver NVMe
# Estatísticas de I/O por dispositivo
$ iostat -x 1
Buffering e Caching
BUFFERING: armazenar dados temporariamente durante transferência
Sem buffer: CPU ←──→ Dispositivo (byte a byte)
Buffer simples: CPU ←→ [Buffer] ←→ Dispositivo
Buffer duplo: CPU ←→ [Buf A] [Buf B] ←→ Dispositivo
(enquanto um enche, outro esvazia)
Buffer circular: produtor-consumidor com array circular
Usado em: pipes, sockets, áudio, serial
CACHING: manter cópia de dados frequentemente acessados
Page cache do Linux:
- Dados lidos do disco ficam em cache na RAM
- Próxima leitura: cache hit (não precisa ir ao disco)
- Escritas: write-back (escreve no cache, flush periódico)
- free -h mostra: "buff/cache" é memória usada para cache
$ free -h
total used free shared buff/cache available
Mem: 16G 4.2G 2.1G 500M 9.7G 11G
9.7G em cache! O Linux usa RAM "livre" para cache
automaticamente. Não é desperdício — é otimização.
No harness.os
O subsistema de E/S do SO tem paralelos diretos com a arquitetura do harness.os. A organização em camadas, a abstração de dispositivos e o caching são padrões que aparecem em qualquer sistema que precise gerenciar acesso a recursos diversos:
Camadas de E/S do SO Camadas do harness.os
═══════════════════ ═════════════════════
User space (read/write) Agente (log_decision, get_knowledge)
Processo não sabe o hardware Agente não sabe o banco
Subsistema de I/O MCP Server (harness-os)
Buffering, caching, scheduling Tool routing, caching, rate limiting
Device drivers Neon MCP / SQL layer
Conhece o hardware Conhece o Postgres
Hardware Neon Postgres (cloud DB)
Disco, rede, etc. Storage, WAL, replication
O princípio é o mesmo:
Abstração em camadas isola complexidade.
Cada camada tem interface simples e implementação complexa.
Adicionar novo "dispositivo" = adicionar novo driver,
sem mudar as camadas acima.
Exercícios
- Execute
cat /proc/interruptsno seu sistema. Identifique: qual IRQ é do teclado? Qual dispositivo gera mais interrupções? Execute o comando duas vezes com intervalo de 10 segundos e calcule a taxa de interrupções por segundo para o timer. - Explique por que DMA é essencial para SSDs NVMe que operam a 7 GB/s. Calcule: se a CPU transferisse byte a byte com interrupção por byte, quantas interrupções por segundo seriam necessárias?
- O Linux trata "tudo como arquivo" (
/dev/sda,/dev/null,/dev/random). Explique como a tabelafile_operationsdo driver permite queread()funcione de forma diferente para cada dispositivo, apesar de ser a mesma system call.
Resumo
Verifique seu entendimento
Qual é a principal vantagem do DMA sobre E/S programada (polling)?
- Dispositivos se comunicam via port-mapped I/O (instruções IN/OUT) ou memory-mapped I/O (load/store)
- E/S programada (polling) desperdiça CPU; E/S por interrupção libera a CPU entre eventos
- DMA permite transferências diretas dispositivo-memória sem intervenção da CPU
- O subsistema de E/S é organizado em camadas: user space, kernel, drivers, hardware
- Device drivers implementam interface padronizada (file_operations no Linux)
- Buffering e caching (page cache) otimizam desempenho de I/O