En la lección anterior tratamos el disco como si fuera el dispositivo del sistema. Pero meteo-01 tiene mucho más: dos SSD NVMe, un disco mecánico, una tarjeta de red, un reloj de tiempo real, un terminal serie, un generador de números aleatorios y varios controladores USB. Y las estaciones meteorológicas que le envían datos tienen sensores, relojes y radios. Ninguno de estos dispositivos se parece a los demás: difieren en velocidad por un factor de un millón, en unidad de transferencia, en si se puede leer, escribir o ambas cosas, y en si responden en microsegundos o en minutos.

El sistema operativo tiene que ofrecer una interfaz coherente para todos ellos sin conocer los detalles de ninguno. Esta lección trata de cómo lo consigue: qué clasificación usa, por qué en UNIX "todo es un fichero", cómo aparecen los ficheros en /dev, y cuáles son las tres formas que tiene la CPU de hablar con un dispositivo. Terminaremos siguiendo el camino de una lectura de estación desde la tarjeta de red hasta el búfer del ingestor, dejando el mecanismo interno de las interrupciones para la lección siguiente, que lo desarrolla en detalle.

Contenido

  1. Por qué el sistema operativo uniformiza los dispositivos
  2. La estructura del subsistema de E/S
  3. Clasificación: bloque, carácter y red
  4. "Todo es un fichero" y el directorio /dev
  5. Números mayor y menor
  6. udev y la creación dinámica de dispositivos
  7. Buses y enumeración: lspci, lsusb, lsblk
  8. Las tres formas de hablar con un dispositivo
  9. Puertos de E/S y E/S mapeada en memoria
  10. Técnicas del subsistema: buffering, caching, spooling y reserva
  11. Tratamiento de errores y reintentos
  12. El camino de una lectura de estación

Por qué el sistema operativo uniformiza los dispositivos

Mira la variedad real a la que se enfrenta el núcleo de meteo-01:

Dispositivo Velocidad Unidad Operaciones Latencia
Teclado 100 B/s Carácter Leer Humana
Ratón 500 B/s Carácter Leer Humana
Reloj de tiempo real Registro Leer/escribir ns
Terminal serie 11 KB/s Carácter Ambas ms
Red 1 Gb/s 125 MB/s Paquete Ambas µs
Disco duro 150 MB/s Bloque Ambas ms
SSD NVMe 3.500 MB/s Bloque Ambas µs
GPU 500 GB/s Comando Ambas µs

Entre el teclado y la GPU hay nueve órdenes de magnitud de diferencia en velocidad. Y sin embargo, el programador del ingestor escribe:

read(fd, buffer, 1024);

y esa misma línea funciona si fd es un socket de red, un fichero en el SSD, un terminal o el generador de aleatorios. Esa uniformidad no es comodidad: es lo que hace posible escribir software.

Sin ella, cada programa tendría que conocer el modelo exacto de cada dispositivo. Un cambio de tarjeta de red obligaría a recompilar todas las aplicaciones. Es exactamente el problema que el sistema operativo como máquina extendida resuelve, y que planteamos en 01-01.

Los objetivos concretos del subsistema de E/S son cinco:

Objetivo Qué significa
Independencia del dispositivo El programa no sabe qué hardware hay debajo
Nomenclatura uniforme Un nombre es un nombre, sin importar el dispositivo
Tratamiento de errores Los errores se gestionan lo más abajo posible
Transferencia síncrona y asíncrona El programa elige si bloquearse o no
Compartición y dedicación Un disco se comparte; una impresora, no

La estructura del subsistema de E/S

El subsistema se organiza en capas, cada una añadiendo abstracción sobre la anterior:

flowchart TD
    A["Aplicación<br/>ingestor: read(fd, buf, 1024)"] --> B
    B["Biblioteca C<br/>traduce a la llamada al sistema"] --> C
    C["Interfaz de llamadas al sistema<br/>read / write / ioctl"] --> D
    D["Software de E/S independiente del dispositivo<br/>nombres, buffering, caché, permisos, errores"] --> E
    E["Controladores de dispositivo<br/>específicos de cada modelo"] --> F
    F["Manejadores de interrupción"] --> G
    G["Hardware<br/>controlador del dispositivo + dispositivo"]

El reparto de responsabilidades:

Capa Qué hace Ejemplo
Aplicación Pide datos con nombres lógicos read(fd, buf, 1024)
Independiente del dispositivo Todo lo común a todos Buffering, caché, comprobación de permisos
Controlador Lo específico de un modelo Escribir registros de la tarjeta e1000e
Manejador de interrupción Reacciona al aviso del hardware Marcar la transferencia como completada

El punto clave está en la capa independiente del dispositivo: es la que hace todo el trabajo común una sola vez, para que cada controlador solo tenga que implementar lo verdaderamente específico. Un controlador de red no tiene que saber nada de permisos, ni de nombres, ni de buffering: solo cómo hablar con su chip.

Y aquí conecta con la lección 01-05: los controladores viven dentro del núcleo en un sistema monolítico como Linux, cargados como módulos. Por eso un e1000e defectuoso puede tumbar la máquina entera, mientras que en un microkernel viviría en espacio de usuario a costa de más IPC.

Clasificación: bloque, carácter y red

Linux clasifica los dispositivos en tres grandes familias, y esa clasificación determina qué interfaz ofrece cada uno:

Dispositivo de bloque Dispositivo de carácter Dispositivo de red
Unidad de acceso Bloques de tamaño fijo (512 B - 4 KB) Flujo de bytes Paquetes
Acceso aleatorio , se puede saltar a cualquier bloque No, secuencial No aplica
Almacenamiento intermedio Caché de páginas del núcleo Directo o mínimo Colas de socket
Se puede montar No No
Interfaz Fichero en /dev Fichero en /dev Socket, sin fichero
Ejemplos /dev/sda, /dev/nvme0n1 /dev/tty0, /dev/random, /dev/null eth0, lo

Dispositivos de bloque. La lección anterior trató su gestión completa: array de bloques numerados por LBA, planificación de peticiones, caché. La característica que los define es el acceso aleatorio: puedes leer el bloque 5.000 sin haber leído los 4.999 anteriores.

Dispositivos de carácter. Son un flujo: los bytes van llegando y no puedes retroceder. Un teclado, un puerto serie, un generador de aleatorios. No tiene sentido "buscar la posición 500" en un teclado.

$ ls -l /dev/random /dev/null /dev/zero /dev/tty0
crw-rw-rw- 1 root root  1,   8 ago 31 09:14 /dev/random
crw-rw-rw- 1 root root  1,   3 ago 31 09:14 /dev/null
crw-rw-rw- 1 root root  1,   5 ago 31 09:14 /dev/zero
crw--w---- 1 root tty   4,   0 ago 31 09:14 /dev/tty0

La c inicial indica character device. Fíjate en que /dev/null y /dev/zero no corresponden a hardware alguno: son dispositivos virtuales implementados enteramente en software. /dev/null descarta todo lo que se le escribe; /dev/zero produce ceros infinitos. Que existan demuestra que la abstracción de dispositivo es lo bastante general como para envolver cosas que no son dispositivos.

Dispositivos de red. Son la excepción interesante: no tienen fichero en /dev.

$ ls /dev | grep -i eth
(nada)

$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN
2: enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP

La razón es de fondo: un fichero implica un flujo de bytes ordenado y sin pérdidas, y la red no es eso. Los paquetes pueden perderse, duplicarse o llegar desordenados, y cada uno lleva metadatos (direcciones, puertos, protocolo) que no encajan en read()/write(). Por eso UNIX inventó una abstracción distinta —el socket, con socket(), bind(), sendto(), recvfrom()— para lo que no cabía en la de fichero.

Es una lección de diseño valiosa: una abstracción buena tiene límites, y forzarla más allá de ellos produce interfaces peores. Los creadores de BSD lo reconocieron y crearon una segunda abstracción en lugar de deformar la primera.

Aun así, una vez abierto, un socket se maneja con un descriptor de fichero y admite read() y write(). La abstracción de fichero cubre el uso; solo la creación y la configuración necesitan interfaz propia.

"Todo es un fichero" y el directorio /dev

El principio que define a UNIX: los dispositivos se presentan como ficheros especiales en el sistema de archivos.

Las consecuencias son enormes y muy prácticas:

# Los mismos permisos que un fichero normal
$ ls -l /dev/nvme0n1
brw-rw---- 1 root disk 259, 0 ago 31 09:14 /dev/nvme0n1

# Las mismas herramientas
$ sudo dd if=/dev/nvme0n1 of=/backup/mbr.img bs=512 count=1
$ head -c 32 /dev/urandom | base64

# La misma redirección
$ /opt/meteora/bin/agregador --verbose > /dev/null 2>&1

