wandres.dev
SPINLOCKS · spin_lock, irqsave, reglas

La sección crítica: nunca duermas con el lock cogido

La regla de oro del spinlock: mientras lo tienes, no puedes dormir. Nada de kmalloc GFP_KERNEL, copy_to_user ni mutex dentro. Por qué es fatal, qué lo detecta, y el arte de las secciones cortísimas.

⏱ 12 min

Un spinlock deshabilita la preempción en tu núcleo: mientras lo tienes cogido, el planificador no puede sacarte. De ahí sale la regla más importante y más violada de toda la concurrencia del kernel: dentro de una sección crítica de spinlock no puedes dormir. Ni un kmalloc que reclame memoria, ni un copy_to_user que falle de página, ni un mutex que se bloquee. Aquí verás por qué eso cuelga la máquina y cómo mantener la sección diminuta.

🎯 Al terminar esta lección sabrás
  • Entender qué significa “dormir” en el kernel y por qué está prohibido con un spinlock.
  • Reconocer las llamadas que duermen: GFP_KERNEL, copy_to_user, mutex_lock.
  • Usar GFP_ATOMIC y el patrón de preparar-fuera, entrar-corto.
  • Saber qué mecanismos del kernel cazan un “sleeping while atomic”.

Qué significa “dormir” y por qué es fatal

“Dormir” es cualquier operación que pueda llamar a schedule() para ceder la CPU: esperar memoria, una página, un mutex, un temporizador, datos de disco. Con un spinlock cogido, la preempción está deshabilitada (nivel 15.1), y dormir en ese estado provoca tres desastres a la vez:

  1. Corrección rota — el planificador detecta que intentas cambiar de tarea con preempt_count != 0 y grita BUG: scheduling while atomic. Ceder la CPU con preempción deshabilitada rompe su invariante central.
  2. Liveness rota — si logras dormir, otro núcleo que gira esperando tu lock quema el 100% de su CPU sin hacer nada, quizá durante milisegundos. Una espera activa pensada para durar nanosegundos se convierte en una catástrofe.
  3. Deadlock — la tarea que liberaría el recurso que esperas podría necesitar el lock que tú retienes. Nadie avanza.

El catálogo de lo prohibido

Este código parece inocente y es una bomba: cada línea marcada puede dormir.

spin_lock(&dev->lock);

buf = kmalloc(4096, GFP_KERNEL);   /* MAL: puede bloquear reclamando memoria */
if (copy_to_user(dst, buf, n))     /* MAL: un fallo de página duerme */
	...;
mutex_lock(&dev->cfg_mutex);       /* MAL: un mutex duerme si está cogido */
msleep(10);                        /* MAL: dormir explícito, obvio */
p = vmalloc(size);                 /* MAL: vmalloc puede dormir */

spin_unlock(&dev->lock);
🚫

Asignaciones que duermen

kmalloc(GFP_KERNEL), kzalloc(GFP_KERNEL), vmalloc, kvmalloc. El flag GFP_KERNEL autoriza al asignador a reclamar memoria, y reclamar duerme.

🚫

La frontera con el usuario

copy_to_user, copy_from_user, get_user, put_user (nivel 12). La dirección de usuario puede no estar residente: el fallo de página duerme para traer la página.

🚫

Locks que duermen

mutex_lock, down (semáforo), down_read/down_write. Por definición ceden la CPU si el recurso está ocupado.

🚫

Esperas explícitas

msleep, schedule, schedule_timeout, wait_event, wait_for_completion. Todas llaman al planificador de forma directa.

GFP_ATOMIC y preparar-fuera

Si de verdad necesitas asignar dentro del lock, existe GFP_ATOMIC: le prohíbe al asignador dormir y le deja tirar de las reservas de emergencia.

spin_lock(&dev->lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);   /* no duerme; puede fallar más fácil */
if (!p) {
	spin_unlock(&dev->lock);       /* libera antes de salir por error */
	return -ENOMEM;
}
p->id = id;
list_add(&p->nodo, &dev->cola);
spin_unlock(&dev->lock);

Pero GFP_ATOMIC tira de un pozo pequeño y falla con más facilidad bajo presión de memoria. La solución idiomática casi siempre es hacer el trabajo pesado fuera y entrar al lock solo para tocar memoria acotada:

