← Fundamentos de Sistemas Operativos Bloque 4 · Interrupciones · Tema 17 de 34

Tema 17

Interrupciones de hardware

En el bloque anterior, el kernel actuaba porque un programa se lo pedía. Pero una tecla pulsada, un paquete que llega por la red o un disco que termina de leer no esperan a que nadie pregunte. En este tema vemos cómo consigue un dispositivo detener al procesador en mitad de un programa, y qué hace el kernel cuando eso ocurre.

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 sistemaExcepciónInterrupción de hardware
La provocaEl programa, a propósitoEl programa, sin quererUn dispositivo
CausaLa instrucción syscallUna instrucción que no puede completarseUn suceso exterior a la CPU
MomentoSiempre en el mismo punto del programaSiempre en el mismo punto del programaEn cualquier punto
TipoSíncronaSíncronaAsíncrona
El kernel trabaja paraEl programa que llamóEl programa que fallóQuien haya pedido esa operación; casi nunca el programa interrumpido
Ejemplowrite(1, …)Leer una dirección inválidaSe 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.

Tres dispositivos, teclado, reloj y disco, envían sus pedidos por líneas IRQ distintas al controlador de interrupciones, que entrega a la CPU un número de vector Teclado Reloj Disco IRQ 1 IRQ 8 IRQ 14 vector Controlador de interrupciones ordena y prioriza CPU salta al kernel
El controlador recibe los pedidos de todos los dispositivos y le indica a la CPU, con un número, cuál debe atender.

El controlador cumple cuatro tareas:

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.

Interrupciones sin cable

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:

IRQVector
Nombre completoInterrupt Request: pedido de interrupciónVector de interrupción
IdentificaEl origen: qué dispositivo pideEl destino: qué entrada de la tabla debe usar la CPU
Lo usaEl kernel y sus driversEl procesador
Rango en x86-64El que el kernel necesiteDe 0 a 255
Dónde se vePrimera columna de /proc/interruptsNormalmente 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í:

VectoresUsoEjemplos
0 a 31Excepciones, reservadas por el fabricante del procesador.0: división por cero. 13: fallo de protección general. 14: fallo de página.
32 a 255Interrupciones 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:

IRQDispositivo tradicional
0Temporizador del sistema
1Teclado
8Reloj de tiempo real, el del experimento del tema 8
12Ratón o panel táctil
14 y 15Discos 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:

lo que hace la CPU, en pseudocódigo
/* 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.

  1. Un dispositivo pide atención. El controlador de interrupciones lo registra y activa la señal hacia la CPU. hardware
  2. 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
  3. 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
  4. Salta a la rutina indicada por la tabla de vectores. modo kernel
  5. El kernel guarda el resto de los registros. Con eso tiene el contexto completo de lo interrumpido. modo kernel
  6. Identifica la IRQ y llama al manejador del driver que se registró para ella. modo kernel
  7. Avisa al controlador de interrupciones de que terminó, para que pueda entregar la siguiente. modo kernel
  8. 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
  9. El programa continúa en la instrucción siguiente. No tiene forma de saber que fue interrumpido. modo usuario
Línea de tiempo: un programa se ejecuta en modo usuario, llega una interrupción, el kernel ejecuta el manejador en modo kernel y el mismo programa continúa MODO USUARIO MODO KERNEL llega la interrupción programa manejador el mismo programa
Para el programa, la interrupción es un hueco invisible en el tiempo: una instrucción que tardó unos microsegundos más en llegar.

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:

el manejador del teclado, simplificado
/* 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:

ReglaMotivo
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:

  1. El programa llamó a read y no había datos. El kernel lo marcó como dormido y entregó la CPU a otro proceso. antes
  2. Se pulsa una tecla. El controlador del teclado guarda su código en el registro de datos y activa la IRQ 1. hardware
  3. El controlador de interrupciones entrega el vector a la CPU, que en ese momento ejecuta un programa cualquiera. hardware
  4. La CPU suspende ese programa y salta, a través de la tabla de vectores, al kernel. cambio de modo
  5. 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
  6. 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
  7. 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
  8. read devuelve 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 superiorMitad inferior
Qué esEl manejador propiamente dichoTrabajo diferido, que el manejador deja anotado
Cuándo se ejecutaDe inmediato, al llegar la interrupciónPoco después, al terminar el manejador
InterrupcionesDeshabilitadas, al menos la propiaHabilitadas: puede ser interrumpida
Qué haceLo urgente: recoger el dato y silenciar al dispositivoLo costoso: procesar el dato
DuraciónMicrosegundosLa que haga falta
En Linuxhardirqsoftirq

Linux lleva la cuenta del trabajo diferido en un archivo aparte, con una fila por tipo de tarea y una columna por procesador:

terminal (extracto)
$ 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:

terminal (salida 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
ColumnaContenido
PrimeraEl 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-MSIPor dónde llega: una línea física del controlador de interrupciones, o un mensaje MSI.
edge / fasteoiCómo se señala: con un pulso, o manteniendo la línea activa hasta ser atendido.
ÚltimaEl nombre con que el driver registró su manejador.

Las filas numeradas se reconocen con lo visto en este tema y en el 8:

Las filas con sigla no provienen de dispositivos externos, y son las más activas:

SiglaQué cuenta
LOCLas interrupciones del temporizador que cada núcleo lleva incorporado. Son, con diferencia, las más numerosas. Les dedicamos el tema 19.
RES, CAL, TLBInterrupciones 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.
NMIInterrupciones 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ó:

terminal
$ 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.

Según el equipo

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:

interrupciones.c
#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;
}
terminal
$ 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:

MecanismoEfectoCómo
Deshabilitar las interrupciones en un núcleoEse núcleo no acepta ninguna hasta nuevo aviso.Las instrucciones privilegiadas cli y sti del tema 12.
Enmascarar una IRQSe 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:

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:

CampoSignificadoSe corresponde con
hiTiempo en manejadores de interrupciones de hardware.La mitad superior
siTiempo 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:

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

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

  1. Ejecuta cat /proc/interrupts y 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?
  2. Con el comando watch de la sección 11, pulsa diez teclas y comprueba cuánto avanza el contador. Luego mantén una tecla pulsada: ¿qué ocurre?
  3. Ejecuta ./interrupciones y, mientras corre, lanza en otra terminal ./tiempos calcular. ¿Cuántas interrupciones por segundo se añaden? Compara con el valor de grep 'CONFIG_HZ=' /boot/config-$(uname -r).
  4. Repite el ejercicio anterior lanzando dos copias de ./tiempos calcular a la vez. ¿Cuánto aumenta ahora, y por qué?
  5. Modifica interrupciones.c para que informe también el contador ctxt de /proc/stat, que cuenta las veces que la CPU pasó de un proceso a otro. ¿Guardan relación los dos valores?
  6. Observa /proc/softirqs con watch -n 1 -d mientras descargas un archivo. ¿Qué filas avanzan, y en qué núcleos?
  7. Elige una IRQ de tu disco y lee /proc/irq/N/smp_affinity_list. ¿Coincide con la columna donde crece su contador?
  8. Abre top, pulsa la tecla 1 para ver cada núcleo por separado y observa los campos hi y si mientras copias un archivo grande.
  9. Calcula el promedio de interrupciones por segundo desde el arranque, dividiendo el total de intr por el tiempo que informa cat /proc/uptime. ¿Por qué es menor que el valor instantáneo con el equipo en uso?