# Las mismas llamadas al sistema
$ strace -e open,read,write cat /dev/zero 2>&1 | head -3

Tres ventajas concretas de esto:

  1. Un solo modelo de permisos. Que /dev/nvme0n1 pertenezca al grupo disk con permisos rw-rw---- es lo que impide que el usuario meteora lea el disco crudo saltándose los permisos del sistema de archivos. Sin el modelo de ficheros haría falta un sistema de permisos aparte para dispositivos.
  2. Reutilización de herramientas. cat, dd, cp, grep funcionan sobre dispositivos sin haber sido programados para ello.
  3. Composición. Puedes redirigir, canalizar y encadenar dispositivos como cualquier otra cosa.

Un ejemplo que aprovecha las tres a la vez:

$ sudo dd if=/dev/nvme0n1p2 bs=4M status=progress | gzip -9 > /backup/meteora.img.gz

Copia un dispositivo de bloque completo, lo comprime al vuelo y lo guarda. dd no sabe nada de NVMe, gzip no sabe nada de dispositivos, y ninguno de los dos ha necesitado saberlo.

Y el operador > de la shell aprovecha justo lo que vimos en 02-01: la shell hace fork, en el hijo abre el fichero como descriptor 1, y luego execve. El programa nuevo escribe en el descriptor 1 sin enterarse de nada.

Números mayor y menor

Un fichero de dispositivo no contiene datos: contiene una referencia a un controlador. Esa referencia son dos números.

$ ls -l /dev/sda /dev/tty0 /dev/nvme0n1 /dev/null
brw-rw---- 1 root disk    8,   0 ago 31 09:14 /dev/sda
crw--w---- 1 root tty     4,   0 ago 31 09:14 /dev/tty0
brw-rw---- 1 root disk  259,   0 ago 31 09:14 /dev/nvme0n1
crw-rw-rw- 1 root root    1,   3 ago 31 09:14 /dev/null

Analicemos la línea de /dev/sda campo a campo:

b   rw-rw----  1  root  disk   8,   0   ago 31 09:14  /dev/sda
│   └───┬───┘        └─┬─┘    └┬┘  └┬┘
│    permisos      propietario │    └── número MENOR: qué instancia
│                    y grupo   └─────── número MAYOR: qué controlador
└── tipo: b = bloque, c = carácter

Donde un fichero normal mostraría su tamaño en bytes, un fichero de dispositivo muestra dos números separados por coma. Ese es el detalle que delata que no es un fichero corriente.

Número Qué identifica Quién lo interpreta
Mayor El controlador que gestiona el dispositivo El núcleo, para elegir el driver
Menor Qué instancia concreta dentro de ese controlador El propio controlador

Ejemplos que aclaran la mecánica:

$ ls -l /dev/sda /dev/sda1 /dev/sda2 /dev/sdb
brw-rw---- 1 root disk 8,  0 ago 31 09:14 /dev/sda
brw-rw---- 1 root disk 8,  1 ago 31 09:14 /dev/sda1
brw-rw---- 1 root disk 8,  2 ago 31 09:14 /dev/sda2
brw-rw---- 1 root disk 8, 16 ago 31 09:14 /dev/sdb

Todos tienen mayor 8 (el controlador de discos SCSI/SATA), y el menor los distingue: 0 es el disco sda entero, 1 y 2 sus particiones, 16 es el siguiente disco. La convención asigna 16 menores por disco, lo que permite 15 particiones cada uno.

Los mayores están registrados oficialmente:

$ cat /proc/devices
Character devices:
  1 mem
  4 tty
  5 /dev/tty
 10 misc
 13 input
189 usb_device

Block devices:
  8 sd
  9 md
 11 sr
252 device-mapper
259 blkext

Cuando abres /dev/sda, el núcleo lee el mayor (8), busca en esta tabla qué controlador lo ha registrado, y le pasa la operación junto con el menor para que sepa a qué disco se refiere. Ese es todo el mecanismo de despacho de E/S.

Puedes crear ficheros de dispositivo a mano, aunque hoy casi nunca haga falta:

$ sudo mknod /dev/midisco b 8 0
$ sudo mknod /dev/miconsola c 4 0

Prueba conceptualmente reveladora: /dev/midisco con mayor 8 y menor 0 es exactamente /dev/sda. El nombre no significa nada; lo único que importa son los dos números. Los nombres son una convención humana.

udev y la creación dinámica de dispositivos

Antiguamente /dev era un directorio normal con miles de ficheros creados de antemano por si acaso. Era un desastre: no se sabía cuáles correspondían a hardware presente, y conectar algo nuevo requería crear el fichero a mano.

Hoy /dev es un sistema de ficheros virtual (devtmpfs) que refleja el hardware realmente presente, gestionado por udev.

$ mount | grep devtmpfs
udev on /dev type devtmpfs (rw,nosuid,relatime,size=3980212k,nr_inodes=995053,mode=755)

Cuando se conecta un dispositivo, la secuencia es:

sequenceDiagram
    participant HW as Hardware
    participant K as Núcleo
    participant U as udevd
    participant FS as /dev
    HW->>K: se conecta un dispositivo USB
    K->>K: detecta y carga el módulo del driver
    K->>K: crea el nodo básico en devtmpfs
    K->>U: evento uevent por netlink
    U->>U: consulta /lib/udev/rules.d y /etc/udev/rules.d
    U->>FS: aplica permisos, propietario y enlaces simbólicos
    U->>U: ejecuta las acciones programadas

Observarlo en directo es la mejor forma de entenderlo:

$ udevadm monitor --property --subsystem-match=usb
KERNEL[1284.221] add   /devices/pci0000:00/0000:00:14.0/usb2/2-1 (usb)
ACTION=add
DEVNAME=/dev/bus/usb/002/007
DEVTYPE=usb_device
ID_VENDOR=FTDI
ID_MODEL=FT232R_USB_UART
ID_SERIAL_SHORT=A50285BI
MAJOR=189
MINOR=134

Y consultar todo lo que udev sabe de un dispositivo concreto:

$ udevadm info --query=all --name=/dev/nvme0n1
P: /devices/pci0000:00/0000:00:1d.0/0000:04:00.0/nvme/nvme0/nvme0n1
N: nvme0n1
S: disk/by-id/nvme-Samsung_SSD_980_PRO_1TB_S5GXNX0T123456
S: disk/by-path/pci-0000:04:00.0-nvme-1
E: DEVTYPE=disk
E: ID_MODEL=Samsung SSD 980 PRO 1TB
E: ID_SERIAL_SHORT=S5GXNX0T123456

Las líneas S: son enlaces simbólicos alternativos, y resuelven un problema muy real: el nombre nvme0n1 depende del orden de detección, que puede cambiar entre arranques. Los enlaces por identificador de serie no cambian nunca.

$ ls -l /dev/disk/by-id/ | head -4
lrwxrwxrwx 1 root root 13 ago 31 09:14 nvme-Samsung_SSD_980_PRO_1TB_S5GXNX0T123456 -> ../../nvme0n1
lrwxrwxrwx 1 root root 15 ago 31 09:14 nvme-Samsung_SSD_980_PRO_1TB_S5GXNX0T123456-part1 -> ../../nvme0n1p1

Por eso /etc/fstab debe usar UUID o /dev/disk/by-id/, nunca /dev/sda1. Si añades un disco y el orden cambia, un fstab con nombres directos puede montar el volumen equivocado. Lo veremos aplicado en Particiones, Montaje y Sistema de Archivos Virtual.

Reglas de udev

Puedes definir tus propias reglas. Un caso real de Meteora: una de las estaciones se conecta por USB-serie al servidor para diagnóstico, y quieres que aparezca siempre con el mismo nombre y accesible por el usuario meteora.

# /etc/udev/rules.d/70-meteora.rules
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", \
  ATTRS{serial}=="A50285BI", \
  SYMLINK+="meteora/estacion-norte", OWNER="meteora", GROUP="meteora", MODE="0660"

Cómo funciona cada parte:

  • SUBSYSTEM=="tty": solo aplica a dispositivos de terminal serie.
  • ATTRS{idVendor}/ATTRS{idProduct}: identifican el chip FTDI concreto.
  • ATTRS{serial}: distingue esta estación de otra con el mismo chip. Sin él, dos adaptadores idénticos coincidirían con la regla.
  • SYMLINK+=: crea /dev/meteora/estacion-norte, un nombre estable e independiente de si el núcleo la llamó ttyUSB0 o ttyUSB3.
  • OWNER/GROUP/MODE: hacen el dispositivo accesible al usuario meteora sin necesidad de root.

