Sistemas Operativos de Multiprogramación

De Wikitronica
Saltar a: navegación, buscar
Este artículo está incompleto. Necesita trabajo adicional. Revisar la discusión.

También llamado sistemas operativos multitarea o multiproceso, se distingue por la habilidad de soportar dos o más procesos activos simultáneamente. El término multiprogramación denota un sistema operativo que, además de soportar procesos concurrentes múltiples, permite que residan simultáneamente en la memoria primaria las instrucciones y los datos procedentes de dos o más procesos distintos.

El concepto de multiprogramación no se debe confundir con operación de multiproceso. Si bien el primer término implica el último, no sucede en viceversa. La operación multiproceso es la gestión de la totalidad de los recursos del sistema de la computadora mediante la implementación de cauce segmentado.

La multiprogramación modificó el esquema de implementación de sistemas operativos por lotes al permitir alcanzar una mayor interacción entre los usuarios y programas en ejecución, un mayor rendimiento total del sistema y la creación de sistemas dinámicos que facilitan la implementación y estandarización de nuevos programas y conexiones con sistemas externos.

Tipos de sistemas operativos de multiprogramación

Sistemas de tiempo compartido

El tiempo compartido es bastante popular en sistemas multiprogramados y multiusuarios. Estos sistemas de multiacceso dedican la mayor parte del tiempo de ejecución de un programa único de una gran aplicación. Son elementos dedicados que poseen un buen tiempo de respuesta del terminal y tratan con frecuencia de proporcionar un reparto equitativo del los recursos comunes para dar la ilusión a cada usuario de poseer la máquina para el mismo. Por ejemplo, cuando se carga el sistema, se hace esperar más a los usuarios con más requerimientos de proceso pedido.

El algoritmo empleado en el despacho de las actividades es único de la filosofía de estos sistemas. El mismo consiste en la ejecución de los programas con prioridad rotatoria que se incrementa con las esperas y cae después de que se dispensó el servicio. El sistema operativo interrumpe un programa y lo pone al final de la cola de programas en espera, cuando es más largo que la fracción de tiempo definida por el sistema, para prevenir a los programas de la monopolización del procesador.

El gestor de memoria del sistema proporciona el aislamiento y protección de los programas en ejecución. Sin embargo, se puede emplear un espacio de memoria compartido controlado por el sistema operativo.

Los componentes de entrada salida se rigen por un gestor I/O que debe permitir la cooperación con múltiples usuarios y dispositivos. Debido a la lenta respuesta de los terminales humanos y la reacción o respuesta humana, el procesamiento de las interrupciones del terminal no necesitan ser críticas para el tiempo.

El gestor de archivos, por otro lado, debe proporcionar protección y control de acceso, dado a la posibilidad de concurrencia y de conflictos al acceder a los archivos.

Sistemas de tiempo real

Se usan en entornos donde se deben aceptar y procesar en tiempo breve y sin tiempos muertos un gran número de sucesos, en su mayoría externos al sistema de computadora. Son asuntos secundarios la conveniencia del usuario y la utilización de recursos.

Estos sistemas se caracterizan principalmente por encargarse de procesos que se activan al ocurrir el suceso relacionado, señalado frecuentemente por una interrupción. Se consigue el multiproceso planificando los procesos independientes unos de otros. Se asigna a cada proceso un cierto nivel de prioridad que corresponde a la importancia relativa de los sucesos que sirve. El procesador está normalmente asignado al proceso con más alta prioridad entre los que están listos para ejecutarse. Esta forma de planificación se llama planificación basada en la prioridad preferente.

A diferencia de los sistemas de tiempo compartido, el gestor de memoria está diseñado para tener un menor tiempo de respuesta debido a que los programas generalmente residen permanentemente en la memoria ya que usualmente son sistemas estáticos no diseñados para permitir la implementación de programas externos.

El tiempo crítico del gestor de dispositivo es una de las características principales de los sistemas de tiempo real. Además de las formas sofisticadas proporcionadas de gestión de interrupciones y almacenamiento intermedio, estos sistemas proporcionan frecuentemente llamadas del sistema que permiten a los procesos de usuario conectarse directamente a vectores de interrupción y sucesos de servicio. De esta forma, los programas pueden ceder espacio en procesador a otras ejecuciones con el levantamiento de una interrupción.

