Vam tancar la lliçó anterior amb tres caixes sense obrir i un problema plantejat amb números. Les caixes: què fa exactament la CPU quan arriba una interrupció, com un controlador es registra davant del nucli i quin contracte compleix, i com funciona el DMA per dins. El problema: a 500.000 lectures per segon, un nucli sencer es dedicaria només a atendre interrupcions, i això no pot ser la solució final.

Aquesta lliçó obre les tres caixes i resol el problema. Veuràs el recorregut complet d'una interrupció des de la línia IRQ fins al gestor, entendràs per què el codi d'un gestor no pot dormir ni bloquejar-se, i per què aquesta restricció obliga a partir la feina en dues meitats. Aprendràs a llegir /proc/interrupts línia a línia, que és una de les sortides més informatives del sistema. I seguiràs el camí complet d'un paquet amb lectures d'estació, aquesta vegada sense deixar cap capa sense explicar.

És l'última lliçó del mòdul 2, així que al final recapitularem les set i enllaçarem amb el mòdul 3.

Contingut

  1. Què és un controlador de dispositiu
  2. El contracte amb el nucli: la interfície d'operacions de fitxer
  3. Registre del driver i mòduls carregables
  4. Espai de nucli davant d'espai d'usuari per als drivers
  5. Interrupcions: línia IRQ, vector i taula de vectors
  6. La controladora d'interrupcions: del PIC a l'APIC
  7. Què fa la CPU en rebre una interrupció
  8. El context d'interrupció i les seves regles
  9. Emmascarament i interrupcions compartides
  10. Meitat superior i meitat inferior
  11. /proc/interrupts interpretat línia a línia
  12. MSI i MSI-X
  13. DMA en detall: descriptors, coherència i IOMMU
  14. El camí complet d'un paquet amb lectures
  15. Latència d'interrupció i el seu impacte
  16. Tancament del mòdul 2

Què és un controlador de dispositiu

Un controlador de dispositiu (device driver) és el codi que tradueix les operacions genèriques del nucli en les operacions concretes d'un model de maquinari.

Convé distingir dues coses que en català tenen noms gairebé idèntics i es confonen constantment:

Controlador de dispositiu (driver) Controladora del dispositiu (controller)
Què és Programari dins del nucli Maquinari: el xip de la targeta
On viu A la RAM, com a part del nucli A la mateixa targeta
Exemple El mòdul igb El xip Intel I210

En aquesta lliçó, quan diguem driver ens referim al programari; quan diguem controladora del dispositiu o xip, al maquinari.

El driver és l'única peça del sistema que coneix els detalls bruts: quin registre cal escriure per iniciar una transferència, en quin bit hi ha el senyal de «llest», quants microsegons cal esperar després d'un reinici, quina errata de silici té la revisió B2 del xip.

$ lsmod | head -8
Module                  Size  Used by
igb                   270336  0
nvme                   49152  4
nvme_core             143360  5 nvme
raid1                  49152  1
md_mod                176128  2 raid1
ext4                  942080  2
xhci_pci               24576  0

I la seva magnitud dins del nucli de Linux és reveladora:

$ du -sh /lib/modules/$(uname -r)/kernel/drivers/
198M    /lib/modules/6.1.0-13-amd64/kernel/drivers/
$ du -sh /lib/modules/$(uname -r)/kernel/
312M    /lib/modules/6.1.0-13-amd64/kernel/

Els drivers són el 63 % del codi del nucli. I a l'arbre de fonts de Linux la proporció és similar: més de la meitat dels milions de línies són controladors de dispositiu. El nucli pròpiament dit —planificador, memòria, sistemes de fitxers, xarxa— és la part petita.

El contracte amb el nucli: la interfície d'operacions de fitxer

Un driver no pot fer el que vulgui: ha de complir un contracte amb el nucli. Aquest contracte és una estructura de punters a funció. Per a un dispositiu de caràcter:

#include <linux/fs.h>

static const struct file_operations meteora_fops = {
    .owner          = THIS_MODULE,
    .open           = meteora_open,
    .release        = meteora_release,
    .read           = meteora_read,
    .write          = meteora_write,
    .unlocked_ioctl = meteora_ioctl,
    .poll           = meteora_poll,
    .llseek         = no_llseek,
};

Com funciona el mecanisme, que és el mateix despatx per taula que vam veure a 01-06 amb les crides al sistema:

  • Quan un procés crida read() sobre /dev/meteora/estacio-nord, el nucli recorre el camí que ja coneixes: syscall → taula de crides → sys_read → capa VFS.
  • La capa VFS consulta el número major del fitxer, troba el driver registrat i crida el punter .read del seu file_operations.
  • Aquest punter apunta a meteora_read, codi específic d'aquest dispositiu.

És polimorfisme implementat amb punters a funció en C: la mateixa crida read() acaba executant codi diferent segons el dispositiu, sense que el codi que crida en sàpiga res.

Els mètodes habituals del contracte:

Mètode Quan el crida el nucli Què ha de fer
.open En obrir el fitxer de dispositiu Reservar recursos, comprovar disponibilitat
.release En tancar l'últim descriptor Alliberar recursos
.read En un read() Copiar dades a l'espai d'usuari
.write En un write() Copiar dades des de l'espai d'usuari
.unlocked_ioctl En un ioctl() Operacions que no encaixen a llegir/escriure
.poll En select/poll/epoll Indicar si hi ha dades disponibles
.mmap En un mmap() Mapejar memòria del dispositiu al procés

ioctl mereix un comentari. És la via d'escapament del model «tot és un fitxer»: permet operacions que no són llegir ni escriure. Expulsar un CD, configurar la velocitat d'un port sèrie, consultar estadístiques d'una targeta. Que existeixi demostra els límits de l'abstracció de fitxer, exactament com els sockets en demostraven els límits per un altre costat. ioctl és el reconeixement pragmàtic que no tot cap a read/write.

Un esquelet de driver simplificat:

/* meteora_drv.c — esquelet il·lustratiu, no compilable tal com és */
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define METEORA_MAJOR 0        /* 0 = que el nucli n'assigni un de lliure */

static int major_assignat;

static ssize_t meteora_read(struct file *f, char __user *buf,
                            size_t count, loff_t *pos)
{
    char dades[24];
    int llegits;

    /* 1. Llegir del maquinari (registres MMIO) */
    llegits = llegir_del_dispositiu(dades, sizeof dades);
    if (llegits < 0)
        return -EIO;                    /* es convertirà en errno = EIO */

    if (count > (size_t)llegits)
        count = llegits;

    /* 2. Copiar a l'espai d'usuari: MAI amb memcpy */
    if (copy_to_user(buf, dades, count))
        return -EFAULT;

    return count;                        /* bytes llegits */
}

static int __init meteora_init(void)
{
    major_assignat = register_chrdev(METEORA_MAJOR, "meteora", &meteora_fops);
    if (major_assignat < 0) {
        pr_err("meteora: no s'ha pogut registrar el driver\n");
        return major_assignat;
    }
    pr_info("meteora: registrat amb major %d\n", major_assignat);
    return 0;
}

static void __exit meteora_exit(void)
{
    unregister_chrdev(major_assignat, "meteora");
    pr_info("meteora: descarregat\n");
}

