Entrada/salida y dispositivos

"Si no tuvieras dispositivos, el SO sería sólo un programa. Lo que lo hace SO es manejar todo lo que está afuera de la CPU."

Qué vas a aprender en este capítulo

Tu computadora interactúa con el mundo: discos, redes, teclados, pantallas, USB, GPUs. El kernel orquesta esa interacción. Vas a aprender la taxonomía de dispositivos, las tres formas en que la CPU se comunica con ellos (polling, interrupciones, DMA), cómo se organizan los drivers en Linux, cómo el SO planifica lecturas en discos, y por qué la idea unix "todo es un archivo" sigue siendo tan elegante después de 50 años.

7.1 La idea: una abstracción para todos

💡 Intuición

Un teclado, un disco, una impresora, una placa de red, una webcam — son piezas de hardware completamente distintas. Si el SO tuviera que tratarlas cada una con su propia API, tu programa sería imposible de escribir.

Unix tomó una decisión radical: todos los dispositivos son archivos. Leés el teclado con read(). Escribís a la impresora con write(). La red, el sonido, hasta el reloj — todos archivos en /dev.

Eso te da una API uniforme. Un programa que copia archivos también puede copiar bytes desde una webcam a un archivo de video, o desde la red a la pantalla. Las syscalls son las mismas; los detalles los maneja el driver correspondiente.

Ejemplo cotidiano:

$ cat /dev/random | head -c 16    # 16 bytes aleatorios del kernel
$ cat /dev/null > archivo.log     # vaciás un log
$ ls > /dev/tty                   # imprime a tu terminal

/dev/random, /dev/null, /dev/tty parecen archivos pero son interfaces a dispositivos o servicios del kernel. La uniformidad simplifica todo: una vez que entendés read/write, ya manejás "casi todo".

7.2 Categorías de dispositivos

📐 Fundamento

Unix los divide en tres tipos:

Dispositivos por bloques (block devices)

  • Datos en bloques de tamaño fijo (típicamente 512 bytes o 4 KB).
  • Acceso aleatorio: podés ir directamente al bloque N.
  • Ejemplos: discos (HDD, SSD, NVMe), particiones, dispositivos RAID.
  • En /dev aparecen como /dev/sda, /dev/sdb1, /dev/nvme0n1.

Dispositivos por caracteres (character devices)

  • Datos como flujo de bytes, sin estructura fija.
  • Acceso secuencial: leer byte tras byte.
  • Ejemplos: teclados, mouse, terminales, puertos serie.
  • En /dev: /dev/tty1, /dev/random, /dev/null, /dev/zero.

Dispositivos especiales (pseudo-devices)

  • No corresponden a hardware real, pero el kernel los expone como archivos.
  • Ejemplos:
    • /dev/null — descarta todo lo que se le escriba.
    • /dev/zero — devuelve ceros infinitos al leer.
    • /dev/random, /dev/urandom — números aleatorios.

ls -l muestra el tipo en el primer carácter:

Letra Tipo
- archivo regular
d directorio
l symlink
b block device
c character device
p pipe (FIFO)
s socket
$ ls -l /dev/sda /dev/null /dev/tty
brw-rw---- 1 root disk    8,  0 may  5 /dev/sda
crw-rw-rw- 1 root root    1,  3 may  5 /dev/null
crw-rw-rw- 1 root tty     5,  0 may  5 /dev/tty

Los números (8, 0) y (1, 3) son major y minor — identifican qué driver y qué instancia.

7.3 Cómo se comunica la CPU con los dispositivos

📐 Fundamento

Tres mecanismos clásicos:

1. Polling (consulta activa)

La CPU pregunta al dispositivo si está listo, en un loop:

while (!dispositivo.listo()) {
    ;  // espera ocupada
}
leer_dato();

Ventaja: simple. Desventaja: gasta CPU mientras espera. Si el dispositivo es lento (segundos), tirás millones de ciclos al vacío.

Útil para dispositivos ultrarrápidos donde el overhead de las interrupciones es mayor que el de pollear.

2. Interrupciones

El dispositivo interrumpe a la CPU cuando termina:

  1. La CPU le dice al dispositivo "empezá".
  2. La CPU se va a hacer otra cosa.
  3. Cuando el dispositivo termina, levanta una señal de interrupción.
  4. La CPU pausa lo que hacía, salta al manejador de interrupciones (ISR — interrupt service routine).
  5. El ISR atiende, y la CPU vuelve a lo que hacía.