Generalmente estos sistemas de operación no poseen ningún tipo de almacenamiento secundario, solamente en aquellos sistemas de grandes magnitudes en los cuales si es requerido se necesita la implementación de un gestor de archivos que como en otros tipos de sistemas operativos debe manejar protección y control de acceso.

Sistemas operativos combinados

Lamentablemente el uso de sistemas operativos de tiempo son demasiado específicos y por lo tanto, a pesar de su gran utilidad en sistemas estáticos de gran escala, no satisfacen la necesidades de los usuarios en computadoras personales y emergentes dispositivos portátiles. Por dicha razón surge la necesidad de implementar sistemas operativos combinados que emplean las ventajas de los sistemas operativos de multiprogramación conjuntamente con los sistemas operativos por lotes.

Los sistemas combinados se caracterizan por poseer bloques o lotes que se ejecutan con una menor prioridad que el resto de las aplicaciones. Así se logra el uso completo del procesador al eliminar los tiempos muertos o de espera.

Diseño e implementación de los sistemas operativos

Requerimientos funcionales

  • Los procesos se crean en respuesta a los requerimientos explícitos o implícitos de los usuarios.
  • Los procesos emiten peticiones de los servicios y recursos del sistema.
  • El sistema operativo proporciona funciones de supervisión y control para la gestión de los procesos, tales como la creación y la retirada.
  • La gestión de la memoria consiste principalmente en la asignación de la memoria física a los requerimientos de los procesos.
  • Llamadas a sistema por parte de los usuarios.
  • Integridad de los archivos ante el mal funcionamiento del equipo y caídas del sistema.
  • Implementación de archivos comunes y documentos compartidos entre las distintas tareas.

Implementación

El desarrollo de sistemas operativos de gran complejidad, como cualquier proyecto de programación de grandes extensiones, generalmente termina en el fracaso y el desastre. La implementación de subconjuntos del sistema, por otro lado, permite la elaboración de estratos de jerarquía que permiten obtener una mayor abstracción de programación, confiabilidad en las etapas inferiores, así como también la habilidad de modificar secciones por separado o la inclusión de nuevos elementos o customización del ambiente de usuario o interface.

Podemos así subdividir un sistema en diversos estratos que generalmente son implementados:

  • Núcleo: con frecuencia llamado Kernel, realiza operaciones proporcionadas directamente sobre el hardware, gestionando los procesos en funcionamiento mediante estructuras de datos que representan el estado del sistema. Es aquí donde se encuentra el planificador el cual se encarga de determinar que proceso se debe ejecutar al finar la tarea actual. Este scheduler se encarga primoramente de cambiar el contexto de ejecución, lo que incluye guardar el estado en proceso y restaurar el de llegada. Por otro lado, este nivel se encarga de gestionar la interrupciones y proporcionar las facilidades para la conexión de las rutinas de servicio a las interrupciones de Hardware. También se puede proporcionar en este nivel los mecanismos básicos para la sincronización entre procesos y comunicación como semáforos o mensajes.
  • I/O básica: conformado por las instrucciones de bajo nivel que permiten la implementación de los bloques de datos de la memoria secundaria. Proporciona una abstracción de almacenamiento como una matriz lineal de bloques de datos.
  • Gestión de memoria: en este nivel se asigna memoria principal a los procesos a ejecutar y se librea cuando no se necesite más. Es aquí donde se de lleva a cabo la implementación de la memoria virtual, que da al programador la ilusión de poseer un basto recurso de memoria de almacenamiento. Estos módulos manejan las interrupciones de Hardware que señalan los intentos direccionar los datos que no se encuentran en la memoria principal; en tal caso los bloques de datos no encontrados son traídos desde el almacenamiento secundario usando las facilidades del nivel anterior de I/O básica.
  • Sistema de archivos y dispositivos: proporciona las facilidades para el almacenamiento durante largo tiempo y la manipulación de archivo. Posee un nivel más sofisticado en la asignación de espacio y acceso a los datos en almacenamiento secundario que el nivel de I/O básico. En este nivel, los archivos no se modifican mediante el direccionamiento de los bloques de datos ya que la lectura y escritura de datos se hace por medio de direcciones relativas al archivo a modificar. Así también, en este nivel se puentean y gestionan los dispositivos externos y periféricos mediante una vista uniforme proporcionada por una interfaz estándar que también se extiende a una facilidad de comunicación entre programas, llamada encauzamiento (pippe), que es esencialmente un canal de comunicación virtual de un sentido.
  • Intérprete del lenguaje de órdenes: este nivel proporciona la interfaz entre los usuarios interactivos y el sistema operativo. Los módulos de este nivel, emplean las facilidades proporcionadas por niveles bajos para aceptar líneas de órdenes desde los terminal. Entonces se reconocen estas líneas de entrada para separar las órdenes de los parámetros e identificar el tipo de servicio requerido.

