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
- Què és un controlador de dispositiu
- El contracte amb el nucli: la interfície d'operacions de fitxer
- Registre del driver i mòduls carregables
- Espai de nucli davant d'espai d'usuari per als drivers
- Interrupcions: línia IRQ, vector i taula de vectors
- La controladora d'interrupcions: del PIC a l'APIC
- Què fa la CPU en rebre una interrupció
- El context d'interrupció i les seves regles
- Emmascarament i interrupcions compartides
- Meitat superior i meitat inferior
/proc/interruptsinterpretat línia a línia- MSI i MSI-X
- DMA en detall: descriptors, coherència i IOMMU
- El camí complet d'un paquet amb lectures
- Latència d'interrupció i el seu impacte
- 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
.readdel seufile_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 dememcpy(). És obligatori i no és una formalitat. El punterbufve 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_uservalida 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. Unmemcpydirecte seria una vulnerabilitat d'escalada de privilegis de manual. La marca__useral prototip permet, a més, que eines d'anàlisi estàtica detectin l'error.- Retornar
-EIOi-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-1méserrno. register_chrdevamb 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_initimodule_exitdefineixen 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 simpleSIGSEGV.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 | Sí | Fallada de pàgina, divisió per zero |
| Crida al sistema | Instrucció syscall deliberada |
Sí | 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 | Sí: dirigeix la interrupció a una CPU concreta |
| Prioritats | Fixes per número | Programables |
| Repartiment de càrrega | No | Sí, entre CPU |
| MSI | No | Sí |
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:
- 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.
- 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.
- Es canvia a la pila de nucli, exactament com en un
syscall. El gestor no fa servir mai la pila del procés interromput. - 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.
- 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:
- Comprovar si la interrupció és seva abans de res, perquè la línia pot estar compartida. Retornar
IRQ_NONEpermet que el nucli provi amb el driver registrat següent. - 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.
spin_lock_irqsaveen lloc demutex_lock. Un mutex dorm si està ocupat; un spinlock gira esperant, que és l'únic que es pot fer en context atòmic. La variantirqsavedesa 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.- 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 | 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 89102Lectura 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.TIMERestà repartit uniformement, com ha de ser: cada nucli té el seu temporitzador local de l'APIC.BLOCKsó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 shootdownsEstructura 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.
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:
É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:
- Escassetat: hi ha poques línies, d'aquí les compartides.
- 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.
- 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 | Sí | 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=00000000Enable+ 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 TAILAquest 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: 0rx_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
- Identifica el problema principal i justifica'l amb almenys tres dades de les dues sortides.
- Per què
LOCés 3,6 vegades més gran a CPU0 que als altres? - Què indica que
nvme0q1estigui repartit ienp3s0-rx-0no? - Proposa una solució concreta amb les ordres exactes, i explica què milloraria cadascuna.
- 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:
- Quines operacions van a la meitat superior i quines a la inferior? Justifica cadascuna.
- Quin mecanisme de meitat inferior faries servir per a cada grup: softirq, tasklet o workqueue? Per què?
- Calcula el temps amb les interrupcions deshabilitades en el teu disseny i compara'l amb fer-ho tot a la meitat superior.
- Si el dispositiu genera 2.000 interrupcions per segon, calcula el percentatge de CPU en tots dos dissenys.
- 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- Localitza on s'estan perdent els paquets. Hi ha almenys dos punts de pèrdua diferents: identifica'ls i explica en què es diferencien.
- Calcula el percentatge de pèrdua i què significa en lectures perdudes al dia.
- Proposa una solució completa per a cada punt de pèrdua, amb ordres concretes.
- 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. - 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ó.
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:
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:
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:
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 espera2. 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 propitask_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:
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)
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).
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 lecturesUn 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 elstime_squeezea 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.
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:
- L'
agregadorambnice −5rep ~75 % de la CPU en conflicte (taula de pesos de 02-02). - És un procés limitat per CPU: quan s'executa, reté la CPU mil·lisegons sencers.
- Les softirqs NET_RX competeixen amb ell per CPU0.
- Endarrerir la softirq significa no buidar l'anell →
time_squeeze→rx_missed_errors. - L'
agregadormal 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
- Conceptes Bàsics de Sistemes Operatius
- Història i Evolució dels Sistemes Operatius
- Tipus de Sistemes Operatius
- Funcions Principals d'un Sistema Operatiu
- Arquitectura del Nucli: Monolític, Microkernel i Híbrid
- Mode Usuari, Mode Nucli i Crides al Sistema
Mòdul 2: Gestió de Recursos
- Gestió de Processos
- Planificació de la CPU
- Gestió de Memòria
- Memòria Virtual i Paginació
- Gestió d'Emmagatzematge
- Gestió de Dispositius
- Controladors, Interrupcions i Operacions d'E/S
Mòdul 3: Concurrència
- Conceptes de Concurrència
- Fils i Processos
- Comunicació entre Processos (IPC)
- Sincronització i Exclusió Mútua
- Problemes Clàssics de Concurrència
- Interbloquejos: Prevenció, Detecció i Recuperació
Mòdul 4: Estructures de Fitxers
- Sistemes de Fitxers
- Estructures de Directoris
- Particions, Muntatge i Sistema de Fitxers Virtual
- Gestió de Fitxers
- Assignació d'Espai, Journaling i Integritat
- Seguretat i Permisos de Fitxers
Mòdul 5: Protecció i Seguretat del Sistema
- Principis de Protecció i Control d'Accés
- Usuaris, Autenticació i Escalada de Privilegis
- Amenaces Habituals i Enfortiment del Sistema
- Auditoria, Registres i Resposta a Incidents
Mòdul 6: Virtualització i Contenidors
- Virtualització: Hipervisors i Màquines Virtuals
- Contenidors: Namespaces i cgroups
- El Sistema Operatiu al Núvol
- Sistemes Operatius Mòbils i de Temps Real
