Las variables volátiles

Son aquellas variables cuyo valor siempre que se lee, se lee de memoria, y siempre que se escribe, se escribe en memoria. Pasan el mínimo tiempo posible en los registros del microprocesador.
Al declararlas volátiles se le está indicando al compilador que no haga optimizaciones que eviten accesos a memoria para leerlas/escribirlas.

En un código como éste:

moehren+=7;
moehren*=2;

El compilador podría optimizar y generar código que:

  1. lee el valor de la variable de la pila (p.ej. x<-[moehren]) y lo guarda en un registro del micro
  2. suma 7 al registro (x+7)
  3. multiplica el registro por 2 ((x+7)*2)
  4. guarda el registro en la posición de la pila dedicada a la variable moehren ((x+7)*2->[moehren])

Hasta aquí todo en orden, el valor de mohren==(mohren+7)*2 y el número de accesos a memoria se minimiza, lo que (casi) siempre es de agradecer.

Y ahora vamos a suponer, que mientras una sentencia y otra, se recibe una interrupción y se ejecuta entre medias una sentencia:

moehren-=8;

El orden de ejecución sería como sigue:

  1. moehren+=7;
  2. moehren-=8;
  3. moehren*=2;

Con las optimizaciones del compilador esto queda:

  1. lee el valor de la variable (x<-[moehren]) de la pila y lo guarda en un registro del micro
  2. suma 7 al registro (x+7)
  3. salta a la ISR (rutina de servicio de la interrupción, que es, sencillamente, una función que se ejecutará en el medio)
  4. guarda los registros en la pila
  5. lee el valor de la variable de la pila (y<-[moehren]) y lo guarda en un registro del micro
  6. le resta 8 a ese registro (y-8)
  7. guarda ese registro en la posición de la pila dedicada a la variable moehren (y-8 -> [moehren])
  8. restaura los registros guardados en la pila y retorna a donde estaba antes de saltar a la ISR
  9. multiplica el registro por 2 ((x+7)*2)
  10. guarda el registro en la posición de la pila dedicada a la variable moehren ((x+7)*2->[moehren])

Es decir, que el valor de moehren==(moehren+7)*2, como si la ISR no se hubiera ejecutado.

Si por el contratio declaramos moehren como volatile:

volatile int moehren;

Cada vez que se lea/escriba esa variable, se accederá a memoria, lo cual es más lento, pero nos evita situaciones como la anterior.

El orden de ejecución del código generado en un caso similar al ejemplo anterior sería algo así:

  1. lee el valor de la variable (x<-[moehren]) de la pila a un registro
  2. suma 7 al registro (x+7)
  3. guarda el registro en la variable moehren (x+7 -> [moehren])
  4. salta a la ISR
  5. guarda los registros en la pila
  6. lee el valor de la variable (y<-[moehren], o sea y==x+7) de la pila a un registro
  7. le resta 8 a ese registro (y-8, que es lo mismo que x+7-8)
  8. guarda el registro en la variable moehren (y -> [moehren], o bien x+7-8 -> [moehren])
  9. restaura los registros guardados en la pila y retorna a donde estaba antes de saltar a la ISR
  10. lee el valor de la variable (z<-[moehren], z==x+7-8) de la pila a un registro
  11. multiplica el registro por 2 (z*2, (x+7-8)*2)
  12. guarda el registro en la variable moehren (z -> [moehren], o bien (x+7-8)*2 -> [moehren])

El valor que tiene ahora moehren==(moehren-1)*2. Ahora sí que se nota la ejecución de la ISR.

Ojo, porque con esto no se evita la necesidad de las primitivas de sincronización. Nada garantiza que dos procesos no accedan paralelamente y de forma no atómica a la variable en memoria. Si estamos trabajando en un entorno monotarea (un microcontrolador, normalmente), esto nos dará igual.

Lenguajes de Programación

Comments (0)

Permalink

Spinlocks

Un spinlock es un mecanismo para controlar bloqueos. Quien haya echado un vistazo al código fuente del kernel Linux lo habrá visto sin tener que profundizar mucho.

Básicamente es un procedimiento que, mediante espera ocupada, permanece inactivo hasta que se libera el bloqueo, en cuyo caso pasa a tomarlo.

Realmente es la forma más sencilla de acceder a un bloqueo, con el único inconveniente de que está consumiendo CPU a cambio de no hacer nada más que esperar:

mientras haya bloqueo { repetir } adquiere bloqueo

Lo extraño aquí es que se utilice espera ocupada, ya que siempre se trata de evitar este método. La idea es que los spinlocks van a ser muy breves, la espera ocupada va a ser tan breve y fugaz, que sería muy laborioso para el procesador y el sistema operativo andarse con procedimientos más avanzados. No merecería la pena por unos pocos nanosegundos de espera ocupada andar montando todo un circo con semáforos, o señales o algún otro mecanismo para sincronismo.

Desde luego, si van a ser más de unos nanosegundos no es nada recomendable que los hilos pierdan su cuota de tiempo de proceso iterando sin hacer nada productivo.

Otro problema potencial es que desde que se sale del bucle hasta que se apropia del bloqueo puede surgir una condición de carrera. Para evitar los problemas que ello acarrea se usarán instrucciones atómicas en ensamblador que comprueben el bloqueo y lo adquieran, pero obviamente, esto requiere soporte por parte de la arquitectura. Por supuesto, hay más soluciones, como el algoritmo de Peterson y el de Dekker.

Bibliografía:

  • Entrada en la Wikipedia EN
  • El fichero /Documentation/spinlocks.txt en el código fuente de Linux contiene información acerca de los spinlocks y su implementación en el kernel linux.

Sistemas operativos

Comments (0)

Permalink