Implementación de sistemas operativos en el microprocesador MC9S08QE128

Si bien este microprocesador puede parecer bajo el entorno actual un tanto lento, no se debe olvidar que años atrás con microprocesadores de mucha menor capacidad de procesamiento ya existían sistemas operativos en computadoras, redes de comunicación, almacenamiento de datos bancarios, cohetes espaciales, etc. Si treinta años atrás la tecnología era suficiente para el desarrollo de estos sistemas, con mayor razón es posible en la actualidad. Problemáticas de implementación radican en el mal uso y mala elección de estrategias de programación del sistema operativo.

El micro disponible en el DEMOQE128 parece no haber sido diseñado con el objetivo de la implementación de sistemas operativos ya que por ejemplo no posee múltiples Stack Pointers (SP) para realizar el direccionamiento de las tareas en memoria principal y contiene numerosas aplicaciones y elementos innecesarios para tal fin. Sin embargo posee elementos suficientes para el desarrollo de un sistema operativo sencillo que puede ser empelado en la elaboración de proyectos de pequeña escala.

A continuación se expondrán algunos de los elementos principales que podría tener un sistema operativo elaborado para este microprocesador.

Task Control Block

Un task control block, es una estructura utilizada para manejar y administrar las tareas que se quieren ejecutar en un sistema operativo. Esta estructura debe poseer todos los atributos y campos necesarios para asegurar que el sistema operativo pueda cargar la tarea adecuada y ejecutarla. El TCB (Task Control Block), debe contener el identificador y la pila de la tarea correspondiente y dependiendo de la implementación, si es una lista doblemente enlazada en la que se encuentran los TCB de las tareas a ejecutar, el apuntador a la tarea siguiente y la tarea previa para que el despachador tenga la información necesaria para poder cargar la siguiente tarea a ejecutarse.

Un ejemplo en lenguaje C de un TCB se muestra a continuación:

typedef struct _TCB {

         char *StackPtr;	            //Apuntador al Stack 

	struct _TCB *Next;	    //Apuntador al siguiente TCB (tarea siguiete).
	struct _TCB *Prev;	    //Apuntador al TCB anterior  (tarea anterior).

	char  Stack[STACK_DEPTH];   //Tamaño del stack de la tarea.

	int  Task;      	    //Identificador de la tarea.

	char estado;                //Estado de la tarea (corriendo, bloqueado, en espera, etc.)

} TCB;

Tick

El tick o time switch es la interrupción del proceso en ejecución para la sustitución por otro en un sistema en el que dichas actividades comparten por separado, períodos de ejecución en el procesador. Este período de ejecución se recomienda que sea menor al tiempo de reacción del ser humano (0.12 segundos). Generalmente se emplea un temporizador de 10 nanosegundos(ns) que interrumpe el proceso de ejecución. Puede ser empleado para ello el Real Time Counter o RTC.

Implementación de timers o temporizador

Si bien la interrupción por serial, teclado o software están permitidas en el desarrollo de sistemas operativos, normalmente se estandarizan las interrupciones de los contadores para demandar menor hardware así como también para crear un mayor grado de abstracción en niveles superiores de programación.

En sistemas operativos basados en UNIX así como también en Windows, generalmente se emplea un número muy limitado de contadores. Estos elementos son globales en todas las aplicaciones que se ejecutan, ya que estos son gestionados por el Kernel. Usualmente se usan 2 contadores: uno de 10 nanosegundos y otro de 1 milisegundo. El primero además de ser empleado según la necesidad de los usuarios se utiliza como tick del sistema.

Implementando una lista con contadores con un ID correspondiente a dicho time counter, se pueden implementar los 10 nanosegundos y 1 milisegundo para interrumpir una tarea luego de un tiempo subdividido por la combinación de dichos contadores globales.

