4.2. Deadlock: Simple y Avanzado

Hay un fallo de codificación donde un pedazo de código intenta obtener un spinlock dos veces: él esperará siempre, esperando a que el bloqueo sea liberado (spinlocks, rwlocks y semáforos no son recursivos en Linux). Esto es trivial de diagnosticar: no es un tipo de problema de estar-cinco-noches-despierto-hablando-con-los-suaves-conejitos-del-código.

Para un caso ligeramente más complejo, imagínate que tienes una región compartida por un bottom half y un contexto de usuario. Si usas una llamada spin_lock() para protegerla, es posible que el contexto de usuario sea interrumpido por el bottom half mientras mantiene el bloqueo, y el bottom half entonces esperará para siempre para obtener el mismo bloqueo.

Ambas son llamadas deadlock (bloqueo muerto), y como se mostró antes, puede ocurrir con una CPU simple (aunque no en compilaciones para UP, ya que los spinlocks se desvanecen en la compilación del núcleo con CONFIG_SMP=n. Aún tendrás corrupción de datos en el segundo ejemplo).

Este bloqueo completo es fácil de diagnosticar: en equipos SMP el cronómetro guardián o compilado con DEBUG_SPINLOCKS establecido (include/linux/spinlock.h) nos mostrará esto inmediatamente cuando suceda.

Un problema más complejo es el también llamado `abrazo mortal', involucrando a dos o más bloqueos. Digamos que tienes una tabla hash: cada entrada en la tabla es un spinlock, y una cadena de objetos ordenados. Dentro de un manejador softirq, algunas veces quieres alterar un objeto de un lugar de la tabla hash a otro: coges el spinlock de la vieja cadena hash y el spinlock de la nueva cadena hash, y borras el objeto de la vieja y lo insertas en la nueva.

Aquí hay dos problemas. El primero es que si tu código siempre intenta mover el objeto a la misma cadena, él se hará un deadlock cuando se intente bloquear dos veces. El segundo es que si la misma softirq u otra CPU está intentando mover otro objeto en la dirección inversa podría pasar lo siguiente:

Tabla 4-1. Consecuencias

CPU 1CPU 2
Pilla bloqueo A -> OKPilla bloqueo B -> OK
Pilla bloqueo B -> spinPilla bloqueo A -> spin

Las dos CPUs esperarán para siempre, esperando a que el otro libere su bloqueo. Él parecerá, olerá, y se sentirá como si cayera el sistema.

4.2.1. Preveniendo los Deadlocks

Los libros de texto te dirán que si siempre bloqueas en el mismo orden, nunca obtendrás esta clase de deadlock. La práctica te dirá que este tipo de aproximación no escala bien: cuando creo un nuevo bloqueo, no entiendo suficientemente el núcleo para imaginarme dónde está él en la jerarquía de los 5000 bloqueos.

Los mejores bloqueos están encapsulados; nunca estarán expuestos en las cabeceras, y nunca se mantendrán a través de llamadas a funciones no triviales fuera del mismo archivo. Puedes leer a través de este código y ver que nunca hará deadlock, porque nunca intenta tener otro bloqueo mientras tiene el uso. La gente usando tu código no necesita saber nunca que estás usando un bloqueo.

Un problema clásico aquí es cuando suministras retrollamadas o trampas: si las llamas con el bloqueo mantenido, arriesgas un deadlock simple, o un abrazo mortal (¿quién sabe lo que hará la llamada?). Recuerda, los otros programadores andan detrás de ti, por lo tanto no hagas esto.

4.2.2. Sobreentusiasmo en la Prevención de Deadlocks

Los deadlocks son problemáticos, pero no son tan malos como la corrupción de datos. El código que obtiene un bloqueo de lectura, busca una lista, falla al encontrar lo que quiere, tira el bloqueo de lectura, obtiene un bloqueo de escritura e inserta el objeto tiene una condición de carrera.

Si no ves porqué, por favor permanece jodidamente lejos de mi código.