Interrupciones PCI: de las líneas legacy a MSI/MSI-X
Las interrupciones de un dispositivo PCI: las líneas compartidas INTx de la era legacy frente a las modernas MSI y MSI-X, donde una interrupción es una escritura en memoria. La API unificada pci_alloc_irq_vectors y pci_irq_vector, y por qué MSI es mejor: sin líneas compartidas, más vectores y orden garantizado con el DMA.
Un dispositivo que hace DMA (30.5) necesita avisar cuando termina, y para eso interrumpe a la CPU. La forma en que lo hace ha vivido una revolución silenciosa: de la línea física compartida de la era PCI —lenta, contenida, ambigua— a la interrupción moderna, que no es un cable sino una escritura en memoria. Ese cambio no es cosmético: elimina las líneas compartidas, multiplica los vectores por miles y, de propina, arregla una carrera de ordenación que atormentaba a los drivers legacy.
- Entender las interrupciones legacy
INTx: líneas físicas compartidas y por qué obligan a sondear. - Ver
MSIcomo una interrupción que es una escritura en memoria, no un cable. - Ver
MSI-Xcomo miles de vectores independientes, uno por cola de hardware. - Pedir vectores con
pci_alloc_irq_vectorsy resolverlos conpci_irq_vector.
INTx: las líneas compartidas de la era legacy
El PCI clásico tenía cuatro líneas físicas de interrupción —INTA# a INTD#—, activas por nivel y compartidas por muchos dispositivos. Cuando una se activa, el kernel no sabe quién la disparó: llama a todos los manejadores registrados en esa línea, y cada uno debe leer el registro de estado de su dispositivo para averiguar si fue el culpable. Si no lo fue, devuelve IRQ_NONE; si lo fue, atiende y devuelve IRQ_HANDLED.
static irqreturn_t midev_isr(int irq, void *data)
{
struct midev *dev = data;
u32 status = ioread32(dev->regs + REG_ISR);
if (!status)
return IRQ_NONE; /* no era mia: otra tarjeta comparte la linea */
ioread32(dev->regs + REG_ISR); /* leer para hacer ack del nivel */
iowrite32(status, dev->regs + REG_ISR); /* limpiar las causas atendidas */
/* ...procesar el evento... */
return IRQ_HANDLED;
}
Ese sondeo de “¿fui yo?” en cada interrupción de cualquier vecino es puro desperdicio, y compartir la línea impide dirigir la interrupción a un núcleo concreto. Peor aún: con un cable, la señal de interrupción y los datos que anuncia viajan por caminos distintos y pueden desordenarse.
MSI: la interrupción como escritura en memoria
MSI (Message Signaled Interrupts) abandona el cable. Para interrumpir, el dispositivo ejecuta una escritura en memoria a una dirección especial (la región del controlador de interrupciones local) con un dato que codifica el vector. No hay línea que compartir: cada dispositivo tiene su propio mensaje, entregado directamente. MSI admite hasta 32 vectores, pero deben ser un bloque contiguo potencia de dos.
La consecuencia más sutil es la ordenación. Como el mensaje MSI es una escritura posted que viaja por el mismo canal PCIe que los datos del DMA, no puede adelantar a esos datos: cuando la CPU ve la interrupción, la RAM ya contiene lo que el dispositivo escribió. La carrera legacy —interrumpir antes de que el DMA aterrice— desaparece por construcción.
Con INTx, un dispositivo podía levantar la línea justo antes de que su última escritura DMA llegara a memoria; el driver leía datos rancios. La solución legacy era una lectura MMIO de “barrera” al principio del manejador, que fuerza a vaciar las escrituras pendientes —cara y fácil de olvidar. Con MSI la barrera es gratis: interrupción y datos comparten camino y orden.
MSI-X: un vector por cola, dirigido a su núcleo
MSI-X lleva la idea al extremo: hasta 2048 vectores por función, cada uno con su propia pareja de dirección y dato en una tabla (la MSI-X table, alojada en un BAR). Cada vector puede apuntar a un núcleo distinto. Ese es el fundamento del hardware multicola moderno: una NIC de servidor reparte el tráfico en decenas de colas de recepción, asigna un vector MSI-X a cada una, y fija la afinidad de cada vector al núcleo que procesará esa cola. Resultado: paralelismo real, sin líneas compartidas y sin locks entre colas —el hardware que hace posible el networking per-CPU y sin cerrojos de los niveles anteriores.
flowchart LR Q0[Cola RX 0] --> V0[Vector MSI-X 0] --> C0[Nucleo 0] Q1[Cola RX 1] --> V1[Vector MSI-X 1] --> C1[Nucleo 1] Q2[Cola RX 2] --> V2[Vector MSI-X 2] --> C2[Nucleo 2]
La API unificada: pci_alloc_irq_vectors
Desde hace años no se programa MSI, MSI-X e INTx por separado. Una sola función, pci_alloc_irq_vectors, pide un rango de vectores y elige el mejor mecanismo disponible: intenta MSI-X, si no MSI, y como último recurso INTx. Devuelve cuántos vectores consiguió. Luego pci_irq_vector traduce cada índice al número de IRQ Linux que se pasa a request_irq.
#define MIDEV_NVEC 4
static int midev_setup_irqs(struct pci_dev *pdev, struct midev *dev)
{
unsigned long flags;
int nvec, i, ret;
/* pide de 1 a NVEC; prefiere MSI-X, luego MSI, luego INTx */
nvec = pci_alloc_irq_vectors(pdev, 1, MIDEV_NVEC,
PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_INTX);
if (nvec < 0)
return nvec;
/* solo INTx comparte la linea: entonces hace falta IRQF_SHARED */
flags = (pdev->msix_enabled || pdev->msi_enabled) ? 0 : IRQF_SHARED;
for (i = 0; i < nvec; i++) {
int irq = pci_irq_vector(pdev, i);
ret = request_irq(irq, midev_isr, flags, DRV_NAME, dev);
if (ret) {
while (--i >= 0)
free_irq(pci_irq_vector(pdev, i), dev);
pci_free_irq_vectors(pdev);
return ret;
}
}
dev->nvec = nvec;
return 0;
}
static void midev_teardown_irqs(struct pci_dev *pdev, struct midev *dev)
{
int i;
for (i = 0; i < dev->nvec; i++)
free_irq(pci_irq_vector(pdev, i), dev);
pci_free_irq_vectors(pdev);
}
Fíjate en el detalle de flags: cuando la API cae a INTx, la línea es compartida y request_irq exige IRQF_SHARED más un manejador que devuelva IRQ_NONE si no era suyo; con MSI/MSI-X el vector es exclusivo y no hace falta. Para trabajo pesado en el manejador, se usaría request_threaded_irq para diferirlo a un hilo.
INTx (legacy)
Cuatro líneas físicas compartidas, activas por nivel. Obligan a sondear “¿fui yo?”, no se dirigen a un núcleo y sufren carreras de ordenación con el DMA. Solo como último recurso.
MSI / MSI-X
La interrupción es una escritura en memoria. Sin líneas compartidas, hasta 2048 vectores en MSI-X, afinidad por núcleo y orden garantizado con los datos del DMA. El estándar actual.
Interioriza la abstracción que MSI hace explícita, porque reordena tu modelo entero de E/S: una interrupción no es un evento eléctrico, es un mensaje. Durante décadas la interrupción fue un cable —una tensión en un pin— y esa física arrastraba todos sus males: los pines son escasos, así que había que compartirlos; están fuera de banda respecto a los datos, así que podían desordenarse; y son globales, así que no se podían dirigir a un núcleo. MSI disuelve la pared entre “interrumpir” y “transferir” al darse cuenta de que el dispositivo ya sabe escribir en memoria —eso es el DMA— y de que una interrupción puede ser, simplemente, una escritura más a una dirección mágica. En cuanto la interrupción es un dato que viaja por el mismo canal que el DMA, sus tres maldiciones se evaporan a la vez: no hay pines que compartir porque los mensajes son infinitos; no hay carrera de ordenación porque la escritura de la interrupción no puede adelantar a las escrituras de datos que la preceden en el mismo canal; y se puede dirigir a cualquier núcleo porque el destino es solo otra dirección. MSI-X remata la jugada rompiendo esa interrupción única en miles, una por cola de hardware, cada una clavada a la CPU que la va a procesar. Reconoce el patrón: es el mismo que el DMA y las variables per-CPU del nivel 26. Matar el recurso compartido —la línea, la RAM central, el contador global— y darle a cada actor su propio canal privado. La transición de “un cable que todos vigilan” a “un mensaje directo al núcleo correcto” es, punto por punto, la misma descentralización del poder que ha vertebrado todo tu ascenso por el kernel.
- Ejecuta
lspci -vsobre tu tarjeta de red y localiza las capacidadesMSIyMSI-X; anota cuántos vectores soporta cada una. - Mira
/proc/interruptsy cuenta las líneas de tu NIC: verás una IRQ por cola. Explica por qué hay varias y a qué núcleo apunta cada una. - Lee
/proc/irq/<n>/smp_affinityde una de esas colas y razona cómo MSI-X hace posible fijar cada vector a un núcleo. - Escribe con palabras la carrera que
INTxsufre entre la señal y los datos del DMA, y por qué la escritura posted de MSI la elimina sin barrera explícita. - Modifica
midev_setup_irqspara pedir solo un vector y explica qué cambia enflagssi la API acaba usando INTx.