• No results found

El kernel se puede ver como un servidor encargado de responder a peticiones hechas por los programas de aplicación o por los circuitos que generan interrupciones, las cuales aparecen en cualquier momento y evitan que la ejecución del mismo este serializada, es decir, el kernel debe estar intercalando la ejecución de las diferentes rutinas si desea responder a cada petición. Para esto, se implementan una serie de técnicas de sincronización como se mencionaron anteriormente en la visión general de Linux. Acá se verá muy someramente cuales técnicas se implementan para sincronizar el kernel, en especial en las denominadas zonas críticas.

2.6.5.1. Necesidad de sincronización

Los caminos de control kernel juegan un papel similar a los procesos pero de una manera más rudimentaria, ya que estos no poseen un descriptor de proceso y no son planificados. En la forma más simple, la CPU debería ejecutar un camino de control desde la primera instrucción hasta la última, sin embargo, lo normal es que la CPU intercale caminos de control.

La intercalación de caminos de control permite el buen desempeño de todo el sistema, en especial, a la hora de atender interrupciones y excepciones. Sin embargo, muchas estructuras del kernel son compartidas en modo kernel y por lo tanto varios caminos de control modificando las mismas estructuras sin que haya un control sobre esto puede acarrear graves problemas, de aquí surge la sincronización, donde el acceso y modificación de los datos debe ser controlado en regiones críticas del kernel.

Como se había dicho, el kernel de Linux no es apropiativo, en este caso un proceso en modo kernel no puede ser apropiado por otro proceso. De manera particular veamos lo siguiente:

• Un proceso en modo kernel no es suspendido por un proceso, a no ser que éste ceda voluntariamente la CPU.

• Los manejadores de interrupciones y excepciones pueden interrumpir un proceso en modo kernel, sin embargo, cuando el manejador termina el camino de control es reanudado.

• Un camino de control que maneja una interrupción no puede ser suspendido por otro camino de control que ejecuta una función diferible o una rutina de llamada al sistema.

kernel, ya que cualquier estructura que no sea modificada por manejadores de interrupciones y excepciones, puede ser accedida seguramente y por lo tanto no será necesaria la sincronización. Ahora, si un proceso cede voluntariamente la CPU, este deberá dejar las estructuras de datos en un estado estable y más aún, cuando reanude su ejecución deberá comprobar la integridad de ellas, ya que los datos de tales podrían haber sido modificadas por otros procesos totalmente diferentes que hicieron uso de las mismas rutinas, del mismo código o de los mismos datos.

En sistemas multiprocesador, esto se complica bastante, ya que se pueden tener muchas CPUs ejecutando código kernel al mismo tiempo, lo que lleva a plantear una serie de técnicas que permita a las rutinas acceder de forma segura.

2.6.5.2. Técnicas de sincronización

Varias son las técnicas que utiliza el sistema para sincronizar las regiones críticas del kernel, incluso cuando varios procesadores son manejados. De nuevo, la explicación acá es simple, no se entra en mucho detalle de todo esto. Para mayor información, refiérase a la documentación mencionada en este trabajo [13].

2.6.5.2.1. Atomic operation

Muchas instrucciones de lenguaje ensamblador son de la forma: leer-modificar-escribir, donde tales instrucciones acceden a memoria dos veces, la primera para leer el valor viejo y la segunda para escribir el nuevo valor. Suponga ahora que existen dos caminos de control kernel que se ejecutan en dos CPUs intentando “leer-modificar-escribir” con la instrucción inc, en la misma posición de memoria y al mismo tiempo sin ejecutar Atomic Operations. Inicialmente ambos procesos intentan leer la misma dirección pero un circuito que se encarga de manejar la memoria evita esto dándole solo acceso a un proceso y retrasando la lectura para el otro, igual, pese al retraso, los dos terminan realizando la misma lectura. Ahora, cuando van a escribir el nuevo dato, nuevamente el controlador de memoria evita que accedan al tiempo, permitiendo la escritura de uno y luego de otro, pero al final, escribiendo el mismo valor, así la intercalación de las operaciones “leer-modificar-escribir” terminan siendo una sola función que gastó mas tiempo y obtuvo un valor erróneo. La idea de

Atomic Operations es que una instrucción se desarrolle a nivel del chip sin ser interrumpida a mitad

de camino. Estas funciones generalmente son base de otros mecanismos más flexibles para tratar zonas críticas. En C, el compilador no garantiza que se usen las Atomic Operations sin embargo,

