1. Tres piezas con nombres parecidos
En el tema 5 dijimos que los dispositivos tienen dos rasgos que condicionan a todo sistema operativo: son lentos y son muy distintos entre sí. Vimos lo primero en el tema 7. Ahora nos ocupamos de lo segundo: qué hay entre la CPU y un dispositivo, y cómo se entienden.
Intervienen tres piezas, y en español sus nombres se prestan a confusión, así que conviene fijarlos desde el principio:
| Pieza | Qué es | Ejemplo |
|---|---|---|
| Dispositivo | El aparato físico que hace el trabajo. | Los platos y el cabezal de un disco; las teclas; la antena. |
| Controlador de hardware (device controller) | Un circuito electrónico que maneja el dispositivo y lo conecta al bus. | El chip controlador NVMe; el controlador USB. |
| Controlador de dispositivo (device driver) | Un programa, parte del kernel, que sabe conversar con un controlador de hardware concreto. | El módulo nvme de Linux. |
Para evitar la ambigüedad, llamaremos controlador al circuito y driver al programa, que es además el término que se usa a diario.
El controlador existe para resolver una diferencia de naturaleza. El dispositivo trabaja con fenómenos físicos —campos magnéticos, voltajes, ondas de radio— y a su propio ritmo; la CPU trabaja con bits y a miles de millones de operaciones por segundo. El controlador traduce entre ambos mundos y le ofrece a la CPU una interfaz hecha de lo único que ella entiende: unas pocas celdas donde leer y escribir.
2. Los registros del controlador
Esas celdas son los registros del controlador. No deben confundirse con los registros de la CPU del tema 6: estos pertenecen al controlador y son su panel de mandos. Casi todos los controladores, por distintos que sean, tienen registros de tres clases:
| Registro | Lo usa la CPU para… | Ejemplos de contenido |
|---|---|---|
| De estado | Leer en qué situación está el dispositivo. | ¿Está ocupado? ¿Terminó? ¿Hubo un error? ¿Hay un dato disponible? |
| De control | Escribir órdenes y configuración. | «Lee el bloque 5000», «activa las interrupciones», «reinicia». |
| De datos | Leer o escribir los datos que se transfieren. | El código de la tecla pulsada; el byte que se va a enviar. |
Cada bit de un registro de estado o de control suele tener un significado propio. En un registro de estado de 8 bits, por ejemplo, el bit 0 puede indicar «hay un dato listo» y el bit 7 «se produjo un error». Usar un dispositivo consiste, en esencia, en una coreografía de lecturas y escrituras sobre esos bits: escribir una orden, consultar el estado, recoger el dato.
Cuáles son los registros, en qué orden deben usarse y qué significa cada bit lo define el fabricante en la documentación del controlador. Ese conocimiento es precisamente lo que un driver lleva dentro, y es la razón por la que cada controlador necesita el suyo.
3. Cómo llega la CPU a esos registros
Para leer o escribir un registro del controlador, la CPU necesita una manera de nombrarlo. Hay dos, y ambas se reducen a darle una dirección.
Puertos de entrada y salida
El procesador dispone de un segundo espacio de direcciones, pequeño e independiente del de la memoria, cuyos elementos se llaman puertos. Cada registro de un controlador recibe un número de puerto, y la CPU tiene instrucciones específicas para ese espacio: en x86, in lee de un puerto y out escribe en él. Es el método tradicional de la arquitectura x86, que admite 65 536 puertos.
Entrada y salida mapeada en memoria
La alternativa es reservar un rango de direcciones de memoria y conectarlo al controlador en lugar de a la RAM. Leer o escribir esas direcciones no accede a ninguna celda de memoria: llega a los registros del controlador. Se la conoce como E/S mapeada en memoria, o MMIO por sus siglas en inglés.
Su ventaja es la simplicidad: no hacen falta instrucciones especiales. El mismo mov que copia una variable sirve para dar una orden a un disco, y en C un registro de dispositivo se maneja con un puntero común. Por eso es el método dominante en el hardware actual, y el único en arquitecturas como ARM.
| Puertos de E/S | E/S mapeada en memoria | |
|---|---|---|
| Espacio de direcciones | Propio, separado de la memoria | El mismo que la memoria |
| Instrucciones | Especiales: in y out | Las normales de acceso a memoria |
| Dónde se usa | Dispositivos heredados de la PC | Casi todo el hardware moderno |
| Quién puede usarlo | Solo el kernel, en ambos casos | |
La última fila es la que más importa al curso. Si un programa cualquiera pudiera escribir en los registros del controlador de disco, podría borrar los archivos de todos los usuarios saltándose cualquier permiso. Por eso la CPU rechaza las instrucciones in y out en modo usuario, y la unidad de gestión de memoria impide a los programas alcanzar las direcciones de los dispositivos. El acceso al hardware es un privilegio del kernel; lo veremos en el tema 12.
La E/S mapeada en memoria rompe una intuición: leer dos veces la misma dirección puede dar resultados distintos, y el simple hecho de leer puede cambiar el estado del dispositivo; por ejemplo, retirar un dato de su cola. Los compiladores, que suponen lo contrario, necesitan que se les avise: es el uso principal de la palabra clave volatile de C.
4. El mapa de puertos y de memoria
El kernel lleva el registro de qué driver usa cada rango de puertos y de memoria, y lo publica en dos archivos. Hay que consultarlos con sudo, porque a los usuarios comunes el kernel les oculta las direcciones por seguridad.
$ sudo cat /proc/ioports
0000-0cf7 : PCI Bus 0000:00
0000-001f : dma1
0020-0021 : pic1
0040-0043 : timer0
0060-0060 : keyboard
0064-0064 : keyboard
0070-0071 : rtc0
…
Cada línea es un rango de puertos, en hexadecimal, y el nombre de quien lo tiene asignado. Aparecen componentes que conoceremos mejor más adelante: pic1 es un controlador de interrupciones, timer0 es el temporizador, y keyboard es el controlador del teclado, con su registro de datos en el puerto 0x60 y el de estado en el 0x64. La línea resaltada, rtc0, es el reloj de hardware; la usaremos enseguida.
$ sudo cat /proc/iomem
00001000-0009d3ff : System RAM
00100000-7a5d1fff : System RAM
…
dc300000-dc303fff : 0000:03:00.0
dc300000-dc303fff : nvme
…
100000000-47dffffff : System RAM
Este segundo archivo es el mapa del espacio de direcciones de memoria física, y muestra con claridad la E/S mapeada en memoria: la mayor parte de los rangos corresponde a RAM (System RAM), pero entre ellos hay rangos que pertenecen a dispositivos. El resaltado, de 16 KiB, no es memoria: son los registros del controlador del disco NVMe, a los que accede el driver nvme.
5. Hablar con un dispositivo real
Vamos a conversar directamente con un controlador, sin pasar por su driver. Elegimos el más sencillo de una PC: el reloj de tiempo real (RTC), el chip alimentado por una pila que mantiene la fecha y la hora con el equipo apagado. Acabamos de ver que ocupa los puertos 0x70 y 0x71. Su protocolo tiene dos pasos:
- Escribir en el puerto
0x70el número del dato que se quiere consultar: 0 para los segundos, 2 para los minutos, 4 para las horas. - Leer el valor del puerto
0x71.
El primer puerto hace de registro de control y el segundo de registro de datos. El programa siguiente aplica ese protocolo tres veces:
#include <stdio.h>
#include <sys/io.h>
#define PUERTO_INDICE 0x70
#define PUERTO_DATO 0x71
static int leer_rtc(int registro)
{
outb(registro, PUERTO_INDICE); /* elegir qué dato se quiere */
int bcd = inb(PUERTO_DATO); /* leerlo */
return (bcd >> 4) * 10 + (bcd & 0x0f);
}
int main(void)
{
/* pedir al kernel permiso para usar 2 puertos a partir de 0x70 */
if (ioperm(PUERTO_INDICE, 2, 1) == -1) {
perror("ioperm");
return 1;
}
printf("Hora del reloj de hardware: %02d:%02d:%02d\n",
leer_rtc(4), leer_rtc(2), leer_rtc(0));
return 0;
}
Las funciones outb e inb ejecutan las instrucciones de máquina out e in. La última línea de leer_rtc convierte el valor, porque este chip guarda cada número en un formato llamado BCD, con un dígito decimal en cada mitad del byte. La llamada ioperm es la clave del experimento: como las instrucciones de puertos están prohibidas en modo usuario, el programa debe pedirle al kernel que haga una excepción.
$ gcc -Wall -Wextra -g rtc.c -o rtc
$ ./rtc
ioperm: Operation not permitted
$ sudo ./rtc
Hora del reloj de hardware: 13:32:07
$ cat /sys/class/rtc/rtc0/time # lo mismo, a través del driver del kernel
13:32:07
Las dos ejecuciones enseñan cosas distintas:
- Sin privilegios, el kernel se niega. Un usuario común no puede obtener acceso a los puertos. Es la protección de la sección 3 en funcionamiento.
- Con privilegios, el programa habla con el chip. Seis instrucciones de máquina, tres
outy tresin, bastaron para obtener la hora. El resultado coincide con el que ofrece el kernel a través de su propio driver, que por dentro hace esto mismo. La hora suele estar en tiempo universal (UTC), no en tu hora local.
Solo en procesadores x86-64, y sobre Linux nativo o una máquina virtual. En ARM no existen los puertos de E/S y el programa no compila. En WSL puede no haber un reloj de este tipo. Y en equipos con el arranque seguro activado, el kernel puede rechazar ioperm incluso con sudo. Si es tu caso, basta con leer el ejemplo. Limítate además a los puertos indicados: escribir en puertos al azar puede desconfigurar el hardware.
6. El problema de esperar
El reloj responde al instante, pero es una excepción. Lo normal es que entre la orden y el resultado pase mucho tiempo, medido en la escala de la CPU: una tecla tarda lo que tarde la persona en pulsarla; un disco, millones de ciclos de reloj. La pregunta es qué hace la CPU mientras tanto y cómo se entera de que el dispositivo terminó. Hay tres respuestas, que aparecieron sucesivamente en la historia y hoy conviven.
7. E/S programada: preguntar
La estrategia más simple es que la CPU, después de dar la orden, consulte una y otra vez el registro de estado hasta que el bit de «listo» se active. Se llama E/S programada, y a la consulta repetida, sondeo (polling). En código, el patrón es este:
escribir(REG_CONTROL, ORDEN_LEER); /* dar la orden */
while ((leer(REG_ESTADO) & BIT_LISTO) == 0)
; /* esperar preguntando */
dato = leer(REG_DATOS); /* recoger el resultado */
El bucle vacío del medio se denomina espera activa: la CPU está ocupada al cien por ciento sin hacer nada útil. Es como quedarse de pie frente al horno mirando por el cristal hasta que la comida esté lista.
Es un método fácil de programar y no requiere nada del hardware, pero desperdicia justamente el recurso que la multiprogramación quería aprovechar. Solo tiene sentido cuando la espera es brevísima, o cuando el dispositivo es tan rápido que los datos llegan sin pausa y atenderlos de otro modo costaría más.
8. E/S por interrupciones: ser avisado
La alternativa es invertir la iniciativa: en lugar de preguntar la CPU, avisa el dispositivo. El controlador dispone de una línea por la que puede enviar una señal a la CPU, llamada interrupción. La secuencia pasa a ser:
- El driver escribe la orden en el registro de control y vuelve inmediatamente.
- El kernel entrega la CPU a otro programa. El que pidió la operación queda en espera.
- Cuando el dispositivo termina, su controlador activa la señal de interrupción.
- La CPU la detecta al final de la instrucción en curso —el cuarto paso del ciclo que vimos en el tema 6—, suspende lo que estaba haciendo y salta a una rutina del driver.
- Esa rutina recoge el resultado de los registros y marca como listo al programa que esperaba.
Siguiendo con la analogía: se pone la comida en el horno, se activa el temporizador y uno se dedica a otra cosa hasta que suena.
Este mecanismo es el que hizo posible la multiprogramación y sigue siendo la base de toda la entrada y salida. Le dedicamos el tema 17 completo. Tiene, sin embargo, una limitación: si en cada interrupción el driver debe copiar los datos uno por uno desde el registro de datos a la memoria, un dispositivo rápido interrumpiría a la CPU millones de veces por segundo y la mantendría ocupada copiando.
9. DMA: que el dispositivo copie
Para las transferencias grandes existe un tercer método, el acceso directo a memoria, o DMA (direct memory access). La idea es que el controlador escriba o lea los datos directamente en la RAM, sin que pasen por la CPU:
- El driver le indica al controlador tres cosas: qué operación realizar, en qué dirección de memoria dejar los datos y cuántos bytes transferir.
- El kernel entrega la CPU a otro programa.
- El controlador toma el control del bus por su cuenta y copia el bloque completo a la memoria, o desde ella.
- Al terminar toda la transferencia, envía una sola interrupción.
La CPU interviene únicamente al principio y al final, sin importar si se transfieren cuatro kilobytes o cuatro megabytes. En la analogía, sería un repartidor con llave que deja la compra ya guardada en la nevera y toca el timbre al salir.
Los discos, las tarjetas de red y las tarjetas gráficas trabajan así. El DMA tiene una implicación de seguridad que no conviene pasar por alto: un controlador capaz de escribir en cualquier dirección de memoria puede sobrescribir el kernel. Los equipos actuales incluyen por eso un componente, la IOMMU, que limita las zonas de memoria que cada dispositivo puede tocar.
Comparación
| E/S programada | Por interrupciones | DMA | |
|---|---|---|---|
| ¿Quién avisa que terminó? | Nadie: la CPU pregunta | El controlador | El controlador |
| ¿Quién copia los datos? | La CPU | La CPU | El controlador |
| CPU durante la espera | Ocupada | Libre | Libre |
| Interrupciones por transferencia | Ninguna | Una por dato | Una por bloque |
| Uso típico | Arranque; esperas muy cortas | Teclado, ratón, puertos serie | Discos, red, vídeo |
10. Cómo descubre el kernel el hardware
Queda una pregunta: ¿cómo sabe el kernel qué dispositivos hay y qué driver corresponde a cada uno? En los primeros equipos había que indicárselo a mano. Los buses modernos, como PCI Express y USB, permiten que el kernel lo averigüe solo, mediante un proceso llamado enumeración.
Cada dispositivo conectado a uno de esos buses lleva grabada una pequeña ficha de identificación que incluye, entre otros datos, un identificador de fabricante y un identificador de producto. Al arrancar, o al conectar algo, el kernel recorre el bus, lee las fichas y hace tres cosas con cada dispositivo: le asigna un rango de direcciones para sus registros, le asigna una línea de interrupción y busca, entre los drivers disponibles, uno que declare saber manejar ese par de identificadores.
El comando lspci muestra el resultado. Veamos el disco NVMe que apareció en el tema 5:
$ sudo lspci -v -s 03:00.0
03:00.0 Non-Volatile memory controller: Samsung Electronics Co Ltd NVMe SSD Controller SM981/PM981/PM983
Flags: bus master, fast devsel, latency 0, IRQ 16
Memory at dc300000 (64-bit, non-prefetchable) [size=16K]
Kernel driver in use: nvme
En cuatro fragmentos resaltados está casi todo el tema:
Memory at dc300000 … [size=16K]: los registros del controlador están mapeados en memoria, en el mismo rango que vimos en/proc/iomem.IRQ 16: la línea de interrupción por la que avisa que terminó.bus master: el controlador puede tomar el control del bus, es decir, hacer DMA.Kernel driver in use: nvme: el driver que el kernel le asignó.
La misma información está disponible como archivos en el directorio /sys, que el kernel usa para publicar su inventario de dispositivos:
$ cd /sys/bus/pci/devices/0000:03:00.0
$ cat vendor device # identificadores de fabricante y de producto
0x144d
0xa808
$ basename $(readlink driver) # driver asignado
nvme
El par 144d:a808 identifica a ese modelo de controlador en cualquier equipo del mundo, y es lo que permite que conectes un dispositivo nuevo y funcione sin configuración: el kernel lee los identificadores y carga el driver adecuado. En las placas basadas en ARM, donde muchos dispositivos no pueden identificarse solos, esa información se le entrega al kernel en un archivo de descripción del hardware llamado device tree.
11. Tipos de dispositivos
La variedad de dispositivos es enorme, también en velocidad: un teclado produce unos pocos bytes por segundo y un disco NVMe, miles de millones. Para poder tratarlos de manera uniforme, los sistemas tipo Unix los agrupan según cómo se accede a sus datos:
| Tipo | Cómo se accede | Ejemplos |
|---|---|---|
| De bloques | Los datos están en bloques de tamaño fijo con dirección. Se puede leer o escribir cualquier bloque, en cualquier orden. | Discos rígidos, SSD, memorias USB |
| De caracteres | Los datos son un flujo de bytes que llega o sale en secuencia. No se puede «ir a una posición». | Teclado, ratón, terminal, puerto serie |
| De red | Envían y reciben paquetes. Se usan mediante sockets, no como archivos. | Ethernet, Wi-Fi |
Esta clasificación es la que permite que un programa lea de un disco o de un teclado con la misma llamada al sistema, como hacía copiar.c en el tema 2. La retomamos en el tema 29, donde veremos cómo aparecen los dispositivos en el directorio /dev.
12. El recorrido completo de una lectura
Reunamos todas las piezas siguiendo un pedido de principio a fin. Un programa llama a read para leer parte de un archivo cuyos datos no están en la caché de disco:
- El programa pide leer. Llama a
readcon un descriptor de archivo, una dirección donde dejar los datos y una cantidad de bytes. No sabe nada de discos. modo usuario - El kernel traduce el pedido. Su parte independiente del hardware averigua a qué archivo corresponde el descriptor, comprueba que los datos no estén en la caché y determina qué bloques del disco hacen falta. modo kernel
- El driver da la orden. Convierte «lee estos bloques» en el formato que exige ese controlador y lo escribe en sus registros, indicando en qué memoria dejar los datos. modo kernel
- El kernel cede la CPU. El programa queda en espera y otro pasa a ejecutarse. otro programa
- El controlador trabaja. Lee los bloques del dispositivo y los copia a la memoria por DMA. Cuando termina, envía una interrupción. hardware
- El driver atiende la interrupción. Consulta el registro de estado para confirmar que no hubo errores y avisa al resto del kernel. modo kernel
- El kernel completa el pedido. Copia los datos a la dirección que indicó el programa, guarda una copia en la caché de disco y marca al programa como listo para continuar. modo kernel
- El programa sigue. Para él,
readfue una función que tardó un poco en volver. modo usuario
El recorrido muestra cómo se organiza el software de entrada y salida en capas: el programa, la parte genérica del kernel, el driver y, debajo, el controlador y el dispositivo. Solo el driver conoce los detalles del hardware, y eso tiene dos consecuencias que explican mucho sobre los sistemas operativos reales:
- Los drivers son la mayor parte del kernel. Hay miles de controladores distintos en el mercado, y cada uno necesita su driver. Más de la mitad del código fuente de Linux son drivers.
- Los drivers son la principal fuente de fallos graves. Se ejecutan en modo kernel, con acceso a todo. Un error en uno de ellos puede detener el sistema completo, y a menudo los escribe el fabricante del hardware, no el del sistema operativo.
13. Resumen
- Entre la CPU y un dispositivo hay un controlador, un circuito que lo maneja. El driver es el programa del kernel que sabe usar ese controlador.
- El controlador se presenta como un conjunto de registros de estado, de control y de datos. Usar un dispositivo es leer y escribir en ellos.
- La CPU alcanza esos registros mediante puertos de E/S, con instrucciones especiales, o mediante E/S mapeada en memoria, con las instrucciones normales.
- En ambos casos el acceso está reservado al kernel: un programa común no puede tocar el hardware.
- Para esperar a un dispositivo hay tres métodos: sondear su estado, recibir una interrupción o delegar la copia en el controlador mediante DMA.
- Con DMA, la CPU solo interviene al iniciar la transferencia y al recibir la interrupción final.
- El kernel enumera los buses, identifica cada dispositivo por sus identificadores de fabricante y producto, y le asigna direcciones, una interrupción y un driver.
- Los dispositivos se clasifican en de bloques, de caracteres y de red.
14. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué diferencia hay entre un controlador de hardware y un driver?
El controlador es un circuito electrónico que maneja físicamente el dispositivo y lo conecta al bus. El driver es software: la parte del kernel que conoce los registros de ese controlador y sabe en qué orden usarlos.
2. ¿Para qué sirve cada una de las tres clases de registros de un controlador?
El registro de estado informa la situación del dispositivo: ocupado, listo, con error. El de control recibe las órdenes y la configuración. El de datos contiene la información que se transfiere en un sentido u otro.
3. ¿Qué ventaja tiene la E/S mapeada en memoria frente a los puertos?
No necesita instrucciones especiales: los registros del dispositivo se leen y escriben con las mismas instrucciones que la memoria, lo que simplifica el procesador y permite escribir los drivers en C usando punteros.
4. ¿Por qué ./rtc falla sin sudo?
Porque las instrucciones de acceso a puertos están prohibidas en modo usuario. El programa pide al kernel una excepción mediante ioperm, y el kernel solo la concede a un usuario con privilegios de administrador.
5. ¿Por qué la espera activa es un mal método para leer de un disco?
Porque el disco tarda una cantidad de tiempo enorme en la escala de la CPU. Durante toda esa espera el procesador estaría ejecutando un bucle de consulta en lugar de dedicarse a otro programa.
6. Un disco transfiere un bloque de 4096 bytes. ¿Cuántas interrupciones recibe la CPU con DMA? ¿Y si el driver copiara de a un byte por interrupción?
Con DMA, una sola, al terminar toda la transferencia. Sin DMA y de a un byte, 4096.
7. Conectas un dispositivo USB que el equipo nunca había visto y funciona de inmediato. ¿Qué hizo el kernel?
Detectó la conexión, leyó la ficha de identificación del dispositivo con sus identificadores de fabricante y de producto, buscó un driver que declarara manejar ese par y lo asoció al dispositivo.
8. ¿Por qué un error en un driver puede colgar todo el sistema, y un error en un editor de texto no?
El driver forma parte del kernel y se ejecuta en modo privilegiado, con acceso a toda la memoria y a todo el hardware. El editor se ejecuta en modo usuario, aislado: si falla, el kernel lo finaliza sin que el resto se vea afectado.
Para practicar
- Ejecuta
lspci -ky haz una lista de tus dispositivos PCI con el driver que usa cada uno. - Elige uno y búscalo con
sudo lspci -v -sseguido de su dirección. Identifica su rango de memoria, su interrupción y si puede hacer DMA. Luego localiza ese rango ensudo cat /proc/iomem. - Con
lsusb, anota el par de identificadores de tu teclado o tu ratón. Desconéctalo, vuelve a conectarlo y ejecutasudo dmesg | tailpara ver cómo lo registró el kernel. - Si tu entorno lo permite, modifica
rtc.cpara mostrar también el día, el mes y el año, que están en los registros 7, 8 y 9 del reloj. - Ejecuta
cat /proc/interrupts. Aún no lo hemos estudiado, pero busca en él el driver de tu disco y el de tu tarjeta de red. ¿Qué crees que cuentan las columnas numéricas?