Ventaja: la CPU se libera mientras el dispositivo trabaja. Desventaja: cada interrupción cuesta (cambio de contexto). Si hay muchas, interrupt storm y todo va lento.

Ejemplo: el teclado. Cada tecla que apretás genera una interrupción.

3. DMA (Direct Memory Access)

Para transferencias grandes, ni polling ni interrupciones por byte sirven — la CPU se ahogaría. DMA es un controlador especializado que mueve datos entre dispositivo y memoria sin involucrar a la CPU.

Ciclo:

  1. La CPU programa al controlador DMA: "transferí 1 MB del disco al buffer en RAM 0x80000".
  2. La CPU se va.
  3. DMA hace la transferencia, accediendo directamente a la RAM.
  4. Cuando termina, una sola interrupción avisa a la CPU.

Ventaja: la CPU dedica casi cero ciclos al transporte de datos. Discos, redes, GPUs — todos usan DMA.

El paisaje moderno: una placa de red de 10 Gbps mueve gigabytes por segundo sin pisar la CPU. Sin DMA, eso sería físicamente imposible.

7.4 Drivers

📐 Fundamento

Un driver es código del kernel que sabe cómo hablar con un tipo específico de hardware. Cada modelo de placa de red, cada SSD, cada GPU tiene su driver.

Estructura típica en Linux:

+------------------+
| Aplicación       |   ← read(), write()
+------------------+
| VFS              |   ← traduce a operaciones de dispositivo
+------------------+
| Driver genérico  |   ← p.ej. "block layer" o "tty layer"
+------------------+
| Driver específico|   ← p.ej. driver de NVMe Samsung
+------------------+
| Hardware         |
+------------------+

Por eso podés cambiar tu disco SATA por uno NVMe sin que cambien tus programas — la capa de bloques abstrae los detalles.

Drivers en Linux: módulos. Pueden estar:

  • Compilados al kernel (built-in, siempre cargados).
  • Como módulos (.ko), cargables con insmod / modprobe y descargables con rmmod.
$ lsmod                          # módulos cargados
$ modprobe nombre_modulo         # cargar
$ modinfo nombre                  # info del módulo

Trabajar sin reiniciar. Cuando enchufás un USB, el kernel detecta el evento, busca un driver compatible, lo carga. Comando dmesg muestra el log:

$ dmesg | tail -5
[12345.678] usb 2-1: new high-speed USB device number 4
[12345.700] usb-storage 2-1: USB Mass Storage device detected
[12345.701] sd 6:0:0:0: [sdb] 30277632 512-byte logical blocks

7.5 Planificación de disco (HDD)

📐 Fundamento

En un disco mecánico (HDD), los datos están en platos giratorios y un cabezal se mueve para leer/escribir. El movimiento del cabezal es caro (milisegundos). Si recibís 100 pedidos de bloques en posiciones aleatorias, el orden importa muchísimo.

Algoritmos clásicos

FCFS (orden de llegada). Simple, pero el cabezal va y viene sin sentido.

SSTF (Shortest Seek Time First). Atender el más cercano al cabezal actual. Eficiente, pero starvation posible para extremos.

SCAN (elevator). Cabezal va en una dirección hasta el extremo, después al revés. Como un ascensor. Razonable y justo.

C-SCAN. Variante: solo atiende en una dirección, vuelve al inicio sin atender. Más equitativo aún.

Ejemplo. Cabezal en 50, pedidos: 10, 22, 75, 90, 35.

Algoritmo Orden Movimientos totales
FCFS 50→10→22→75→90→35 40+12+53+15+55 = 175
SSTF 50→35→22→10→75→90 15+13+12+65+15 = 120
SCAN (subiendo) 50→75→90→100→35→22→10 25+15+10+65+13+12 = 140

En SSDs

No hay cabezal. Acceso aleatorio es casi tan rápido como secuencial. Los algoritmos de scheduling se simplifican mucho: ya no es necesario ordenar por posición, solo por prioridad.

Linux moderno usa multiqueue I/O scheduler (mq-deadline, kyber, bfq, none):

$ cat /sys/block/sda/queue/scheduler
[none] mq-deadline kyber bfq

Para un SSD, none suele ser óptimo (entregar en orden, no reordenar). Para un HDD, bfq o mq-deadline.

7.6 Comandos útiles

🛠️ En la práctica

Listar discos y particiones:

$ lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
sda      8:0    0 465.8G  0 disk
├─sda1   8:1    0   512M  0 part /boot/efi
└─sda2   8:2    0 465.3G  0 part /

