SISTEMA OPERATIVO MH 1.7 Diciembre, 1999 Carlos Villarrubia Luis Jiménez Eduardo Domínguez Escuela Superior de Informática Universidad de Castilla - La Mancha Ciudad Real, España Introducción MH es un sistema operativo multihilo creado con fines docentes. Sus objetivos son ilustrar de la forma más sencilla los conceptos teóricos comunes en los sistemas operativos modernos con un ejemplo real. Por comodidad de desarrollo, MH necesita del sistema operativo MS-DOS para su inicialización. Una vez inicializado MS-DOS no se utiliza nunca, e inclusive MH inhibe cualquier llamada a MS-DOS por parte de los hilos del sistema. MH es un sistema monoproceso con capacidad para la gestión de varios hilos de ejecución de ese proceso. Las llamadas al sistema están divididas en dos grupos: Un grupo para la creación, destrucción y gestión de hilos y otro grupo para la entrada/salida a través de la consola del sistema. El único proceso del sistema corresponde a la imagen del programa (que denominaremos en lo que sigue init.mh, aunque su nombre es arbitrario). Dentro de este proceso se pueden crear hilos de ejecución adi- cionales. Además se tienen tres funciones para realizar entrada y salida por la consola del sistema. Arquitectura del sistema El diseño de MH corresponde a un sistema operativo monolítico con una división en módulos según el objetivo de las variables y funciones del sistema. El sistema esta dividido en los siguientes módulos : consola : Incluye todo el tratamiento y funciones para la lectura y escritura de caracteres por la consola del sistema. hilos : Constituye la implementación de la gestión de los hilos del sistema. lista : Tiene funciones para la manipulación de las diferentes listas de hilos. memoria : Contiene el código para la inicialización y asignación de memoria del sistema. varios : Funciones de utilidad no específicas de MH. Adicionalmente se tiene un módulo (mh.c) con la inicialización del sistema. Gestión de procesos MH sólo tiene un proceso (proceso init) con un hilo de ejecución asociado y un espacio de memoria. Este proceso puede, de forma dinámica, crear y destruir hilos de ejecución adicionales. Un hilo de ejecución tiene pocos recursos asociados de forma individual. Exactamente un hilo de ejecu- ción sólo posee de forma individual una entrada en la tabla de hilos y una pila de tamaño predeterminado. La estructura de datos más importante para la gestión de hilos es la tabla de hilos (extern THilo thTablaHilos[NUMMAXHILOS])cuyos elementos son del tipo THilo. La implementación de la tabla de hilos se realiza con un array con un tamaño determinado. La identificación de cada hilo se hace a través del índice del hilo en el array. typedef struct { int estado; /* Estado del hilo */ int tipo; /* Tipo de hilo: nucleo o usuario */ int modo; /* Modo de ejecucion: nucleo o usuario */ unsigned ss; /* Segmento de pila */ unsigned isp; /* Puntero inicial de pila */ unsigned sp; /* Puntero de pila en modo */ int far *pmarca; /* Puntero a la marca del segmento de pila */ TRegsPila far *pregs; /* Puntero a la estructura inicial de re- gistros */ THiloFunc pfunc; /* Puntero a la funcion de un hilo del nucleo */ int arg; /* Argumento de la funcion de un hilo del nucleo */ unsigned long diftick; /* Diferencia de ticks para hilo dormido */ int antid; /* Indice del anterior hilo en la lista */ int sigid; /* Indice al hilo posterior en la lista */ } THilo; A su vez tenemos otras variables útiles para la gestión de hilos como int nHiloActual que indica el hilo actual en ejecución e int hHiloAnterior que indica el anterior hilo en ejecución. MH utiliza dos clases de hilos de ejecución : hilos del núcleo e hilos de usuario. La única diferencia entre ellos es que un hilo del núcleo utiliza el espacio de direcciones de memoria del núcleo y un hilo de usua- rio el espacio de direcciones de memoria del proceso del usuario (init). Los estados de los hilos son : EJECUCION, LISTO, DORMIDO y BLOQUEADO. Un hilo sólo puede estar en una lista, por lo tanto, los campos antid y sigid indican el antecesor y sucesor. Todo el estado de un hilo no se guarda en su estructura de la tabla de hilos. Cuando un hilo está en un estado diferente a ejecución el contenido de los registros del procesador excepto SS y SP se guarda en la cima de la pila. A tal efecto se utiliza la definición de funciones de manejo de interrupciones del compila- dor. De tal forma que una declaración del tipo : void interrupt manejador(void) tiene un inicio y fin con la siguiente secuencia de instrucciones máquina. push ax push bx push cx push dx push es push ds push si push di push bp teniendo en cuenta que cuando se produce una interrupción el procesador introduce el contenido de los registro de flags, CS e IP en la pila antes del inicio del manejador de interrupción tenemos la siguiente situación de la pila antes de la ejecución del manejador. Cima de la pila è BP DI SI DS ES DX CX BX AX IP CS FLAGS cuando acaba la función de manejo de la interrupción el compilador introduce las siguientes instrucciones máquina para recuperar el contexto de ejecución del hilo. pop bp pop di pop si pop ds pop es pop dx pop cx pop bx pop ax iret donde iret recupera los valores de los registro IP, CS y FLAGS de la pila. Con este esquema la conmutación de contexto entre dos hilos se realiza de forma automática por las fun- ciones definidas como manejadores de interrupciones donde en su cuerpo se cambian los valores de con- trol de la pila del procesador (SS y SP). void interrupt ConmutarContexto(void) { thTablaHilos[nHiloAnterior].sp = _SP; _SS = thTablaHilos[nHiloActual].ss; _SP = thTablaHilos[nHiloActual].sp; } La manipulación del contexto de ejecución de un proceso se realiza a través del campo estructura pregs de su entrada a la tabla de hilos que apunta a la cima de la pila que tiene tantos campos como registros el procesador excepto SS y SP. Estos campos son particularmente importantes en la inicialización de los hilos. La planificación de los hilos se realiza en la función void Planificador(void)donde se manipula la lista de hilos en estado de preparados para ejecución Tlista tlHilosPreparados y se actuali- za el valor de las variables nHiloActual y nHiloAnterior. La planificación puede ser apropiativa o no en función de la variable bApropiativo. Si esta variable tiene un valor distinto de 0 el manejador del reloj expulsará del procesador a este hilo cuando haya consumido TICKSQUANTUM ticks consecu- tivos de reloj. El número de ticks de reloj por segundo se define en la inicialización con la variable lHZReloj que puede tener un valor mínimo de 19 ticks/segundo y un valor máximo de 1.193.180 ticks/segundo aunque el límite real estará en función de la velocidad del procesador del sistema (típicamente a un máximo de 100.000 ticks/segundo en un sistema Pentium 133). Gestión de memoria La gestión de memoria es muy simple. En la inicialización se calcula la memoria total del sistema y se va asignando memoria para las pilas de todos los hilos posibles del sistema. No existe posibilidad, en la ver- sión actual, de tener un manejo de memoria dinámico. Cada hilo tiene una pila con un tamaño predeterminado que se asigna en la inicialización. Al fin de cada pila existe una marca con un valor predefinido que se comprueba periódicamente para detectar si ese hilo ha utilizado más zona de pila. Gestión de entrada/salida Los únicos dispositivos periféricos utilizados por MH son el teclado y la pantalla asociados al computa- dor. A este par de dispositivos se les denomina consola del sistema. La utilización de la pantalla es simple, pues es un dispositivo direccionable por posiciones de memoria. En la inicialización el sistema reconoce el modo gráfico de la tarjeta (monocromático o color) que es utilizado posteriormente. El teclado de la consola es el único dispositivo que puede dejar a un hilo en estado de BLOQUEADO. Esto es debido a que es posible no tener ningún carácter en el buffer de lectura del teclado. Creación de programas para MH El compilador utilizado para el sistema operativo MH y sus programas de aplicación ha sido Borland C++ 3.1. Este entorno de programación por defecto genera aplicaciones para el sistema operativo MS-DOS. En el caso del desarrollo del programa init.mh se tienen que utilizar los prólogos y epílogos de MH y no de MS-DOS. Esto se realiza enlazando en un archivo .COM de MS_DOS el modulo ccom.obj y los archivos objetos del programa init. Además se necesita enlazar la biblioteca de interfaz con MH, sistema.lib, así como la bi- blioteca crtl.lib para realizar aritmética con enteros largos u otras operaciones habituales en init. Por últi- mo se necesita convertir el archivo .COM al formato de MH, lo que se consigue mediante el ejecutable CONV.EXE. Para construir el ejecutable de la aplicación, init.mh, se utiliza un archivo Makefile en el subdirectorio init (directorio de la aplicación) que automatiza las tareas de generación. Este archivo Makefile deberá ser modificado para construir un ejecutable a partir de varios módulos (el ejecutable ejemplo init.mh sólo tiene uno, init.c) añadiendo las reglas de construcción correspondientes, y también debe ser modificado si se desea cambiar el nombre del ejecutable de la aplicación. Todas las llamadas al sistema se realizan a través de la interrupción 134 y los argumentos de las llamadas se pasan a través de los registros del procesador. Para facilitar la programación con MH se tiene una bi- blioteca sistema.lib que realiza estas operaciones y ofrece un entorno de programación en base a funcio- nes C. Se necesita por tanto incluir el correspondiente archivo de cabecera sistema.h en los módulos fuente de init que realicen llamadas al sistema operativo MH para poder compilarlos. Las diferentes llamadas al sistema con su interfaz en C son : Gestión de hilos : int MHNuevoHilo(pHiloFunc pHFuncion, int nArg); int MHTerminarHilo(int IdHilo); int MHFinHilo(void); int MHDormirHilo(unsigned long NumTicks); Gestión de la consola : char MHLeerCaracter(void); int MHEscribirCadena(const char far *c); int MHEscribirCaracter(char c); Un ejemplo de proceso init con la creación de tres hilos de ejecución que imprimen concurrentemente tres caracteres diferentes sería : #include “sistema.h” void Hilo(int narg) { int i; for (i=0 ; i<300; i++) MHEscribirCaracter(‘A’ + narg); } void main(void) { MHNuevoHilo(Hilo, 0); MHNuevoHilo(Hilo, 1); MHNuevoHilo(Hilo, 2); } Cambios respecto a la versión 1.6: - Corrección de errores en el manejador de la interrupción del reloj.