Ese último punto es importante desde el punto de vista de seguridad: en lugar de ejecutar el proceso de diagnóstico como root para que pueda abrir el puerto serie, se le dan permisos exactos sobre ese dispositivo concreto. Es el principio de mínimo privilegio, que desarrollaremos en Principios de Protección y Control de Acceso.

# Recargar las reglas y aplicarlas sin desconectar el dispositivo
$ sudo udevadm control --reload-rules
$ sudo udevadm trigger --subsystem-match=tty

$ ls -l /dev/meteora/
lrwxrwxrwx 1 root root 10 ago 31 09:22 estacion-norte -> ../ttyUSB0

Otra regla útil: fijar el planificador de E/S por tipo de dispositivo, retomando lo de la lección anterior.

# /etc/udev/rules.d/60-scheduler.rules
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", \
  ATTR{queue/scheduler}="mq-deadline"
ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", \
  ATTR{queue/scheduler}="none"

Así el planificador correcto se aplica automáticamente en cada arranque y a cada disco que se conecte, según sea rotacional o no.

Buses y enumeración: lspci, lsusb, lsblk

Los dispositivos se conectan a través de buses, y cada bus tiene su propio mecanismo de enumeración: la forma en que el sistema descubre qué hay conectado.

Bus Enumeración Herramienta Conexión en caliente
PCI / PCIe Espacio de configuración estándar lspci Limitada
USB Consulta del descriptor al conectar lsusb
SATA/SCSI Consulta al controlador lsblk, lsscsi
I²C / SPI Declarada en el árbol de dispositivos i2cdetect No
$ lspci
00:00.0 Host bridge: Intel Corporation 8th Gen Core Processor Host Bridge
00:14.0 USB controller: Intel Corporation 200 Series PCH USB 3.0 xHCI Controller
00:17.0 SATA controller: Intel Corporation 200 Series PCH SATA controller [AHCI mode]
00:1d.0 PCI bridge: Intel Corporation 200 Series PCH PCI Express Root Port #9
03:00.0 Ethernet controller: Intel Corporation I210 Gigabit Network Connection
04:00.0 Non-Volatile memory controller: Samsung Electronics NVMe SSD Controller

El identificador 03:00.0 sigue el formato bus:dispositivo.función, y es la dirección física de la tarjeta en la topología PCI. La versión detallada revela cómo habla el núcleo con ella:

$ sudo lspci -v -s 03:00.0
03:00.0 Ethernet controller: Intel Corporation I210 Gigabit Network Connection (rev 03)
        Subsystem: Intel Corporation Device 0000
        Flags: bus master, fast devsel, latency 0, IRQ 128
        Memory at df200000 (32-bit, non-prefetchable) [size=128K]
        I/O ports at e000 [size=32]
        Memory at df280000 (32-bit, non-prefetchable) [size=16K]
        Capabilities: [70] MSI-X: Enable+ Count=5 Masked-
        Kernel driver in use: igb

Cada línea dice algo que usaremos:

  • bus master: la tarjeta puede iniciar transferencias DMA por su cuenta, sin que la CPU copie los datos.
  • IRQ 128: la línea de interrupción asignada. Aparecerá en /proc/interrupts.
  • Memory at df200000 [size=128K]: sus registros están mapeados en memoria en esa dirección física.
  • I/O ports at e000: además tiene puertos de E/S clásicos.
  • MSI-X: Enable+ Count=5: usa interrupciones señalizadas por mensaje, con 5 vectores distintos.
  • Kernel driver in use: igb: qué módulo la gestiona.

Esa última línea es la primera comprobación cuando un dispositivo no funciona: si no aparece, no hay driver cargado y el hardware está presente pero inerte. Es exactamente el escenario que vimos en 01-05 con lsmod y modinfo.

$ lsusb
Bus 002 Device 007: ID 0403:6001 Future Technology Devices International FT232 Serial (UART) IC
Bus 001 Device 002: ID 8087:0024 Intel Corp. Integrated Rate Matching Hub

$ lsusb -t
/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/6p, 5000M
    |__ Port 1: Dev 7, If 0, Class=Vendor Specific Class, Driver=ftdi_sio, 12M

lsusb -t muestra el árbol con el driver de cada nodo (ftdi_sio para la estación) y la velocidad negociada (12 Mb/s, USB 1.1 completa, suficiente para un puerto serie).

Y el mapa completo de dispositivos de bloque, que vimos en la lección anterior:

$ lsblk -o NAME,ROTA,SIZE,TYPE,MOUNTPOINT,MODEL
NAME        ROTA   SIZE TYPE  MOUNTPOINT         MODEL
nvme0n1        0 931,5G disk                     Samsung SSD 980 PRO 1TB
└─nvme0n1p2    0   931G part
  └─md0        0   931G raid1 /var/lib/meteora
sda            1   7,3T disk                     ST8000NM0055-1RM112
└─sda1         1   7,3T part  /backup

Las tres formas de hablar con un dispositivo

Cuando la CPU necesita transferir datos con un dispositivo, hay tres mecanismos posibles. Este apartado los contrasta; el mecanismo interno de las interrupciones y del DMA es el tema de la próxima lección, aquí solo interesa cuándo se usa cada uno y cuánto cuesta.

E/S programada con espera activa

La CPU consulta repetidamente un registro de estado del dispositivo hasta que indica que está listo, y entonces transfiere los datos ella misma, byte a byte o palabra a palabra.

/* Esquema conceptual: NO es código real de núcleo */
while ((leer_registro(ESTADO) & LISTO) == 0)
    ;                              /* espera activa: quemar CPU */
escribir_registro(DATOS, byte);    /* transferir un byte */
  • Ventaja: latencia mínima y simplicidad total. Sin interrupciones ni sincronización.
  • Inconveniente: la CPU no hace nada más mientras espera.

Con un dispositivo lento el desperdicio es catastrófico:

Impresora a 100 caracteres/segundo
Tiempo por carácter: 10 ms
Ciclos de CPU desperdiciados por carácter (a 3 GHz): 30.000.000

Treinta millones de ciclos por letra. Aun así, la espera activa sigue siendo la opción correcta en tres casos: cuando el dispositivo responde en menos de lo que costaría una interrupción (unos pocos microsegundos), durante el arranque —cuando el sistema de interrupciones aún no está configurado—, y en un manejador de pánico, cuando ya no se puede confiar en nada más.

E/S por interrupciones

La CPU inicia la operación y se olvida: pone el proceso a dormir y planifica otro. Cuando el dispositivo termina, envía una interrupción y el núcleo despierta al proceso.

1. El ingestor llama a read() sobre el socket
2. No hay datos → el núcleo lo pone en TASK_INTERRUPTIBLE (estado S)
3. El planificador elige otro proceso (02-02)
4. Llega un paquete → la tarjeta genera una interrupción
5. El manejador copia los datos a la cola del socket
6. Marca el ingestor como ejecutable (estado R)
7. El planificador lo vuelve a ejecutar y read() retorna

Reconocerás cada paso: son exactamente las transiciones del diagrama de estados de 02-01. Los estados S y D existen precisamente por esto.

  • Ventaja: la CPU se aprovecha durante la espera.
  • Inconveniente: cada interrupción cuesta entre 1 y 5 µs, y la CPU sigue copiando los datos byte a byte.

Y ahí está el problema que queda por resolver: con una tarjeta de red a 1 Gb/s recibiendo paquetes de 1.500 bytes:

Paquetes por segundo: 125.000.000 / 1.500 = 83.333 paquetes/s
Con una interrupción por paquete: 83.333 interrupciones/s
A 3 µs cada una: 0,25 segundos de CPU por segundo = 25 % de un núcleo

Un cuarto de núcleo solo en atender interrupciones, sin contar la copia de los datos.

DMA (acceso directo a memoria)

El DMA es un controlador que transfiere datos entre el dispositivo y la memoria sin intervención de la CPU. La CPU solo programa la operación (dirección de destino, tamaño) y recibe una única interrupción al final.

Sin DMA, leer 4 KB del disco:
  4.096 transferencias de 1 byte por la CPU
  ~4.096 × 2 instrucciones = 8.192 instrucciones

Con DMA:
  1 programación del descriptor (~20 instrucciones)
  1 interrupción al completarse (~3 µs)

Tabla comparativa de coste de CPU

Transferir 1 MB desde un dispositivo:

Técnica Intervención de la CPU Interrupciones CPU consumida Cuándo usarla
Espera activa Copia cada palabra + espera 0 ~100 % Dispositivos rapidísimos, arranque, pánico
Por interrupciones Copia cada palabra Miles 30-60 % Dispositivos lentos con poco volumen
DMA Programar y recoger 1 < 1 % Todo lo que mueva volumen

Con números concretos para 1 MB desde el SSD:

Espera activa:      262.144 palabras de 4 bytes × ~4 ciclos = ~1.000.000 ciclos
                    + espera de 300 µs bloqueando la CPU

Por interrupciones: 262.144 copias + ~256 interrupciones × 3 µs = 768 µs de CPU

DMA:                1 descriptor + 1 interrupción = ~5 µs de CPU

Un factor de más de 150 frente a la E/S por interrupciones. Por eso todo dispositivo que mueva volumen usa DMA: discos, red, GPU, sonido. El bus master que veíamos en lspci es precisamente la capacidad de hacerlo.

Regla de decisión resumida:

Situación Técnica
Latencia crítica, dispositivo listo en < 5 µs Espera activa
Dispositivo lento, pocos datos (teclado, ratón) Interrupciones
Volumen de datos (disco, red, gráficos) DMA
Tasa de interrupciones muy alta DMA + polling adaptativo (NAPI, en 02-07)

Puertos de E/S y E/S mapeada en memoria

Queda una pregunta: cómo se accede físicamente a los registros de un dispositivo. Hay dos enfoques.

Espacio de E/S separado

x86 tiene un espacio de direcciones aparte, de 65.536 puertos, con instrucciones dedicadas:

in  al, 0x60      ; leer un byte del puerto 0x60 (controlador del teclado)
out 0x3F8, al     ; escribir un byte al puerto 0x3F8 (puerto serie COM1)

Recordarás in y out de la tabla de instrucciones privilegiadas de 01-06: solo se pueden ejecutar en modo núcleo. Ese es el mecanismo que impide que un programa de usuario hable directamente con el hardware.

$ sudo cat /proc/ioports | head -8
0000-0cf7 : PCI Bus 0000:00
  0000-001f : dma1
  0040-0043 : timer0
  0060-0060 : keyboard
  0064-0064 : keyboard
  0070-0077 : rtc0
  02f8-02ff : serial
  03f8-03ff : serial

Ahí están los puertos históricos: 0x60 y 0x64 para el teclado, 0x3F8 para COM1, 0x70 para el reloj de tiempo real. Números que llevan sin cambiar desde el IBM PC de 1981.

E/S mapeada en memoria (MMIO)

Los registros del dispositivo se asignan a direcciones del espacio de memoria física. Leer o escribir en ellas es acceder al dispositivo.

$ sudo cat /proc/iomem | grep -A2 'PCI Bus 0000:03'
df200000-df21ffff : 0000:03:00.0
  df200000-df21ffff : igb
df280000-df283fff : 0000:03:00.0

Los 128 KB en 0xdf200000 son los registros de la tarjeta de red que vimos en lspci. El driver igb los ha reservado y accede a ellos con instrucciones normales de memoria.

Comparación de los dos enfoques:

Puertos de E/S MMIO
Instrucciones in/out, privilegiadas mov normales
Espacio de direcciones Separado, 64 KB Compartido con la RAM
Tamaño de transferencia Limitado Cualquiera, incluidas ráfagas
Protección Solo por modo (todo o nada) Por página, vía MMU
Cacheable No Debe deshabilitarse (bit PCD)
Arquitecturas Casi solo x86 Universal

MMIO ha ganado, y por una razón que ahora entiendes perfectamente: al vivir en el espacio de direcciones normal, la protección la aplica la MMU con la granularidad de página que estudiamos en 02-04. Se puede dar a un proceso acceso a los registros de un dispositivo concreto sin darle acceso a nada más. Con puertos de E/S la protección es de todo o nada.

Además, MMIO permite que un driver acceda a los registros con código C normal, sin ensamblador, lo que hace el código portable entre arquitecturas.

Un detalle crítico: las páginas de MMIO deben marcarse como no cacheables (bit PCD de la tabla de páginas, que vimos en 02-04). Si la CPU cacheara un registro de estado del dispositivo, leería un valor obsoleto de la caché en lugar del estado real del hardware, y el driver se colgaría esperando una condición que ya se cumplió. Es un ejemplo perfecto de cómo dos mecanismos correctos por separado —caché y MMIO— se destruyen mutuamente si no se coordinan.

Técnicas del subsistema: buffering, caching, spooling y reserva

La capa independiente del dispositivo aplica cuatro técnicas generales. Todas resuelven un desajuste distinto.

Buffering

Un búfer es un área de memoria intermedia entre el productor y el consumidor. Resuelve tres problemas distintos:

Problema Ejemplo Cómo lo resuelve el búfer
Desajuste de velocidad Red a 125 MB/s, disco a 150 MB/s Absorbe las ráfagas
Desajuste de tamaño Paquetes de 1.500 B, bloques de 4 KB Acumula hasta completar
Semántica de copia El proceso modifica el búfer tras write() Se copia antes de escribir

Ya calculamos su impacto en 01-06: agrupar 170 lecturas antes de escribir reducía las llamadas al sistema en un factor de 167.

Por qué el doble búfer. Con un solo búfer aparece un problema de bloqueo:

Con UN búfer:
[llenar búfer] → [vaciar búfer] → [llenar búfer] → [vaciar búfer]
   dispositivo      proceso          dispositivo      proceso
   ← el dispositivo está PARADO →   ← el proceso está PARADO →

Mientras el proceso procesa el búfer, el dispositivo no puede escribir en él y debe esperar. Y viceversa. Las dos partes se serializan.

Con DOS búferes:
Búfer A: [llenar]  [procesar] [llenar]  [procesar]
Búfer B:           [llenar]   [procesar][llenar]
         ← ambos trabajan EN PARALELO →

Mientras el dispositivo llena el búfer A, el proceso procesa el B. Al terminar, intercambian. El rendimiento pasa de 1/(t_llenar + t_procesar) a 1/max(t_llenar, t_procesar).

Cálculo para el ingestor:

Llenar un búfer desde la red: 8 ms
Procesar el búfer:            5 ms

Un búfer:   1 / (8 + 5) = 76,9 búferes/segundo
Dos búferes: 1 / max(8, 5) = 1/8 = 125 búferes/segundo
Mejora: 62,5 %

La generalización es el búfer circular con N posiciones, que es lo que usan las colas de sockets y las colas de descriptores de las tarjetas de red modernas.

Caching

Un búfer y una caché se confunden a menudo, pero son cosas distintas:

Búfer Caché
Qué contiene La única copia de los datos en tránsito Una copia de datos que existen en otro sitio
Si se pierde Se pierden los datos Solo se pierde rendimiento
Propósito Adaptar velocidades y tamaños Evitar accesos lentos

La caché de páginas de Linux es la caché de disco del sistema, y es la que veíamos como buff/cache en free -h:

$ free -h
               total        used        free      shared  buff/cache   available
Mem:           7,8Gi       3,1Gi       412Mi       528Mi       4,3Gi       3,9Gi

Esos 4,3 GB son contenido de ficheros mantenido en RAM. Cuando el agregador lee 2026-08-31.dat por segunda vez, no toca el disco: los datos ya están ahí. Es la misma caché que respalda las páginas de mmap() que usamos en 02-04.

Comprobarlo es sencillo y muy ilustrativo:

$ sync && echo 3 | sudo tee /proc/sys/vm/drop_caches   # vaciar la caché
$ time cat /var/lib/meteora/lecturas/2026-08-31.dat > /dev/null
real    0m0,118s

$ time cat /var/lib/meteora/lecturas/2026-08-31.dat > /dev/null
real    0m0,006s

20 veces más rápido la segunda vez, sin haber tocado el disco. Ese factor de 20 es lo que la caché de páginas aporta continuamente y de forma invisible.

Spooling

Spool viene de Simultaneous Peripheral Operation On-Line. Es la técnica para dispositivos que no se pueden compartir intercalando operaciones.

Si dos procesos escriben simultáneamente en una impresora sin coordinación, la salida sale mezclada línea a línea. El spooling lo resuelve: cada trabajo se escribe completo en un directorio de cola, y un demonio los imprime uno a uno, en orden.

$ ls -l /var/spool/cups/
$ ls /var/spool/cron/crontabs/
$ ls /var/spool/mail/

Lo interesante es que el spooling sigue muy vivo aunque las impresoras ya no importen: cron, el correo y las colas de mensajería usan el mismo patrón. La idea general —encolar trabajos en almacenamiento persistente y procesarlos secuencialmente con un único consumidor— es uno de los patrones más reutilizados de la informática.

En Meteora, el envío nocturno de backup al servidor remoto funciona así: los ficheros se dejan en un directorio de salida y un proceso los transfiere de uno en uno, reintentando los que fallan.

Reserva de dispositivo

Algunos dispositivos deben usarse en exclusiva. El sistema ofrece mecanismos para reservarlos:

