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.
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.
- 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_ATOMICy 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:
- Corrección rota — el planificador detecta que intentas cambiar de tarea con
preempt_count != 0y gritaBUG: scheduling while atomic. Ceder la CPU con preempción deshabilitada rompe su invariante central. - 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.
- 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) sí 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.
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.
- Escribe a propósito el antipatrón: un
kmalloc(GFP_KERNEL)dentro de unspin_lock. - Compila con
CONFIG_DEBUG_ATOMIC_SLEEPy provoca elBUG: sleeping function...en QEMU. - Reescríbelo sacando la asignación fuera del lock; deja dentro solo el
list_add. - Repite el ejercicio con
copy_to_user: cópialo a un buffer del kernel fuera del lock. - Mide (o razona) cuántas instrucciones quedan dentro de la sección crítica final.