module_init(meteora_init);
module_exit(meteora_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Driver d'exemple per a estacions Meteora");

Punts que cal entendre d'aquest codi:

  • copy_to_user() en lloc de memcpy(). És obligatori i no és una formalitat. El punter buf ve de l'espai d'usuari i no és de fiar: podria apuntar a memòria del nucli, a una adreça no mapejada o a memòria d'un altre procés. copy_to_user valida el rang, gestiona la fallada de pàgina si la pàgina no hi és, i retorna el nombre de bytes que no ha pogut copiar. Un memcpy directe seria una vulnerabilitat d'escalada de privilegis de manual. La marca __user al prototip permet, a més, que eines d'anàlisi estàtica detectin l'error.
  • Retornar -EIO i -EFAULT, en negatiu. Aquest és exactament el conveni que vam rastrejar a 01-06: el nucli retorna l'error com un valor negatiu petit, i la biblioteca C el converteix en -1 més errno.
  • register_chrdev amb major 0 demana al nucli que assigni un número lliure. Fixar un major a mà només funciona per als números reservats oficialment.
  • module_init i module_exit defineixen els punts d'entrada i de sortida del mòdul, que és el que fa possible carregar-lo i descarregar-lo en calent.

Registre del driver i mòduls carregables

Reprenem aquí allò de 01-05: els mòduls carregables permeten afegir codi al nucli en execució.

$ modinfo igb
filename:       /lib/modules/6.1.0-13-amd64/kernel/drivers/net/ethernet/intel/igb/igb.ko
version:        5.6.0-k
license:        GPL v2
description:    Intel(R) Gigabit Ethernet Network Driver
alias:          pci:v00008086d00001521sv*sd*bc*sc*i*
depends:        i2c-algo-bit,dca
parm:           max_vfs:Maximum number of virtual functions (uint)

La línia alias és la clau de la càrrega automàtica: diu que aquest mòdul gestiona el dispositiu PCI amb fabricant 8086 (Intel) i dispositiu 1521 (I350). Quan el bus PCI enumera una targeta amb aquests identificadors, udev busca un mòdul amb l'àlies corresponent i el carrega sense intervenció humana.

$ lspci -nn | grep -i ethernet
03:00.0 Ethernet controller [0200]: Intel Corporation I210 [8086:1533] (rev 03)

$ sudo modprobe -c | grep 8086.*1533
alias pci:v00008086d00001533sv*sd*bc*sc*i* igb

Aquí hi ha la cadena completa: l'identificador del maquinari condueix al nom del mòdul.

# Càrrega i descàrrega manuals
$ sudo modprobe igb
$ sudo modprobe -r igb        # descàrrega (falla si està en ús)
$ lsmod | grep igb
igb                   270336  0     ← el 0 és el comptador d'ús

# Veure els paràmetres configurables
$ ls /sys/module/igb/parameters/
max_vfs

Avantatges dels mòduls, reprenent la discussió d'arquitectura del nucli de 01-05:

Avantatge Detall
Nucli petit Només es carrega el que hi ha present
Actualització sense reiniciar Canviar un driver defectuós en calent
Desenvolupament àgil Compilar i provar sense reiniciar la màquina
Maquinari dinàmic USB connectat en calent

I una limitació important: un mòdul s'executa amb tots els privilegis del nucli. No hi ha aïllament. Un mòdul amb una errada pot corrompre qualsevol estructura del sistema.

Espai de nucli davant d'espai d'usuari per als drivers

Aquí reprenem i tanquem el debat d'arquitectures de 01-05, ara amb dades concretes.

Driver al nucli Driver a l'espai d'usuari
Privilegis Anell 0, accés total Anell 3, restringit
Una fallada provoca Kernel panic o corrupció Mor un procés
Rendiment Màxim, sense canvis de context Canvis de context i IPC
Depuració Difícil (printk, kgdb, bolcats) Amb gdb normal
Exemples Gairebé tots els de Linux FUSE, CUPS, SPDK, DPDK

Per què un driver defectuós és perillós. Un driver corre a l'anell 0, així que:

  • Pot escriure a qualsevol adreça de memòria física, incloses les estructures del nucli i la memòria de qualsevol procés.
  • Pot executar instruccions privilegiades: deshabilitar interrupcions, canviar la taula de pàgines, reprogramar la MMU.
  • No hi ha MMU que el protegeixi d'ell mateix: els mecanismes de protecció que vam estudiar a 02-03 i 02-04 protegeixen els processos entre si, però el nucli és per damunt d'ells.

Un punter nul en un driver:

$ sudo dmesg -T | tail -12
[Sun Aug 31 11:02:44 2026] BUG: kernel NULL pointer dereference, address: 0000000000000018
[Sun Aug 31 11:02:44 2026] #PF: supervisor read access in kernel mode
[Sun Aug 31 11:02:44 2026] Oops: 0000 [#1] PREEMPT SMP NOPTI
[Sun Aug 31 11:02:44 2026] CPU: 2 PID: 0 Comm: swapper/2 Tainted: G  O  6.1.0-13-amd64
[Sun Aug 31 11:02:44 2026] RIP: 0010:meteora_interrupt_handler+0x2a/0x120 [meteora_drv]
[Sun Aug 31 11:02:44 2026] Call Trace:
[Sun Aug 31 11:02:44 2026]  __handle_irq_event_percpu+0x46/0x180
[Sun Aug 31 11:02:44 2026]  handle_irq_event+0x38/0x80
[Sun Aug 31 11:02:44 2026] Kernel panic - not syncing: Fatal exception in interrupt

Com es llegeix aquesta traça:

  • supervisor read access in kernel mode: la fallada s'ha produït a l'anell 0. Si hagués estat en usuari, seria un simple SIGSEGV.
  • RIP: meteora_interrupt_handler+0x2a [meteora_drv]: l'adreça exacta on ha fallat, amb el mòdul culpable identificat entre claudàtors.
  • Tainted: G O: el nucli està «contaminat» per un mòdul extern. Els desenvolupadors del nucli fan servir aquesta marca per saber que el problema pot no ser seu.
  • Fatal exception in interrupt: i aquí hi ha el més greu. La fallada s'ha produït dins d'un gestor d'interrupció, on el nucli no pot simplement matar el procés responsable (no hi ha procés: Comm: swapper/2 és el fil ociós). No hi ha recuperació possible: kernel panic.

Compara-ho amb el mateix error a meteo-api: mor un procés, systemd el reinicia, es perden unes peticions. Aquí mor la màquina sencera.

Drivers a l'espai d'usuari. L'alternativa existeix i es fa servir en casos concrets:

Tecnologia Per a què Per què en usuari
FUSE Sistemes de fitxers (sshfs, s3fs) Un bug no tomba el sistema
CUPS Impressió Complexitat enorme, rendiment irrellevant
libusb Dispositius USB específics No cal un mòdul per cada andròmina
DPDK / SPDK Xarxa i emmagatzematge d'altíssim rendiment Evita el nucli per complet

El cas de DPDK és paradoxal i molt instructiu: mou el driver a l'espai d'usuari no per seguretat, sinó per rendiment. Mapeja els registres de la targeta directament dins del procés i fa sondeig continu, eliminant interrupcions i crides al sistema. Aconsegueix processar milions de paquets per segon a canvi de dedicar nuclis sencers a girar en un bucle. És exactament l'espera activa de la lliçó anterior, triada deliberadament perquè a aquestes taxes surt més barata que interrompre.

Interrupcions: línia IRQ, vector i taula de vectors

Una interrupció és un senyal del maquinari que fa que la CPU suspengui el que està fent i executi un codi predeterminat.

El vocabulari, que cal tenir clar:

Terme Què és
Línia IRQ La connexió física (o lògica) per la qual el dispositiu avisa
Vector El número que identifica quin gestor cal executar (0-255 a x86)
IDT Interrupt Descriptor Table: la taula de 256 entrades amb les adreces dels gestors
ISR Interrupt Service Routine: el codi del gestor

A x86-64 hi ha 256 vectors, repartits així:

Vectors Ús
0-31 Excepcions de la CPU (divisió per zero, fallada de pàgina, etc.)
32-47 IRQ heretades del PIC (teclat, temporitzador, disc)
48-238 Interrupcions de dispositius, MSI/MSI-X
239-255 Interrupcions entre processadors (IPI), temporitzador local de l'APIC

Alguns vectors d'excepció que ja coneixes de lliçons anteriors:

Vector Excepció On va aparèixer
0 Divisió per zero
6 Instrucció invàlida
13 Fallada de protecció general 01-06, la instrucció cli en usuari
14 Fallada de pàgina 02-04, tot el mecanisme de memòria virtual

I aquí convé fixar una classificació que es confon sovint:

Tipus Origen Síncrona Exemple
Interrupció Maquinari extern No Arriba un paquet de xarxa
Excepció / trampa La mateixa CPU en executar Fallada de pàgina, divisió per zero
Crida al sistema Instrucció syscall deliberada read() de l'ingestor

La diferència clau és la sincronia. Una excepció es produeix sempre al mateix punt si repeteixes el programa amb les mateixes dades: és conseqüència de la instrucció que s'estava executant. Una interrupció arriba quan al món exterior li ve de gust i pot caure entre dues instruccions qualssevol. Aquesta asincronia és la font de tota la dificultat del codi d'interrupcions.

La IDT viu a la memòria i la seva adreça és al registre IDTR, carregat amb la instrucció privilegiada lidt que vam veure a la taula de 01-06. Cada entrada conté l'adreça del gestor, el segment i els permisos.

La controladora d'interrupcions: del PIC a l'APIC

La CPU té molt poques potes d'interrupció, així que cal un xip intermediari que multiplexi les línies de tots els dispositius.

PIC 8259A (1976): dos xips encadenats, 15 línies IRQ útils.

IRQ Dispositiu clàssic
0 Temporitzador del sistema
1 Teclat
3 COM2
4 COM1
8 Rellotge de temps real
14 Disc IDE primari

Aquella assignació de 1981 encara es reconeix avui a /proc/interrupts.

APIC (Advanced Programmable Interrupt Controller) el va substituir, i les seves millores són les que fan viable un servidor multiprocessador:

PIC 8259A APIC
Línies 15 24 per I/O APIC, uns quants per sistema
Multiprocessador No : dirigeix la interrupció a una CPU concreta
Prioritats Fixes per número Programables
Repartiment de càrrega No Sí, entre CPU
MSI No

L'APIC té dues parts:

  • Local APIC: un per nucli, integrat a la CPU. Rep les interrupcions dirigides a aquell nucli i gestiona el temporitzador local i les interrupcions entre processadors.
  • I/O APIC: al xipset. Rep les línies dels dispositius i les encamina al Local APIC del nucli que correspongui.

Que l'APIC pugui dirigir una interrupció a un nucli concret és el que permet el smp_affinity que veurem en parlar de /proc/interrupts, i el que fa possible que una targeta amb diverses cues reparteixi la seva feina entre nuclis.

Què fa la CPU en rebre una interrupció

Aquest és el recorregut exacte, i convé comparar-lo mentalment amb el de la crida al sistema de 01-06:

sequenceDiagram
    participant D as Dispositiu
    participant A as APIC
    participant C as CPU
    participant K as Nucli
    D->>A: activa la seva línia IRQ
    A->>A: prioritza i tria el nucli destinatari
    A->>C: senyal d'interrupció + vector
    Note over C: acaba la instrucció en curs
    C->>C: desa RIP, RSP, RFLAGS i CS a la pila de nucli
    C->>C: canvia a anell 0 i a la pila de nucli
    C->>C: deshabilita interrupcions (segons la porta de la IDT)
    C->>C: consulta la IDT[vector]
    C->>K: salta al gestor
    K->>K: desa la resta de registres
    K->>K: executa el gestor del driver
    K->>A: envia EOI (fi d'interrupció)
    K->>K: restaura registres
    K->>C: iret
    Note over C: continua la instrucció següent

Els detalls que importen:

  1. La instrucció en curs s'acaba. La CPU no interromp a mitja instrucció (amb comptades excepcions per a instruccions molt llargues, que són represes). Això garanteix un estat consistent.
  2. Es desa l'estat mínim automàticament: punter d'instrucció, punter de pila, registre de banderes i selector de segment. Ho fa el maquinari, no el programari.
  3. Es canvia a la pila de nucli, exactament com en un syscall. El gestor no fa servir mai la pila del procés interromput.
  4. Les interrupcions es deshabiliten si l'entrada de la IDT és una interrupt gate (l'habitual). Això evita que una altra interrupció s'imbriqui immediatament.
  5. L'EOI és obligatori. Si el gestor no envia el senyal de fi d'interrupció a l'APIC, aquell dispositiu no tornarà a interrompre mai més. És un dels errors clàssics en escriure drivers, i el seu símptoma és que el dispositiu funciona exactament una vegada i després es queda mut.

Comparació amb el que ja saps:

Crida al sistema (01-06) Interrupció
Qui la inicia El procés, amb syscall El maquinari
Quan es produeix En un punt determinista En qualsevol moment
Canvia de procés No No (però pot provocar que se'n canviï després)
Canvia de pila Sí, a la de nucli Sí, a la de nucli
Context Del procés que ha cridat De ningú en particular
Cost 50-500 ns 1-5 µs

La fila crucial és la penúltima: una crida al sistema s'executa en nom d'un procés concret, mentre que una interrupció s'executa en el context de qui toqués estar executant-se, que pot ser qualsevol. D'aquí surten totes les restriccions de l'apartat següent.

El context d'interrupció i les seves regles

Un gestor d'interrupció s'executa en context d'interrupció, també anomenat context atòmic. És un entorn amb regles molt estrictes:

Regla Per què
No pot dormir ni bloquejar-se No hi ha cap procés a qui tornar la CPU: no hi ha task_struct propi per posar en estat S
No pot cridar funcions que puguin dormir kmalloc(GFP_KERNEL), mutex_lock, copy_to_user
No pot accedir a l'espai d'usuari Podria provocar una fallada de pàgina, que implicaria dormir
Ha de ser molt ràpid Amb les interrupcions deshabilitades, tota la resta espera
Ha de fer servir forrellats especials spin_lock_irqsave, mai un mutex

Comprendre el perquè de la primera regla és la clau de tota la resta. Quan l'ingestor crida read() i no hi ha dades, el nucli el posa en estat S i planifica un altre procés: hi ha un task_struct on aparcar-lo. Però un gestor d'interrupció no pertany a cap procés. Si s'adormís, a qui es posaria en estat S? Al procés que casualment s'estava executant, que no hi té res a veure? I qui el despertaria?

Recorda la traça del kernel panic: Comm: swapper/2. El gestor es va executar «sobre» el fil ociós, que era el que hi havia en aquell nucli. No era la seva interrupció ni la seva feina.

/* MALAMENT: això penja el sistema */
static irqreturn_t gestor_dolent(int irq, void *dev)
{
    char *buf = kmalloc(4096, GFP_KERNEL);   /* pot dormir! */
    mutex_lock(&el_meu_mutex);                /* pot dormir! */
    copy_to_user(desti, dades, 24);           /* pot fallar la pàgina! */
    msleep(10);                               /* dorm explícitament! */
    return IRQ_HANDLED;
}

/* BÉ */
static irqreturn_t gestor_bo(int irq, void *dev)
{
    struct meteora_dev *d = dev;
    u32 estat;
    unsigned long flags;

    /* 1. És meva, aquesta interrupció? (línia compartida) */
    estat = readl(d->regs + REG_ESTAT);
    if (!(estat & INT_PENDENT))
        return IRQ_NONE;                      /* no és meva, que la vegi un altre */

    /* 2. Reconèixer-la al maquinari: imprescindible */
    writel(estat, d->regs + REG_ESTAT);

    /* 3. Forrellat d'espera activa que deshabilita interrupcions */
    spin_lock_irqsave(&d->lock, flags);
    d->paquets_pendents++;
    spin_unlock_irqrestore(&d->lock, flags);

    /* 4. Delegar la feina pesada a la meitat inferior */
    napi_schedule(&d->napi);

    return IRQ_HANDLED;
}

Les quatre decisions del gestor correcte:

  1. Comprovar si la interrupció és seva abans de res, perquè la línia pot estar compartida. Retornar IRQ_NONE permet que el nucli provi amb el driver registrat següent.
  2. Reconèixer-la al maquinari escrivint al registre d'estat. Sense això, el dispositiu continuaria afirmant la línia i es generaria una tempesta d'interrupcions infinita.
  3. spin_lock_irqsave en lloc de mutex_lock. Un mutex dorm si està ocupat; un spinlock gira esperant, que és l'únic que es pot fer en context atòmic. La variant irqsave desa a més l'estat de les interrupcions i les deshabilita, i evita l'interbloqueig amb un mateix si la mateixa interrupció es repetís.
  4. Delegar tota la feina real a la meitat inferior. Només s'ha incrementat un comptador i s'ha programat feina diferida.

Emmascarament i interrupcions compartides

Emmascarament és deshabilitar temporalment una interrupció, i hi ha tres nivells:

Nivell Com Abast
Global a la CPU cli / sti Totes les interrupcions emmascarables
Per línia, a l'APIC Registre de màscara Una línia concreta
Amb desament d'estat spin_lock_irqsave Global, restaurant l'estat previ

cli és la instrucció privilegiada que vam provar a 01-06 i que provocava un SIGSEGV en mode usuari. Ara entens per què ha de ser privilegiada amb tanta rotunditat: un procés que pogués deshabilitar les interrupcions impediria que el temporitzador les hi tragués, monopolitzant la CPU per sempre i trencant completament la planificació expropiativa de 02-02.

Les NMI (Non-Maskable Interrupt) no es poden emmascarar de cap manera. Es reserven per a fallades catastròfiques: error de paritat de memòria, el temporitzador de vigilància, senyals de fallada de maquinari. Si el sistema deixa de respondre del tot, la NMI del watchdog és la que força un bolcat de diagnòstic.

Interrupcions compartides. Amb poques línies IRQ i molts dispositius, uns quants poden compartir línia:

$ cat /proc/interrupts | grep -E '^ *(16|17|18):'
 16:      1204      0      0      0  IO-APIC  16-fasteoi   ehci_hcd:usb1, i801_smbus
 17:     28841      0      0      0  IO-APIC  17-fasteoi   snd_hda_intel

La IRQ 16 la comparteixen la controladora USB i el bus SMBus. Quan s'activa, el nucli no sap quin dels dos ha estat, així que crida els gestors de tots els drivers registrats en aquella línia, en ordre, fins que un retorna IRQ_HANDLED.

D'aquí la importància de la comprovació de l'apartat anterior: un driver que retornés IRQ_HANDLED sense comprovar si la interrupció era seva robaria les interrupcions de l'altre dispositiu, que deixaria de funcionar amb un símptoma impossible de diagnosticar.

/* Registrar un gestor compartit */
ret = request_irq(irq, gestor_bo,
                  IRQF_SHARED,          /* la línia es pot compartir */
                  "meteora",            /* nom a /proc/interrupts */
                  dev);                 /* es passa al gestor per identificar-lo */

L'últim argument és essencial en línies compartides: és el que permet que el gestor sàpiga a quina instància de dispositiu correspon la crida.

Meitat superior i meitat inferior

Aquí hi ha la idea central del disseny d'interrupcions a Linux, i resol una tensió real.

La tensió: un gestor d'interrupció ha de ser rapidíssim, perquè s'executa amb interrupcions deshabilitades i bloqueja tota la resta. Però la feina que genera una interrupció pot ser considerable: processar un paquet de xarxa implica recórrer la pila IP, buscar el socket, actualitzar estadístiques, despertar processos.

La solució: partir-la en dues.

Meitat superior (top half) Meitat inferior (bottom half)
Quan Immediatament, en context d'interrupció Diferida, poc després
Interrupcions Deshabilitades (com a mínim la seva) Habilitades
Pot dormir No Segons el mecanisme
Durada Microsegons Pot ser llarga
Què fa Reconèixer el maquinari, desar dades, programar la inferior El processament real

Els tres mecanismes de meitat inferior a Linux:

Mecanisme Context Pot dormir? Concurrència Ús
softirq Atòmic No En paral·lel en diverses CPU Xarxa, bloc, temporitzadors
tasklet Atòmic No Només una instància alhora Drivers senzills
workqueue Procés Fils del nucli Feina que necessita dormir
  • softirq: la més ràpida i l'única que escala. N'hi ha un nombre fix definit en temps de compilació, així que només la fan servir els subsistemes principals del nucli. La mateixa softirq es pot executar simultàniament en diverses CPU, cosa que exigeix que el seu codi sigui reentrant.
  • tasklet: construïda sobre softirq, més fàcil de fer servir. Una tasklet concreta no s'executa mai en dues CPU alhora, cosa que simplifica la sincronització a costa d'escalabilitat.
  • workqueue: s'executa en un fil del nucli, és a dir, en context de procés. És l'única que pot dormir, així que és l'opció quan cal reservar memòria amb GFP_KERNEL, esperar un mutex o fer E/S.
$ cat /proc/softirqs
                    CPU0       CPU1       CPU2       CPU3
          HI:          0          0          0          0
       TIMER:     284102     271884     269110     265882
      NET_TX:      12841       1102        884        791
      NET_RX:    1284102      42881      38104      36992
       BLOCK:     102841      98221      94102      91884
     TASKLET:       2841       1102        884        702
       SCHED:     284102     198221     194102     191884
     RCU:          98221      94102      91884      89102

Lectura d'aquesta sortida:

  • NET_RX és 30 vegades més gran a CPU0 (1.284.102 davant de ~40.000). Tot el trànsit de xarxa entrant es processa en un sol nucli. Amb una targeta d'una sola cua és l'esperable; amb una de multicua, indica que l'afinitat d'interrupcions no està ben repartida.
  • TIMER està repartit uniformement, com ha de ser: cada nucli té el seu temporitzador local de l'APIC.
  • BLOCK són les interrupcions de dispositius de bloc completades, correlacionades amb l'activitat del RAID que vam veure a 02-05.

L'exemple de la targeta de xarxa

/* MEITAT SUPERIOR: microsegons, en context d'interrupció */
static irqreturn_t igb_msix_ring(int irq, void *data)
{
    struct igb_q_vector *q_vector = data;

    igb_write_itr(q_vector);          /* ajustar la moderació */
    napi_schedule(&q_vector->napi);   /* programar la meitat inferior */

    return IRQ_HANDLED;
}

/* MEITAT INFERIOR: softirq NET_RX, amb interrupcions habilitades */
static int igb_poll(struct napi_struct *napi, int budget)
{
    /* Processar fins a 'budget' paquets (típicament 64) */
    clean_complete = igb_clean_rx_irq(q_vector, budget);

    if (!clean_complete)
        return budget;                /* en queden més: continuaré en sondeig */

    napi_complete_done(napi, work_done);
    igb_ring_irq_enable(q_vector);    /* rehabilitar interrupcions */
    return work_done;
}

El repartiment és clar: la meitat superior triga menys d'1 µs i només programa feina. La meitat inferior processa fins a 64 paquets recorrent tota la pila de xarxa, i pot trigar desenes de microsegons, però amb les interrupcions habilitades, així que no bloqueja res.

NAPI: la solució a la tempesta d'interrupcions

Aquí resolem el problema que vam deixar plantejat. La clau és a igb_ring_irq_enable: mentre hi ha paquets per processar, les interrupcions d'aquella cua estan deshabilitades i el nucli consulta activament (sondeig).

Càrrega baixa (800 paquets/s):
  Arriba un paquet → interrupció → NAPI en processa 1 → no n'hi ha més
  → rehabilita interrupcions → torna a mode interrupció
  Resultat: ~800 interrupcions/s. Latència mínima.

Càrrega alta (500.000 paquets/s):
  Arriba un paquet → interrupció → NAPI deshabilita interrupcions
  → processa 64 → encara n'hi ha més → processa 64 més → ...
  → quan la cua es buida, rehabilita interrupcions
  Resultat: unes poques milers d'interrupcions/s en lloc de 500.000

NAPI és un híbrid adaptatiu: interrupcions amb càrrega baixa (latència mínima) i sondeig amb càrrega alta (rendiment màxim). Canvia de mode tot sol, sense configuració.

Els números per a Meteora:

Situació actual (800 lectures/s):
  Mode interrupció, ~800 interrupcions/s
  800 × 3 µs = 2,4 ms/s = 0,24 % d'un nucli

Amb 500.000 lectures/s SENSE NAPI:
  500.000 × 3 µs = 1,5 s de CPU per segon → més d'un nucli sencer

Amb 500.000 lectures/s AMB NAPI:
  Processa lots de 64 en sondeig
  ~7.800 cicles de sondeig/s × 3 µs ≈ 23 ms/s = 2,3 % d'un nucli
  Reducció: 65×

És una de les solucions més elegants del nucli de Linux: reconèixer que cap de les dues tècniques de la lliçó anterior és sempre millor, i canviar-hi segons la càrrega.

/proc/interrupts interpretat línia a línia

$ cat /proc/interrupts
            CPU0       CPU1       CPU2       CPU3
   0:         41          0          0          0   IO-APIC    2-edge      timer
   1:       1204          0          0          0   IO-APIC    1-edge      i8042
   8:          1          0          0          0   IO-APIC    8-edge      rtc0
   9:          0          0          0          0   IO-APIC    9-fasteoi   acpi
  16:      28841          0          0          0   IO-APIC   16-fasteoi   ehci_hcd:usb1, i801_smbus
 128:          0          0          0          0   PCI-MSI 1572864-edge   enp3s0
 129:    1284102          0          0          0   PCI-MSI 1572865-edge   enp3s0-rx-0
 130:          0     284102          0          0   PCI-MSI 1572866-edge   enp3s0-tx-0
 131:      98221      94102      91884      89102   PCI-MSI 524288-edge    nvme0q0
 132:     284102     271884     269110     265882   PCI-MSI 524289-edge    nvme0q1
 NMI:          0          0          0          0   Non-maskable interrupts
 LOC:    2841022    2718840    2691100    2658820   Local timer interrupts
 RES:      28410      27188      26911      26588   Rescheduling interrupts
 CAL:       1204       1102        884        791   Function call interrupts
 TLB:      12841      11022       8840       7910   TLB shootdowns

Estructura d'una línia:

 129:    1284102          0          0          0   PCI-MSI 1572865-edge   enp3s0-rx-0
 └┬─┘    └──────── recompte per CPU ────────┘       └─ tipus ┘ └dispar─┘   └── driver ──┘
IRQ

Anàlisi línia a línia del que diu aquest sistema:

Línia Què revela
0: timer Només 41 activacions. El temporitzador heretat gairebé no es fa servir: Linux empra el temporitzador local de l'APIC (línia LOC)
1: i8042 1.204 pulsacions de teclat. En un servidor sense monitor, segurament de la consola de gestió
16: ehci_hcd:usb1, i801_smbus Interrupció compartida: dos drivers a la mateixa línia
128-130: enp3s0 Tres vectors MSI-X per a una sola targeta: un de control, un de recepció, un de transmissió
129: enp3s0-rx-0 1.284.102 interrupcions totes a CPU0. Tota la recepció de xarxa en un nucli
131-132: nvme0q0, nvme0q1 Repartit uniformement entre els 4 nuclis. Això és NVMe funcionant com cal: una cua per nucli
LOC El temporitzador que dispara la planificació. Uniforme, com ha de ser
RES Un nucli demana a un altre que replanifiqui. Lligat al balanceig de càrrega de 02-02
TLB Invalidacions de TLB entre CPU: quan un nucli canvia una taula de pàgines, avisa els altres perquè invalidin les seves entrades. Directament relacionat amb el cost del canvi de context de 02-01 i amb la TLB de 02-04

El diagnòstic que salta a la vista: enp3s0-rx-0 concentra 1,28 milions d'interrupcions a CPU0 mentre els altres tres nuclis estan a zero. Amb 800 lectures/s no és cap problema, però és el coll d'ampolla que apareixeria en créixer.

$ cat /proc/irq/129/smp_affinity
1
$ cat /proc/irq/129/smp_affinity_list
0

La màscara 1 (binari 0001) significa «només CPU0». Per repartir-la:

# Permetre que la faci servir qualsevol nucli
$ echo f | sudo tee /proc/irq/129/smp_affinity

# O fixar-la a un nucli concret que no sigui el que fa servir l'agregador
$ echo 2 | sudo tee /proc/irq/129/smp_affinity    # només CPU1

I aquí apareix una decisió interessant que enllaça amb 02-02: a la lliçó de planificació vam confinar l'agregador als nuclis 2 i 3 amb taskset. Dirigir les interrupcions de xarxa a CPU0 i CPU1 completa aquesta partició: el processament de paquets i el càlcul de mitjanes deixen de competir tant per CPU com per memòria cau.

El dimoni irqbalance fa aquest repartiment automàticament, però en sistemes amb requisits de latència estrictes se sol desactivar per fixar l'afinitat a mà.

Observar l'activitat en temps real:

$ watch -n1 'grep -E "enp3s0|nvme" /proc/interrupts'

És la manera més ràpida de comprovar si un dispositiu està generant interrupcions. Si un dispositiu no respon i el seu comptador no augmenta, el problema és al maquinari o a l'encaminament de la interrupció, no al programari de dalt.

MSI i MSI-X

Les interrupcions clàssiques fan servir línies físiques: una pota dedicada del dispositiu a l'APIC. Això té tres problemes:

  1. Escassetat: hi ha poques línies, d'aquí les compartides.
  2. Condicions de cursa: el dispositiu pot afirmar la línia abans que les dades que ha escrit per DMA hagin arribat a la memòria. El gestor llegiria dades incompletes.
  3. Un sol vector per dispositiu: no es pot distingir «he rebut un paquet» de «he acabat de transmetre».

MSI (Message Signaled Interrupts) elimina les línies: el dispositiu escriu un valor en una adreça de memòria especial, i aquesta escriptura és la interrupció.

Línia IRQ MSI MSI-X
Mecanisme Pota física Escriptura en memòria Escriptura en memòria
Vectors per dispositiu 1 Fins a 32 Fins a 2.048
Compartides No No
Cursa amb el DMA Possible No No
Destinatari per vector Fix Un per a tots Un per vector

Els dos avantatges decisius:

S'elimina la cursa amb el DMA. Com que la interrupció és una escriptura pel mateix bus PCIe que les dades, l'ordenació del bus garanteix que les dades ja han arribat quan arriba la interrupció. La línia física anava per un camí diferent i es podia avançar.

MSI-X permet un vector per cua i per nucli. Això és el que fa possible el repartiment perfecte de l'NVMe que vèiem més amunt:

 131:      98221      94102      91884      89102   PCI-MSI 524288-edge   nvme0q0
 132:     284102     271884     269110     265882   PCI-MSI 524289-edge   nvme0q1

Recorda de 02-05 que NVMe té fins a 65.535 cues i el seu avantatge és la paral·lelització. MSI-X és la peça que la completa: cada cua té el seu vector d'interrupció dirigit al seu propi nucli, així que no hi ha ni un forrellat compartit ni una CPU que centralitzi la feina.

$ sudo lspci -v -s 03:00.0 | grep -A2 MSI-X
        Capabilities: [70] MSI-X: Enable+ Count=5 Masked-
                Vector table: BAR=3 offset=00000000

Enable+ confirma que està actiu, i Count=5 que la targeta té 5 vectors: control, dues cues de recepció i dues de transmissió.

DMA en detall: descriptors, coherència i IOMMU

El DMA transfereix dades entre dispositiu i memòria sense CPU. Aquí hi ha com funciona per dins.

Descriptors

Les targetes modernes no reben una ordre per transferència: treballen amb anells de descriptors, estructures en memòria que la CPU omple i el dispositiu consumeix.

/* Descriptor de recepció simplificat, estil Intel */
struct rx_descriptor {
    u64 adreca_buffer;       /* adreça FÍSICA on escriure */
    u16 longitud;            /* bytes rebuts (ho omple la targeta) */
    u16 checksum;
    u8  estat;               /* bit DD: Descriptor Done */
    u8  errors;
    u16 vlan;
};

El cicle de vida:

 Anell de 256 descriptors a la RAM

 ┌──────┬──────┬──────┬──────┬──────┬──────┐
 │ D0   │ D1   │ D2   │ D3   │ ...  │ D255 │
 └──────┴──────┴──────┴──────┴──────┴──────┘
    ↑                    ↑
   HEAD                 TAIL
 (la targeta)          (el driver)

1. El driver reserva 256 memòries intermèdies i omple les adreces físiques
2. Escriu TAIL en un registre de la targeta: "n'hi ha 256 de lliures"
3. Arriba un paquet: la targeta escriu per DMA a la memòria de HEAD,
   omple longitud i posa el bit DD, i avança HEAD
4. La targeta genera una interrupció (MSI-X)
5. El driver recorre des de la seva posició buscant descriptors amb DD=1
6. Processa els paquets, reassigna memòries noves i avança TAIL

Aquest disseny d'anell és el que permet rebre a màxima velocitat: la targeta pot escriure diversos paquets sense cap intervenció de la CPU, i el driver els recull en lot quan li toca. És la mateixa idea de la doble memòria intermèdia de 02-06, generalitzada a 256 posicions.

Coherència de memòria cau

Aquí hi ha un problema subtil i molt real. La CPU té memòries cau; el DMA escriu directament a la RAM, saltant-se-les.

Problema en la lectura (DMA → memòria):
  1. La CPU havia llegit la memòria intermèdia abans: en té una còpia a la L1
  2. El DMA escriu dades noves a la RAM
  3. La CPU llegeix la memòria intermèdia → obté la còpia ANTIGA de la memòria cau
  → Llegeix dades obsoletes

Problema en l'escriptura (memòria → DMA):
  1. La CPU escriu a la memòria intermèdia → queda a la memòria cau (write-back)
  2. El DMA llegeix de la RAM → obté dades ANTIGUES
  → Envia escombraries

Les solucions, en ordre de preferència:

Solució Com Cost
Coherència per maquinari El bus invalida les línies de memòria cau afectades Cap, ho fa el xipset
Memòria no emmagatzemable a la memòria cau Marcar les pàgines amb PCD Accessos de CPU molt lents
Buidatge explícit dma_sync_single_for_cpu/device Instruccions addicionals

A x86 el maquinari és coherent i el problema no existeix. A ARM i altres arquitectures cal sincronitzar explícitament, i per això l'API de DMA de Linux és portable:

/* Reservar memòria coherent: el nucli tria l'estratègia
   adequada segons l'arquitectura */
desc = dma_alloc_coherent(&pdev->dev, mida, &dma_handle, GFP_KERNEL);

/* Per a memòries intermèdies normals, marcar el traspàs de propietat */
dma_sync_single_for_cpu(&pdev->dev, dma_handle, len, DMA_FROM_DEVICE);
/* ... la CPU llegeix les dades ... */
dma_sync_single_for_device(&pdev->dev, dma_handle, len, DMA_FROM_DEVICE);

Fixa't en el concepte que expressen aquestes crides: la propietat de la memòria intermèdia es traspassa entre CPU i dispositiu. Mentre és del dispositiu, la CPU no l'ha de tocar, i a l'inrevés. És un patró que reapareixerà amb un altre nom al mòdul 3.

IOMMU

Un problema de seguretat de primer ordre: el DMA fa servir adreces físiques i se salta la MMU. Una targeta compromesa —o amb un microprogramari maliciós, o un dispositiu Thunderbolt connectat per un atacant— podria escriure a qualsevol adreça física, inclosa la memòria del nucli. És l'atac DMA, i s'ha explotat a la pràctica.

La IOMMU (Intel VT-d, AMD-Vi) és una MMU per a dispositius:

flowchart LR
    DEV["Dispositiu<br/>adreça DMA<br/>0x1000"] --> IOMMU
    IOMMU{"IOMMU<br/>té permís<br/>aquest dispositiu?"}
    IOMMU -->|sí| RAM["RAM<br/>adreça física<br/>0x7A34000"]
    IOMMU -->|no| FAULT["Fallada DMA<br/>→ registrada i bloquejada"]

És exactament el mateix mecanisme que la MMU de 02-04 —taules de traducció, comprovació de permisos, fallada si no escau— però aplicat als dispositius en lloc dels processos. Cada dispositiu té el seu propi espai d'adreces de DMA i només pot accedir al que se li ha assignat.

$ dmesg | grep -i -E 'dmar|iommu' | head -4
[    0.000000] DMAR: IOMMU enabled
[    0.212841] DMAR: Intel(R) Virtualization Technology for Directed I/O
[    0.213102] iommu: Default domain type: Translated

$ ls /sys/class/iommu/
dmar0  dmar1

A més de la seguretat, la IOMMU habilita el passthrough de dispositius a màquines virtuals: es pot donar una targeta física a una VM amb la garantia que el seu driver, encara que estigui compromès, no pot tocar la memòria de l'amfitrió ni de les altres VM. És una peça essencial de la virtualització, i tornarà a Virtualització: Hipervisors i Màquines Virtuals.

El camí complet d'un paquet amb lectures

Ara sí, el recorregut complet sense caixes negres. Una estació envia una lectura de 24 bytes per UDP i l'ingestor la rep.

sequenceDiagram
    participant E as Estació
    participant N as NIC I210
    participant M as RAM
    participant C as CPU
    participant S as softirq NET_RX
    participant K as Pila de xarxa
    participant I as ingestor

    E->>N: paquet UDP (66 bytes al cable)
    N->>N: valida el CRC de la trama Ethernet
    N->>M: DMA escriu a la memòria del descriptor HEAD
    N->>M: marca DD=1 i la longitud al descriptor
    N->>C: MSI-X: escriptura que genera el vector 129
    C->>C: desa estat, salta a la IDT[129]
    C->>C: MEITAT SUPERIOR igb_msix_ring (< 1 µs)
    C->>S: napi_schedule() i deshabilita aquesta IRQ
    Note over C: iret: la CPU continua el que feia
    S->>S: MEITAT INFERIOR: softirq NET_RX
    S->>M: recorre descriptors amb DD=1
    S->>K: lliura el paquet a la pila de xarxa
    K->>K: Ethernet → IP: comprova destinatari i suma
    K->>K: IP → UDP: comprova el port 9010
    K->>K: busca el socket que escolta al 9010
    K->>K: encua el paquet a la memòria del socket
    K->>I: marca l'ingestor com a executable (S → R)
    S->>N: si no hi ha més paquets, rehabilita la IRQ
    Note over I: el planificador tria l'ingestor (02-02)
    I->>I: read() retorna amb 24 bytes a la memòria intermèdia

Recorregut pas a pas, amb el cost i la lliçó de referència:

# Pas Qui Cost Lliçó
1 Arriba la trama, es valida el CRC Maquinari NIC ~0,5 µs
2 DMA a la memòria del descriptor Motor DMA 0 CPU 02-07
3 Es marca DD=1 al descriptor NIC 0 CPU 02-07
4 MSI-X genera el vector 129 NIC → APIC 02-07
5 La CPU desa estat i salta a la IDT Maquinari CPU ~0,5 µs 02-07
6 Meitat superior: programa NAPI Driver igb < 1 µs 02-07
7 Meitat inferior: softirq NET_RX Nucli ~5 µs 02-07
8 Pila IP i UDP, cerca del socket Nucli ~3 µs
9 Encuat a la memòria del socket Nucli ~0,5 µs 02-06 (buffering)
10 L'ingestor passa de S a R Nucli ~0,3 µs 02-01 (estats)
11 El planificador el tria CFS/EEVDF variable 02-02
12 read() copia a l'espai d'usuari Crida al sistema ~1 µs 01-06
Latència total des del cable fins a l'ingestor: ~12 µs
més el temps d'espera del planificador (0 a diversos ms segons càrrega)

Dues observacions que resumeixen el mòdul sencer:

La CPU intervé 12 µs per a un paquet que ha trigat 0,5 µs a arribar. El cost no és moure 66 bytes, sinó creuar capes: interrupció, canvi de context d'interrupció, pila de xarxa, despertar el procés, crida al sistema. És la mateixa lliçó del cost del syscall de 01-06 i del canvi de context de 02-01, aplicada a l'E/S.

El pas 11 és el que més varia. Els passos 1 a 10 són deterministes i sumen ~12 µs. El pas 11 depèn de la càrrega i del nice de l'ingestor, i pot ser zero o diversos mil·lisegons. La latència real està dominada per la planificació, no pel maquinari, i per això a 02-02 vam donar a l'ingestor prioritat elevada.

Latència d'interrupció i el seu impacte

La latència d'interrupció és el temps des que el dispositiu la genera fins que comença a executar-se el gestor.

Els seus components:

Component Cost típic De què depèn
Propagació per l'APIC 0,1-0,3 µs Maquinari
Acabar la instrucció en curs 0-0,1 µs Pot ser llarga (rep movsb)
Esperar que es rehabilitin les interrupcions 0 - molts µs Codi del nucli amb cli
Desar l'estat i saltar a la IDT 0,3-0,5 µs Maquinari
Entrar al gestor amb memòries cau fredes 0,5-2 µs Pressió de memòria cau

El component crític és el tercer: si un altre codi del nucli té les interrupcions deshabilitades, la interrupció espera. Aquest és el pitjor cas, i és el que determina la previsibilitat del sistema.

$ sudo cyclictest -p 80 -t 4 -n -D 60
T: 0 ( 4102) P:80 I:1000 C:  60000 Min:      2 Act:    3 Avg:    4 Max:      87
T: 1 ( 4103) P:80 I:1500 C:  40000 Min:      2 Act:    3 Avg:    4 Max:      64

Latència mitjana de 4 µs i màxima de 87 µs. Per a Meteora és excel·lent. Per a control industrial amb terminis de 50 µs, aquest màxim de 87 µs seria un incompliment.

Nucli Latència màxima típica Adequat per a
Estàndard (CONFIG_PREEMPT_NONE) mil·lisegons Servidors de rendiment
Voluntari (PREEMPT_VOLUNTARY) centenars de µs Escriptori
Expropiatiu (PREEMPT) desenes de µs Multimèdia, baixa latència
PREEMPT_RT < 10 µs Temps real dur

PREEMPT_RT aconsegueix aquesta garantia convertint gairebé tots els gestors d'interrupció en fils del nucli planificables i substituint els spinlocks per mutex expropiables. Redueix el rendiment màxim a canvi de previsibilitat, que és exactament el compromís del temps real que vam plantejar a 01-03: predictible, no ràpid. El detall correspon a Sistemes Operatius Mòbils i de Temps Real.

Eines de diagnòstic:

# Interrupcions en temps real
$ watch -n1 'grep -E "enp3s0|nvme" /proc/interrupts'

# Missatges del nucli amb marca de temps
$ sudo dmesg -T | grep -iE 'irq|dma|error' | tail -20

# Estadístiques detallades de la targeta
$ sudo ethtool -S enp3s0 | grep -E 'rx_packets|rx_dropped|rx_missed|rx_no_buffer'
     rx_packets: 1284102
     rx_dropped: 0
     rx_missed_errors: 0
     rx_no_buffer_count: 0

# Moderació d'interrupcions
$ sudo ethtool -c enp3s0
rx-usecs: 3
rx-frames: 0

rx_missed_errors i rx_no_buffer_count són les mètriques crítiques per a Meteora. Si són diferents de zero, s'estan perdent lectures: la targeta ha rebut paquets però no hi havia descriptors lliures on escriure'ls, perquè el driver no els va reciclar a temps. Com que les lectures perdudes són irrecuperables, aquestes dues xifres haurien d'estar monitorades amb alerta.

# Ampliar l'anell de descriptors si apareixen pèrdues
$ sudo ethtool -g enp3s0
Ring parameters for enp3s0:
Pre-set maximums:
RX:             4096
Current hardware settings:
RX:             256

$ sudo ethtool -G enp3s0 rx 2048

Passar de 256 a 2.048 descriptors dona al sistema vuit vegades més marge per absorbir ràfegues abans de perdre paquets. El cost és memòria (2.048 memòries intermèdies de 2 KB = 4 MB) i una mica més de latència en el pitjor cas. Per a un sistema on perdre dades és irreversible, és un intercanvi clarament favorable.

Errors Habituals i Consells

Fer feina pesada a la meitat superior. És l'error de disseny número u en drivers. Amb les interrupcions deshabilitades, tot el sistema espera. Un gestor que trigui 500 µs fa perdre paquets a la targeta de xarxa i provoca xruns a l'àudio. La regla: reconèixer el maquinari, desar el mínim, programar la meitat inferior, sortir.

Fer servir mutex_lock en context d'interrupció. Un mutex dorm, i en context atòmic dormir penja el sistema. Fes servir spin_lock_irqsave. I no oblidis l'irqsave: sense això, si la mateixa interrupció torna a entrar mentre tens el forrellat, et bloqueges contra tu mateix.

Oblidar l'EOI o el reconeixement al maquinari. El símptoma és inconfusible: el dispositiu funciona exactament una vegada i després es queda mut, o genera una tempesta infinita d'interrupcions que congela la màquina.

Retornar IRQ_HANDLED sense comprovar si la interrupció és teva. En una línia compartida, robes les interrupcions de l'altre dispositiu, que deixa de funcionar amb un símptoma impossible de relacionar amb el teu driver.

Fer servir memcpy en lloc de copy_to_user. És una vulnerabilitat d'escalada de privilegis, no un detall d'estil. El punter ve d'usuari i no és de fiar.

Ignorar rx_missed_errors. És la mètrica que diu si estàs perdent dades a la targeta. A Meteora, on les lectures perdudes són irrecuperables, hauria de tenir alerta configurada.

Desactivar irqbalance sense fixar l'afinitat a mà. Et quedes amb totes les interrupcions a CPU0, que és el pitjor dels dos móns: sense repartiment automàtic i sense repartiment manual.

Consell de diagnòstic: davant d'un dispositiu que no respon, l'ordre és: watch -n1 cat /proc/interrupts (genera interrupcions?), dmesg -T | tail -50 (hi ha errors del driver?), ethtool -S o l'eina equivalent (hi ha pèrdues?), cat /proc/irq/N/smp_affinity (estan totes en un nucli?). Si el comptador d'interrupcions no augmenta, el problema és per sota del driver: encaminament de la interrupció, MSI mal configurat o maquinari. Si augmenta però no arriben dades, el problema és per damunt.

Exercicis

Exercici 1: analitzar /proc/interrupts

Aquest és l'estat de meteo-01 durant el pic de les 8:00:

            CPU0       CPU1       CPU2       CPU3
   1:       1204          0          0          0   IO-APIC    1-edge      i8042
  16:      28841          0          0          0   IO-APIC   16-fasteoi   ehci_hcd:usb1, i801_smbus
 128:          2          0          0          0   PCI-MSI 1572864-edge   enp3s0
 129:    8421022          0          0          0   PCI-MSI 1572865-edge   enp3s0-rx-0
 130:     284102          0          0          0   PCI-MSI 1572866-edge   enp3s0-tx-0
 131:      12841      11022       8840       7910   PCI-MSI 524288-edge    nvme0q0
 132:     284102     271884     269110     265882   PCI-MSI 524289-edge    nvme0q1
 LOC:    9841022    2718840    2691100    2658820   Local timer interrupts
 RES:     284102      27188      26911      26588   Rescheduling interrupts
 TLB:     128410      11022       8840       7910   TLB shootdowns

I en paral·lel:

$ mpstat -P ALL 1 1
CPU  %usr %nice %sys %iowait %irq %soft %idle
all  18,2   4,1  12,4     1,2  0,8  14,1   49,2
  0   4,1   0,0  28,2     0,4  3,2  56,4    7,7
  1  24,1   0,0   6,8     1,6  0,0   0,2   67,3
  2  21,8  16,4   7,1     1,4  0,0   0,1   53,2
  3  22,8   0,0   7,5     1,4  0,0   0,1   68,2
  1. Identifica el problema principal i justifica'l amb almenys tres dades de les dues sortides.
  2. Per què LOC és 3,6 vegades més gran a CPU0 que als altres?
  3. Què indica que nvme0q1 estigui repartit i enp3s0-rx-0 no?
  4. Proposa una solució concreta amb les ordres exactes, i explica què milloraria cadascuna.
  5. Com comprovaries si s'estan perdent lectures, i què faries si fos així?

Exercici 2: dissenyar la divisió d'un driver

Estàs escrivint el driver d'un dispositiu que recull dades d'un grup d'estacions connectades per un bus propietari. Quan arriba un lot de lectures, el driver ha de:

  • (a) Llegir el registre d'estat del dispositiu. Triga 0,5 µs.
  • (b) Reconèixer la interrupció escrivint en un registre. Triga 0,3 µs.
  • (c) Copiar 64 lectures de 24 bytes des de la memòria intermèdia de DMA. Triga 8 µs.
  • (d) Validar les sumes de comprovació de les 64 lectures. Triga 45 µs.
  • (e) Reservar memòria per emmagatzemar-les. Pot necessitar dormir.
  • (f) Escriure-les en un fitxer de /var/lib/meteora/lectures/. Triga mil·lisegons.
  • (g) Despertar el procés que espera a read(). Triga 0,3 µs.

Respon:

  1. Quines operacions van a la meitat superior i quines a la inferior? Justifica cadascuna.
  2. Quin mecanisme de meitat inferior faries servir per a cada grup: softirq, tasklet o workqueue? Per què?
  3. Calcula el temps amb les interrupcions deshabilitades en el teu disseny i compara'l amb fer-ho tot a la meitat superior.
  4. Si el dispositiu genera 2.000 interrupcions per segon, calcula el percentatge de CPU en tots dos dissenys.
  5. Què passaria si posessis (e) o (f) a la meitat superior?

Exercici 3: diagnosticar pèrdua de lectures

L'equip de Meteora informa que falten lectures: les dades del dia tenen buits. Investigues i trobes:

$ sudo ethtool -S enp3s0 | grep -E 'rx_packets|dropped|missed|no_buffer|fifo'
     rx_packets: 68420112
     rx_dropped: 0
     rx_missed_errors: 284102
     rx_no_buffer_count: 128410
     rx_fifo_errors: 284102

$ sudo ethtool -g enp3s0
Pre-set maximums:
RX:             4096
Current hardware settings:
RX:             256

$ cat /proc/net/softnet_stat | head -2
0102a4c1 00000000 00028f41 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00004a12 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

$ ss -uln
State   Recv-Q  Send-Q  Local Address:Port
UNCONN  212992  0             0.0.0.0:9010

$ cat /proc/sys/net/core/rmem_max
212992

$ ps -eo pid,ni,stat,comm -u meteora
    PID  NI STAT COMMAND
   1842   0 Ssl  ingestor
   1877  -5 Ssl  agregador
  1. Localitza on s'estan perdent els paquets. Hi ha almenys dos punts de pèrdua diferents: identifica'ls i explica en què es diferencien.
  2. Calcula el percentatge de pèrdua i què significa en lectures perdudes al dia.
  3. Proposa una solució completa per a cada punt de pèrdua, amb ordres concretes.
  4. Hi ha un error de configuració evident a la sortida de ps. Identifica'l i corregeix-lo, relacionant-lo amb el que vam veure a 02-02.
  5. Dissenya el monitoratge que hauria detectat això abans que l'equip se n'adonés.

Solucions

Solució 1

1. El problema principal: CPU0 saturada pel processament de xarxa.

Les evidències:

Evidència Dada Què significa
enp3s0-rx-0 8.421.022 interrupcions, totes a CPU0 Tota la recepció en un nucli
%soft a CPU0 56,4 % davant de ~0,1 % als altres Més de mig nucli en softirqs
%idle a CPU0 7,7 % davant de 53-68 % CPU0 saturada, les altres a mig gas
%sys a CPU0 28,2 % davant de ~7 % També la feina de nucli s'hi concentra
RES a CPU0 284.102 davant de ~27.000 Deu vegades més replanificacions

El quadre és inequívoc: CPU0 està al 92 % d'ocupació mentre les altres tres estan a menys de la meitat. El sistema té capacitat de sobres en agregat (49,2 % d'%idle global) però un nucli és el coll d'ampolla.

I la dada que ho fa greu: si CPU0 se satura del tot, la softirq NET_RX no podrà buidar l'anell de descriptors a temps i es començaran a perdre lectures, que a Meteora és irreversible.

2. Per què LOC és 3,6 vegades més gran a CPU0.

LOC són les interrupcions del temporitzador local de l'APIC, que disparen la planificació. En un sistema amb NO_HZ (tickless), el temporitzador s'atura als nuclis ociosos per estalviar energia i només s'activa quan hi ha feina.

CPU0 té un 7,7 % d'%idle: està treballant gairebé tota l'estona, així que el seu temporitzador pràcticament no s'atura mai. Els altres tres, amb un 53-68 % d'inactivitat, passen llargs períodes sense ticks.

LOC alt no és la causa del problema: és un símptoma més que CPU0 no descansa.

3. nvme0q1 repartit davant d'enp3s0-rx-0 concentrat.

La diferència és l'arquitectura de cues, i és exactament el que vam veure a 02-05 i a l'apartat d'MSI-X:

NVMe: l'estàndard defineix una cua d'enviament i recepció per nucli. Cada cua té el seu vector MSI-X dirigit al seu nucli. Quan un procés a CPU2 fa E/S, fa servir la cua de CPU2 i la seva interrupció arriba a CPU2. Sense forrellats compartits i sense concentració.

 132:     284102     271884     269110     265882   nvme0q1

El repartiment gairebé perfecte confirma que funciona com cal.

La targeta I210: només té una cua de recepció (rx-0, i ho confirma el Count=5 d'MSI-X: control, 2 RX, 2 TX en el millor cas, i aquí només se'n fa servir una). Tot el trànsit entrant hi passa, i el seu vector està dirigit a CPU0.

És una limitació combinada de maquinari i configuració, i per això les targetes de servidor modernes tenen múltiples cues RX amb RSS (Receive Side Scaling), que reparteix els fluxos entre cues segons un hash de les capçaleres.

4. Solució concreta.

a) Comprovar quantes cues admet la targeta:

$ sudo ethtool -l enp3s0
Channel parameters for enp3s0:
Pre-set maximums:
RX:             4
Combined:       4
Current hardware settings:
RX:             1
Combined:       1

Si n'admet 4, activar-les:

$ sudo ethtool -L enp3s0 combined 4

Què millora: la targeta passa a tenir 4 cues de recepció, cadascuna amb el seu vector MSI-X. Amb RSS, els paquets de fluxos diferents es reparteixen entre les quatre per hash, i cada cua interromp un nucli diferent. La feina de %soft es divideix per quatre.

b) Repartir l'afinitat de les interrupcions:

# Veure els vectors actuals
$ grep enp3s0 /proc/interrupts

# Assignar cada cua a un nucli
$ echo 1 | sudo tee /proc/irq/129/smp_affinity   # rx-0 → CPU0
$ echo 2 | sudo tee /proc/irq/133/smp_affinity   # rx-1 → CPU1
$ echo 4 | sudo tee /proc/irq/134/smp_affinity   # rx-2 → CPU2
$ echo 8 | sudo tee /proc/irq/135/smp_affinity   # rx-3 → CPU3

Què millora: garanteix el repartiment en lloc de deixar-lo a l'atzar.

c) Si la targeta només admet una cua, activar RPS per programari:

$ echo e | sudo tee /sys/class/net/enp3s0/queues/rx-0/rps_cpus

e és 1110 en binari: CPU1, CPU2 i CPU3. Què millora: RPS (Receive Packet Steering) fa en programari el que RSS fa en maquinari: la meitat superior continua executant-se a CPU0, però el processament de la pila de xarxa es distribueix als altres tres nuclis. S'exclou CPU0 deliberadament per descarregar-la.

d) Ajustar la moderació d'interrupcions:

$ sudo ethtool -C enp3s0 rx-usecs 50

Què millora: la targeta espera fins a 50 µs abans d'interrompre, agrupant diversos paquets per interrupció. Amb 8,4 milions d'interrupcions, agrupar-les de 10 en 10 les redueix a 840.000. El cost és fins a 50 µs més de latència per paquet, perfectament assumible a Meteora, on importa molt més no perdre lectures que lliurar-les 50 µs abans.

