wandres.dev
LOCKS QUE DUERMEN · mutex, rwsem, completion

Contexto que puede dormir vs atómico

La distinción que gobierna qué lock puedes usar: en contexto de proceso puedes dormir; en contexto atómico (IRQ, spinlock cogido) no. might_sleep y cómo el kernel vigila la regla.

⏱ 13 min

Antes de elegir un lock, una pregunta manda sobre todas: ¿puede dormir este código? No es una cuestión de estilo, es física del planificador. La respuesta parte en dos el universo de la sincronización del kernel, y equivocarte no da un warning amable: cuelga la máquina.

🎯 Al terminar esta lección sabrás
  • Distinguir contexto de proceso de contexto atómico.
  • Entender por qué dormir en contexto atómico corrompe el sistema.
  • Leer preempt_count, el contador que rastrea el contexto.
  • Usar might_sleep y CONFIG_DEBUG_ATOMIC_SLEEP como red de seguridad.

Dos mundos: dormir o no dormir

“Dormir” en el kernel tiene un significado técnico exacto: llamar a schedule(), poner la tarea en TASK_INTERRUPTIBLE o TASK_UNINTERRUPTIBLE y ceder la CPU a otra tarea hasta que algo la despierte. Una función “puede dormir” si en algún punto podría bloquearse así.

En contexto de proceso —el código que corre en nombre de una tarea, como el que atiende una syscall— dormir es legal: hay una task_struct que guardar y a la que volver. Aquí puedes coger un mutex, un semáforo, un rwsem, o esperar en una cola con wait_event.

En contexto atómico dormir está prohibido, porque no hay a quién ceder o hacerlo provocaría un interbloqueo. Estás en contexto atómico cuando:

Manejador de interrupción

Un hardirq no tiene una task_struct propia detrás. Si duermes, el planificador no tiene proceso que guardar ni al que regresar: pánico.

🌊

Softirq o tasklet

La mitad inferior de la interrupción. Corre con la preempción restringida y sin contexto de proceso: tampoco puede bloquearse.

🔒

Con un spinlock cogido

El spinlock gira en otra CPU esperándote. Si duermes sin soltarlo, esa CPU gira para siempre. Interbloqueo clásico.

🚫

Preempción deshabilitada

Tras preempt_disable() o dentro de rcu_read_lock(), el planificador tiene prohibido cambiar de tarea: dormir es imposible por definición.

flowchart TD
A[Voy a coger un lock o llamar algo que bloquea] --> B{En que contexto estoy}
B -->|Contexto de proceso| C[Puedo dormir]
B -->|IRQ softirq o NMI| D[Contexto atomico: NO puedo dormir]
B -->|Spinlock cogido o preempcion off| D
C --> E[mutex, rwsem, semaphore, wait_event, kmalloc GFP_KERNEL]
D --> F[Solo spinlock, atomic_t, RCU lectura, kmalloc GFP_ATOMIC]
style C fill:#a6e3a1,color:#11111b
style D fill:#f38ba8,color:#11111b

preempt_count: el contador que lo sabe todo

El kernel no adivina el contexto: lo cuenta. Cada CPU lleva un preempt_count, una palabra dividida en campos de bits. Cada campo cuenta anidamientos de un tipo de contexto atómico:

/* include/linux/preempt.h (esquema del preempt_count por CPU)
 *
 *   NMI     HARDIRQ    SOFTIRQ    PREEMPT
 *   1 bit   4 bits     8 bits     8 bits
 *
 * Cualquier campo distinto de cero => estamos en contexto atómico.
 */
static inline bool in_task(void)
{
	return !(preempt_count() & (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET));
}

static inline bool in_interrupt(void)
{
	return preempt_count() & (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_MASK);
}

Cuando coges un spinlock, spin_lock() hace preempt_disable(), que incrementa el campo PREEMPT. Al entrar un hardirq, el kernel suma HARDIRQ_OFFSET. Así, en cualquier instante, un preempt_count() distinto de cero significa “no me puedes quitar la CPU” y, por transitividad, “no puedo dormir”. Esa es toda la magia: dormir mientras preempt_count no es cero es el pecado capital de la concurrencia del kernel.

might_sleep: la alarma temprana

¿Cómo sabes que una función puede dormir? El propio kernel lo declara. Las funciones que pueden bloquearse empiezan con might_sleep(), una anotación que, con CONFIG_DEBUG_ATOMIC_SLEEP, comprueba preempt_count y grita si te has colado en atómico:

void *kmalloc(size_t size, gfp_t flags)
{
	/* con GFP_KERNEL el asignador puede entrar en reclamo y dormir;
	 * con GFP_ATOMIC no. La anotación lo refleja: */
	might_sleep_if(gfpflags_allow_blocking(flags));
	...
}

