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
- Por qué el sistema operativo uniformiza los dispositivos
- La estructura del subsistema de E/S
- Clasificación: bloque, carácter y red
- "Todo es un fichero" y el directorio
/dev - Números mayor y menor
- udev y la creación dinámica de dispositivos
- Buses y enumeración:
lspci,lsusb,lsblk - Las tres formas de hablar con un dispositivo
- Puertos de E/S y E/S mapeada en memoria
- Técnicas del subsistema: buffering, caching, spooling y reserva
- Tratamiento de errores y reintentos
- 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:
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 | Sí, 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 | Sí | 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 sí 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:
- Un solo modelo de permisos. Que
/dev/nvme0n1pertenezca al grupodiskcon permisosrw-rw----es lo que impide que el usuariometeoralea 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. - Reutilización de herramientas.
cat,dd,cp,grepfuncionan sobre dispositivos sin haber sido programados para ello. - Composición. Puedes redirigir, canalizar y encadenar dispositivos como cualquier otra cosa.
Un ejemplo que aprovecha las tres a la vez:
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:
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óttyUSB0ottyUSB3.OWNER/GROUP/MODE: hacen el dispositivo accesible al usuariometeorasin 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 |
Sí |
| SATA/SCSI | Consulta al controlador | lsblk, lsscsi |
Sí |
| 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: igbCada 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, 12Mlsusb -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 CPUUn 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:
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.
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 hizoread()obtiene-1conerrno = 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) | Sí, 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
- Clasifica cada entrada por tipo y explica cómo lo sabes.
- ¿Por qué
/dev/sday/dev/sda1comparten el mayor pero no el menor? - Si el usuario
meteora(grupometeora) ejecutadd if=/dev/sda of=/tmp/copia bs=1M count=10, ¿funciona? ¿Y sobre/dev/meteora/estacion-norte? - ¿Qué representa el
7en la última línea, donde las demás tienen dos números? - Escribe una regla de udev que dé al grupo
meteoraacceso 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-01recibiendo 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:
- Escribe la secuencia de órdenes que ejecutarías, en orden, y qué buscas en cada una.
- Para cada uno de estos cuatro resultados posibles, di qué concluyes y qué harías:
- a)
lspcino muestra ninguna tarjeta nueva. - b)
lspcila muestra pero sin líneaKernel driver in use. - c)
lspcila muestra con driver,ip linkla muestra comoenp5s0en estadoDOWN. - d)
dmesgmuestraigb 0000:05:00.0: Failed to initialize MSI-X interrupts.
- a)
- Una vez funcionando, escribe una regla de udev que le dé un nombre estable
estaciones0y explica por qué no basta conip 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.
- Propietario
root, conrw.meteorano es root. - Grupo
disk, conrw. Habría que comprobar simeteorapertenece adisk:
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:
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:
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:
-
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/shadowy las claves privadas de TLS. Los permisos de fichero se aplican a través del sistema de archivos; el acceso al dispositivo lo salta. -
Viola el principio de mínimo privilegio. Si el
agregadornecesita 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. -
Amplía enormemente la superficie de ataque. Si alguien compromete un proceso que corre como
meteora, obtiene lectura completa del disco. -
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:
- Escalabilidad. Si Meteora crece a 500.000 lecturas/s, sin DMA la copia sola consumiría un núcleo entero.
- No es opcional. Las tarjetas modernas solo funcionan por DMA: el driver
igbprograma 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:
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/:
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.
# ¿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 usaNAME, noSYMLINK, 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 | Sí |
| Se aplica antes que la configuración de red | No | Sí |
| Depende del nombre inicial | Sí (si cambia, falla) | No, usa la MAC |
| Funciona con conexión en caliente | No | Sí |
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
- Conceptos Básicos de Sistemas Operativos
- Historia y Evolución de los Sistemas Operativos
- Tipos de Sistemas Operativos
- Funciones Principales de un Sistema Operativo
- Arquitectura del Núcleo: Monolítico, Microkernel e Híbrido
- Modo Usuario, Modo Núcleo y Llamadas al Sistema
Módulo 2: Gestión de Recursos
- Gestión de Procesos
- Planificación de la CPU
- Gestión de Memoria
- Memoria Virtual y Paginación
- Gestión de Almacenamiento
- Gestión de Dispositivos
- Controladores, Interrupciones y Operaciones de E/S
Módulo 3: Concurrencia
- Conceptos de Concurrencia
- Hilos y Procesos
- Comunicación entre Procesos (IPC)
- Sincronización y Exclusión Mutua
- Problemas Clásicos de Concurrencia
- Interbloqueos: Prevención, Detección y Recuperación
Módulo 4: Estructuras de Archivos
- Sistemas de Archivos
- Estructuras de Directorios
- Particiones, Montaje y Sistema de Archivos Virtual
- Gestión de Archivos
- Asignación de Espacio, Journaling e Integridad
- Seguridad y Permisos de Archivos
Módulo 5: Protección y Seguridad del Sistema
- Principios de Protección y Control de Acceso
- Usuarios, Autenticación y Escalada de Privilegios
- Amenazas Comunes y Endurecimiento del Sistema
- Auditoría, Registros y Respuesta a Incidentes
Módulo 6: Virtualización y Contenedores
- Virtualización: Hipervisores y Máquinas Virtuales
- Contenedores: Namespaces y cgroups
- El Sistema Operativo en la Nube
- Sistemas Operativos Móviles y de Tiempo Real
