• No se han encontrado resultados

Scheduling para sistemas de tiempo real

In document Conceptos de Sistemas Operativos (página 58-61)

En esta sección se verán los algoritmos de scheduling necesarios para poder soportar la computación en tiempo real.

La computación en tiempo real se divide en dos tipos. Los sistemas de tiempo real duros son requeridos para completar una tarea critica en una cantidad garantizada de tiempo. Generalmente, un proceso es declarado con la cantidad de tiempo limite que debe ser completado o realizar un I/O. Ante esto, el scheduler puede aceptar el proceso, garantizando que será completado en el tiempo establecido, o lo rechaza. Para que el scheduler garantice que completara un proceso, necesita saber que cantidad de tiempo lleva cada función del sistema operativo. En sistemas con almacenamiento secundario o memoria virtual, ésta garantía es imposible que se dé, ya que estos subsistemas causan una inevitable e imprevisible variación en la cantidad de tiempo que tardan para ejecutar un proceso. Así, los sistemas de tiempo real duros están compuestos de software de propósito especial corriendo en un hardware dedicado a sus procesos críticos, y carece de la funcionalidad completa de las modernas computadoras y sistemas operativos.

Los sistemas de tiempo real suaves o blandos son menos restrictos. Su implementación necesita cuidado en el diseño de los schedulers y los aspectos relacionados con el sistema operativo. Primero, el sistema debe tener scheduler de prioridades, y los procesos de tiempo real deben tener la prioridad más alta. La prioridad de los procesos de tiempo real no debe disminuir en el tiempo, aun aunque la prioridad de los procesos de tiempo no real puedan hacerlo. Segundo, la latencia de dispatch debe ser baja.

Es difícil asegurar la propiedad de que la latencia del dispatch debe ser baja. El problema es que muchos sistemas operativos, incluyendo muchas versiones de UNIX, están forzadas a esperar por la completitud de una llamada al sistema, o por un bloque de I/O que toma lugar antes de hacer el context switch. La latencia del dispatch en tales sistemas puede ser larga, ya que algunas llamadas al sistema son complejas y algunos dispositivos de I/O son lentos.

Para mantener la latencia del dispatch baja, se necesita permitir que las llamadas al sistema puedan ser desalojadas. Existen varias formas de conseguir este objetivo. Una es la de insertar puntos de desalojo en las llamadas al sistema largas, el cual chequea si un proceso de alta prioridad debe ser ejecutado. En tal caso, es decir, que exista un proceso de alta prioridad, toma lugar un context switch y, cuando el proceso de alta prioridad termina, el proceso interrumpido continúa con la llamada al sistema. Los puntos de desalojos se deben ubicar en lugares seguros del kernel, es decir, cuando las estructuras de datos del kernel no están siendo modificadas.

Otra forma de tratar con el desalojo es el de hacer todo el kernel desalojable. Ante el hecho de que la actual operación esta asegurada, todas las estructuras de datos del kernel deben estar protegidas por medio del uso de varios mecanismos de sincronización. Con éste método, el kernel siempre puede ser desalojable, ya que cualquier dato del kernel que esta siendo modificado esta protegido de modificaciones que puedan hacer los procesos de alta prioridad. Este método es el utilizado por Solaris 2.

En caso de que un proceso de alta prioridad necesite modificar un dato del kernel que en este momento esta siendo modificado por un proceso de baja prioridad, entonces el proceso de alta prioridad debe esperar hasta que el proceso de baja prioridad termine.

La figura 5.8 muestra la latencia del dispatch.

La fase de conflicto de la latencia del dispatch tiene tres componentes: Desalojo de cualquier proceso corriendo en el kernel.

Proceso de baja prioridad liberando los recursos necesitados por el proceso de alta prioridad. Context switching del actual proceso al proceso de alta prioridad.

Evento Procesando la interrupción Intervalo de respuesta Proceso a ejecutar disponible Conflictos Dispatch

Latencia del dispatch Ejecución del proceso en tiempo real Respuesta al

evento

Tiempo

6. Sincronización de procesos