/* Abrir un puerto serie en exclusiva */
int fd = open("/dev/meteora/estacion-norte", O_RDWR | O_NOCTTY);
if (flock(fd, LOCK_EX | LOCK_NB) == -1) {
    fprintf(stderr, "El puerto ya está en uso por otro proceso\n");
    return 1;
}

flock con LOCK_EX | LOCK_NB intenta un bloqueo exclusivo sin esperar: si otro proceso ya tiene el puerto, falla inmediatamente en lugar de quedarse colgado.

La reserva exclusiva introduce el riesgo de interbloqueo: si el proceso A tiene el puerto serie y espera el módem, y B tiene el módem y espera el puerto serie, ninguno avanza. Ese problema tiene lección propia: Interbloqueos.

Tratamiento de errores y reintentos

El principio general: los errores se tratan lo más cerca posible del hardware, y solo suben si no se pueden resolver abajo.

Nivel del dispositivo:  reintento automático del propio hardware
Nivel del controlador:  reintentos, reasignación de sectores
Nivel independiente:    traducción a un código de error estándar
Nivel de aplicación:    decide qué hacer (reintentar, avisar, abortar)

Un ejemplo real de la cadena completa, visto desde el registro del núcleo:

$ sudo dmesg -T | tail -6
[Sun Aug 31 03:22:11 2026] ata3.00: exception Emask 0x0 SAct 0x0 SErr 0x0 action 0x0
[Sun Aug 31 03:22:11 2026] ata3.00: irq_stat 0x40000001
[Sun Aug 31 03:22:11 2026] ata3.00: failed command: READ DMA EXT
[Sun Aug 31 03:22:11 2026] ata3.00: status: { DRDY ERR }
[Sun Aug 31 03:22:11 2026] ata3.00: error: { UNC }
[Sun Aug 31 03:22:14 2026] sd 2:0:0:0: [sda] tag#20 Sense Key : Medium Error [current]

Traducción de la traza:

  • UNC (uncorrectable): el disco no ha podido leer un sector ni siquiera con su corrección de errores interna.
  • El controlador reintentó varias veces (de ahí los 3 segundos entre la primera y la última línea).
  • Al agotar los reintentos, generó un Medium Error.
  • El sistema de archivos lo recibe como EIO, y el proceso que hizo read() obtiene -1 con errno = EIO.

Ese errno es exactamente el mecanismo que rastreamos hasta su origen en 01-06: un valor negativo devuelto por el núcleo que la libc convierte en -1 más errno.

Categorías de error y qué hacer con cada una:

Error Significado ¿Reintentar?
EIO Error de E/S físico Puede que una vez; si persiste, hardware defectuoso
EAGAIN No hay datos ahora (no bloqueante) , no es un error real
EINTR Interrumpido por una señal Sí, siempre
ENOSPC Sin espacio No: hay que liberar espacio
ENODEV El dispositivo ha desaparecido No: desconectado
EBUSY En uso por otro Quizá, tras esperar

EINTR merece insistencia porque es la fuente de errores más traicionera. Ya lo vimos en el ejemplo de buffering de 01-06: si llega una señal mientras el proceso está bloqueado en read() o write(), la llamada retorna con EINTR sin haber hecho nada. No es un error: hay que reintentar. Ignorarlo produce pérdidas de datos esporádicas e irreproducibles.

ssize_t leer_robusto(int fd, void *buf, size_t n) {
    ssize_t r;
    do {
        r = read(fd, buf, n);
    } while (r == -1 && errno == EINTR);
    return r;
}

Cuatro líneas que evitan una clase entera de fallos intermitentes.

El camino de una lectura de estación

Cerramos aplicando todo a un recorrido concreto: una estación meteorológica envía una lectura de 24 bytes y el ingestor la recibe. Aquí vemos qué capas intervienen y qué hace cada una; el mecanismo interno de la interrupción y del DMA es el contenido de la próxima lección.

flowchart TD
    A["Estación meteorológica<br/>envía 24 bytes por UDP"] --> B
    B["Red física<br/>llega a la tarjeta I210"] --> C
    C["Tarjeta de red<br/>valida la trama Ethernet"] --> D
    D["DMA<br/>copia el paquete a un búfer de RAM<br/>sin intervención de la CPU"] --> E
    E["Interrupción<br/>la tarjeta avisa a la CPU: IRQ 128"] --> F
    F["Controlador igb<br/>reconoce la interrupción"] --> G
    G["Pila de red<br/>IP → UDP → busca el socket destino"] --> H
    H["Cola del socket<br/>el paquete se encola"] --> I
    I["El núcleo marca el ingestor<br/>como ejecutable: S → R"] --> J
    J["Planificador<br/>elige al ingestor (02-02)"] --> K
    K["read() retorna<br/>los 24 bytes en el búfer de usuario"]

Recorrido con el papel de cada capa:

Paso Capa Qué ocurre Lección
1-3 Hardware La tarjeta recibe y valida la trama
4 DMA El paquete llega a la RAM sin CPU 02-07
5 Interrupción La tarjeta avisa: hay trabajo 02-07
6 Controlador Código específico de la tarjeta I210 02-07
7 Independiente del dispositivo Pila de red común a todas las tarjetas
8 Buffering La cola del socket absorbe las ráfagas Esta lección
9 Gestión de procesos El ingestor pasa de S a R 02-01
10 Planificación Compite por la CPU 02-02
11 Llamada al sistema Copia al espacio de usuario 01-06

Fíjate en la división de trabajo. Los pasos 1-6 son específicos de esta tarjeta: solo el driver igb sabe cómo leer sus registros. A partir del paso 7 el código es común a todas las tarjetas de red del mundo: la pila IP/UDP no sabe ni le importa qué hardware trajo el paquete. Esa frontera es exactamente la que dibujamos en el diagrama de capas del apartado 2, y es la que permite que Linux soporte cientos de tarjetas distintas con una sola pila de red.

Y observa una cosa más: el ingestor no ha participado en nada de esto. Estaba dormido en estado S desde su read(). Todo el trabajo lo han hecho el hardware, el DMA, el manejador de interrupción y la pila de red. El proceso solo se despierta cuando ya hay datos esperándole.

Un cálculo de coste que motiva la próxima lección. Con 800 lecturas/segundo:

Con una interrupción por paquete:
  800 interrupciones/s × 3 µs = 2,4 ms/s = 0,24 % de un núcleo
  Manejable con esta carga.

Si Meteora creciera a 500.000 lecturas/segundo:
  500.000 × 3 µs = 1,5 segundos de CPU por segundo
  Más de un núcleo entero solo atendiendo interrupciones.

Ese es el problema de la tormenta de interrupciones, y su solución —que el sistema deje de usar interrupciones y pase a consultar activamente cuando la carga es alta— es uno de los mecanismos más elegantes del núcleo de Linux. Se llama NAPI y lo veremos en detalle a continuación.

Errores Comunes y Consejos

Usar /dev/sda1 en /etc/fstab. El nombre depende del orden de detección y puede cambiar al añadir hardware. Usa siempre UUID= o /dev/disk/by-id/. El día que añadas un disco y el sistema no arranque, entenderás por qué.

Confundir búfer y caché. Un búfer contiene la única copia de datos en tránsito; perderlo pierde datos. Una caché contiene una copia de algo que existe en otro sitio; perderla solo cuesta rendimiento. drop_caches vacía cachés, nunca búferes con datos pendientes.

Ejecutar como root para acceder a un dispositivo. Casi siempre la solución correcta es una regla de udev que dé permisos exactos sobre ese dispositivo concreto al usuario que lo necesita. Root para leer un puerto serie es dar mil permisos para usar uno.

Ignorar EINTR. Es la causa más frecuente de fallos intermitentes en código de E/S. Cualquier read, write o close sobre un descriptor bloqueante debe contemplarlo.

Creer que un dispositivo de red debería tener fichero en /dev. No lo tiene, y es una decisión de diseño consciente: los paquetes no son un flujo de bytes fiable y ordenado. La abstracción correcta es el socket.

Interpretar mal lspci cuando algo no funciona. Si el dispositivo aparece pero no hay línea Kernel driver in use, el hardware está presente y sin driver. Si no aparece en absoluto, es un problema físico o de BIOS. Son dos diagnósticos completamente distintos.

Consejo de diagnóstico: cuando un dispositivo no funciona, el orden que funciona es: lspci/lsusb (¿lo ve el sistema?), lspci -v | grep -i driver (¿tiene driver?), dmesg -T | tail -50 (¿qué dijo el núcleo al detectarlo?), ls -l /dev/... (¿existe el nodo y con qué permisos?), y udevadm info --query=all --name=... (¿qué sabe udev?). Cinco comprobaciones que localizan el problema en la capa exacta.

Ejercicios

Ejercicio 1: interpretar ficheros de dispositivo