Si llamas a algo que duerme desde un manejador de IRQ, el kernel te lo dice sin ambigüedad en dmesg:

BUG: sleeping function called from invalid context at mm/slub.c:4372
in_atomic(): 1, irqs_disabled(): 1, non_block: 0, pid: 0, name: swapper/2
Call Trace:
 __might_resched+0x...
 kmalloc_noprof+0x...
 mi_isr+0x2a/0x80 [mi_modulo]
 __handle_irq_event_percpu+0x...

Esa traza es oro: la línea in_atomic(): 1 confirma el contexto, y la pila señala la función culpable. might_sleep() convierte un cuelgue intermitente e indepurable en un mensaje reproducible en el primer arranque de pruebas.

💡
No confíes en in_atomic para decidir en caliente

Existe in_atomic(), pero no lo uses para ramificar tu lógica: en kernels sin preempción no siempre detecta que tienes un spinlock cogido, porque preempt_disable puede no dejar rastro contable. La verdad es que el contexto lo debe conocer quien llama a tu función, no un chequeo en tiempo de ejecución. Por eso la red de seguridad correcta es might_sleep() con CONFIG_DEBUG_ATOMIC_SLEEP en pruebas, y no un if (in_atomic()) en producción.

⚠️
La elección de GFP no es cosmética

kmalloc(n, GFP_KERNEL) puede dormir esperando a que se libere memoria; en un manejador de IRQ eso es ilegal. Ahí necesitas GFP_ATOMIC, que nunca duerme (a cambio de poder fallar antes y de tirar de una reserva de emergencia). El flag de asignación es, en el fondo, otra forma de la misma pregunta: ¿estoy en un contexto donde puedo dormir?

La regla de anidamiento: el contexto se hereda hacia dentro

El contexto atómico es contagioso hacia dentro. En cuanto coges un spinlock, todo lo que llames desde ahí hereda la prohibición de dormir, hasta que lo sueltes. De ahí una regla de oro sobre el orden en que se anidan los locks:

/* CORRECTO: primero el que duerme, dentro el que gira */
mutex_lock(&m);
	spin_lock(&s);
		/* aquí NO puedo dormir: manda el spinlock */
	spin_unlock(&s);
	/* aquí puedo volver a dormir */
mutex_unlock(&m);

/* PROHIBIDO: coger un mutex teniendo un spinlock cogido */
spin_lock(&s);
	mutex_lock(&m);		/* BUG: mutex_lock puede dormir en atómico */
	mutex_unlock(&m);
spin_unlock(&s);

Puedes coger un spinlock teniendo un mutex, pero nunca un mutex teniendo un spinlock: la primitiva que duerme siempre va por fuera, la que gira por dentro. Esta jerarquía no es estética; es la consecuencia directa de que un preempt_count distinto de cero prohíbe dormir en todo lo que quede dentro.

La pregunta que organiza toda la sincronización

Casi todo lo que aprenderás sobre locks del kernel cuelga de este único eje. ¿Por qué existen dos familias enteras de locks —los que giran, como los spinlocks, y los que duermen, como el mutex, el semáforo y el rwsem? Por esta distinción. ¿Por qué un mismo dato a veces se protege con spinlock y a veces con mutex? Depende de si el código que lo toca corre en contexto atómico. ¿Por qué hay dos flags de kmalloc? Lo mismo. Cuando dudes qué primitiva usar, no empieces por la primitiva: empieza por el contexto. Traza mentalmente quién llama a tu función —¿una syscall?, ¿un temporizador?, ¿una IRQ?— y la respuesta a “¿puedo dormir aquí?” elige la primitiva casi sola. Los ingenieros del kernel llevan esta pregunta grabada; es lo primero que se preguntan al leer código ajeno. Interiorízala y la mitad de las decisiones de concurrencia dejan de ser dudas: se vuelven consecuencias de una sola verdad sobre dónde corre tu código.

⚔️ Provoca y lee el sleep-in-atomic
  1. Compila un kernel de pruebas con CONFIG_DEBUG_ATOMIC_SLEEP=y.
  2. En un módulo, coge un spinlock y, con él cogido, llama a msleep(10) o a kmalloc(64, GFP_KERNEL).
  3. Carga el módulo y captura el BUG: sleeping function... en dmesg.
  4. Identifica en la traza la línea in_atomic() y la función culpable en la pila.
  5. Corrígelo: suelta el spinlock antes de dormir, o cambia a GFP_ATOMIC. Verifica que el warning desaparece.