Un proceso cooperador es aquel que afecta o es afectado por otros procesos que están ejecutando en el sistema. Los procesos cooperadores pueden o compartir directamente un espacio de direcciones lógicas (ambos: código y datos), o pueden compartir datos sólo a través de archivos. El caso anterior es conseguido por medio del uso de procesos livianos o threads. El acceso concurrente a los datos compartidos puede resultar en la inconsistencia de éstos datos. En ésta parte se verán mecanismos que aseguren la ejecución ordenada de los procesos cooperadores que comparten espacio de direcciones lógicas para que los datos estén consistentes.

Conceptos básicos

En el capítulo 4 se vio un modelo de un sistema consistente de un número de procesos cooperando secuencialmente, todos corriendo asincrónicamente y posiblemente compartiendo datos. Este modelo fue ilustrado con el esquema de buffer limitado.

Retornemos a la solución de la memoria compartida para el problema del buffer limitado que se vio en la sección 4.4. Como se señaló, nuestra solución permitía que como mucho n-1 ítems estén en el buffer en un mismo tiempo. Supongamos que queremos modificar el algoritmo para remediar ésta deficiencia. Una posibilidad es la de agregar una variable entera counter, inicializada en 0, el cual es decrementada cada vez que se elimina un ítem del buffer, y es incrementada cada vez que se agrega un ítem en el buffer. El código del proceso productor puede ser modificado como sigue:

repeat ...

producir un ítem en nextp ...

while counter = n do no-op; buffer[in] := nextp;

in := in + 1 mod n; counter = counter + 1; until false;

El código del proceso consumidor seria: repeat

while counter = 0 do no-op;

nextc = buffer[out];

out := out + 1 mod n; counter = counter - 1; ...

consumir el ítem en nextc ...

until false;

Aunque ambas rutinas parecen estar correctamente escritas, no funcionaran bien en el momento de ser ejecutadas concurrentemente. Como ejemplo, supongamos que el valor de la variable counter es 5, y que tanto el proceso productor como el consumidor están a punto de ejecutar la sentencia “counter = counter + 1” y “counter = counter – 1” respectivamente, concurrentemente. Siguiendo con la ejecución de estas dos sentencias, el valor de la variable counter puede ser 4, 5 o 6; y el único resultado correcto es 5, el cual es generado correctamente en caso de que el productor y el consumidor ejecuten separadamente.

Veamos porque el valor de counter puede ser incorrecto. Notemos que la sentencia “counter = counter + 1” puede ser implementada en lenguaje de maquina como sigue:

register1 := counter; register1 := register1 + 1;

counter = register1;

donde register1 es un registro local de la CPU. Similarmente, la sentencia “counter = counter – 1” puede ser implementada como sigue:

register2 := counter; register2 := register2 - 1; counter = register2;

donde nuevamente register2 es un registro local de la CPU. Note que los registros 1 y 2 podrían ser los mismos, ya que los contenidos de los registros se almacenan y luego se vuelven a cargar por el manejador de las interrupciones.

La ejecución concurrente de la sentencia “counter = counter + 1” y “counter = counter – 1” es equivalente a una ejecución secuencial donde las instrucciones de maquinas anteriores podrían entremezclarse. Una de éstas mezcladas podría ser:

T0: productor ejecuta register1 := counter {register1 = 5} T1: productor ejecuta register1 := register1 + 1 {register1 = 6} T2: consumidor ejecuta register2 := counter {register2 = 5} T3: consumidor ejecuta register2 := register2 - 1 {register2 = 4} T4: productor ejecuta counter := register1 {counter = 6} T5: consumidor ejecuta counter := register2 {counter = 4}

Note que llegamos a un estado incorrecto de counter, el cual recuerda que hay 4 buffers llenos, cuando en realidad hay 5. En caso de que cambiemos de lugar las sentencias T4 y T5, llegaremos a un valor de 6 para counter. Este error se debe a que permitimos a los procesos manipular la variable counter concurrentemente. Para evitar que este error se produzca debemos asegurar que sólo un proceso a la vez podrá manipular la variable counter. Ante esto se necesita alguna forma de sincronización de procesos. En este capítulo se verán métodos que garantizan la consistencia de los datos compartidos.

In document Conceptos de Sistemas Operativos (página 58-61)