Dada esta salida:

$ ls -l /dev/sda /dev/sda1 /dev/nvme0n1 /dev/tty1 /dev/null /dev/meteora/estacion-norte
brw-rw----  1 root disk    8,   0 ago 31 09:14 /dev/sda
brw-rw----  1 root disk    8,   1 ago 31 09:14 /dev/sda1
brw-rw----  1 root disk  259,   0 ago 31 09:14 /dev/nvme0n1
crw--w----  1 root tty     4,   1 ago 31 09:14 /dev/tty1
crw-rw-rw-  1 root root    1,   3 ago 31 09:14 /dev/null
lrwxrwxrwx  1 root root         7 ago 31 09:22 /dev/meteora/estacion-norte -> ttyUSB0
  1. Clasifica cada entrada por tipo y explica cómo lo sabes.
  2. ¿Por qué /dev/sda y /dev/sda1 comparten el mayor pero no el menor?
  3. Si el usuario meteora (grupo meteora) ejecuta dd if=/dev/sda of=/tmp/copia bs=1M count=10, ¿funciona? ¿Y sobre /dev/meteora/estacion-norte?
  4. ¿Qué representa el 7 en la última línea, donde las demás tienen dos números?
  5. Escribe una regla de udev que dé al grupo meteora acceso de solo lectura a /dev/sda, y explica si es buena idea.

Ejercicio 2: elegir la técnica de E/S y calcular su coste

Para cada uno de estos dispositivos de Meteora, decide qué técnica de E/S es la adecuada (espera activa, interrupciones o DMA) y justifica con un cálculo:

  • A: Sensor I²C en una estación, que devuelve 2 bytes en 8 µs tras la petición. Se consulta una vez por minuto. El microcontrolador va a 80 MHz.
  • B: Tarjeta de red de meteo-01 recibiendo 800 lecturas/segundo de 24 bytes en paquetes UDP de 66 bytes.
  • C: SSD NVMe leyendo los 17 MB de 2026-08-31.dat.
  • D: Adaptador USB-serie de diagnóstico a 9.600 baudios, que envía 200 caracteres cada vez que se conecta.

Para cada uno indica la técnica, el cálculo del coste de CPU y qué pasaría si eligieras mal.

Ejercicio 3: diagnosticar un dispositivo que no funciona

Has conectado una segunda tarjeta de red a meteo-01 para separar el tráfico de las estaciones del tráfico de la API, pero no aparece como interfaz. Diseña el procedimiento de diagnóstico completo:

  1. Escribe la secuencia de órdenes que ejecutarías, en orden, y qué buscas en cada una.
  2. Para cada uno de estos cuatro resultados posibles, di qué concluyes y qué harías:
    • a) lspci no muestra ninguna tarjeta nueva.
    • b) lspci la muestra pero sin línea Kernel driver in use.
    • c) lspci la muestra con driver, ip link la muestra como enp5s0 en estado DOWN.
    • d) dmesg muestra igb 0000:05:00.0: Failed to initialize MSI-X interrupts.
  3. Una vez funcionando, escribe una regla de udev que le dé un nombre estable estaciones0 y explica por qué no basta con ip link set name.

Soluciones

Solución 1

1. Clasificación.

Entrada Tipo Cómo lo sé
/dev/sda Bloque Primer carácter b; muestra dos números (8, 0) en vez de tamaño
/dev/sda1 Bloque Igual, con menor 1
/dev/nvme0n1 Bloque b, mayor 259 (blkext)
/dev/tty1 Carácter Primer carácter c
/dev/null Carácter c, mayor 1 (mem), menor 3
/dev/meteora/estacion-norte Enlace simbólico l inicial y flecha ->

La señal más fiable es la columna donde un fichero normal pondría el tamaño: si hay dos números separados por coma, es un fichero de dispositivo.

2. Mismo mayor, distinto menor.

El mayor 8 identifica el controlador (sd, discos SCSI/SATA). Ambos son gestionados por el mismo driver, así que comparten mayor.

El menor identifica qué instancia concreta dentro de ese driver:

menor 0  → el disco sda completo, desde el LBA 0 hasta el final
menor 1  → la partición sda1, un rango de LBA dentro de sda

Una partición no es un dispositivo distinto: es una ventana sobre un rango de bloques del mismo disco físico. El driver sd recibe el menor, consulta su tabla de particiones y aplica el desplazamiento correspondiente.

La convención asigna 16 menores por disco: menores 0-15 para sda y sus 15 particiones, 16-31 para sdb, y así sucesivamente. Por eso /dev/sdb tiene menor 16.

3. Permisos del usuario meteora.

Sobre /dev/sda: NO funciona.

brw-rw---- 1 root disk 8, 0 /dev/sda
  • Propietario root, con rw. meteora no es root.
  • Grupo disk, con rw. Habría que comprobar si meteora pertenece a disk:
$ groups meteora
meteora : meteora

No pertenece.

  • Otros: ---, sin ningún permiso.

Resultado: dd: failed to open '/dev/sda': Permiso denegado.

Y está bien que sea así. Acceso de lectura al disco crudo significa poder leer cualquier fichero del sistema saltándose por completo los permisos del sistema de archivos: /etc/shadow incluido. Es equivalente a ser root para lectura.

Sobre /dev/meteora/estacion-norte: SÍ funciona, si la regla de udev de la lección está activa.

El enlace apunta a ttyUSB0, y los permisos que cuentan son los del destino, no los del enlace (los lrwxrwxrwx de un enlace simbólico son siempre así y no significan nada). Con la regla:

crw-rw---- 1 meteora meteora 188, 0 ago 31 09:22 /dev/ttyUSB0

Propietario meteora con rw: acceso concedido. Aunque dd sobre un puerto serie leería el flujo entrante, no un contenido con estructura.

4. El número 7 del enlace simbólico.

Es el tamaño en bytes del enlace, es decir, la longitud de la cadena que contiene:

"ttyUSB0" = 7 caracteres

Un enlace simbólico sí es un fichero de verdad: su contenido es la ruta de destino. Por eso muestra un tamaño donde los ficheros de dispositivo muestran mayor y menor. Los dispositivos reales no muestran tamaño porque no contienen nada: solo referencian un driver.

Comprobación:

$ readlink /dev/meteora/estacion-norte
ttyUSB0
$ stat -c '%s bytes' /dev/meteora/estacion-norte
7 bytes

5. Regla de udev para dar lectura de /dev/sda al grupo meteora.

# /etc/udev/rules.d/71-meteora-disco.rules
SUBSYSTEM=="block", KERNEL=="sda", GROUP="meteora", MODE="0640"

Resultado: brw-r----- 1 root meteora 8, 0 /dev/sda.

¿Es buena idea? Rotundamente no.

El razonamiento:

  1. Elude por completo los permisos del sistema de archivos. Leyendo el dispositivo crudo se accede a los bytes de todos los ficheros de ese disco, incluidos /etc/shadow y las claves privadas de TLS. Los permisos de fichero se aplican a través del sistema de archivos; el acceso al dispositivo lo salta.

  2. Viola el principio de mínimo privilegio. Si el agregador necesita leer datos, necesita leer ficheros de /var/lib/meteora/lecturas/, no el disco entero. El permiso concedido es varios órdenes de magnitud mayor que la necesidad.

  3. Amplía enormemente la superficie de ataque. Si alguien compromete un proceso que corre como meteora, obtiene lectura completa del disco.

  4. No hay ningún caso legítimo en Meteora que lo requiera. Los usos reales del disco crudo —clonado, análisis forense, recuperación— son tareas administrativas puntuales, no operaciones de un servicio.

Alternativa correcta según lo que se necesite:

Necesidad real Solución correcta
Leer los datos de las lecturas Permisos sobre /var/lib/meteora/, que ya los tiene
Consultar el espacio libre df, que no requiere permisos especiales
Ver el estado SMART del disco sudo con una entrada concreta para smartctl
Hacer una copia del disco Tarea administrativa, con root y puntual
# Si de verdad hiciera falta consultar SMART sin ser root:
# /etc/sudoers.d/meteora-smart
meteora ALL=(root) NOPASSWD: /usr/sbin/smartctl -a /dev/sda

Esto concede exactamente una orden con exactamente unos argumentos, en lugar de acceso completo al disco. Es la diferencia entre una llave de una puerta y una llave maestra del edificio.

Solución 2

A: Sensor I²C, 2 bytes en 8 µs, una vez por minuto, microcontrolador a 80 MHz.

Técnica: espera activa.

Ciclos desperdiciados esperando: 8 µs × 80 MHz = 640 ciclos
Frecuencia: 1 vez por minuto
Coste de CPU: 640 ciclos / (60 s × 80.000.000 ciclos/s) = 0,00000013 %

