¿Qué es un spinlock en Linux?


32

Me gustaría saber sobre los spinlocks de Linux en detalle; alguien podría explicarme?

Respuestas:


34

Un bloqueo de giro es una forma de proteger un recurso compartido de ser modificado por dos o más procesos simultáneamente. El primer proceso que intenta modificar el recurso "adquiere" el bloqueo y continúa su camino, haciendo lo que necesitaba con el recurso. Cualquier otro proceso que posteriormente intente adquirir el bloqueo se detiene; se dice que "giran en su lugar" esperando que el primer proceso libere el bloqueo, de ahí el nombre de bloqueo de giro.

El kernel de Linux utiliza bloqueos de giro para muchas cosas, como cuando se envían datos a un periférico en particular. La mayoría de los periféricos de hardware no están diseñados para manejar múltiples actualizaciones de estado simultáneas. Si tienen que suceder dos modificaciones diferentes, una debe seguir estrictamente a la otra, no pueden superponerse. Un bloqueo de giro proporciona la protección necesaria, asegurando que las modificaciones sucedan una a la vez.

Los bloqueos de giro son un problema porque los bloques de giro que el núcleo de la CPU del hilo no realizan ningún otro trabajo. Si bien el kernel de Linux proporciona servicios multitarea a los programas de espacio de usuario que se ejecutan en él, esa función multitarea de uso general no se extiende al código del kernel.

Esta situación está cambiando y 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 solo bloqueo de giro que protegía todos los recursos compartidos, llamado Big Kernel Lock (BKL ) Comenzando con Linux 2.2, el BKL se está dividiendo lentamente en muchos bloqueos independientes que protegen una clase de recurso más enfocada. Hoy, con el kernel 2.6, el BKL todavía existe, pero solo lo usa un 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 de núcleo útil.

Hay un límite para la utilidad de romper 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 núcleo, no se puede volver a realizar la tarea, para hacer otra cosa hasta que se libere el bloqueo. Simplemente se sienta y gira hasta que se libera el bloqueo.

Los bloqueos de giro pueden convertir efectivamente una monstruosa caja 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 de giro. Este es el límite principal para la escalabilidad del kernel de Linux: duplicar los núcleos de CPU de 2 a 4 probablemente duplicará la velocidad de una caja de Linux, pero duplicarlo de 16 a 32 probablemente no lo hará, con la mayoría de las cargas de trabajo.


@Warren: Un par de dudas: me gustaría saber más sobre este Big Kernel Lock y sus implicaciones. También entiendo el último párrafo "duplicar los núcleos de CPU de 2 a 4 probablemente duplicará la velocidad de una caja de Linux, pero duplicarlo de 16 a 32 probablemente no"
Sen

2
Re: implicaciones de BKL: pensé que dejé eso claro arriba. Con solo un bloqueo en el núcleo, cada vez que dos núcleos intentan hacer algo protegido por BKL, un núcleo se bloquea mientras el primero termina de usar su recurso protegido. Cuanto más fino sea el bloqueo, menor será la posibilidad de que esto suceda, por lo que mayor será la utilización del procesador.
Warren Young

2
Re: duplicación: quiero decir que hay una ley de rendimientos decrecientes al agregar núcleos de procesador a una computadora. A medida que aumenta el número de núcleos, también lo hace la posibilidad de que dos o más de ellos necesiten acceder a un recurso protegido por un bloqueo particular. El aumento de la granularidad de la cerradura reduce las posibilidades de tales colisiones, pero también hay una sobrecarga al agregar demasiadas. Puede ver esto fácilmente en las supercomputadoras, que a menudo tienen miles de procesadores en estos días: la mayoría de las cargas de trabajo son ineficientes porque no pueden evitar la inactividad de muchos procesadores debido a la contención de recursos compartidos.
Warren Young

1
Si bien esta es una explicación interesante (+1 para eso), no creo que sea efectiva para transmitir la diferencia entre los spinlocks y otros tipos de bloqueos.
Gilles 'SO- deja de ser malvado'

2
Si uno quiere saber la diferencia entre un bloqueo de giro y, por ejemplo, un semáforo, esa es una pregunta diferente. Otra pregunta buena pero tangencial es, ¿qué tiene el diseño del kernel de Linux que hace que los bloqueos de giro sean buenas opciones en lugar de algo más común en el código de usuario, como un mutex. Esta respuesta divaga mucho como está.
Warren Young el

11

