1. Quién despierta al kernel
En el tema 11 dijimos que el kernel no es un programa que corre en paralelo con los demás: solo se ejecuta cuando algo lo invoca, y hay tres cosas que pueden hacerlo. El bloque 3 estuvo dedicado a la primera, las llamadas al sistema. Este bloque trata de las otras dos, y empieza por la que no depende de ningún programa.
El problema que resuelve lo planteamos en el tema 8. Los dispositivos son lentísimos comparados con la CPU, y sus tiempos son impredecibles: nadie sabe cuándo se pulsará una tecla. Si la CPU tuviera que preguntarles continuamente si tienen novedades, gastaría casi todo su tiempo preguntando. La solución es que avise el dispositivo.
Una interrupción de hardware es una señal eléctrica con la que un dispositivo comunica a la CPU que requiere atención. Al recibirla, la CPU suspende lo que está ejecutando, pasa a modo kernel y salta a una rutina del kernel preparada para ese dispositivo. Cuando la rutina termina, lo suspendido continúa como si nada hubiera ocurrido.
Es el cuarto paso del ciclo de instrucción que dejamos pendiente en el tema 6: después de ejecutar cada instrucción, y antes de captar la siguiente, la CPU comprueba si hay una interrupción esperando.
Su importancia es difícil de exagerar. Sin interrupciones, un sistema operativo solo recuperaría el control cuando los programas quisieran devolvérselo. Con ellas, el hardware se lo devuelve a la fuerza, miles de veces por segundo. La multiprogramación de los años sesenta, que repasamos en el tema 3, existió a partir del momento en que las máquinas tuvieron interrupciones.
2. Sin relación con el programa en curso
Las tres entradas al kernel provocan lo mismo en el procesador: un cambio a modo kernel y un salto a una dirección fijada de antemano. Lo que las distingue es su causa, y en particular su relación con el programa que se estaba ejecutando.
| Llamada al sistema | Excepción | Interrupción de hardware | |
|---|---|---|---|
| La provoca | El programa, a propósito | El programa, sin querer | Un dispositivo |
| Causa | La instrucción syscall | Una instrucción que no puede completarse | Un suceso exterior a la CPU |
| Momento | Siempre en el mismo punto del programa | Siempre en el mismo punto del programa | En cualquier punto |
| Tipo | Síncrona | Síncrona | Asíncrona |
| El kernel trabaja para | El programa que llamó | El programa que falló | Quien haya pedido esa operación; casi nunca el programa interrumpido |
| Ejemplo | write(1, …) | Leer una dirección inválida | Se pulsó una tecla |
Síncrona significa que el suceso está ligado a una instrucción concreta: si el programa se ejecuta otra vez con los mismos datos, ocurre en el mismo lugar. Asíncrona significa lo contrario: el suceso llega cuando llega, sin relación alguna con lo que la CPU estaba haciendo.
La última fila es la consecuencia más importante. Cuando llega la interrupción del disco con los datos que pidió el programa A, lo más probable es que la CPU esté ejecutando el programa B, porque A está dormido esperando. El programa B es interrumpido por algo que no le concierne, y pierde unos microsegundos trabajando, sin saberlo, para otro. Las excepciones son el contenido del tema 18.
3. Del dispositivo a la CPU
Una computadora tiene decenas de dispositivos capaces de interrumpir, y un procesador no puede tener un cable para cada uno. Entre ellos hay un intermediario, el controlador de interrupciones, un circuito que recibe todos los pedidos y los presenta a la CPU de a uno.
El controlador cumple cuatro tareas:
- Reúne. Cada dispositivo llega a él por una entrada distinta, de modo que sabe quién pide.
- Prioriza. Si varios piden a la vez, entrega primero el más urgente y retiene los demás.
- Identifica. No se limita a avisar a la CPU de que hay una interrupción: le entrega un número que dice cuál.
- Enmascara. El kernel puede ordenarle que ignore temporalmente una entrada determinada.
En las PC originales este circuito se llamaba PIC y tenía quince entradas. Es el pic1 que apareció en la lista de puertos del tema 8. Los equipos actuales usan su sucesor, el APIC, que está repartido en dos partes: una general, que recibe a los dispositivos, y una dentro de cada núcleo del procesador, de modo que cada interrupción pueda dirigirse a un núcleo concreto.
Los dispositivos modernos, como los discos NVMe y las tarjetas de red, ya no usan una línea física. Para interrumpir, escriben un valor en una dirección de memoria especial, y esa escritura llega al procesador como una interrupción. El método se llama MSI, de Message Signaled Interrupts. Su ventaja es que un mismo dispositivo puede tener muchas interrupciones distintas; un disco NVMe usa una por cada núcleo del procesador.
4. Dos números: la IRQ y el vector
En este tema aparecen dos numeraciones que es fácil confundir, porque las dos identifican interrupciones. Se refieren a extremos distintos del camino de la figura anterior:
| IRQ | Vector | |
|---|---|---|
| Nombre completo | Interrupt Request: pedido de interrupción | Vector de interrupción |
| Identifica | El origen: qué dispositivo pide | El destino: qué entrada de la tabla debe usar la CPU |
| Lo usa | El kernel y sus drivers | El procesador |
| Rango en x86-64 | El que el kernel necesite | De 0 a 255 |
| Dónde se ve | Primera columna de /proc/interrupts | Normalmente no se ve |
El kernel decide qué vector corresponde a cada IRQ y se lo comunica al controlador de interrupciones. Durante décadas la regla fue fija, vector = IRQ + 32; hoy la asignación es dinámica. Para quien administra o programa sobre Linux, el número que importa es la IRQ.
Los 256 vectores de un procesador x86-64 se reparten así:
| Vectores | Uso | Ejemplos |
|---|---|---|
| 0 a 31 | Excepciones, reservadas por el fabricante del procesador. | 0: división por cero. 13: fallo de protección general. 14: fallo de página. |
| 32 a 255 | Interrupciones de hardware y usos propios del kernel. | Teclado, disco, red, temporizador, avisos entre núcleos. |
128 (0x80) | Un caso histórico dentro del rango anterior. | La antigua entrada de las llamadas al sistema en 32 bits. |
La tabla reúne cosas que ya conocíamos por separado. Los vectores 13 y 14 son las dos excepciones que provocamos en el tema 12 con privilegiada.c y espiar.c. Y el 128 es el int $0x80 del tema 13: las llamadas al sistema de 32 bits se hacían simulando por software una interrupción. Todas las entradas al kernel, salvo la instrucción syscall moderna, pasan por la misma tabla.
Las primeras IRQ conservan las asignaciones de las PC de los años ochenta, y todavía se las encuentra en cualquier equipo:
| IRQ | Dispositivo tradicional |
|---|---|
| 0 | Temporizador del sistema |
| 1 | Teclado |
| 8 | Reloj de tiempo real, el del experimento del tema 8 |
| 12 | Ratón o panel táctil |
| 14 y 15 | Discos de la interfaz antigua |
5. La tabla de vectores
El vector es un índice. La CPU lo usa para consultar un arreglo en memoria, la tabla de vectores de interrupción, que en x86 se llama IDT (Interrupt Descriptor Table). La entrada n contiene la dirección de la rutina del kernel que atiende el vector n.
Es la misma idea de la tabla de llamadas al sistema del tema 13, con una diferencia: aquella la consulta el kernel, por software; esta la consulta el procesador, por sí mismo, como parte de su funcionamiento. En pseudocódigo, lo que hace el hardware al aceptar una interrupción es:
/* entre una instrucción y la siguiente */
if (hay_interrupcion_pendiente && interrupciones_habilitadas) {
vector = pedir_numero_al_controlador();
guardar(contador_de_programa, puntero_de_pila, registro_de_estado);
pasar_a_modo_kernel();
contador_de_programa = tabla_de_vectores[vector];
}
La tabla la construye el kernel durante el arranque, y le indica al procesador dónde está con la instrucción lidt. En el tema 12 la incluimos entre las instrucciones privilegiadas, con el argumento de que un programa podría sustituir las rutinas del kernel por las suyas. Ahora se entiende el alcance: quien controla esa tabla decide qué código se ejecuta, con privilegio total, cada vez que un dispositivo interrumpe. Protegerla es proteger el sistema entero.
Aquí se cumple una vez más la regla que enunciamos entonces: cualquiera puede provocar la entrada al kernel —un programa con syscall, un dispositivo con una interrupción—, pero nadie más que el kernel elige lo que se ejecuta a continuación.
6. Qué hace la CPU al ser interrumpida
La secuencia completa tiene una exigencia que la distingue de una llamada al sistema. El programa interrumpido no pidió nada y no está preparado para nada: puede estar en mitad de un cálculo, con valores a medio usar en todos sus registros. Al volver, debe encontrarlos exactamente como estaban.
- Un dispositivo pide atención. El controlador de interrupciones lo registra y activa la señal hacia la CPU. hardware
- La CPU termina la instrucción en curso. Una instrucción nunca queda a medias: la interrupción se atiende en la frontera entre dos. modo usuario
- La CPU guarda lo imprescindible y cambia de modo. Salva el contador de programa, el puntero de pila y el registro de estado en la pila de kernel, pasa al anillo 0 y deshabilita las demás interrupciones. Es la transición del tema 12. cambio de modo
- Salta a la rutina indicada por la tabla de vectores. modo kernel
- El kernel guarda el resto de los registros. Con eso tiene el contexto completo de lo interrumpido. modo kernel
- Identifica la IRQ y llama al manejador del driver que se registró para ella. modo kernel
- Avisa al controlador de interrupciones de que terminó, para que pueda entregar la siguiente. modo kernel
- Restaura los registros y ejecuta la instrucción de retorno, que recupera el contador de programa, la pila y el modo anteriores. cambio de modo
- El programa continúa en la instrucción siguiente. No tiene forma de saber que fue interrumpido. modo usuario
La interrupción puede llegar también mientras la CPU ya está en modo kernel, atendiendo una llamada al sistema. En ese caso la secuencia es la misma, sin el cambio de modo: se interrumpe al propio kernel.
En el paso 8 hay una posibilidad que la figura no muestra y que será el tema central del tema 19: antes de volver, el kernel puede decidir que la CPU pase a otro programa. Una interrupción es siempre una oportunidad para reorganizar el reparto del procesador.
7. El manejador y sus reglas
La rutina que atiende a un dispositivo se llama manejador de interrupción, o ISR, de Interrupt Service Routine. Es una función del driver, que este registra ante el kernel al inicializarse indicando para qué IRQ. El siguiente fragmento muestra, simplificado, el del teclado de una PC:
/* Se ejecuta cada vez que el controlador del teclado interrumpe. */
irqreturn_t atender_teclado(int irq, void *datos)
{
unsigned char codigo = inb(0x60); /* leer el registro de datos: qué tecla fue */
guardar_en_cola(codigo); /* dejarla donde la encontrará un read */
despertar_a_quien_espere(); /* marcar como listo al proceso que dormía */
return IRQ_HANDLED; /* «sí, era para mí, y ya está atendida» */
}
/* Al inicializarse el driver: asociar la función a la IRQ 1. */
request_irq(1, atender_teclado, 0, "i8042", NULL);
El puerto 0x60 es el registro de datos del teclado que vimos en el mapa de puertos del tema 8, y la instrucción privilegiada in es la que lo lee. El manejador hace tres cosas, y son las tres que hace casi cualquier manejador: recoger el dato del dispositivo, dejarlo en un lugar donde el resto del kernel lo encuentre, y despertar a quien lo estaba esperando.
El valor devuelto existe porque una IRQ puede estar compartida por varios dispositivos. En ese caso el kernel llama a los manejadores de todos, y cada uno consulta a su dispositivo si fue él quien interrumpió; el que no, responde IRQ_NONE.
Un manejador no es código corriente. Se ejecuta en una situación particular, el contexto de interrupción, que impone restricciones estrictas:
| Regla | Motivo |
|---|---|
| Debe ser muy breve. | Mientras se ejecuta, lo interrumpido está detenido y otras interrupciones esperan. |
| No puede bloquearse ni dormir. | No se ejecuta en nombre de ningún proceso. No hay a quién dormir, ni nadie que lo despierte después. |
| No puede acceder a la memoria de un programa. | El proceso que estaba en la CPU es uno cualquiera, sin relación con el dispositivo. |
| No puede suponer nada sobre lo interrumpido. | Puede haber detenido a un programa, a una llamada al sistema a medio hacer o a otra parte del kernel. |
La segunda regla contrasta con lo que vimos en el tema 13. Una llamada al sistema puede bloquearse porque hay un proceso que la hizo y que puede quedar dormido. Una interrupción no pertenece a nadie: ocurre encima de lo que hubiera, y tiene que terminar para que eso continúe.
8. Una tecla, de principio a fin
Con todas las piezas presentadas se puede seguir un caso completo, que une este tema con los del bloque anterior. En el tema 16 conectamos strace a un programa detenido en read(0, . Esto es lo que ocurre desde que se pulsa una tecla hasta que esa llamada vuelve:
- El programa llamó a
ready no había datos. El kernel lo marcó como dormido y entregó la CPU a otro proceso. antes - Se pulsa una tecla. El controlador del teclado guarda su código en el registro de datos y activa la IRQ 1. hardware
- El controlador de interrupciones entrega el vector a la CPU, que en ese momento ejecuta un programa cualquiera. hardware
- La CPU suspende ese programa y salta, a través de la tabla de vectores, al kernel. cambio de modo
- El manejador lee el código del puerto
0x60, lo guarda en una cola y marca como listo al proceso que dormía. modo kernel - El kernel vuelve de la interrupción. El programa suspendido continúa, o el planificador decide dar la CPU al que acaba de despertar. modo kernel
- Cuando le llega el turno, el proceso despertado continúa dentro de su
read. El kernel copia los caracteres disponibles a su búfer. modo kernel readdevuelve la cantidad de bytes, y el programa sigue en la instrucción siguiente. modo usuario
En la práctica, entre los pasos 5 y 7 interviene un subsistema que convierte los códigos de teclas en caracteres y, en una terminal, retiene la línea hasta que se pulsa Enter. Por eso en el tema 14 read devolvía la línea completa y no letra por letra.
Lo que importa del recorrido es su estructura. Hay dos entradas al kernel, separadas en el tiempo y de origen distinto: una llamada al sistema que deja a un proceso esperando, y una interrupción que lo libera. Toda la entrada y salida de un sistema operativo funciona con esas dos mitades.
9. Dos mitades
La regla de la brevedad choca con la realidad de algunos dispositivos. Cuando llega un paquete de red hay mucho que hacer: verificarlo, identificar el protocolo, encontrar la conexión a la que pertenece, entregar los datos. Hacerlo todo dentro del manejador mantendría las interrupciones deshabilitadas demasiado tiempo.
La solución, común a todos los sistemas operativos, es dividir el trabajo en dos partes:
| Mitad superior | Mitad inferior | |
|---|---|---|
| Qué es | El manejador propiamente dicho | Trabajo diferido, que el manejador deja anotado |
| Cuándo se ejecuta | De inmediato, al llegar la interrupción | Poco después, al terminar el manejador |
| Interrupciones | Deshabilitadas, al menos la propia | Habilitadas: puede ser interrumpida |
| Qué hace | Lo urgente: recoger el dato y silenciar al dispositivo | Lo costoso: procesar el dato |
| Duración | Microsegundos | La que haga falta |
| En Linux | hardirq | softirq |
Linux lleva la cuenta del trabajo diferido en un archivo aparte, con una fila por tipo de tarea y una columna por procesador:
$ cat /proc/softirqs
CPU0 CPU1 CPU2 CPU3
TIMER: 812044 790312 801120 795407
NET_TX: 412 18 22 15
NET_RX: 201455 3120 2984 3051
BLOCK: 91034 48211 310 288
SCHED: 640112 611870 625004 618233
RCU: 903311 887420 894512 890106
NET_RX y NET_TX son el procesamiento de paquetes recibidos y enviados; BLOCK, la finalización de operaciones de disco; TIMER y SCHED, tareas disparadas por el temporizador. Si en un momento dado hay demasiado trabajo diferido, el kernel deja de hacerlo al volver de cada interrupción y lo pasa a unos hilos propios, uno por procesador: son los ksoftirqd/N que aparecieron en la lista de hilos del kernel del tema 11. Verlos consumir CPU en top indica que el equipo recibe más interrupciones de las que puede procesar cómodamente.
10. Leer /proc/interrupts
El kernel cuenta cada interrupción que atiende, y publica los contadores en /proc/interrupts. En el tema 8 lo dejamos como ejercicio sin explicación. Esta es una salida típica de un equipo con cuatro núcleos, abreviada:
$ cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
0: 9 0 0 0 IO-APIC 2-edge timer
1: 1842 0 0 0 IO-APIC 1-edge i8042
8: 0 1 0 0 IO-APIC 8-edge rtc0
9: 0 12 0 0 IO-APIC 9-fasteoi acpi
12: 0 0 5120 0 IO-APIC 12-edge i8042
124: 0 0 0 48211 PCI-MSI 524288-edge nvme0q0
125: 91034 0 0 0 PCI-MSI 524289-edge nvme0q1
131: 0 201455 0 0 PCI-MSI 1048576-edge enp3s0
NMI: 14 13 14 13 Non-maskable interrupts
LOC: 3120447 2987102 3054418 3011950 Local timer interrupts
RES: 40122 38817 41190 39554 Rescheduling interrupts
CAL: 120344 118201 119876 121002 Function call interrupts
TLB: 18220 17981 18410 18102 TLB shootdowns
| Columna | Contenido |
|---|---|
| Primera | El número de IRQ, o una sigla para las interrupciones internas del sistema. |
CPU0, CPU1… | Cuántas veces atendió cada núcleo esa interrupción desde el arranque. |
IO-APIC / PCI-MSI | Por dónde llega: una línea física del controlador de interrupciones, o un mensaje MSI. |
edge / fasteoi | Cómo se señala: con un pulso, o manteniendo la línea activa hasta ser atendido. |
| Última | El nombre con que el driver registró su manejador. |
Las filas numeradas se reconocen con lo visto en este tema y en el 8:
- IRQ 1 y 12,
i8042: el controlador del teclado y del panel táctil. Es el nombre que usamos en elrequest_irqde la sección 7. - IRQ 8,
rtc0: el reloj de hardware que leímos por sus puertos en el tema 8. - IRQ 124 y 125,
nvme0q…: el disco, con una interrupción por cada cola de pedidos. Usa MSI, y por eso sus números de IRQ son altos. - IRQ 131,
enp3s0: la tarjeta de red. - IRQ 0,
timer: el temporizador clásico. Su contador está casi en cero, porque Linux lo usa solo durante el arranque y después lo reemplaza por otro.
Las filas con sigla no provienen de dispositivos externos, y son las más activas:
| Sigla | Qué cuenta |
|---|---|
LOC | Las interrupciones del temporizador que cada núcleo lleva incorporado. Son, con diferencia, las más numerosas. Les dedicamos el tema 19. |
RES, CAL, TLB | Interrupciones que un núcleo envía a otro para pedirle algo: que revise qué proceso debe ejecutar, que ejecute una función, que descarte datos de memoria obsoletos. |
NMI | Interrupciones no enmascarables, que veremos en la sección 12. |
Las filas RES, CAL y TLB muestran un uso del mecanismo que no habíamos previsto: en un equipo con varios núcleos, el propio kernel emplea interrupciones para coordinarlos. Un núcleo no puede modificar lo que hace otro; solo puede interrumpirlo.
La distribución en columnas tampoco es casual. Cada interrupción se entrega a un núcleo, y el kernel procura que la de un dispositivo llegue siempre al mismo, para aprovechar los datos que ese núcleo ya tiene en su caché. El archivo /proc/irq/N/smp_affinity_list indica qué núcleos pueden atender la IRQ N, y un administrador puede modificarlo. En muchas distribuciones, el servicio irqbalance ajusta ese reparto según la carga.
11. Verlas ocurrir
Los contadores solo crecen, y lo interesante es verlos crecer. El comando watch repite otro a intervalos y, con la opción -d, resalta lo que cambió:
$ watch -n 1 -d 'grep -E "CPU|i8042|nvme|LOC" /proc/interrupts'
Con eso en pantalla, cada pulsación de tecla suma dos al contador de i8042: una interrupción al bajar la tecla y otra al soltarla. Copiar un archivo grande en otra terminal hace avanzar las filas del disco. Y la fila LOC no se detiene nunca.
La fila i8042 corresponde al teclado integrado de un portátil y al de una máquina virtual. Un teclado USB interrumpe a través del controlador USB, en una fila llamada xhci_hcd. En WSL no hay dispositivos reales: el archivo muestra unas pocas filas de dispositivos virtuales, además de las internas. Y en una sesión remota, las teclas llegan como paquetes de red.
Para medir el ritmo global, el kernel publica también un total en /proc/stat, en la línea que empieza con intr. El siguiente programa lo lee una vez por segundo e informa la diferencia:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
/* Busca en /proc/stat la línea que empieza con `nombre` y devuelve su primer número. */
static long long leer_contador(const char *nombre)
{
FILE *f = fopen("/proc/stat", "r");
char palabra[64];
long long valor = -1;
if (f == NULL)
return -1;
while (fscanf(f, "%63s", palabra) == 1) {
if (strcmp(palabra, nombre) == 0) {
if (fscanf(f, "%lld", &valor) != 1)
valor = -1;
break;
}
}
fclose(f);
return valor;
}
int main(void)
{
long long antes = leer_contador("intr");
if (antes == -1) {
fprintf(stderr, "No se pudo leer /proc/stat\n");
return 1;
}
printf("Interrupciones desde el arranque: %lld\n", antes);
for (int i = 0; i < 8; i++) {
sleep(1);
long long ahora = leer_contador("intr");
printf("En el ultimo segundo: %lld\n", ahora - antes);
antes = ahora;
}
return 0;
}
$ gcc -Wall -Wextra -g interrupciones.c -o interrupciones
$ ./interrupciones
Interrupciones desde el arranque: 48213377
En el ultimo segundo: 398
En el ultimo segundo: 412
En el ultimo segundo: 405
En el ultimo segundo: 1389
En el ultimo segundo: 1402
En el ultimo segundo: 421
En el ultimo segundo: 396
En el ultimo segundo: 408
Un equipo en reposo atiende algunos cientos de interrupciones por segundo. Las dos líneas resaltadas corresponden al momento en que, en otra terminal, se ejecutó ./tiempos calcular, el programa del tema 11 que hace sumas durante dos segundos sin pedir nada a nadie.
El resultado parece contradictorio: un programa que no usa ningún dispositivo provocó unas mil interrupciones adicionales por segundo. La explicación está en la fila LOC. Un núcleo sin trabajo se queda dormido, y el kernel silencia su temporizador para no despertarlo inútilmente. En cuanto un programa lo ocupa, el temporizador vuelve a interrumpirlo a intervalos fijos, entre 250 y 1000 veces por segundo según cómo esté configurado el kernel. Es la respuesta a la pregunta que dejamos planteada en el tema 11: así recupera el kernel la CPU de un programa que nunca lo llama. Lo estudiamos en el tema 19.
12. Cuando no se puede interrumpir
Hay momentos en que el kernel no puede permitirse una interrupción: por ejemplo, mientras modifica una estructura de datos que el manejador también usa. Si la interrupción llegara a mitad del cambio, el manejador encontraría los datos en un estado incoherente. Para esos tramos, el kernel dispone de dos niveles de control:
| Mecanismo | Efecto | Cómo |
|---|---|---|
| Deshabilitar las interrupciones en un núcleo | Ese núcleo no acepta ninguna hasta nuevo aviso. | Las instrucciones privilegiadas cli y sti del tema 12. |
| Enmascarar una IRQ | Se ignora un dispositivo concreto; los demás siguen interrumpiendo. | Una orden al controlador de interrupciones. |
En ninguno de los dos casos la interrupción se pierde. El controlador la retiene como pendiente y la entrega en cuanto vuelve a estar permitida. Lo que se pierde es tiempo: el dispositivo espera más de lo debido. Por eso el kernel mantiene esos tramos tan cortos como puede, y por eso cli es una instrucción privilegiada: un programa que deshabilitara las interrupciones y entrara en un bucle se quedaría con el procesador para siempre, sin que ni el temporizador pudiera quitárselo.
Existe una categoría que escapa a ese control, la interrupción no enmascarable, o NMI. Ni cli ni el controlador pueden bloquearla. Se reserva para situaciones que no admiten espera:
- Fallos graves del hardware, como un error de memoria que no se pudo corregir.
- La vigilancia del propio kernel. Un mecanismo llamado watchdog genera una NMI periódica en cada núcleo y comprueba que el kernel sigue respondiendo. Si un núcleo quedó atascado con las interrupciones deshabilitadas, la NMI es lo único que todavía puede alcanzarlo, y permite registrar dónde se detuvo.
- La medición de rendimiento. Herramientas como
perfla usan para tomar muestras de lo que ejecuta la CPU, incluso dentro de los tramos protegidos.
Es la fila NMI de /proc/interrupts, con sus contadores pequeños.
13. El costo, y las tormentas
Una interrupción es más barata que la espera que evita, pero no es gratuita. Cada una implica la transición del tema 12, guardar y restaurar registros y ejecutar el manejador: del orden de un microsegundo. Tiene además un costo indirecto: el código y los datos del manejador desplazan de la caché los del programa interrumpido, que al volver se ejecuta más despacio durante un momento.
El tiempo que el sistema dedica a esto es visible. Son dos de los campos de la tercera línea de top que explicamos en el tema 12:
| Campo | Significado | Se corresponde con |
|---|---|---|
hi | Tiempo en manejadores de interrupciones de hardware. | La mitad superior |
si | Tiempo en trabajo diferido. | La mitad inferior, /proc/softirqs |
En un equipo de escritorio los dos están cerca de cero. En un servidor que recibe mucho tráfico de red, si puede ser una parte apreciable del total.
Con las cifras actuales, el costo puede volverse un problema. Una tarjeta de red de alta velocidad recibe más de un millón de paquetes por segundo. Si cada uno produjera una interrupción, la CPU pasaría todo su tiempo entrando y saliendo de manejadores sin llegar a procesar los paquetes. La situación se llama tormenta de interrupciones, y hay dos defensas:
- Agrupar. El dispositivo espera unos microsegundos y avisa una sola vez por varios paquetes. Es la misma idea del búfer de stdio del tema 15, aplicada al hardware: menos cruces, y más trabajo en cada uno.
- Volver a preguntar. Cuando el tráfico es intenso, el driver de red deshabilita la interrupción de la tarjeta y pasa a consultarla periódicamente; cuando afloja, la habilita de nuevo.
La segunda defensa es el sondeo que descartamos en el tema 8, rehabilitado. La comparación entre los dos métodos nunca tuvo un ganador absoluto: avisar conviene cuando los sucesos son escasos, y preguntar, cuando son tan frecuentes que siempre hay algo que recoger. Los sistemas actuales cambian de uno a otro según la carga.
14. Resumen
- Una interrupción de hardware es la señal con que un dispositivo pide atención a la CPU, que suspende lo que hace y salta a una rutina del kernel.
- Es asíncrona: llega en cualquier momento y no tiene relación con el programa interrumpido.
- El controlador de interrupciones reúne los pedidos de los dispositivos, los prioriza y entrega a la CPU un número que identifica al que debe atender.
- La IRQ identifica el origen y la usa el kernel; el vector es el índice que usa el procesador en su tabla.
- La tabla de vectores contiene las direcciones de las rutinas del kernel. La construye el kernel en el arranque, y solo él puede modificarla.
- Al ser interrumpida, la CPU termina la instrucción en curso, guarda el estado, pasa a modo kernel y salta. Al volver, el programa no nota nada.
- El manejador debe ser breve y no puede bloquearse, porque no se ejecuta en nombre de ningún proceso.
- El trabajo costoso se difiere a una mitad inferior, que se ejecuta con las interrupciones habilitadas.
- La entrada y salida combina dos entradas al kernel: una llamada al sistema que deja a un proceso esperando y una interrupción que lo despierta.
/proc/interruptsmuestra cuántas interrupciones de cada origen atendió cada núcleo. Las más numerosas son las del temporizador.- El kernel puede deshabilitar o enmascarar las interrupciones por tramos breves; quedan pendientes, no se pierden. Las NMI no se pueden bloquear.
- Cuando los sucesos son muy frecuentes, agruparlos o volver al sondeo resulta más eficiente que una interrupción por cada uno.
15. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué quiere decir que una interrupción de hardware es asíncrona?
Que no está ligada a ninguna instrucción del programa en ejecución. La provoca un suceso exterior, y puede llegar entre dos instrucciones cualesquiera. Una llamada al sistema o una excepción, en cambio, ocurren siempre en el mismo punto del programa.
2. Llega la interrupción del disco con los datos que pidió el programa A. ¿Qué programa es interrumpido?
El que esté usando la CPU en ese momento, que casi nunca es A: A está dormido, esperando esos datos. El interrumpido es otro programa, o el propio kernel, sin relación con la operación.
3. ¿Qué funciones cumple el controlador de interrupciones?
Reúne los pedidos de todos los dispositivos, decide cuál entregar primero cuando hay varios, le indica a la CPU con un número cuál es, y permite al kernel enmascarar entradas concretas.
4. ¿Qué diferencia hay entre una IRQ y un vector?
La IRQ identifica el origen de la interrupción, el dispositivo, y es el número con que trabajan el kernel y los drivers. El vector es el índice de la tabla que consulta el procesador para saber adónde saltar. El kernel establece la correspondencia entre ambos.
5. ¿En qué se parecen y en qué se diferencian la tabla de vectores y la tabla de llamadas al sistema?
Las dos asocian un número a la dirección de una rutina del kernel, y las dos existen para que nadie fuera del kernel elija qué código se ejecuta. La de llamadas la consulta el kernel por software; la de vectores la consulta el procesador por sí mismo al aceptar una interrupción.
6. ¿Por qué la CPU termina la instrucción en curso antes de atender una interrupción?
Para que el estado del programa sea coherente y pueda reanudarse. Si una instrucción quedara a medias, no bastaría con guardar el contador de programa y los registros para continuar exactamente donde estaba.
7. Una llamada al sistema puede bloquearse y un manejador de interrupción no. ¿Por qué?
La llamada al sistema se ejecuta en nombre del proceso que la hizo, y ese proceso puede quedar dormido hasta que se cumpla lo que espera. El manejador no pertenece a ningún proceso: se ejecuta encima de lo que hubiera, y mientras no termine, eso no puede continuar.
8. ¿Para qué se divide el tratamiento de una interrupción en dos mitades?
Para que la parte que se ejecuta con las interrupciones deshabilitadas sea mínima. La mitad superior hace solo lo urgente; el procesamiento costoso se difiere a la mitad inferior, que se ejecuta enseguida pero puede ser interrumpida.
9. En /proc/interrupts, ¿qué cuenta la fila LOC y por qué es la mayor?
Las interrupciones del temporizador incorporado a cada núcleo. Es la mayor porque el temporizador interrumpe a cada núcleo activo cientos de veces por segundo, mucho más que cualquier dispositivo en un equipo normal.
10. Al ejecutar un programa que solo calcula, las interrupciones por segundo aumentan. ¿De dónde salen?
Del temporizador. Un núcleo ocioso está dormido y con el temporizador silenciado. Al ocuparlo un programa, el temporizador vuelve a interrumpirlo periódicamente, para que el kernel pueda recuperar el control.
11. El kernel deshabilita las interrupciones y, mientras tanto, se pulsa una tecla. ¿Se pierde?
No. El controlador de interrupciones la mantiene pendiente y la entrega cuando las interrupciones se habilitan de nuevo. Solo se atiende con retraso.
12. ¿Por qué una tarjeta de red muy rápida puede pasar de las interrupciones al sondeo?
Porque con más de un millón de paquetes por segundo, una interrupción por paquete consumiría toda la CPU solo en entrar y salir de manejadores. Cuando siempre hay paquetes esperando, consultar periódicamente cuesta menos que ser avisado cada vez.
Para practicar
- Ejecuta
cat /proc/interruptsy localiza las filas de tu disco, tu tarjeta de red y tu teclado. ¿Usan una línea física o MSI? ¿Qué núcleo atiende cada una? - Con el comando
watchde la sección 11, pulsa diez teclas y comprueba cuánto avanza el contador. Luego mantén una tecla pulsada: ¿qué ocurre? - Ejecuta
./interrupcionesy, mientras corre, lanza en otra terminal./tiempos calcular. ¿Cuántas interrupciones por segundo se añaden? Compara con el valor degrep 'CONFIG_HZ=' /boot/config-$(uname -r). - Repite el ejercicio anterior lanzando dos copias de
./tiempos calculara la vez. ¿Cuánto aumenta ahora, y por qué? - Modifica
interrupciones.cpara que informe también el contadorctxtde/proc/stat, que cuenta las veces que la CPU pasó de un proceso a otro. ¿Guardan relación los dos valores? - Observa
/proc/softirqsconwatch -n 1 -dmientras descargas un archivo. ¿Qué filas avanzan, y en qué núcleos? - Elige una IRQ de tu disco y lee
/proc/irq/N/smp_affinity_list. ¿Coincide con la columna donde crece su contador? - Abre
top, pulsa la tecla1para ver cada núcleo por separado y observa los camposhiysimientras copias un archivo grande. - Calcula el promedio de interrupciones por segundo desde el arranque, dividiendo el total de
intrpor el tiempo que informacat /proc/uptime. ¿Por qué es menor que el valor instantáneo con el equipo en uso?