Justificación:

  • 640 ciclos es menos de lo que costaría una interrupción. Configurar el vector, guardar el contexto, ejecutar el manejador y restaurar cuesta del orden de 200-500 ciclos en un microcontrolador, más la complejidad del código. La interrupción sería más cara que la espera.
  • Es un microcontrolador con FreeRTOS, como establecimos en 01-03. No hay multitarea pesada compitiendo: no hay nada mejor que hacer durante esos 8 µs.
  • La simplicidad tiene valor propio en sistemas empotrados: menos código, menos estado, menos formas de fallar.

Si eligieras mal (interrupciones): funcionaría, pero con más código, más complejidad y más consumo de CPU que la propia espera. Un caso raro donde la solución "avanzada" es objetivamente peor.

B: Tarjeta de red, 800 lecturas/s en paquetes UDP de 66 bytes.

Técnica: DMA con interrupciones (y moderación de interrupciones).

Paquetes por segundo: 800
Datos por segundo: 800 × 66 B = 52,8 KB/s

Con espera activa: la CPU no podría hacer nada más. Descartado.

Con interrupciones sin DMA:
  Copia por CPU: 66 bytes = ~17 palabras de 4 bytes por paquete
  800 × (17 copias × 4 ciclos + 3 µs de interrupción)
  ≈ 800 × 3,02 µs = 2,4 ms/s = 0,24 % de un núcleo

Con DMA:
  800 interrupciones/s × 3 µs = 2,4 ms/s = 0,24 %
  La copia la hace el DMA: coste de CPU ≈ 0

Observación honesta: con solo 800 paquetes/s, DMA y las interrupciones puras dan un coste casi idéntico, porque el coste está dominado por la interrupción, no por copiar 66 bytes. El DMA gana claramente cuando los paquetes son grandes o abundantes.

Pero hay dos razones sólidas para usar DMA igualmente:

  1. Escalabilidad. Si Meteora crece a 500.000 lecturas/s, sin DMA la copia sola consumiría un núcleo entero.
  2. No es opcional. Las tarjetas modernas solo funcionan por DMA: el driver igb programa descriptores y la tarjeta escribe directamente en RAM. La E/S programada ya no existe en este hardware.

Si eligieras mal (espera activa): un núcleo entero consultando el registro de la tarjeta permanentemente, para recibir 52,8 KB/s. Absurdo.

C: SSD NVMe leyendo 17 MB.

Técnica: DMA, sin ninguna duda.

Datos: 17.280.000 bytes = 4.320.000 palabras de 4 bytes

Espera activa o interrupciones con copia por CPU:
  4.320.000 copias × ~4 ciclos = 17.280.000 ciclos
  A 3 GHz = 5,76 ms de CPU pura solo copiando

Con DMA en peticiones de 1 MB:
  17 descriptores + 17 interrupciones × 3 µs = 51 µs de CPU

Factor de mejora: 5.760 µs / 51 µs = 113×

Y además hay un argumento cualitativo aún más fuerte:

Tiempo de lectura del SSD: 17 MB / 3.500 MB/s = 4,9 ms

Con espera activa, la CPU estaría bloqueada 4,9 ms sin hacer nada. A 3 GHz son casi 15 millones de ciclos perdidos, y equivale a más de un quantum completo del planificador (4 ms, según 02-02). Con DMA, esos 4,9 ms los aprovecha otro proceso.

Si eligieras mal: además del desperdicio, el proceso no podría bloquearse en estado S, rompiendo todo el modelo de planificación que estudiamos en 02-02.

D: USB-serie a 9.600 baudios, 200 caracteres al conectar.

Técnica: interrupciones (que es lo que hace el driver ftdi_sio).

9.600 baudios con 8N1 = 8 bits de datos + 1 de inicio + 1 de parada = 10 bits por carácter
Caracteres por segundo: 9.600 / 10 = 960 c/s
Tiempo por carácter: 1,04 ms
Tiempo total de 200 caracteres: 208 ms

Con espera activa:
  208 ms de CPU bloqueada a 3 GHz = 624.000.000 ciclos desperdiciados
  Y 208 ms son 52 quanta de 4 ms: 52 turnos robados a otros procesos

Con interrupciones (agrupando por lotes, como hace USB):
  ~4 interrupciones × 3 µs = 12 µs de CPU

Factor: 208.000 µs / 12 µs = 17.333×

Justificación: 1,04 ms por carácter es una eternidad para una CPU. Es exactamente el escenario para el que se inventaron las interrupciones: dispositivo lentísimo, datos escasos, esperas larguísimas.

Detalle interesante: USB no genera una interrupción por byte. El controlador USB agrupa las transferencias en paquetes y el chip FTDI tiene su propio búfer, así que 200 caracteres pueden llegar en 3 o 4 transferencias. Es buffering aplicado en el propio hardware, exactamente por las razones del apartado 10.

Si eligieras mal (espera activa): 208 ms de CPU bloqueada. En meteo-01, con ingestor recibiendo 800 lecturas/s, esos 208 ms significarían 166 lecturas sin atender. Un puerto serie de diagnóstico habría degradado el servicio de producción.

Resumen:

Caso Técnica Coste de CPU Criterio decisivo
A: sensor I²C Espera activa 0,0000001 % 640 ciclos < coste de una interrupción
B: red DMA 0,24 % Escalabilidad y hardware moderno
C: SSD DMA 0,001 % Volumen de datos: 113× de mejora
D: serie Interrupciones 0,006 % Dispositivo lentísimo, datos escasos

La regla que emerge: espera activa cuando la espera es más corta que la interrupción; DMA cuando hay volumen; interrupciones en todo lo demás.

Solución 3

1. Secuencia de diagnóstico.

# Paso 1: ¿El bus PCI ve la tarjeta?
$ lspci | grep -i ethernet
# Busco: una segunda línea de controlador Ethernet

# Paso 2: ¿Tiene driver asociado?
$ lspci -v -s 05:00.0 | grep -E 'Kernel driver|Kernel modules'
# Busco: "Kernel driver in use: igb"

# Paso 3: ¿Qué dijo el núcleo al detectarla?
$ sudo dmesg -T | grep -iE 'eth|igb|e1000|link' | tail -30
# Busco: errores de inicialización, firmware, interrupciones

# Paso 4: ¿Existe la interfaz de red?
$ ip link show
# Busco: una segunda interfaz además de lo y enp3s0

# Paso 5: ¿Está el módulo cargado?
$ lsmod | grep -E 'igb|e1000'
$ modinfo igb | head -5

# Paso 6: ¿Qué sabe udev del dispositivo?
$ udevadm info --query=all --path=/sys/class/net/enp5s0 2>/dev/null

# Paso 7: ¿Hay conflicto de interrupciones?
$ cat /proc/interrupts | grep -i eth

La lógica del orden es ir de abajo arriba: hardware → driver → núcleo → interfaz. No tiene sentido investigar la configuración de red si el bus PCI ni siquiera ve la tarjeta.

2. Los cuatro escenarios.

a) lspci no muestra ninguna tarjeta nueva.

Conclusión: el problema está por debajo del sistema operativo. Linux no puede gestionar hardware que el bus PCI no enumera; no es un problema de drivers ni de configuración.

Causas posibles y qué hacer:

# ¿Está bien asentada físicamente en la ranura?
# → Apagar, revisar el conector, volver a montar

# ¿La ranura PCIe está habilitada en la BIOS?
# → Revisar la configuración de la BIOS/UEFI

# ¿La ranura funciona?
# → Probar la tarjeta en otra ranura

# ¿La tarjeta funciona?
# → Probarla en otra máquina

# Forzar un reescaneo del bus PCI sin reiniciar:
$ echo 1 | sudo tee /sys/bus/pci/rescan
$ lspci | grep -i ethernet

El reescaneo es útil porque descarta el caso de una tarjeta insertada en caliente que no fue detectada. Si tras él sigue sin aparecer, el problema es físico o de BIOS.

b) Aparece en lspci pero sin Kernel driver in use.

Conclusión: el hardware está presente y correctamente enumerado, pero no hay driver que lo gestione. La tarjeta existe e está inerte.

$ sudo lspci -v -s 05:00.0
05:00.0 Ethernet controller: Intel Corporation I350 Gigabit Network Connection
        Kernel modules: igb
        (no aparece "Kernel driver in use")

Si aparece Kernel modules: igb, el núcleo sabe qué módulo la gestionaría pero no lo ha cargado.

# Cargarlo manualmente
$ sudo modprobe igb
$ dmesg -T | tail -20
$ lspci -v -s 05:00.0 | grep 'Kernel driver'

