Un bloqueo de giro es una forma de proteger un recurso compartido para que no sea modificado por dos o más procesos simultáneamente. El primer proceso que intenta modificar el recurso "adquiere" el candado y sigue su camino, haciendo lo necesario con el recurso. Cualquier otro proceso que posteriormente intente adquirir el bloqueo se detiene; se dice que "giran en su lugar" esperando que el bloqueo sea liberado por el primer proceso, de ahí el nombre bloqueo giratorio.
El kernel de Linux usa bloqueos giratorios para muchas cosas, como cuando envía datos a un periférico en particular. La mayoría de los periféricos de hardware no están diseñados para manejar varias actualizaciones de estado simultáneas. Si tienen que ocurrir dos modificaciones diferentes, una tiene que seguir estrictamente a la otra, no pueden superponerse. Un bloqueo giratorio brinda la protección necesaria, asegurando que las modificaciones se realicen una a la vez.
Los bloqueos giratorios son un problema porque los bloqueos giratorios impiden que el núcleo de la CPU de ese subproceso realice cualquier otro trabajo. Si bien el kernel de Linux proporciona servicios multitarea a los programas de espacio de usuario que se ejecutan bajo él, esa función multitarea de propósito general no se extiende al código del kernel.
Esta situación está cambiando, y lo ha sido durante la mayor parte de la existencia de Linux. Hasta Linux 2.0, el kernel era casi puramente un programa de una sola tarea:cada vez que la CPU ejecutaba el código del kernel, solo se usaba un núcleo de CPU, porque había un bloqueo de giro único que protegía todos los recursos compartidos, llamado Big Kernel Lock (BKL). ). A partir de Linux 2.2, el BKL se está dividiendo lentamente en muchos bloqueos independientes, cada uno de los cuales protege una clase de recurso más específica. Hoy, con el kernel 2.6, el BKL todavía existe, pero solo lo usa el código realmente antiguo que no se puede mover fácilmente a un bloqueo más granular. Ahora es bastante posible que una caja multinúcleo tenga cada CPU ejecutando código kernel útil.
Hay un límite a la utilidad de dividir el BKL porque el kernel de Linux carece de multitarea general. Si un núcleo de CPU se bloquea girando en un bloqueo de giro del kernel, no se puede volver a asignar para hacer otra cosa hasta que se libere el bloqueo. Simplemente se sienta y gira hasta que se libera el bloqueo.
Los bloqueos giratorios pueden convertir efectivamente una caja monstruosa de 16 núcleos en una caja de un solo núcleo, si la carga de trabajo es tal que cada núcleo siempre está esperando un solo bloqueo giratorio. Este es el límite principal de la escalabilidad del kernel de Linux:duplicar los núcleos de la CPU de 2 a 4 probablemente casi duplicará la velocidad de una caja de Linux, pero duplicarla de 16 a 32 probablemente no lo haga, con la mayoría de las cargas de trabajo.
Un bloqueo giratorio es cuando un proceso sondea continuamente para eliminar un bloqueo. Se considera malo porque el proceso consume ciclos (generalmente) innecesariamente. No es específico de Linux, sino un patrón de programación general. Y aunque generalmente se considera una mala práctica, es, de hecho, la solución correcta; hay casos en los que el costo de usar el programador es más alto (en términos de ciclos de CPU) que el costo de los pocos ciclos que se espera que dure el spinlock.
Ejemplo de un spinlock:
#!/bin/sh
#wait for some program to clear a lock before doing stuff
while [ -f /var/run/example.lock ]; do
sleep 1
done
#do stuff
Con frecuencia hay una manera de evitar un bloqueo de giro. Para este ejemplo en particular, hay una herramienta de Linux llamada inotifywait (no suele instalarse de forma predeterminada). Si estuviera escrito en C, simplemente usaría la API de inotificación que proporciona Linux.
El mismo ejemplo, usando inotifywait, muestra cómo lograr lo mismo sin un bloqueo de giro:
#/bin/sh
inotifywait -e delete_self /var/run/example.lock
#do stuff
Cuando un subproceso intenta adquirir un bloqueo, pueden suceder tres cosas si falla:puede intentar y bloquear, puede intentar y continuar, puede intentar y luego ir a dormir diciéndole al sistema operativo que lo despierte cuando ocurra algún evento.
Ahora, intentar y continuar usa mucho menos tiempo que intentar y bloquear. Digamos por el momento que un "intentar y continuar" tomará una unidad de tiempo y un "intentar y bloquear" tomará cien.
Ahora supongamos por el momento que, en promedio, un subproceso tardará 4 unidades de tiempo en mantener el bloqueo. Es un desperdicio esperar 100 unidades. Entonces, en su lugar, escribe un ciclo de "intentar y continuar". En el cuarto intento, normalmente adquirirá el candado. Este es un bloqueo giratorio. Se llama así porque el hilo sigue girando en su lugar hasta que obtiene el bloqueo.
Una medida de seguridad adicional es limitar el número de veces que se ejecuta el bucle. Entonces, por ejemplo, realiza una ejecución de bucle for, digamos, seis veces, si falla, entonces "intenta y bloquea".
Si sabe que un subproceso siempre mantendrá el bloqueo durante, digamos, 200 unidades, entonces está perdiendo el tiempo de la computadora en cada intento y continuar.
Entonces, al final, un bloqueo giratorio puede ser muy eficiente o un desperdicio. Es un desperdicio cuando el tiempo "típico" para mantener un bloqueo es más alto que el tiempo que lleva "intentar y bloquear". Es eficiente cuando el tiempo típico para mantener un candado es mucho menor que el tiempo para "intentar y bloquear".
PD:El libro para leer sobre hilos es "A Thread Primer", si aún puede encontrarlo.