Tasa de E/S:

$ iostat -x 1 5
Device   r/s   w/s   rkB/s   wkB/s  await
sda     12.5   8.3   245.2   142.7   2.13

await = latencia promedio en ms. Si crece, hay contención.

Procesos haciendo I/O:

$ sudo iotop

Muestra en tiempo real qué procesos leen/escriben más.

Inspeccionar un dispositivo:

$ sudo smartctl -a /dev/sda    # salud del disco (S.M.A.R.T.)
$ hdparm -I /dev/sda           # información del HDD
$ nvme list                     # lista NVMe

Ver interrupciones:

$ cat /proc/interrupts | head
            CPU0       CPU1       CPU2       CPU3
   0:         42          0          0          0   IO-APIC    2-edge      timer
   1:        211        102          0          0   IO-APIC    1-edge      i8042
   ...

i8042 es el teclado/mouse PS/2. Cada vez que apretás algo, esos contadores crecen.

7.7 Buffering y caching

📐 Fundamento

Para mitigar la lentitud de los dispositivos, el SO usa buffers y cachés.

Buffering. Acumular datos en RAM antes de transferir. Razones:

  1. Diferencia de velocidad. El productor produce más rápido que el consumidor (o al revés).
  2. Diferencia de tamaño de bloques. El programa escribe 10 bytes; el disco solo acepta bloques de 4 KB.
  3. Atomicidad. Acumular hasta tener un bloque completo y escribirlo de una vez.

Caching. Mantener datos recientes en RAM por si se vuelven a usar.

  • Read cache. Lo viste con la page cache: la primera lectura va al disco, las siguientes son instantáneas.
  • Write cache (write-back). Las escrituras van a RAM y eventualmente al disco. Rápido pero arriesgado.
  • Write-through. Cada escritura va al disco. Lento pero seguro.

sync y fsync:

fsync(fd);    // forzar que los datos del fd lleguen a disco
sync();        // todos los datos pendientes

Programas críticos (bases de datos, transacciones) llaman fsync antes de confirmar al usuario.

7.8 Protocolos modernos: NVMe, NIC, USB

🛠️ En la práctica

NVMe (Non-Volatile Memory Express). Reemplazo de SATA para SSDs. Múltiples colas paralelas (hasta 64K), bajo overhead, latencias de microsegundos. Aprovecha que SSD no necesita el modelo "una cola, un cabezal" de HDD.

Cada CPU tiene su propia cola → no hay contención entre cores. Por eso un NVMe moderno hace 1+ millón de IOPS.

NIC modernas (red). 10/25/100 Gbps requieren:

  • DMA scatter-gather (transferir páginas no contiguas).
  • TSO (TCP Segmentation Offload) — la NIC fragmenta paquetes, no la CPU.
  • RSS (Receive Side Scaling) — diferentes flujos a diferentes CPUs.

Sin esos features de hardware, una NIC de 10 Gbps saturaría una CPU completa solo para mover bytes.

USB. Bus serial complejo. El kernel implementa USB host controller + drivers por clase (mass storage, HID — teclado/mouse, video, audio, ...). Cuando enchufás un mouse USB nuevo, el kernel detecta su descriptor, identifica que es HID, carga el driver HID genérico — sin necesidad de instalar nada.

7.9 Ejercicios

✏️ Ejercicio 7.1 — Identificar tipos

Para cada uno, decí si es block, character o pseudo:

a. /dev/sda1 b. /dev/random c. /dev/null d. /dev/tty e. /dev/loop0 f. /dev/nvme0n1

✏️ Ejercicio 7.2 — Calcular movimientos del cabezal

Cabezal en pista 100. Pedidos: 23, 89, 132, 42, 187, 90, 67. Calculá el movimiento total con FCFS, SSTF y SCAN (dirección inicial: hacia arriba).

✏️ Ejercicio 7.3 — DMA vs polling

Tu programa lee 1 GB de un SSD. La CPU corre a 3 GHz. El SSD entrega 1 GB/s.

a. Sin DMA, con polling, ¿qué % de ciclos se gastan polleando? b. Con DMA + 1 sola interrupción al final, ¿qué % se gasta en transferir?

7.10 Para profundizar


Definiciones nuevas: dispositivo de bloques, de caracteres, polling, interrupción, ISR, DMA, driver, módulo del kernel, scheduling de disco, FCFS, SSTF, SCAN, page cache, fsync, NVMe.