e) Coordinar-ho amb l'afinitat de processos de 02-02:

# L'agregador només a CPU2 i CPU3 (ja ho vam fer a 02-02)
$ sudo taskset -cp 2,3 1877

# L'ingestor a CPU0 i CPU1, on arriben les interrupcions de xarxa
$ sudo taskset -cp 0,1 1842

Què millora: que l'ingestor s'executi al mateix nucli on es processen els seus paquets aprofita la localitat de memòria cau: les dades del paquet ja són a la L1/L2 d'aquell nucli. És l'afinitat de processador de 02-02 aplicada a l'E/S.

5. Comprovar la pèrdua de lectures.

$ sudo ethtool -S enp3s0 | grep -E 'missed|no_buffer|dropped|fifo'
$ ss -uln | grep 9010          # cua del socket
$ cat /proc/net/softnet_stat   # 2a columna = paquets descartats

Interpretació de cada punt de pèrdua:

Mètrica On es perd Causa
rx_missed_errors A la targeta No hi ha descriptors lliures
rx_no_buffer_count A la targeta El driver no recicla memòries a temps
Columna 2 de softnet_stat A la cua del backlog El nucli no processa a temps
Recv-Q ple a ss Al socket L'ingestor no llegeix a temps

Si hi hagués pèrdues, l'actuació per ordre:

# 1. Ampliar l'anell de descriptors
$ sudo ethtool -G enp3s0 rx 2048

# 2. Ampliar la memòria intermèdia del socket
$ sudo sysctl -w net.core.rmem_max=16777216
$ sudo sysctl -w net.core.rmem_default=16777216

# 3. Ampliar la cua del backlog
$ sudo sysctl -w net.core.netdev_max_backlog=5000

# 4. Aplicar el repartiment d'interrupcions del punt 4

Solució 2

1. Repartiment entre meitats.

Op Descripció Meitat Justificació
(a) Llegir l'estat, 0,5 µs Superior Cal saber si la interrupció és nostra i què ha passat. Imprescindible abans de res
(b) Reconèixer la interrupció, 0,3 µs Superior Obligatori: sense això el dispositiu continua afirmant la línia → tempesta infinita
(c) Copiar 64 lectures, 8 µs Superior, amb matís Vegeu la discussió a sota
(d) Validar sumes, 45 µs Inferior 45 µs amb interrupcions deshabilitades és inacceptable, i no és urgent
(e) Reservar memòria Inferior (workqueue) Pot dormir: prohibit en context atòmic
(f) Escriure en un fitxer, ms Inferior (workqueue) E/S de disc: bloqueja, i els mil·lisegons són una eternitat
(g) Despertar el procés, 0,3 µs Inferior S'ha de fer després de (d) i (e): abans no hi ha dades vàlides