Un bloqueo de giro es cuando un proceso continuamente sondea 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 mayor (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 (generalmente no se instala por defecto). Si estuviera escrito en C, simplemente usaría la API de inotify 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

¿Cuál es el papel del planificador allí? ¿O tiene algún papel?
Sen

1
En el método de bloqueo de giro, el planificador reanuda el proceso cada ~ 1 segundo para realizar su tarea (que es simplemente verificar la existencia de un archivo). En el ejemplo de inotifywait, el planificador solo reanuda el proceso cuando finaliza el proceso secundario (inotifywait). Inotifywait también está durmiendo; el planificador solo lo reanuda cuando ocurre el evento inotify.
Shawn J. Goff

Entonces, ¿cómo se maneja este escenario en un sistema de procesador de núcleo único?
Sen


1
Ese script bash es un mal ejemplo de un spinlock. Estás pausando el proceso para que se vaya a dormir. Un spinlock nunca duerme. En una máquina de un solo núcleo, simplemente suspende el programador y continúa (sin esperas ocupadas)
Martin

7

Cuando un subproceso intenta adquirir un bloqueo, pueden suceder tres cosas si falla, puede intentar bloquear, puede intentar y continuar, puede intentar luego irse a dormir y decirle al sistema operativo que lo active cuando ocurra algún evento.

Ahora, probar y continuar utiliza mucho menos tiempo que intentarlo y bloquearlo. Digamos por el momento que un "intentar y continuar" tomará una unidad de tiempo y un "probar y bloquear" tomará cien.

Ahora supongamos por el momento que, en promedio, un hilo tardará 4 unidades de tiempo en mantener el bloqueo. Es un desperdicio esperar 100 unidades. Entonces, en su lugar, escribe un bucle de "prueba y continúa". En el cuarto intento, generalmente adquirirás el bloqueo. Este es un bloqueo de giro. Se llama así porque el hilo sigue girando en su lugar hasta que se bloquea.

Una medida de seguridad adicional es limitar el número de veces que se ejecuta el ciclo. Entonces, en el ejemplo, ejecuta un ciclo for, por ejemplo, seis veces, si falla, entonces "intenta y bloquea".

Si sabe que un hilo siempre mantendrá el bloqueo por ejemplo 200 unidades, entonces está perdiendo el tiempo de la computadora para cada intento y continuar.

Entonces, al final, un bloqueo de giro puede ser muy eficiente o derrochador. Es un desperdicio cuando el tiempo "típico" para mantener un bloqueo es mayor que el tiempo que toma "intentar y bloquear". Es eficiente cuando el tiempo típico para mantener un bloqueo es mucho menor que el tiempo para "intentar bloquear".

Ps: El libro para leer en hilos es "A Thread Primer", si aún puede encontrarlo.


el hilo sigue girando en su lugar hasta que se bloquea . ¿Podrías decirme qué es este giro? ¿Es como si viniera a wait_queue y el programador lo ejecute? Tal vez estoy tratando de llegar al nivel bajo, pero todavía no puedo contener mi duda.
Sen

Girando entre las 3-4 instrucciones del bucle, básicamente.
Paul Stelian

5

Un bloqueo es una forma de sincronizar dos o más tareas (procesos, subprocesos). Específicamente, cuando ambas tareas necesitan acceso intermitente a un recurso que solo puede ser usado por una tarea a la vez, es una forma de que las tareas arreglen para no usar el recurso al mismo tiempo. Para acceder al recurso, una tarea debe realizar los siguientes pasos:

take the lock
use the resource
release the lock

No es posible tomar un candado si otra tarea ya lo ha tomado. (Piense en la cerradura como un objeto simbólico físico. O bien el objeto está en un cajón, o alguien lo tiene en sus manos. Solo la persona que sostiene el objeto puede acceder al recurso). Entonces, "tomar la cerradura" realmente significa "esperar hasta nadie más tiene la cerradura, entonces tómala ”.

Desde un punto de vista de alto nivel, hay dos formas principales de implementar bloqueos: spinlocks y condiciones. Con los candados giratorios, tomar el candado significa simplemente "girar" (es decir, no hacer nada en un bucle) hasta que nadie más tenga el candado. Con condiciones, si una tarea intenta tomar el bloqueo pero está bloqueado porque otra tarea lo retiene, el recién llegado entra en una cola de espera; la operación de liberación indica a cualquier tarea en espera que el bloqueo ahora está disponible.

(Estas explicaciones no son suficientes para permitirle implementar un bloqueo, porque no he dicho nada sobre atomicidad. Pero la atomicidad no es importante aquí).

Los spinlocks son obviamente un desperdicio: la tarea de espera sigue verificando si se ha tomado el bloqueo. Entonces, ¿por qué y cuándo se usa? Los spinlocks a menudo son muy baratos de obtener en el caso de que no se mantenga el bloqueo. Esto lo hace atractivo cuando la posibilidad de mantener el bloqueo es pequeña. Además, los spinlocks solo son viables si no se espera que el bloqueo tarde mucho. Por lo tanto, los spinlocks tienden a usarse en situaciones donde permanecerán retenidos por un tiempo muy corto, de modo que se espera que la mayoría de los intentos tengan éxito en el primer intento, y aquellos que necesitan una espera no esperen mucho.

Hay una buena explicación de los spinlocks y otros mecanismos de concurrencia del kernel de Linux en los Controladores de dispositivos Linux , capítulo 5.


¿Cuál sería una buena manera de implementar otras primitivas de sincronización? Tome un spinlock, verifique si alguien más tiene implementada la primitiva real, luego ¿usa el programador o otorga acceso? ¿Podríamos considerar el bloque sincronizado () en Java como una forma de spinlock, y en las primitivas implementadas correctamente solo usamos un spinlock?
Paul Stelian

@PaulStelian De hecho, es común implementar bloqueos "lentos" de esta manera. No conozco suficiente Java para responder esa parte, pero dudo que eso synchronizedse implemente mediante un spinlock: un synchronizedbloque podría ejecutarse durante mucho tiempo. synchronizedes una construcción de lenguaje para hacer que las cerraduras sean fáciles de usar en ciertos casos, no una primitiva para construir primitivas de sincronización más grandes.
Gilles 'SO- deja de ser malvado'

3

Un spinlock es un bloqueo que funciona desactivando el planificador y posiblemente interrumpe (variante irqsave) en ese núcleo particular en el que se adquiere el bloqueo. Es diferente de un mutex en que deshabilita la programación, por lo que solo su hilo puede ejecutarse mientras se mantiene el spinlock. Un mutex permite que se programen otros subprocesos de mayor prioridad mientras se mantiene, pero no les permite ejecutar simultáneamente la sección protegida. Debido a que los spinlocks desactivan la multitarea, no puede tomar un spinlock y luego llamar a otro código que intente adquirir un mutex. Su código dentro de la sección spinlock nunca debe dormir (el código generalmente duerme cuando encuentra un mutex bloqueado o un semáforo vacío).

Otra diferencia con un mutex es que los hilos generalmente hacen cola para un mutex, por lo que un mutex debajo tiene una cola. Mientras que spinlock solo asegura que no se ejecutará ningún otro hilo, incluso si es necesario. Por lo tanto, nunca debe mantener un spinlock cuando llame a funciones fuera de su archivo que no está seguro de que no dormirán.

Cuando desee compartir su spinlock con una interrupción, debe usar la variante irqsave. Esto no solo deshabilitará el planificador, sino que también deshabilitará las interrupciones. Tiene sentido ¿verdad? Spinlock funciona asegurándose de que nada más se ejecutará. Si no desea que se ejecute una interrupción, desactívela y avance de manera segura a la sección crítica.

En una máquina multinúcleo, un spinlock realmente girará esperando que otro núcleo que sostiene el bloqueo lo libere. Este giro solo ocurre en máquinas multinúcleo porque en las de un solo núcleo no puede suceder (o mantienes presionado spinlock y continúas o nunca corres hasta que se libera el bloqueo).

Spinlock no es un desperdicio donde tiene sentido. Para secciones críticas muy pequeñas sería un desperdicio asignar una cola de tareas mutex en comparación con simplemente suspender el programador por unos pocos microsegundos que se necesita para terminar el trabajo importante. Si necesita dormir o mantener el bloqueo en una operación io (que puede dormir), use un mutex. Ciertamente, nunca bloquee un spinlock y luego intente liberarlo dentro de una interrupción. Si bien esto funcionará, será como la basura arduino de while (flagnotset); en tal caso use un semáforo.

Tome un spinlock cuando necesite una exclusión mutua simple para bloques de transacciones de memoria. Tome un mutex cuando desee que varios subprocesos se detengan justo antes de un bloqueo de mutex y luego se elija el subproceso de mayor prioridad para continuar cuando mutex se libere y cuando bloquee y suelte en el mismo hilo. Tome un semáforo cuando tenga la intención de publicarlo en un hilo o una interrupción y tómelo en otro hilo. Son tres formas ligeramente diferentes de garantizar la exclusión mutua y se utilizan para fines ligeramente diferentes.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.