Por otro lado, no debemos olvidar la implementación de contadores de menor prioridad para llevar la hora en el sistema operativo.

Tareas

Las tareas son un conjunto de funciones o actividades que compartirán el uso de procesador en tiempos distintos de ejecución. Estas generalmente se definen como funciones de ejecución separada.

En sistemas temporales, se emplea un tick que llama a una función despachadora para cambiar de tarea y permitir así la sensación de multiprocesamiento (multitasking).

La estructura que se debe emplear en un micro como el MC9S08QE128 debe diseñarse tomando en cuenta la función del sistema. Si se desea implementar un ambiente que permita la introducción dinámica de nuevos programas por periféricos de entrada salida o de la memoria secundaria, es usualmente recomendable la implementación de una lista enlazada o doble enlazada entre las distintas tareas que se encuentran es espera de ejecución.

De elegir un sistema medianamente estático, con espacios de memoria definidos para cada una de las tareas, se puede utilizar el uso de un arreglo de elementos.

Sin importar la estructura elegida, la misma usualmente posee el valor address al lugar de memoria donde comienza a listarse la tarea, un ID que permite reconocer y buscar los programas en lista de ejecución, apuntadores de enlace (en caso de implementar lista enlazadas), bit de bloqueo y un valor de sleep.

Entre las funciones relacionadas a los procesos de ejecución se encuentra el de crear una nueva tarea, eliminarla, colocarla en modo sleep y el de bloquear la misma.

Tenemos como ejemplo la función de crear una nueva tarea:

void create_task(func_ptr PC, char *stack, int stack_size, char task_id) {

	int *SP;

	SP = (int *) (stack + stack_size - 2);

	*SP-- = (int) PC;

	*SP-- = 0;

	*SP = 0;

	tasks[task_id].SP = SP;

}

Despachador

Del inglés dispatch, es de las primeras funciones que se deben crear para realizar un sistema operativo que causa más problemas y dificultades de entendimiento. Es llamada al entrar en la interrupción tick y permite cambiar la tarea de ejecución en intervalos de tiempo.

#pragma INLINE

void dispatch(char nexto) {

	void *tmp;

	asm tsx;

	asm aix	#0x06;

	asm	sthx tmp;

	tasks[running].SP = tmp;
	
	running = nexto; 

	tmp = tasks[nexto].SP;

	asm ldhx tmp;

	asm sthx;
	
}

De la Función lo primero que se hace notar es el uso de #pragma INLINE. Esto es usado cuando la Función Dispatch es usada dentro de una interrupción (en este caso la de Tick) y se desea ser transparente impidiendo la perdida del Stack pointer al cual se retornará del interrupt.

Dispatch no hace más que cambiar la tarea a ejecutar. Primero que todo, se debe entender el funcionamiento de las interrupciones. El siguiente esquema obtenido del manual muestra los registros que son colocados en el Stack automáticamente al entrar en la interrupción del Tick.

Interrupción en el Stack.

Como se muestra en el esquema, se puede observar que la interrupción deja en el stack la parte alta y baja del SP, el valor de X, el acumulador A y el código de la condición de registro. La función expuesta almacena el valor del SP en X lo que deja al sistema apuntando 5 + 1 (al hacer tsx) posiciones en relación al lugar original en el que se encontraba antes de la acción de la interrupción. Posteriormente se almacena el valor de HX en una variable temporal, y se guarda el sitio de la memoria en el cual el programa se encontraba corriendo antes de la interrupción (tasks[running].SP = tmp;).

Finalmente se cambia la tarea que se encuentra en ejecución por la nueva tarea (next to) así como también se usa el temporal nuevamente para cargar la posición SP particular correspondiente al sitio donde la nueva tarea comenzará o se quedó ejecutando anteriormente y se guarda el dato en el registro SP.

De tal manera, se almacenaron los datos de ejecución al momento de parar la tarea y se cambia a la nueva actividad.

Planificador

Conocido como scheduler, es el módulo que se encarga de llevar a cabo la elección del próximo programa a ejecutar al entrar a la interrupción tick. El siguiente ejemplo es de un sistema en el que la estructura de listado de las tareas es un arreglo. en el mismo, existe un número limitado de tareas definido por la variable MAX_TASK.

