College Online
0%

Subsistema de Entrada/Saída

Módulo 7 · Aula 1 ~25 min de leitura Nível: Intermediário

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.

Diagrama — Componentes de hardware de E/S
┌──────────────────────────────────────────────────────────┐
│                         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

Dois métodos de comunicação com dispositivos
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.

C — E/S programada: enviando um byte pela serial
#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.

Diagrama — Fluxo de E/S por interrupção
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!
C — Handler de interrupção (simplificado)
// 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.

Diagrama — Transferência com DMA
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.

Diagrama — Camadas do subsistema de E/S
┌──────────────────────────────────────┐
│  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.

C — Estrutura de um driver de caractere (Linux)
#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)
*/
Shell — Explorando E/S no Linux
# 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

Técnicas de buffering
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:

Diagrama — Camadas de I/O no harness.os
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

  1. Execute cat /proc/interrupts no 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.
  2. 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?
  3. O Linux trata "tudo como arquivo" (/dev/sda, /dev/null, /dev/random). Explique como a tabela file_operations do driver permite que read() 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)?

  • DMA permite que dispositivos mais lentos sejam usados
  • DMA é mais seguro porque o kernel controla cada byte
  • A CPU fica livre para executar outros processos durante a transferência de dados
  • DMA não precisa de interrupções