existe un tipo de dato en el kernel denominado atomic_t empleado para esta labor. Esta técnica tiene como alcance a todos los procesadores instalados.

2.6.5.2.2. Memory barriers

Los compiladores tratan de optimizar la forma en que quedan las instrucciones de ensamblador en un determinado código con el fin de mejorar el acceso a memoria y más. Esta reorganización puede acarrear problemas a la hora de tratar el tema de sincronización ya que las primitivas de sincronización en sí son Barreras de Memoria y por lo tanto, el reordenamiento puede acarrear que una primitiva quede justo después del lugar que se le quería aplicar una primitiva de sincronización, dejando cierta porción de datos al descubierto. Una barrera de memoria asegura que las operaciones colocadas antes de la primitiva, son procesadas antes de comenzar a ejecutar las instrucciones que están después de la primitiva. El alcance de las Memory Barriers es solo para la CPU local.

2.6.5.2.3. Spin locks

Una técnica ampliamente utilizada para la sincronización es el bloqueo o cerrojo. Cuando un camino de control kernel accede a una estructura de datos compartidos o a una región crítica, éste necesita adquirir un cerrojo. Un recurso protegido por un cerrojo es similar a si en una habitación cerrada usted tuviese un elemento y para hacer uso exclusivo de él, usted (camino de control kernel) le hecha llave a la puerta de la habitación. Otras personas (otros caminos de control) que intenten acceder a la habitación para hacer uso del elemento no podrán porque la puerta está con llave (lock). Una vez la primera persona (camino kernel que bloqueó el recurso) libere el elemento, este saldrá de la habitación abriendo la puerta (open lock) y por lo tanto otra persona podrá acceder a los datos y hacer de nuevo uso exclusivo (lock nuevamente).

Un spin lock es muy utilizado en sistemas multiprocesador. Si un camino de control que va a acceder a una estructura de datos, encuentra que el cerrojo está abierto (spin lock open), este adquiere el bloqueo y continua su ejecución. Por el contrario, si encuentra que está cerrado y además el camino de control se está ejecutando en otra CPU, este seguirá intentando continuamente tener acceso al recurso hasta ser liberado.

suponga una región crítica en donde 3 procesos desean hacer lectura y el 4 desea escribir en ella. El primer proceso de lectura puede colocar un cerrojo de lectura de manera que simultáneamente los 3 procesos puedan leer de la misma estructura de datos pero sin escribir, además el 4 estará a la espera de que sea liberado el recurso para escritura. En cambio el caso de que varios elementos escriban en una misma estructura no es permitido o se tendría problemas de sincronización de datos. Como se ve el alcance de los spin locks es sobre todas las CPUs instaladas.

2.6.5.2.4. Semáforos

Un semáforo es esencialmente un spin lock, donde su funcionalidad está en evitar que se acceda a una estructura de datos mientras este protegida por un semáforo. La diferencia está en que éstos no se quedan intentando acceder a la estructura hasta que este disponible, sino que los procesos que necesitan acceder son dormidos y colocados en una lista de espera, para luego ser despertados una vez la estructura este disponible, es decir, cuando se libere el semáforo.

Consiste en asociar un contador a una estructura de datos con el fin de que los diferentes caminos de control sepan si una estructura está en uso o no. Un semáforo contiene un contador, una lista de procesos en cola y dos métodos, down() y up(). Cada estructura de datos es protegida con un semáforo que inicialmente tiene su contador en 1, indicando que el recurso está disponible. Cuando un camino de control desea acceder a una estructura, este decrementa el valor del contador del semáforo usando el método down(), si el kernel detecta que el nuevo valor no es negativo, le cede la estructura de datos al proceso y decrementa el valor del semáforo. Si el valor del contador queda negativo, el kernel bloquea el proceso y lo manda a una lista de espera mientras el recurso está en uso, muchos otros procesos intentarán acceder a la estructura teniéndose que el contador cada vez se amás negativo y por lo tanto se tengan más procesos en cola. Una vez el proceso que tenga ocupada la estructura termine, hará uso del método up() para incrementar el valor del contador y liberar por lo tanto la estructura, el kernel le asignará el recurso al proceso que está en la lista de espera. Llegará un momento en que el contador será 1 de nuevo y la estructura estará disponible para el primer proceso que desee tomar control. Incluso el valor del contador podrá ser mayor a 1 si se necesita que varios procesos hagan uso de la estructura de datos.

2.6.5.2.5. Otros

varios procesadores, entre otros. Para saber la funcionalidad de tales, se recomienda ver la referencia.