¿Cómo se implementa conditional_wait() a nivel de kernel y hardware/ensamblaje?

¿Cómo se implementa conditional_wait() a nivel de kernel y hardware/ensamblaje?


Entiendo que el subproceso que espera en una variable condicional libera atómicamente el bloqueo y se duerme hasta que lo despierta una señal condicional de otro subproceso (cuando se cumple una condición particular). Después de que se despierta, vuelve a adquirir atómicamente el bloqueo (de alguna manera mágicamente) y se actualiza según sea necesario y desbloquea la sección crítica.


Sería genial si alguien pudiera explicar cómo se implementó este procedimiento conditional_wait() en el kernel y el nivel de hardware/ensamblaje.


¿Cómo se libera el bloqueo y se vuelve a adquirir atómicamente? ¿Cómo lo asegura el núcleo?


¿Qué significa realmente dormir aquí? ¿Significa un cambio de contexto a otro proceso/hilo?


Durante la suspensión del subproceso, ¿cómo se despierta este subproceso mediante la señalización? implementado a nivel de kernel y si se proporciona soporte específico de hardware para estos mecanismos?


Editar:


Parece que "futex" es el tipo que maneja estas cosas de espera/señal. Para acotar mi pregunta:
¿Cómo se implementa/funciona en el nivel bajo la llamada del sistema futex para las variables de condición de espera y notificación?


Respuestas:


En un nivel alto (y dado que está haciendo esta pregunta, lo que necesita es un nivel alto) no es tan complicado. Primero, necesita conocer los niveles de responsabilidad. Hay básicamente 3 capas:



  • Nivel de hardware:generalmente algo que se puede codificar en una sola instrucción ASM

  • Nivel de kernel:algo que hace el kernel del sistema operativo

  • Nivel de aplicación:algo que hace la aplicación


Generalmente, esas responsabilidades no se superponen:el kernel no puede hacer lo que solo el hardware puede hacer, el hardware no puede hacer lo que solo el kernel puede hacer. Teniendo esto en cuenta, es útil recordar que cuando se trata de bloqueo, hay muy poco hardware que sepa al respecto. Básicamente se reduce a



  • aritmética atómica:el hardware puede bloquear una región de memoria en particular (asegúrese de que ningún otro subproceso acceda a ella), realizar operaciones aritméticas en ella y desbloquear la región. Esto solo puede funcionar en la aritmética admitida de forma nativa por el chip (¡sin raíces cuadradas!) y en los tamaños admitidos de forma nativa por el hardware

  • Barreras o vallas de memoria:es decir, introduzca una barrera dentro de un flujo de instrucciones, de modo que cuando la CPU reordene las instrucciones o use cachés de memoria, no cruzarán esas vallas y la caché estará fresca

  • Configuración condicional (comparar y configurar):establezca la región de memoria en el valor A si es B e informe el estado de esta operación (si se configuró o no)


Eso es prácticamente todo lo que la CPU puede hacer. Como puede ver, aquí no hay futex, mutex o variables condicionales. Este material está hecho por kernel que tiene a su disposición operaciones compatibles con CPU.


Veamos en un nivel muy alto cómo el kernel podría implementar la llamada futex. En realidad, futex es un poco complicado, porque es una mezcla de llamadas a nivel de usuario y llamadas a nivel de kernel según sea necesario. Veamos el mutex 'puro', implementado únicamente en el espacio del kernel. En un alto nivel, será lo suficientemente demostrativo.


Cuando se crea inicialmente el mutex, el núcleo le asocia una región de memoria. Esta región tendrá un valor de mutex bloqueado o desbloqueado. Más tarde, se le pide al kernel que bloquee el mutex, primero le indica a la CPU que emita una barrera de memoria. Un mutex debe servir como barrera, de modo que todo lo que se lea/escriba después de adquirir (o liberar) el mutex sea visible para el resto de las CPU. Luego, utiliza la instrucción de comparación y configuración admitida por CPU para establecer el valor de la región de memoria en 1 si se estableció en 0. (Hay mutex reentrantes más complicados, pero no compliquemos la imagen con ellos). La CPU garantiza que incluso si más de un subproceso intenta hacer esto al mismo tiempo, solo uno tendrá éxito. Si la operación tiene éxito, ahora 'mantenemos el mutex'. Una vez que se le pide al núcleo que libere la exclusión mutua, la región de memoria se establece en 0 (no hay necesidad de hacer esto de forma condicional, ¡ya que sabemos que tenemos la exclusión mutua!) y se emite otra barrera de memoria. Kernel también actualiza el estado mutex en sus tablas; consulte a continuación.


Si el bloqueo de mutex falla, el núcleo agrega el subproceso a sus tablas, que enumeran los subprocesos que esperan que se libere un mutex en particular. Cuando se libera el mutex, el kernel verifica qué subprocesos están esperando en este mutex y 'programa' (es decir, se prepara para la ejecución) uno de esos (en caso de que haya más de uno, cuál se programará o despertará depende de multitud de factores, en el caso más simple es simplemente aleatorio). El subproceso programado comienza a ejecutarse, bloquea el mutex nuevamente (¡en este punto puede fallar nuevamente!) y el ciclo de vida continúa.


Espero que tenga al menos medio sentido :)