Discussió sobre (c), que és la decisió interessant: els 8 µs de còpia podrien anar en qualsevol de les dues. Arguments:

  • A favor de la superior: si la memòria intermèdia de DMA és un anell petit, cal buidar-lo aviat perquè el dispositiu pugui continuar escrivint. Endarrerir-ho arrisca pèrdua de dades.
  • A favor de la inferior: 8 µs són 16 vegades més que la resta de la meitat superior, i amb interrupcions deshabilitades és molt.

La resposta professional és no copiar en absolut. El disseny correcte fa servir un anell de descriptors com el de la targeta de xarxa: la meitat superior només anota quins descriptors estan llestos (avançant un índex, ~0,2 µs) i la meitat inferior processa les dades directament a la memòria intermèdia de DMA, sense copiar-les. És exactament el que fa igb_clean_rx_irq.

Disseny final:

MEITAT SUPERIOR (context d'interrupció, ~1 µs):
  (a) llegir estat
  (b) reconèixer la interrupció
  (c') anotar els descriptors llestos i avançar l'índex
       programar la meitat inferior

MEITAT INFERIOR — tasklet o softirq (context atòmic, ~45 µs):
  (d) validar les sumes de comprovació

MEITAT INFERIOR — workqueue (context de procés, ms):
  (e) reservar memòria
  (f) escriure al fitxer
  (g) despertar el procés que espera

2. Mecanisme per a cada grup.

Per a (d), validar sumes: tasklet.

  • No necessita dormir: és càlcul pur sobre dades que ja són a la memòria.
  • S'ha d'executar aviat: les dades ocupen la memòria intermèdia de DMA.
  • No cal una softirq: les softirqs són un recurs escàs, amb un nombre fix compilat al nucli, reservat a subsistemes principals (xarxa, bloc, temporitzadors). Un driver concret fa servir tasklet, que està construïda sobre softirq i és la interfície pensada per a drivers.
  • Avantatge addicional: una tasklet concreta no corre mai en dues CPU alhora, cosa que simplifica enormement la sincronització.

Per a (e), (f) i (g): workqueue, obligatòriament.

  • (e) pot dormir. kmalloc(GFP_KERNEL) es bloqueja si no hi ha memòria lliure immediata, esperant que el nucli recuperi pàgines (tot el mecanisme de 02-04). En context atòmic això penja el sistema. La workqueue és l'única de les tres que pot dormir, perquè s'executa en un fil del nucli, és a dir, en context de procés amb el seu propi task_struct.
  • (f) fa E/S de disc. Mil·lisegons de bloqueig. Impensable fora del context de procés.
  • (g) ha d'anar després, així que acompanya les anteriors.
/* Meitat superior */
static irqreturn_t meteora_irq(int irq, void *dev)
{
    struct meteora_dev *d = dev;
    u32 estat = readl(d->regs + REG_ESTAT);        /* (a) */

    if (!(estat & INT_LOT_LLEST))
        return IRQ_NONE;

    writel(estat, d->regs + REG_ESTAT);            /* (b) */
    d->idx_llest = readl(d->regs + REG_HEAD);      /* (c') */

    tasklet_schedule(&d->tasklet_validar);         /* → (d) */
    return IRQ_HANDLED;
}

/* Meitat inferior atòmica */
static void meteora_validar(unsigned long data)
{
    struct meteora_dev *d = (struct meteora_dev *)data;
    validar_sumes(d);                              /* (d) 45 µs */
    queue_work(d->wq, &d->treball_desar);          /* → (e)(f)(g) */
}

/* Meitat inferior amb context de procés */
static void meteora_desar(struct work_struct *w)
{
    struct meteora_dev *d = container_of(w, struct meteora_dev, treball_desar);
    void *buf = kmalloc(MIDA_LOT, GFP_KERNEL);     /* (e) pot dormir */
    if (!buf) return;
    copiar_i_escriure(d, buf);                     /* (f) E/S: bloqueja */
    kfree(buf);
    wake_up_interruptible(&d->cua_espera);         /* (g) */
}

3. Temps amb interrupcions deshabilitades.

Disseny en dues meitats:

(a) 0,5 µs + (b) 0,3 µs + (c') 0,2 µs + programar 0,1 µs = 1,1 µs

Tot a la meitat superior:

(a) 0,5 + (b) 0,3 + (c) 8 + (d) 45 + (g) 0,3 = 54,1 µs
(i (e) i (f) penjarien el sistema, així que ni tan sols és possible)
Reducció: 54,1 / 1,1 = 49× menys temps amb interrupcions deshabilitades

4. Percentatge de CPU amb 2.000 interrupcions/s.

Disseny en dues meitats:

Fase Temps Amb interrupcions
Meitat superior 1,1 µs Deshabilitades
Tasklet (d) 45 µs Habilitades
Workqueue (e)(f) ~2.000 µs Habilitades, i pot dormir
CPU total:  2.000 × (1,1 + 45) µs = 92,2 ms/s = 9,2 % d'un nucli
CPU amb interrupcions deshabilitades:
            2.000 × 1,1 µs = 2,2 ms/s = 0,22 %

(La workqueue fa E/S: el seu temps és majoritàriament espera de disc, no CPU.)

Tot a la meitat superior:

CPU total: 2.000 × 54,1 µs = 108,2 ms/s = 10,8 %
CPU amb interrupcions deshabilitades: 108,2 ms/s = 10,8 %
Disseny CPU total Amb interrupcions deshabilitades
Dues meitats 9,2 % 0,22 %
Tot a dalt 10,8 % 10,8 %

La mètrica que importa és la segona columna. El consum total gairebé no canvia: la feina s'ha de fer igualment. El que canvia radicalment és quant temps la resta del sistema està cega.

Amb el 10,8 % de les interrupcions deshabilitades, l'impacte a la resta de meteo-01 seria:

Un paquet cada 1,25 ms (800 lectures/s)
Finestres de ceguesa de 54,1 µs, 2.000 vegades per segon
Probabilitat que un paquet arribi durant una finestra: 10,8 %

→ El 10,8 % de les lectures patirien latència addicional
→ Amb ràfegues, pèrdues a rx_missed_errors

5. Si (e) o (f) anessin a la meitat superior.

Amb (e), kmalloc(GFP_KERNEL):

[ 1284.221] BUG: sleeping function called from invalid context at mm/page_alloc.c
[ 1284.221] in_atomic(): 1, irqs_disabled(): 1, pid: 0, name: swapper/2
[ 1284.221] Call Trace: __might_sleep → __alloc_pages → meteora_irq

Si hi ha memòria lliure immediata, kmalloc retorna sense dormir i sembla que funcioni. Quan el sistema estigui sota pressió de memòria —justament quan més importa— intentarà dormir en context atòmic i el sistema es penjarà o entrarà en pànic. És el pitjor tipus de bug: funciona a les proves i falla en producció sota càrrega.

L'alternativa correcta si calgués reservar en context atòmic és GFP_ATOMIC, que no dorm mai però pot fallar més sovint i consumeix d'una reserva d'emergència. Tot i així, aquí la solució de disseny (workqueue) és millor.

Amb (f), escriure en un fitxer:

Encara pitjor. Escriure implica el sistema de fitxers, la memòria cau de pàgines, el planificador d'E/S de 02-05 i esperar el disc: mil·lisegons de bloqueig, amb múltiples punts on el codi dorm.

Conseqüències encadenades:

5 ms d'interrupcions deshabilitades, 2.000 vegades per segon
= 10 segons de ceguesa per segon → impossible, el sistema s'esfondra

I abans d'això:
- El temporitzador perd ticks → el rellotge del sistema s'endarrereix
- La targeta perd paquets → es perden lectures de Meteora
- El watchdog dispara una NMI → pànic del nucli

Conclusió general de l'exercici: la divisió en dues meitats no existeix per repartir feina, sinó per minimitzar la finestra en què el sistema no pot reaccionar al món exterior. Tot allò que no sigui imprescindible fer ara mateix ha de sortir de la meitat superior.

Solució 3

1. Els dos punts de pèrdua.

Punt 1: a la targeta de xarxa (maquinari).

rx_missed_errors: 284102
rx_no_buffer_count: 128410
rx_fifo_errors: 284102

La targeta va rebre els paquets correctament del cable però no tenia on posar-los: l'anell de 256 descriptors era ple perquè la softirq NET_RX no l'havia buidat a temps. Els paquets es van quedar al FIFO intern de la targeta fins a desbordar-lo.

Aquests paquets no van arribar mai a la RAM. El nucli no els va veure mai.

Punt 2: a la cua del backlog del nucli.

$ cat /proc/net/softnet_stat | head -2
0102a4c1 00000000 00028f41 ...
└─ col 1 ─┘└─ col 2 ─┘└─ col 3 ─┘
Columna Significat Valor CPU0
1 Paquets processats 0x0102a4c1 = 16.950.977
2 Paquets descartats per backlog ple 0
3 time_squeeze: vegades que NAPI ha exhaurit el seu pressupost 0x00028f41 = 167.745

La columna 2 és zero, així que no se'n van descartar al backlog. Però la columna 3 és la reveladora: 167.745 vegades la softirq NET_RX va exhaurir el seu pressupost de temps o de paquets i va haver de cedir abans de buidar l'anell.

I aquí hi ha la connexió causal entre tots dos punts: un time_squeeze alt és la causa de rx_missed_errors. La softirq no pot seguir el ritme, l'anell no es buida, i la targeta perd paquets.

A més, la segona línia de softnet_stat (CPU1) mostra 18.962 paquets processats davant dels 16,9 milions de CPU0: tot el processament és en un nucli, el mateix problema de l'exercici 1.

Diferència entre tots dos punts:

Pèrdua a la targeta Pèrdua al backlog
On FIFO del maquinari Cua del nucli a la RAM
Causa Sense descriptors lliures La softirq no processa a temps
Es veu a ethtool -S softnet_stat col. 2
S'arregla amb Anell més gran, més cues Més CPU, RPS, backlog més gran

La cua del socket (ss) mostra Recv-Q: 0, així que l'ingestor sí que llegeix a temps: no hi ha un tercer punt de pèrdua. El problema és anterior a l'aplicació.

2. Percentatge de pèrdua.

Paquets rebuts:      68.420.112
Paquets perduts:        284.102 (rx_missed_errors)
Total ofert:         68.704.214

Pèrdua = 284.102 / 68.704.214 = 0,4135 %

En lectures diàries:

A 800 lectures/s: 800 × 86.400 = 69.120.000 lectures/dia
Perdudes: 69.120.000 × 0,004135 = 285.811 lectures/dia

285.811 lectures perdudes al dia, irrecuperables. Posat en context:

Dades completes del dia:  69.120.000 × 24 B = 1,66 GB
                          (nota: molt per sobre dels 17 MB de referència,
                           cosa que suggereix que el pic de les 8:00 no és
                           representatiu de la mitjana diària)
Buits: 1 de cada 242 lectures

Un 0,41 % sembla poc, però per a mitjanes horàries per estació significa buits sistemàtics a les sèries, i en el pitjor cas una estació concreta podria perdre ratxes senceres si el seu trànsit coincideix amb els moments de desbordament.

3. Solució per a cada punt de pèrdua.

Per a la pèrdua a la targeta:

# a) Ampliar l'anell de descriptors de 256 a 2048
$ sudo ethtool -G enp3s0 rx 2048

# b) Activar múltiples cues si el maquinari ho permet
$ sudo ethtool -l enp3s0
$ sudo ethtool -L enp3s0 combined 4

# c) Moderació d'interrupcions: agrupar en lots
$ sudo ethtool -C enp3s0 rx-usecs 50 rx-frames 32

Per què funciona cadascuna:

  • (a) Dona vuit vegades més marge per absorbir ràfegues. Amb 2.048 descriptors a 800 paquets/s, el sistema té 2,5 segons de coixí en lloc de 0,3. Costa 4 MB de RAM.
  • (b) Reparteix la feina entre quatre nuclis, atacant la causa arrel.
  • (c) Redueix les interrupcions agrupant paquets, i deixa més CPU per al processament real.

Per al time_squeeze:

# d) Augmentar el pressupost de NAPI per cicle
$ sudo sysctl -w net.core.netdev_budget=600          # per defecte 300
$ sudo sysctl -w net.core.netdev_budget_usecs=8000   # per defecte 2000

# e) Ampliar la cua del backlog
$ sudo sysctl -w net.core.netdev_max_backlog=5000    # per defecte 1000

# f) Repartir el processament amb RPS si només hi ha una cua
$ echo e | sudo tee /sys/class/net/enp3s0/queues/rx-0/rps_cpus
  • (d) netdev_budget és quants paquets processa la softirq abans de cedir. Pujar-lo de 300 a 600 redueix els time_squeeze a la meitat. El risc és que la softirq acapari més la CPU, així que convé pujar-lo amb moderació.
  • (f) Reparteix el processament de la pila de xarxa a CPU1-3 encara que la interrupció continuï arribant a CPU0.

Fer-ho persistent:

# /etc/sysctl.d/99-meteora-xarxa.conf
net.core.netdev_budget = 600
net.core.netdev_budget_usecs = 8000
net.core.netdev_max_backlog = 5000
net.core.rmem_max = 16777216

# /etc/udev/rules.d/70-meteora-nic.rules
ACTION=="add", SUBSYSTEM=="net", NAME=="enp3s0", \
  RUN+="/sbin/ethtool -G enp3s0 rx 2048", \
  RUN+="/sbin/ethtool -C enp3s0 rx-usecs 50"

4. L'error de configuració a ps.

    PID  NI STAT COMMAND
   1842   0 Ssl  ingestor      ← nice 0
   1877  -5 Ssl  agregador     ← nice -5

Les prioritats estan invertides. L'agregador té prioritat elevada (−5) i l'ingestor prioritat normal (0). És exactament el contrari del que correspon, i contradiu el que vam establir a 02-02:

Procés Tipus Cost d'endarrerir-lo nice correcte nice actual
ingestor E/S, ràfegues mínimes Irreversible: dades perdudes −10 0
agregador CPU, ràfegues llargues Recuperable: només triga més +10 −5

I això agreuja directament el problema de pèrdua. El raonament causal complet:

  1. L'agregador amb nice −5 rep ~75 % de la CPU en conflicte (taula de pesos de 02-02).
  2. És un procés limitat per CPU: quan s'executa, reté la CPU mil·lisegons sencers.
  3. Les softirqs NET_RX competeixen amb ell per CPU0.
  4. Endarrerir la softirq significa no buidar l'anell → time_squeezerx_missed_errors.
  5. L'agregador mal prioritzat està causant pèrdua de lectures.

La correcció:

$ sudo renice -n -10 -p 1842      # ingestor: prioritat alta
$ sudo renice -n  10 -p 1877      # agregador: prioritat baixa
$ sudo taskset -cp 2,3 1877       # i confinar-lo lluny de CPU0

Persistent a systemd:

# /etc/systemd/system/meteora-ingestor.service.d/prioritat.conf
[Service]
Nice=-10
CPUAffinity=0 1

# /etc/systemd/system/meteora-agregador.service.d/prioritat.conf
[Service]
Nice=10
CPUAffinity=2 3

Això uneix les tres peces del mòdul: prioritat de CPU (02-02), afinitat de processador (02-02) i afinitat d'interrupcions (02-07), coordinades perquè el camí de les lectures —targeta → softirq → socket → ingestor— no competeixi amb el càlcul de mitjanes en cap punt.

5. Monitoratge que ho hauria detectat abans.

La fallada de procés aquí és tan greu com la tècnica: el problema el va detectar l'equip de dades en veure buits, no el sistema. Quan algú mira les sèries, ja s'han perdut lectures irrecuperables durant dies.

Mètriques i llindars:

Mètrica Font Llindar d'avís Llindar crític
rx_missed_errors ethtool -S > 0 (delta) > 100/min
rx_no_buffer_count ethtool -S > 0 (delta) > 100/min
time_squeeze softnet_stat col. 3 > 100/min > 1.000/min
Descarts al backlog softnet_stat col. 2 > 0 > 10/min
Recv-Q del socket 9010 ss -uln > 50 % de rmem_max > 90 %
%soft per CPU mpstat -P ALL > 30 % > 50 %
Lectures escrites/min Fitxer del dia Desviació > 2 % del que s'espera > 5 %

El criteri clau: per a rx_missed_errors el llindar d'avís és qualsevol increment diferent de zero, perquè cada unitat és una lectura perduda per sempre. No és una mètrica de rendiment, és una mètrica de pèrdua de dades.

Script de recollida:

#!/bin/bash
# /usr/local/bin/meteora-metriques-xarxa.sh — executar cada minut
IFACE=enp3s0
EST=/var/lib/meteora/.metriques-xarxa

llegir() { ethtool -S $IFACE | awk -v k="$1:" '$1==k {print $2}'; }

MISSED=$(llegir rx_missed_errors)
NOBUF=$(llegir rx_no_buffer_count)
SQUEEZE=$(awk 'NR==1 {print strtonum("0x" $3)}' /proc/net/softnet_stat)

if [ -f "$EST" ]; then
    read -r PM PN PS < "$EST"
    D_MISSED=$((MISSED - PM))
    D_NOBUF=$((NOBUF - PN))
    D_SQUEEZE=$((SQUEEZE - PS))

    if [ "$D_MISSED" -gt 0 ] || [ "$D_NOBUF" -gt 0 ]; then
        logger -p daemon.crit \
          "METEORA: PERDUA DE LECTURES missed=$D_MISSED nobuf=$D_NOBUF"
    fi
    if [ "$D_SQUEEZE" -gt 100 ]; then
        logger -p daemon.warning \
          "METEORA: softirq saturada, time_squeeze=$D_SQUEEZE/min"
    fi
fi

echo "$MISSED $NOBUF $SQUEEZE" > "$EST"

Comprovació d'extrem a extrem, la més valuosa:

# Comptar lectures realment emmagatzemades i comparar-les amb el que s'espera
ESPERADES=$((800 * 60))
REALS=$(( ($(stat -c %s /var/lib/meteora/lectures/$(date +%F).dat) - MIDA_ANTERIOR) / 24 ))
PERDUA=$(echo "scale=3; (1 - $REALS/$ESPERADES) * 100" | bc)

Aquesta última és la que de debò importa: mesura el resultat, no els indicadors intermedis. Hi pot haver pèrdues en un punt que no estiguis vigilant —el commutador, el cable, la mateixa estació—, i només la comprovació d'extrem a extrem les detecta totes.

Es comparen bé així:

Tipus de mètrica Exemple Detecta
De causa time_squeeze, %soft El problema abans que causi pèrdua
De símptoma rx_missed_errors La pèrdua en el moment en què es produeix
De resultat Lectures escrites/min Qualsevol pèrdua, vingui d'on vingui

Un sistema de monitoratge decent té les tres: les de causa donen marge per actuar, les de símptoma localitzen el punt exacte, i la de resultat garanteix que no s'escapi res. Aquest plantejament el reprendrem a Monitoratge i Diagnòstic de Rendiment.

Conclusió

Un controlador de dispositiu és el programari que tradueix les operacions genèriques del nucli a les operacions concretes d'un model de maquinari, i són el 63 % del codi del nucli de Linux. Compleixen un contracte —una estructura de punters a funció com file_operations— que implementa polimorfisme en C: la mateixa read() acaba en codi diferent segons el major del dispositiu. Es carreguen com a mòduls l'alias dels quals connecta l'identificador PCI del maquinari amb el nom del mòdul, cosa que fa possible la càrrega automàtica. I viuen a l'anell 0, sense xarxa de seguretat: la traça Fatal exception in interrupt que hem llegit no acaba en un SIGSEGV, acaba en un kernel panic.

Una interrupció arriba per una línia IRQ, es tradueix en un vector i la CPU salta a l'entrada corresponent de la IDT, desant estat i canviant a la pila de nucli igual que en una crida al sistema. La diferència decisiva és que una interrupció no pertany a cap procés: s'executa sobre qui toqui, cosa que prohibeix dormir, bloquejar-se, reservar memòria amb GFP_KERNEL o tocar l'espai d'usuari. D'aquesta prohibició neix la divisió en meitat superior —microsegons, reconèixer el maquinari i programar feina— i meitat inferior —softirq, tasklet o workqueue, amb interrupcions habilitades—. A l'exercici n'hem mesurat la diferència: 1,1 µs davant de 54,1 µs de ceguesa del sistema, un factor de 49.

NAPI resol el problema que vam deixar plantejat a la lliçó anterior: alterna entre interrupcions amb càrrega baixa i sondeig amb càrrega alta, i redueix 65 vegades el cost a 500.000 paquets/s. MSI-X elimina les línies físiques, les curses amb el DMA i el límit d'un vector per dispositiu, i permet una cua d'interrupció per nucli —que és el que fa que nvme0q1 estigui perfectament repartit i enp3s0-rx-0 no—. El DMA treballa amb anells de descriptors que deixen que la targeta escrigui diversos paquets sense CPU, exigeix coordinar la coherència de memòria cau, i necessita la IOMMU perquè un dispositiu compromès no pugui escriure a qualsevol adreça física. I /proc/interrupts explica tota aquesta història línia a línia: qui interromp, quant i en quin nucli.

Amb això tanques el mòdul 2, i val la pena veure'n el recorregut complet. Vas començar obrint l'abstracció de procés (02-01): la seva imatge de memòria, el task_struct, els estats amb els seus codis reals, fork amb copy-on-write, execve, els zombis i el cost del canvi de context. Després vas veure com es reparteix la CPU entre ells (02-02): ràfegues, criteris en conflicte, els algorismes clàssics calculats a mà i el temps virtual just de CFS amb nice. Després el segon gran recurs, la memòria (02-03): adreces lògiques i físiques, la MMU, la fragmentació amb números i per què la paginació guanya; i el seu desenvolupament complet (02-04): traducció, taules multinivell, TLB, fallades de pàgina, reemplaçament, hiperpaginació, mmap i l'OOM killer. Després l'emmagatzematge (02-05): la física del disc i de l'SSD, la planificació de peticions i RAID. I per últim els dispositius (02-06): la uniformització, /dev, udev i les tres maneres de parlar amb el maquinari, que aquesta lliçó ha desenvolupat fins al final.

Les set lliçons responen a la mateixa pregunta des d'angles diferents: com es reparteix un recurs escàs entre molts que el volen. CPU, memòria, disc, dispositius. I en totes hi ha aparegut el mateix patró: una abstracció que amaga la complexitat, un mecanisme de maquinari que fa complir les regles, i una política del sistema operatiu que decideix.

Però hem donat per fet una cosa gran. Cada vegada que dos processos compartien una pàgina amb MAP_SHARED, cada vegada que una softirq i un procés tocaven la mateixa cua, cada vegada que dos treballadors de meteo-api escrivien a /dev/shm/meteora-cache, hem dit «això requereix sincronització» i hem tirat endavant. Ha arribat el moment de pagar aquest deute. Què passa exactament quan dos fluxos d'execució toquen la mateixa dada alhora? Per què comptador++ pot perdre increments? I com es coordinen sense destrossar el rendiment que tant ha costat aconseguir?

És el Mòdul 3: Concurrència, i comença a Conceptes de Concurrència.

Fonaments de Sistemes Operatius

Mòdul 1: Introducció als Sistemes Operatius

Mòdul 2: Gestió de Recursos

Mòdul 3: Concurrència

Mòdul 4: Estructures de Fitxers

Mòdul 5: Protecció i Seguretat del Sistema

Mòdul 6: Virtualització i Contenidors

Mòdul 7: Administració i Diagnòstic a la Pràctica

© Copyright 2026. Tots els drets reservats