void scheduler(void) {

	char next;

	next = running + 1;

	do {

		if ( next >= MAX_TASKS ) next = 0;

		if ( tasks[next].SP != 0 ) break;

		next++;

	} while (next != running);

	dispatch(next);

	asm pulh;

	asm rti;

}

Como se puede observar, la función no hace más que asignar el siguiente valor numérico en la lista y comprueba si no sobrepasa el valor máximo de tareas, en cuyo caso resetea el contador a cero. También se verifica si la tarea correspondiente a ese valor numérico realmente existe. Para definir dichas tareas se puede emplear el siguiente método que permite la implementación en funciones no por un número sino por un tag o nombre asignado por el programador.

#define       ONE_TASK (0)

#define       TWO_TASK (SERIAL_TASK + 1)

#define       THREE_TASK (SERIAL_TASK + 2)

#define       FOUR_TASK (SERIAL_TASK + 3)

La implementación en listas enlazadas no difiere mucho, simplemente se se asigna como siguiente tarea a ejecutar la siguiente a la actual.

Si bien el uso secuencial en este módulo de distribución de tiempo es fácil de implementar, no satisface completamente las necesidades del sistema operativo como tal. Es importante la inclusión funciones y elementos que permitan bloquear o colocar en sleep las tareas para así no desaprovechar los recursos en tareas que no deben ser procesadas. Es aún más importante la inclusión de prioridades o listas de prioridades en el planificador.

En la elaboración del sistema operativo se debe colocar un sistema de prioridades en la ejecución de las tareas ya que se logra una mayor eficiencia en el uso de los recursos así como también una mejor respuesta ante las necesidades de los usuarios.

Vector de Interrupción

Ver: Interrupciones

La siguiente función de interrupción llama al planificador que tras determinar la nueva a ejecutar llama al despachador que se encargará de hacer el cambio en memoria por dicha nueva tarea. Es importante eliminar las interrupaciones mientras nos encontramos en el vector de interrupción.

void interrupt VectorNumber_Vtpm1ovf TMR1_ISR(void) {

	TPM1SC_TOF = 0;

	scheduler();

}

Un ejemplo de una rutina que carga la siguiente tarea a ejecutarse en el sistema operativo para el DEMOQE128 utilizando como generador de ticks el modulo del RTC

void interrupt VectorNumber_Vrtc EJEMPLO_RUTINA_CAMBIO_CONTEXO_RTC (void){
   

        RTCSC = RTCSC | 0x80;       //Borra la solicitud de interrupción para poder recibir la siguiente interrupción del RTC.

        __asm sei;                  // Inhabilita las interrupciones.
     
        __asm tsx;                  // Guarda el Stack Pointer(SP) de la tarea actual en el Registro X.
        __asm pshx;                 // Pone en el Stack el byte menos significativo del Stack Pointer(SP).

        __asm pshh;                 // Pone en el stack el byte mas significativo del Stack Pointer (SP)
        __asm ldhx CurrentTask;     // Guardo en H:X la dirección de CurrentTask.
        __asm pula;                 // Saco el primer valor de mi stack (H) y lo guardo en el Acumulador.
        __asm sta , x;              // Guardo H en donde apunta el registro H:X (CurrentTask->StackPtr).
        __asm pula;                 // Saco el primer valor de mi stack (X) y lo guardo en el Acumulador.
        __asm sta 1, x;             // Guardo X donde apunta (H:X + 1). (CurrentTask->StackPtr + 1)
            
        __asm ldhx CurrentTask;    // Cargo en H:X la direccion del CurrentTask.
        __asm aix #2;              // Sumo 2, por el registro H:X que se guardo, para que la direccion del nuevo task sea la adecuada.//
        __asm ldhx , x             // Cargo H:X con lo que apunta CurrentTask.
        __asm sthx CurrentTask;    // Guardo la dirección de la siguiente tarea en CurrentTask.
    
        __asm ldhx ,x              // Carga lo que apunta H:X que es el StackPtr actual.
        __asm txs;                 // Guarda H:X en SP.
        __asm pulh;                // Des-apila el primer elemento de la Pila.
        __asm cli;                 // Habilita las interrupciones.
        __asm rti;                 // Retorna de la interrupción.

}