/* BIEN: duerme lo que haga falta SIN el lock */
nodo = kmalloc(sizeof(*nodo), GFP_KERNEL);
if (!nodo)
	return -ENOMEM;
nodo->id = id;

spin_lock(&dev->lock);
list_add(&nodo->cola, &dev->cola);   /* solo punteros: acotado y rapidísimo */
spin_unlock(&dev->lock);

Qué lo caza: might_sleep y DEBUG_ATOMIC_SLEEP

No dependes solo de tu disciplina. Las funciones que duermen empiezan con la anotación might_sleep(), que con CONFIG_DEBUG_ATOMIC_SLEEP comprueba en runtime si estás en contexto atómico y, si lo estás, vuelca un diagnóstico:

BUG: sleeping function called from invalid context at mm/slub.c:1421
in_atomic(): 1, irqs_disabled(): 0, non_block: 0, pid: 142, name: insmod
CPU: 3 PID: 142 Comm: insmod Not tainted 7.1.0 #1
Call Trace:
 __might_sleep+0x9a/0xa0
 __kmalloc+0x5e/0x340
 mi_driver_write+0x71/0x120 [mi_driver]

El in_atomic(): 1 es la pista: preempt_count no era cero porque tenías un spinlock. Activa siempre CONFIG_DEBUG_ATOMIC_SLEEP en tus kernels de desarrollo; convierte un cuelgue misterioso en producción en un mensaje claro en tu banco de pruebas.

Los contextos atómicos por dentro

“No puedes dormir” es en realidad “estás en contexto atómico”, y el kernel lo sabe por un contador: preempt_count. Cada cosa que prohíbe dormir suma en un campo distinto de ese mismo contador:

  • Tener un spinlock cogido → suma en el campo de preempción.
  • Estar en un manejador de IRQ dura → suma en el campo de hardirq.
  • Estar en un softirq → suma en el campo de softirq.

in_atomic() es cierto si cualquiera de esos campos no es cero. Por eso might_sleep() detecta el problema venga de donde venga: mira el contador, no cómo llegaste hasta ahí.

if (in_atomic() || irqs_disabled())
	/* aquí dormir es ilegal, sin importar quién te llamó */

Una excepción que rompe la intuición: printk (y pr_info, nivel 7) es seguro en contexto atómico. Está diseñado para no dormir —encola en un búfer y difiere el trabajo caro— precisamente para que puedas depurar desde dentro de una sección crítica o un manejador de interrupción sin volar la máquina.

La sección crítica es una apnea: entra, haz lo mínimo, sal

Trata cada spin_lock como una inmersión en apnea. Desde que coges el lock hasta que lo sueltas, estás sin respirar: no puedes dormir, no puedes reclamar memoria, no puedes tocar userspace, no puedes esperar a nadie. Y mientras aguantas, puede haber otros núcleos girando en superficie, quemando CPU pura, esperando a que salgas. Por eso el ideal del kernel no es solo “no dormir”: es que la sección sea tan corta que ni te enteres de que estuviste dentro —un puñado de instrucciones sobre memoria: incrementa, enlaza, copia cuatro campos, sal—. Todo lo caro (asignar, validar, hablar con el usuario) se hace fuera, con la preempción viva. Interioriza esta asimetría —trabajo lento fuera, trabajo mínimo dentro— y habrás entendido no solo los spinlocks, sino la forma en que el kernel piensa la concurrencia: proteger poco, protegerlo brevísimo, y no bloquear jamás a los que esperan.

⚔️ Diseca y acorta una sección crítica
  1. Escribe a propósito el antipatrón: un kmalloc(GFP_KERNEL) dentro de un spin_lock.
  2. Compila con CONFIG_DEBUG_ATOMIC_SLEEP y provoca el BUG: sleeping function... en QEMU.
  3. Reescríbelo sacando la asignación fuera del lock; deja dentro solo el list_add.
  4. Repite el ejercicio con copy_to_user: cópialo a un buffer del kernel fuera del lock.
  5. Mide (o razona) cuántas instrucciones quedan dentro de la sección crítica final.