# Si no existe el módulo, identificar el hardware exacto
$ lspci -nn | grep -i ethernet
05:00.0 Ethernet controller [0200]: Intel Corporation I350 [8086:1521] (rev 01)

# Buscar el driver por el identificador 8086:1521
$ sudo apt install firmware-linux-nonfree   # si necesita firmware

Si el módulo está en la lista negra, aparecerá en /etc/modprobe.d/:

$ grep -r igb /etc/modprobe.d/

Este es el escenario del driver e1000e defectuoso que vimos en 01-05: hardware presente, módulo ausente o bloqueado.

c) Aparece con driver, ip link la muestra como enp5s0 en estado DOWN.

Conclusión: todo el subsistema de dispositivos funciona correctamente. El driver está cargado, el núcleo ha creado la interfaz, udev la ha nombrado. Lo que falta es configuración de red o enlace físico.

$ ip link show enp5s0
3: enp5s0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN
# ¿Hay cable conectado y enlace negociado?
$ sudo ethtool enp5s0 | grep -E 'Link detected|Speed'
Link detected: no

# Si "Link detected: no" → problema físico:
#   cable desconectado o defectuoso, o puerto del switch apagado

# Si "Link detected: yes" → solo falta activarla:
$ sudo ip link set enp5s0 up
$ sudo ip addr add 10.20.0.5/24 dev enp5s0

Link detected es la comprobación decisiva en este escenario: separa un problema de cableado de uno de configuración, que son dos investigaciones completamente distintas.

Para hacerlo persistente:

# /etc/systemd/network/20-estaciones.network
[Match]
Name=estaciones0

[Network]
Address=10.20.0.5/24

d) dmesg muestra Failed to initialize MSI-X interrupts.

Conclusión: el driver se ha cargado pero no ha podido configurar las interrupciones, que como vimos en lspci son MSI-X con 5 vectores. Sin interrupciones funcionando, la tarjeta no puede avisar de que ha recibido paquetes.

$ sudo dmesg -T | grep -A5 'Failed to initialize MSI-X'
[Sun Aug 31 10:14:22 2026] igb 0000:05:00.0: Failed to initialize MSI-X interrupts. Falling back to MSI interrupts.

Si dice Falling back to MSI interrupts, el driver ha degradado a MSI y probablemente funcione, aunque con menos paralelismo: en lugar de 5 vectores (uno por cola), tendrá uno solo, lo que reduce el rendimiento con carga alta.

# Comprobar cuántas líneas usa realmente
$ cat /proc/interrupts | grep enp5s0
 129:  1204  0  0  0  PCI-MSI 2621440-edge  enp5s0

# Una sola línea: está en MSI, no MSI-X (que mostraría 5)

Causas y soluciones:

# ¿Se han agotado los vectores de interrupción del sistema?
$ cat /proc/interrupts | wc -l

# ¿El núcleo tiene MSI deshabilitado por algún parámetro?
$ cat /proc/cmdline | grep -o 'pci=[^ ]*'
# Si aparece pci=nomsi, es la causa: quitarlo de GRUB

# ¿La BIOS tiene el IOMMU o VT-d mal configurado?
# → Revisar en la BIOS

# ¿Hay actualización de firmware o de BIOS pendiente?
$ sudo dmidecode -s bios-version

Si funciona degradada a MSI, es aceptable para Meteora con 800 lecturas/s —ya calculamos que son 2,4 ms/s de CPU—, pero conviene resolverlo para tener margen de crecimiento. El detalle de MSI, MSI-X y por qué importan varios vectores es el contenido de la próxima lección.

3. Regla de udev para un nombre estable.

# /etc/udev/rules.d/70-meteora-red.rules
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="igb", \
  ATTR{address}=="a0:36:9f:12:34:56", \
  ATTR{type}=="1", NAME="estaciones0"

Explicación de cada condición:

  • SUBSYSTEM=="net": solo interfaces de red.
  • ACTION=="add": solo al aparecer el dispositivo.
  • ATTR{address}: la dirección MAC, que es única e inmutable en el hardware. Es lo que garantiza que la regla identifique esta tarjeta y no otra.
  • ATTR{type}=="1": tipo Ethernet, para no coincidir con interfaces virtuales.
  • NAME=: el nombre definitivo. Ojo: para interfaces de red se usa NAME, no SYMLINK, porque la interfaz se renombra en lugar de crear un alias.
$ sudo udevadm control --reload-rules
$ sudo udevadm trigger --subsystem-match=net
$ ip link show estaciones0

Por qué no basta con ip link set name:

ip link set enp5s0 name estaciones0 Regla de udev
Persiste tras reiniciar No
Se aplica antes que la configuración de red No
Depende del nombre inicial (si cambia, falla) No, usa la MAC
Funciona con conexión en caliente No

El problema de fondo es el mismo que con /dev/sda1 en /etc/fstab: el nombre asignado por el núcleo depende de la enumeración del bus PCI, que puede cambiar al añadir o mover hardware. La tarjeta que hoy es enp5s0 puede ser enp6s0 mañana si insertas otra tarjeta en una ranura anterior.

Y hay un problema de orden aún más grave: ip link set name es una orden que se ejecuta después de que el sistema haya arrancado la red. Para entonces, systemd-networkd ya habrá intentado configurar enp5s0 con la configuración de estaciones0, o al revés, y la interfaz podría acabar con la IP equivocada. Con udev, el renombrado ocurre en el momento de la detección, antes de que nada más toque la interfaz.

En Meteora esto es especialmente importante: si estaciones0 (red de estaciones) y la interfaz de la API intercambiaran nombres tras un reinicio, el ingestor escucharía en la red equivocada y las lecturas se perderían sin ningún mensaje de error. Un fallo silencioso e irreversible, exactamente el que hay que diseñar para que no ocurra.

Conclusión

El subsistema de E/S existe para que read(fd, buf, 1024) funcione igual sobre un teclado a 100 B/s y sobre un NVMe a 3.500 MB/s, nueve órdenes de magnitud de diferencia. Lo consigue con una arquitectura en capas donde el software independiente del dispositivo hace todo lo común —nombres, permisos, buffering, caché, errores— y cada controlador solo implementa lo verdaderamente específico de su modelo.

La clasificación en bloque, carácter y red determina la interfaz: los de bloque admiten acceso aleatorio y se montan; los de carácter son flujos secuenciales; los de red no tienen fichero en /dev porque un flujo de bytes fiable no describe bien unos paquetes que pueden perderse y desordenarse, y por eso UNIX creó una segunda abstracción, el socket, en lugar de deformar la primera. El principio "todo es un fichero" unifica permisos, herramientas y composición, y descansa en un mecanismo sorprendentemente simple: los números mayor y menor, donde el mayor elige el controlador y el menor la instancia. udev puebla /dev dinámicamente y sus reglas resuelven dos problemas reales: nombres estables que no dependen del orden de detección, y permisos exactos que evitan tener que ejecutar como root.

Hay tres formas de hablar con un dispositivo, y elegir bien es cuestión de aritmética: espera activa cuando la espera es más corta que una interrupción (o durante el arranque y el pánico), interrupciones para dispositivos lentos con pocos datos, y DMA para todo lo que mueva volumen, con una reducción del coste de CPU de más de 150 veces. Los registros se alcanzan por puertos de E/S o por MMIO, y MMIO ha ganado porque su protección la aplica la MMU con granularidad de página, aunque exige marcar esas páginas como no cacheables. Finalmente, las técnicas del subsistema —buffering (con el doble búfer aportando un 62,5 % de mejora en el ingestor), caching (un factor de 20 en la segunda lectura del fichero diario), spooling y reserva— resuelven cada una un desajuste concreto, y el tratamiento de errores los resuelve lo más abajo posible, dejando arriba solo lo que requiere decisión.

Hemos seguido el camino de una lectura de estación desde la tarjeta hasta el búfer del ingestor y hemos identificado las capas, pero deliberadamente hemos dejado tres cajas sin abrir: qué hace exactamente la CPU al recibir una interrupción, cómo un controlador se registra ante el núcleo y qué contrato cumple, y cómo funciona el DMA por dentro. También ha quedado planteado un problema con números: a 500.000 lecturas por segundo, un núcleo entero se dedicaría solo a atender interrupciones. La solución a eso, y a todo lo anterior, está en Controladores, Interrupciones y Operaciones de E/S, que cierra el módulo.

Fundamentos de Sistemas Operativos

Módulo 1: Introducción a los Sistemas Operativos

Módulo 2: Gestión de Recursos

Módulo 3: Concurrencia

Módulo 4: Estructuras de Archivos

Módulo 5: Protección y Seguridad del Sistema

Módulo 6: Virtualización y Contenedores

Módulo 7: Administración y Diagnóstico en la Práctica

© Copyright 2026. Todos los derechos reservados