El sistema operativo OSO
Departamento de Ingeniería y Tecnología de Computadores
Universidad de Murcia
25 de abril de 2005
Índice
1. Introducción 2
2. Estructura del sistema 3
3. Ejecución del sistema operativo 4
3.1. Iniciación del sistema operativo . . . 4
3.2. Puntos de entrada al sistema operativo . . . 6
4. Gestión de procesos y de memoria 9 4.1. La tabla de procesos . . . 9 4.2. Estado de un proceso . . . 10 4.3. Contexto de un proceso . . . 11 4.4. Gestión de procesos . . . 13 4.4.1. Creación de procesos . . . 16 4.4.2. Planificación de procesos . . . 16 4.4.3. Apropiación de la CPU . . . 17 4.5. Gestión de memoria . . . 17 5. Sistema de ficheros 17 5.1. Gestión de los dispositivos de almacenamiento . . . 23
5.2. Gestión del sistema de ficheros . . . 24
6. Gestión de la E/S 25 6.1. Lectura de un dispositivo de almacenamiento . . . 27
6.2. Salida por pantalla . . . 28
6.3. Entrada por teclado . . . 28
7. Llamadas al sistema 28 7.1. Sincronización de procesos dentro del núcleo . . . 30
7.2. La llamada sys_crear_proceso . . . 31
7.3. La llamada sys_escribir . . . 31
7.4. La llamada sys_leer . . . 31
1. Introducción
Este documento describe la estructura, diseño e implementación del sistema operativo OSO. OSO es un sistema operativo monolítico muy pequeño con el que se pretende mostrar, de manera práctica, algunos de los conceptos fundamentales de los sistemas operativos actuales, en especial, el concepto de multitarea.
Entre los objetivos que han guiado el diseño de este sistema operativo podemos destacar los siguientes: El sistema operativo debe ser lo más pequeño posible. Por este motivo OSO sólo implementa tres llamadas al sistema básicas como son: la creación de procesos, la lectura de teclado y la salida por pantalla. La implementación de estas llamadas al sistema también será un aspecto importante ya que muestran cómo el sistema operativo ofrece sus servicios a los procesos de usuario.
El sistema operativo debe ser multitarea. La multitarea es uno de los conceptos más importantes y es fundamental entender su funcionamiento. En concreto, se pretende mostrar el funcionamiento del cambio de contexto, el bloqueo/desbloqueo de procesos y la planificación de los mismos.
El sistema operativo debe hacer una gestión básica de la memoria. En el caso de OSO la gestión de la memoria la vamos a basar en particiones estáticas de igual tamaño.
El sistema operativo debe gestionar algún tipo de sistema de ficheros. La importancia de ver en detalle el funcionamiento de los sistemas de ficheros es que en ellos se almacenan el código y los datos de los distintos programas que ejecuta el sistema operativo. Este objetivo también implica el uso de algún tipo de dispositivo de almacenamiento. Para simplificar estos dos objetivos de diseño se usa el disquete como dispositivo de almacenamiento y FAT12 como sistema de ficheros.
Por último, el sistema operativo debe ser interactivo. Esta característica facilita mucho el uso del sistema operativo al permitir al usuario controlar el funcionamiento del mismo. Este objetivo se va a conseguir mediante la entrada de información por teclado y la salida de información por pantalla, lo que supone una gestión básica de la E/S por parte del sistema operativo. Esta gestión de la E/S nos va a permitir, además, mostrar de forma natural uno de los aspectos fundamentales de la gestión de procesos como es el bloqueo de un proceso y su desbloqueo posterior.
En cuanto a la implementación son también varios los objetivos que hemos tenido en cuenta. Algunos de ellos son los siguientes:
La implementación debe centrarse en el sistema operativo y no en el hardware. Para cumplir este objetivo se utiliza el «modo real» de los procesadores actuales de Intel y compatibles. Este modo ofrece una visión del hardware muy sencilla, aunque tiene como principal problema el que no ofrece ningún mecanismo de protección hardware al sistema operativo.
La implementación debe hacerse en un lenguaje de alto nivel. En el caso de OSO, el lenguaje utilizado es C. El lenguaje ensamblador también se utiliza pero sólo en aquellos casos en los que es realmente necesario (como es la implementación del cambio de contexto). Un aspecto importante es entender cómo se relaciona el lenguaje de alto nivel con el lenguaje ensamblador. En el caso de C esta relación es mucho más directa que la proporcionada por otros lenguajes de alto nivel como Pascal o C++, lo que justifica el uso de este lenguaje.
El entorno de programación debe ser lo más sencillo y cómodo posible. Este objetivo se consigue de varias maneras, principalmente, utilizando el disquete como dispositivo de almacenamiento y FAT12 como sis-tema de ficheros, al igual que hace el sissis-tema operativo OSO. Al utilizar un sissis-tema de ficheros real se puede modificar su contenido fácilmente mediante las utilidades que existen en los sistemas operativos ac-tuales (como Windows y Linux). Así, podremos cambiar de manera sencilla el núcleo del sistema operativo cuando lo modifiquemos y los programa a ejecutar en él.
En las siguientes secciones vamos a describir cómo se han conseguido estos objetivos. En primer lugar de-scribiremos la estructura general del sistema operativo. Después veremos cómo se han implementado cada uno de sus distintos componentes.
2. Estructura del sistema
La figura 1 describe la estructura general del sistema en el que se ejecuta el sistema operativo OSO.
BIOS de tecladoHardware PIC Núcleo del
sistema operativo Llamadas al sistema Crear
proceso Escribir enpantalla
Leer de teclado Hardware Espacio del núcleo Espacio de usuario
Figura 1: Estructura general de OSO
Como podemos observar, la interfaz de llamadas al sistema sólo proporciona tres servicios básicos a los pro-gramas de usuario: la creación de procesos, la lectura de caracteres de teclado y la salida por pantalla de cadenas de caracteres.
Para implementar las tres llamadas al sistema, OSO necesita acceder al hardware. Esto no siempre es así, es decir, podría haber llamadas al sistema que no necesitaran acceder al final al hardware. OSO accede al hardware, bien directamente, bien indirectamente a través de la BIOS. Aunque el sistema operativo puede omitir la BIOS y acceder directamente a cualquier dispositivo hardware, en el caso de OSO se ha optado por hacer uso de algunos servicios de la BIOS en aras de simplificar la implementación del sistema operativo. No obstante, es importante saber que los sistemas operativos actuales rara vez hacen uso de la BIOS ya que ésta presenta algunos problemas para el sistema operativo (no es reentrante, habilita las interrupciones, etc.).
Como muestra la figura, OSO hace uso de la BIOS para leer de disco y escribir en pantalla, y accede direc-tamente al hardware para leer de teclado y para indicar al PIC (Programmable Interrupt Controller) que una interrupción ha sido tratada.
De las distintas interrupciones existentes, OSO sólo va a controlar dos: la del reloj (o timer) y la del teclado. La interrupción de reloj se va a utilizar para implementar la multitarea. Así, se le va a poder quitar la CPU a un proceso cuando éste lleve demasiado tiempo en ella. La interrupción del teclado, por su parte, se va a utilizar para desbloquear a aquellos procesos bloqueados que esperan un carácter de teclado.
Las llamadas al sistema se solicitan con una interrupción software (ejecución de la instrucción de salto int 22h). Esta interrupción software y las dos interrupciones hardware (la de reloj y la de teclado, ambas iniciadas por la activación eléctrica de cables) constituyen los tres únicos puntos de entrada al sistema operativo, es decir, estas tres interrupciones son las que hacen que el procesador deje de ejecutar código de los procesos de usuario para ejecutar código del sistema operativo.
Un aspecto a tener en cuenta es que OSO se ejecuta en el modo real de los procesadores de Intel y compatibles. Ya que este modo no ofrece ningún tipo de protección, los procesos de usuario podrán acceder directamente al hardware sin que el sistema operativo lo pueda evitar. También podrán acceder directamente a la memoria de otros procesos o a la del propio sistema operativo e interferir en su ejecución.
3. Ejecución del sistema operativo
En esta sección vamos a describir primero qué pasos realiza el sistema operativo cuando comienza a ejecutarse y después veremos los tres puntos de entrada al sistema operativo una vez iniciado el mismo.
3.1. Iniciación del sistema operativo
El siguiente fichero fuente, kernel.c, contiene la función main del sistema operativo OSO, es decir, de-scribe lo que hace el sistema operativo cuando comienza su ejecución:
#include "string.h" #include "fat.h" #include "procesos.h" #include "sais.h"
#include "kernel.h" 5
/* El sistema operativo ejecuta indefinidamente el procedimiento “idle” * mientras espera la llegada de peticiones de servicio y mientras no * haya otros procesos listos para ejecutarse .*/
10
static char molinillo[ ] = {’|’, ’/’, ’-’, ’\\’}; void idle(void)
{
long int i; 15
unsigned char j;
unsigned char far *pantalla = (char far *)0xb8000000; j=0; for(;;) 20 { for (i = 0; i < 2000000; i++) ; pantalla[0] = molinillo[j]; j = (j + 1) % 4; 25 } }
struct dispositivo * disquete = NULL;
30
void main(void) {
kputs("Inicializando la tabla de dispositivos\n\r"); inicializar tabla dispositivos();
35
kputs("Inicializando la tabla de procesos\n\r"); inicializar tabla procesos();
kputs("Creando proceso 0 (idle)\n\r");
crearproceso0(); 40
disquete = abrir dispositivo(0);
kputs("Estableciendo SAIs\n\r");
establecersai(sai timer, 0x08); 45
establecersai(sai teclado, 0x09); establecersai(sai llamadas, 0x22);
kputs("Cargando programa SHELL.COM. . . ");
if (crearproceso(disquete, "SHELL.COM") < 0) 50
kputs("Fallo\n\r"); else
kputs("Correcto\n\r");
kputs("Sistema operativo cargado\n\r"); 55
idle(); }
/* $Id: kernel.c,v 1.5 2004/04/12 17:12:56 piernas Exp $ */ 60
Lo primero que hace el sistema operativo es inicializar algunas estructuras de datos internas para que ten-gan valores conocidos. Estas estructuras son: la tabla de dispositivos de almacenamiento, configurada por la función inicializar_tabla_dispositivos, y la tabla de procesos, configurada por la función inicializar_tabla_procesos.
El siguiente paso es crear el proceso 0 mediante la función crearproceso0. El «proceso 0» va a ser el flujo inicial de la función main de nuestro sistema operativo y es, por tanto, un proceso especial. Este proceso se ejecuta sólo cuando no hay ningún otro proceso listo. En nuestro caso el proceso 0 va a estar ejecutando indefinidamente el código de la función idle a la que se salta al final de la función main.
La implementación de la función idle se encuentra justo antes de la función main. Lo único que hace esta función es ejecutar un bucle infinito que dibuja un «molinillo» en la esquina superior izquierda de la pantalla. El molinillo se construye mostrando repetidamente y de forma indefinida la secuencia de caracteres «|, /, -, \».
Ya que el dispositivo de almacenamiento principal de OSO es el disquete, lo siguiente que se hace, tras crear el proceso 0, es abrir dicho dispositivo de almacenamiento mediante la función abrir_dispositivo. La primera unidad de disquetes se identifica con el número 0 y ése es el número que se pasa como parámetro a la función.
El valor devuelto por la función abrir_dispositivo se almacena en la variable disquete y es un puntero a una estructura de datos que contiene información sobre el sistema de ficheros FAT del disquete. Esta información se utilizará cuando se quiera acceder al disquete para, por ejemplo, cargar un programa.
Lo siguiente que hace el sistema operativo es establecer los tres puntos de entrada al mismo. Estos tres puntos de entrada ya los hemos descrito en la sección 2 anterior y son: llamadas al sistema (que se solicitan mediante una interrupción software), interrupción de reloj e interrupción de teclado.
Para establecer un punto de entrada lo que se hace es modificar un vector de interrupción con la dirección de inicio de la «subrutina de atención a interrupción» (SAI ) correspondiente. La interrupción de reloj tiene asociado el vector 8, la de teclado el vector 9 y la instrucción int 22h el vector 22h. Las subrutinas que se afilian, mediante la función establecersai, con cada uno de estos vectores son, respectivamente, sai_timer, sai_tecladoy sai_llamadas, y se describirán en detalle más tarde.
Lo último que hace el sistema operativo es crear, mediante la función crearproceso, el primer proceso que se ejecutará en el «espacio de usuario». En este caso se trata del programa SHELL.COM que desempeña un papel similar al del proceso init de los sistemas Unix. El programa SHELL.COM es un pequeño intérprete de órdenes que ejecuta cualquier programa cuyo nombre se introduce por teclado. Puede ver más información sobre este programa en el documento titulado «Desarrollo de aplicaciones en el sistema operativo OSO».
Cuando llegamos a este punto, el sistema operativo ya ha terminado de iniciar todos los componentes del sistema. Lo último que hace es saltar a la función idle cuyo código se ejecutará indefinidamente como un proceso más (el «proceso 0» que hemos comentado antes).
El núcleo del sistema operativo es un ejecutable COM que necesita un fichero c0t especial para compilarse. A continuación mostramos dicho fichero que se llama c0t_krnl.asm en nuestro caso:
model tiny .code EXTRN main:NEAR .startup call main 5 END
; $Id: c0t krnl.asm,v 1.3 2004/04/12 16:31:17 piernas Exp $
El fichero c0t_krnl.asm lo único que contiene es una llamada a la función main que acabamos de de-scribir. No obstante, es importante darse cuenta de que el sistema operativo necesita un entorno adecuado para su ejecución. Como podemos ver, dicho entorno no se establece dentro de este fichero (aunque se podría hacer). Nosotros hemos optado por que sea el programa cargador el que establezca el entorno de ejecución adecuado del sistema operativo antes de cederle el control de la CPU.
Finalmente, mostramos el contenido del fichero cabecera kernel.h en el que únicamente podemos destacar la declaración de la variable disquete como variable externa, ya que será utilizada en otros módulos del programa.
#ifndef KERNEL H #define KERNEL H
/* Las siguientes tres constantes se definen y usan en el cargador. Aquí se
* muestran a título informativo */ 5
/* Zona de memoria ocupada por el sistema operativo. La memoria libre empieza a partir de 1000:0000 */
#define INICIO SO ((char far *)0x00500100)
#define FIN SO ((char far *)0x0050FB00) 10
/* Tamaño máximo del sistema operativo: 8 KB */
#define SECTORES SO 16
/* Variable de tipo dispositivo para el disquete (supone que el SO 15
* se ejecuta desde un disquete */
extern struct dispositivo * disquete;
#endif 20
/* $Id: kernel.h,v 1.3 2004/04/12 16:31:17 piernas Exp $ */
3.2. Puntos de entrada al sistema operativo
Como hemos comentado en el apartado anterior, el proceso 0, que es el flujo inicial de nuestro sistema opera-tivo, sólo se ejecuta cuando no hay ningún proceso listo. En realidad, el proceso 0 no ejecuta todo el código del sistema operativo sino sólo una parte muy pequeña del mismo (la función main y las funciones que ésta invoca). Cuando hay procesos listos, la CPU se reparte entre ellos. El proceso 0 no se ejecuta y el sistema operativo permanece a la espera de ser activado para realizar alguna acción.
Como ya hemos comentado en la sección 2, el sistema operativo OSO puede ser activado por tres motivos: una llamada al sistema, una interrupción de teclado o una interrupción de reloj. En estos tres casos el control de la CPU pasa al sistema operativo que ejecuta parte de su código para atender a la interrupción. El código de estos tres puntos de entrada al sistema operativo se encuentra en el fichero sais.c que se muestra a continuación:
#include "asm.h" #include "llamadas.h" #include "io.h" #include "procesos.h"
5
void establecersai (void (*sai) (void), unsigned char numvector) {
unsigned long far * vector;
asm {cli} 10
vector = (long far *) ((int)numvector * 4); *vector = (long)sai;
asm {sti} 15
}
/* SAI para las llamadas al sistema */
void sai llamadas(void)
{ 20 salvar contexto; if ((current−>contexto−>ax >> 8) > NR LLAMADAS) current−>contexto−>ax = −ENOSYS; else 25 current−>contexto−>ax =
(tabla llamadas[current−>contexto−>ax >> 8].func)(); restaurar contexto;
} 30
/* SAI para la interrupción de teclado */
void sai teclado(void) {
salvar contexto; 35
recoger caracter(); asm {
/* EOI. Decimos al PIC que la interrupción ha sido tratada. */ 40
mov al, 20h out 20h, al }
restaurar contexto; 45
}
/* SAI para la interrupción del timer */
static char molinillo[ ] = {’|’, ’/’, ’-’, ’\\’};
static unsigned char j = 0; 50
static unsigned char far *pantalla = (char far *)0xb8000000; void sai timer(void)
{
salvar contexto; 55
/* Dibujamos un molinillo usando las interrupciones del timer */
pantalla[2] = molinillo[j]; j = (j + 1) % 4;
/* Comprobar si se deben parar los motores de las disqueteras */
tick pararmotor();
/* Le decimos al PIC que la interrupción ha sido tratada. */
asm { 65
mov al, 20h out 20h, al }
/* Actualizamos diversos valores, según el estado del proceso 70
interrumpido */
switch(current−>estado) {
case PROCESO SYSCALL: current−>tiempoSYS++;
break; 75
case PROCESO RUN:
current−>tiempoUSER++; current−>quantum−−; break; default: 80 ; /* Situación de error */ }
/* Llamamos al planificador por si hay que cambiar de proceso */
planificador(); 85
restaurar contexto; }
/* $Id: sais.c,v 1.3 2004/04/12 16:31:17 piernas Exp $ */ 90
El fichero cabecera asociado a sais.c es sais.h y es el siguiente:
#ifndef SAIS H #define SAIS H
void establecersai (void (*sai) (void), unsigned char numvector);
void sai llamadas(void); 5
void sai teclado(void); void sai timer(void); #endif
10
/* $Id: sais.h,v 1.2 2004/04/12 16:31:17 piernas Exp $ */
Se definen tres SAIs, una para cada punto de entrada. Además, se define la función establecersai que se utiliza en la función main del núcleo para asociar cada una de estas SAIs con una interrupción concreta.
Como podemos observar, la estructura de las tres SAIs es similar: se salva el contexto del proceso interrumpido, se hace cierto trabajo y se restaura el contexto del proceso al que se le concede la CPU. El proceso reanudado puede ser el mismo proceso interrumpido u otro proceso distinto seleccionado por el planificador. En la sección 4 que hay a continuación podrá encontrar una descripción más detallada sobre el contexto y la planificación de procesos.
La función sai_llamadas atiende las llamadas al sistema. Básicamente, lo que hace es comprobar que la llamada existe (línea 23) y ejecutar la función asociada a dicha llamada (línea 27).
La función sai_teclado atiende las interrupciones de teclado. Esta función llama a la función recoger_caracterpara leer del hardware de teclado el carácter introducido. Ya que la interrupción de teclado es una interrupción hardware, hay que indicar al PIC que la interrupción ha sido tratada (líneas 39–43) para que así el PIC pueda atender a más interrupciones de teclado.
La función sai_timer atiende las interrupciones de reloj. Esta función desempeña tres tareas distintas: llama a la función tick_pararmotor (para parar el motor de la disquetera si ha transcurrido cierto tiempo), actualiza ciertos campos de tiempo que existen en la estructura PCB del proceso actual (líneas 72–82) y llama a la función planificador para cambiar de proceso si es necesario (lo cual ocurrirá cuando el proceso actual haya agotado su quantum).
Al ser las interrupciones de reloj interrupciones hardware, la función sai_timer también necesita decirle al PIC que ha tratado la interrupción (líneas 65–68) para así poder recibir más interrupciones de reloj.
Como podemos ver, la función sai_timer dibuja otro molinillo (como la función idle descrita anterior-mente) para poder ver visualmente cuándo se produce una interrupción de reloj. Este molinillo se dibuja en la segunda posición de la esquina superior izquierda de la pantalla.
La explicación completa de cada una de estas SAIs se verá en las sección correspondiente, cuando veamos procesos, entrada/salida y llamadas al sistema.
4. Gestión de procesos y de memoria
Esta sección describe la gestión de procesos y de memoria que realiza OSO. En especial, la sección se centra en la gestión de procesos ya que la gestión de memoria implementada es muy sencilla y se describirá de manera breve al final de la sección, en el apartado 4.5.
4.1. La tabla de procesos
La gestión de procesos en OSO es bastante simple. OSO mantiene una «tabla de procesos» que contiene un «bloque de control de proceso» o PCB para cada proceso. La estructura PCB se define en el fichero cabecera procesos.hque se muestra a continuación:
#ifndef PROCESOS H #define PROCESOS H #include "fat.h"
5
#define MAX PROCESOS 10 #define QUANTUM 2 struct contexto
{ 10
unsigned int anteriorSS; unsigned int anteriorSP; unsigned int ax; unsigned int bx;
unsigned int cx; 15
unsigned int dx; unsigned int ds; unsigned int es; unsigned int di;
unsigned int si; 20
unsigned int bp; unsigned int ip; unsigned int cs; unsigned int estado;
}; 25
struct PCB {
unsigned int pid;
unsigned int registroSP; unsigned int estado; unsigned int quantum;
int prioridad;
unsigned long tiempoUSER; 35
unsigned long tiempoSYS; unsigned int memoria; struct contexto far * contexto; };
40
extern struct PCB * current; #define PCB LIBRE 0 #define PROCESO LISTO 1
#define PROCESO RUN 2 45
#define PROCESO SYSCALL 3 #define BLOQ TECLADO 4 void inicializar tabla procesos();
void crearproceso0(void); 50
int crearproceso(struct dispositivo far * pdis, char far * programa); void desbloquearprocesos(int bloqueadosen);
void planificador(void); void ceder CPU(void);
55
#endif
/* $Id: procesos.h,v 1.5 2004/04/19 19:12:07 piernas Exp $ */
Para cada proceso la estructura PCB correspondiente almacena: el identificador del proceso (pid), un puntero a la cima de la pila (registroSS y registroSP), el estado del proceso (estado), unidades de tiem-po que le quedan al proceso para agotar su quantum (quantum), la prioridad del proceso (prioridad), unidades de tiempo que el proceso se ha estado ejecutando en modo usuario (tiempoUSER) y en modo núcleo (tiempoSYS), segmento de memoria asignado al proceso (memoria) y un puntero al contexto del proceso (contexto). La utilidad de cada uno de estos campos la veremos más tarde.
La tabla de procesos es una estructura de datos estática por lo que el número máximo de proceso que se pueden ejecutar a la vez está limitado (en este caso, a 10, tal y como indica la constante MAX_PROCESOS definida en la línea 6 del fichero procesos.h).
4.2. Estado de un proceso
Como podemos ver en el fichero procesos.h (líneas 41–45), un proceso puede estar en 4 estados posibles: PROCESO_LISTO. Indica que el proceso está esperando a que se le conceda la CPU.
PROCESO_RUN. Indica que el proceso está usando la CPU.
PROCESO_SYSCALL. Este estado indica que el proceso está en mitad de la ejecución de una llamada al sistema y que no se le puede quitar la CPU. Como veremos en la sección 7, este estado se utiliza cuando un proceso ejecuta una llamada al sistema que necesita hacer uso de la BIOS.
BLOQ_TECLADO. Indica que el proceso está bloqueado esperando a que se introduzca un carácter por teclado. En este estado al proceso no se le puede dar la CPU.
El quinto valor, PCB_LIBRE, no es un estado de proceso y se utiliza para indicar que una entrada de la tabla de procesos está libre.
4.3. Contexto de un proceso
Antes de ver la implementación de procesos en OSO, es importante entender qué es, cómo se guarda y cómo se restaura el contexto de un proceso.
Podemos decir que el «contexto» de un proceso lo constituyen todos aquellos datos que es necesario guardar cuando se interrumpe un proceso para que posteriormente, cuando se reanude el proceso, éste pueda continuar su ejecución de forma normal, como si nunca hubiera sido interrumpido.
En nuestro caso, el contexto de un proceso está formado por todos los registros del procesador. En el fichero asm.hse definen dos macros que permiten guardar el contexto de un proceso interrumpido y reanudar la ejecu-ción de un proceso. El contenido de este fichero es el siguiente:
#ifndef ASM H #define ASM H
/* La interrupción ya ha guardado en la pila CS, IP y los flags. Hay que
* guardar todos los demás registros. Los registros BP, SI y DI los apila 5
* automáticamente el compilador cuando ve que se usan, como será nuestro * caso al hacer “pop di” y “pop si” en restaurar proceso
*
* Las intrucciones que añade el compilador son:
* push bp; 10
* push si; * push di; *
* Para poder acceder a las variables dentro del núcleo, debemos poner en
* DS y en ES valores correctos. Como el núcleo es un programa COM, debemos 15
* hacer que DS y ES tengan el mismo valor que CS. Esto es lo que hacen las * tres últimas instrucciones en ensamblador.
*
* La última parte guarda la información de pila del proceso interrumpido
* en su PCB (guardando previamente la información anterior del PCB en la 20
* propia pila). Además, se hace que el campo “contexto” del PCB apunte a * la cima de la pila para poder acceder fácilmente a los registros
* almacenados y que configuran el contexto. */
25
#define salvar contexto \ asm { \ push es; \ push ds; \ push dx; \ 30 push cx; \ push bx; \ push ax; \ \ mov ax, cs; \ 35 mov ds, ax; \ mov es, ax; \ } \
AX = current−>registroSS; \
asm {push ax} \ 40
AX = current−>registroSP; \ asm {push ax} \
current−>registroSS = SS; \ current−>registroSP = SP; \
current−>contexto = (struct contexto far *) \ 45
(((long)current−>registroSS << 16) + current−>registroSP);
/* Restauramos el contexto para el proceso apuntado por “current”.
* 50
* al final del procedimiento, donde los añade el compilador, ya que * ejecutamos un iret antes.
*/
#define restaurar contexto \ 55
SS = current−>registroSS; \ SP = current−>registroSP; \ asm {pop ax} \
current−>registroSP = AX; \
asm {pop ax} \ 60
current−>registroSS = AX; \
current−>contexto = (struct contexto far *) \
(((long)current−>registroSS << 16) + current−>registroSP); \ asm { \ pop ax; \ 65 pop bx; \ pop cx; \ pop dx; \ pop ds; \ pop es; \ 70 pop di; \ pop si; \ pop bp; \ iret; \ } \ 75 #endif
/* $Id: asm.h,v 1.4 2004/04/19 19:12:07 piernas Exp $ */
Como podemos ver, la macro salvar_contexto almacena en la pila el valor de todos los registros del procesador a excepción de 3: CS, IP y el «registro de estado». Esto es así porque estos 3 registros los apila automáticamente el hardware cuando se produce una interrupción, que es la única causa por la que la ejecución de un proceso se puede interrumpir. Por tanto, no es necesario guardarlos de nuevo.
La macro salvar_contexto hace alguna cosa más. Además de salvar el contexto del proceso actual en la pila del propio proceso, asigna a los registros DS y ES un valor adecuado para así poder acceder a todas las variables definidas dentro del núcleo del sistema operativo (vea las líneas 33–36). Es decir, establece un contexto apropiado para la ejecución del sistema operativo. Ya que no modifica el registro SS podemos decir que cuando un proceso se ejecuta en «modo núcleo» usa la misma pila que usa cuando se ejecuta en «modo usuario». El hecho de que cada proceso tenga su propia pila cuando se ejecuta en «modo núcleo» es importante porque nos permite interrumpir a cualquier proceso dentro del núcleo, haciendo así que el núcleo del sistema operativo sea reentrante.
Lo siguiente que se hace en salvar_contexto es guardar en la pila el valor actual de los campos registroSSy registroSP del PCB del proceso interrumpido (el apuntado por current). Estos cam-pos apuntan en todo momento al último contexto almacenado en la pila. Ya que vamos a almacenar un nuevo contexto, es importante que el valor anterior de registroSS y registroSP se guarde para no perder así la dirección del contexto anterior.
Lo último que hace la macro salvar_contexto es guardar la dirección de la cima de la pila (es decir, la del nuevo contexto) en los campos registroSS y registroSP, y usar el valor de dichos campos para hacer que el campo contexto del PCB del proceso interrumpido apunte a la pila.
El campo contexto, que es de tipo struct contexto far *, nos permite tratar a la pila como a una estructura de datos más y nos facilita el acceso al valor de los registros almacenados en la pila cuando ésta guarda el contexto de un proceso interrumpido. También nos permite modificar fácilmente el valor de uno de dichos registros para así devolver un valor a un proceso cuando éste reanude su ejecución. La utilidad del campo contextola veremos al estudiar la implementación de las llamadas al sistema, en la sección 7.
Para restaurar el contexto de un proceso lo único que hay que hacer es devolver a los registros del procesador el valor que tenían cuando se interrumpió el proceso. Ya que estos registros se salvaron en la pila del proceso basta
con desapilarlos de la misma para restaurarlos. Al igual que antes, no es necesario desapilar los registros CS, IP y el «registro de estado» puesto que esto lo hace automáticamente el hardware cuando se ejecuta la instrucción iret.
La restauración del contexto se hace con la macro restaurar_contexto. Esta macro restaura el contexto del proceso apuntado por la variable current sin importar qué proceso sea éste.
Quizás se esté preguntando por qué es necesario el poder guardar y restaurar varios contextos. La respuesta es que un proceso ya interrumpido (y, por tanto, ejecutando código del núcleo del sistema operativo) puede ser interrumpido de nuevo. Vea el apartado 7.1 para más detalles.
4.4. Gestión de procesos
Una vez descritos el mecanismo de cambio de contexto y la tabla de procesos, es el momento de ver cómo gestiona OSO los procesos. La gestión de procesos se implementa en el fichero procesos.c, cuyo contenido es el siguiente: #include "io.h" #include "string.h" #include "procesos.h" #include "asm.h" 5
struct PCB tablaprocesos[MAX PROCESOS]; struct PCB * current = NULL;
/* El PCB 0 se asigna al sistema operativo, que ocupa los primeros 64 KB
de memoria. El resto de PCBs quedan libres para procesos. */ 10
void inicializar tabla procesos() {
int i;
unsigned int mem = 0;
15
for(i = 0; i < MAX PROCESOS; i++) { tablaprocesos[i].estado = PCB LIBRE; tablaprocesos[i].memoria = mem; mem += 0x1000;
} 20
}
/* El proceso 0 es un hilo dentro del propio sistema operativo */
void crearproceso0(void)
{ 25
current = tablaprocesos; current−>pid = 0;
current−>estado = PROCESO RUN; current−>quantum = QUANTUM; current−>prioridad = 0; 30 current−>tiempoUSER = 0; current−>tiempoSYS = 0; current−>memoria = 0; } 35
/* Crea un nuevo proceso para el fichero ejecutable “programa” del * dispositivo “pdis” */
int crearproceso(struct dispositivo far * pdis, char far * programa) {
int i; 40
struct PCB * proceso; struct contexto far * pila; long memoria;
{ if (tablaprocesos[i].estado == PCB LIBRE) break; } 50 if (i == MAX PROCESOS) return −1; proceso = tablaprocesos + i; memoria = (long)(proceso−>memoria) << 16; 55
if (cargarCOM(pdis, programa, (char far *)((long)memoria + 0x100))) return −1;
pila = (struct contexto far *)(memoria + 0xFFFE − sizeof(struct contexto)); 60
pila−>ax = 0; pila−>bx = 0; pila−>cx = 0; pila−>dx = 0; pila−>di = 0; 65 pila−>si = 0; pila−>bp = 0; pila−>estado = 0x0200; pila−>cs = (long)memoria >> 16; pila−>ip = 0x100; 70 pila−>ds = pila−>cs; pila−>es = pila−>cs; proceso−>pid = i; proceso−>registroSS = pila−>cs; 75
proceso−>registroSP = 0xFFFE − sizeof(struct contexto); proceso−>prioridad = 0;
proceso−>tiempoUSER = 0; proceso−>tiempoSYS = 0;
proceso−>quantum = QUANTUM; 80
asm {cli}
proceso−>estado = PROCESO LISTO; asm {sti}
return i; 85
}
/* Desbloquea a todos los procesos que están esperando en “bloqueadosen” */
void desbloquearprocesos(int bloqueadosen)
{ 90
int i;
for (i = 0; i < MAX PROCESOS; i++)
if (tablaprocesos[i].estado == bloqueadosen)
tablaprocesos[i].estado = PROCESO LISTO; 95
}
/* Selecciona, de forma circular, al siguiente proceso listo tras el * proceso apuntado por “actual” */
static struct PCB * round robin(struct PCB * actual) 100
{
int i = actual − tablaprocesos; int n = MAX PROCESOS;
while (n) 105 { i = (i + 1) % MAX PROCESOS; if (i == 0) { n−−; 110 continue;
}
if (tablaprocesos[i].estado == PROCESO LISTO) break; n−−; 115 } if (n == 0) i = 0; return tablaprocesos + i; 120 }
/* Este es el procedimiento que selecciona el siguiente proceso a ejecutar */
void planificador(void)
{ 125
/* No le podemos quitar la CPU a un proceso mientras se está atendiendo su llamada al sistema. Y hemos de cambiar de proceso si el proceso está bloqueado o si ha agotado su quantum. */
switch(current−>estado) {
case PROCESO SYSCALL: 130
break; case BLOQ TECLADO:
current = round robin(current); current−>estado = PROCESO RUN;
break; 135
case PROCESO RUN: if (current−>quantum)
break;
current−>estado = PROCESO LISTO;
current−>quantum = QUANTUM; 140
current = round robin(current); current−>estado = PROCESO RUN; default:
; /* situación de error */
} 145
}
static void nuevo proceso(void) {
salvar contexto; 150
planificador(); restaurar contexto;
} 155
/* El siguiente procedimiento permiten a un proceso ceder la * CPU a otro proceso. Esto es útil cuando un proceso se bloquea */
void ceder CPU(void)
{ 160
/* Simulamos una interrupción */
asm { pushf cli push cs 165 } nuevo proceso(); }
/* $Id: procesos.c,v 1.6 2004/04/12 16:31:17 piernas Exp $ */ 170
Al principio del fichero se definen las variables tablaprocesos y current. La primera, como su propio nombre indica, es la tabla de procesos que mantiene el sistema operativo. La segunda apunta en todo momento a la entrada de la tabla de procesos correspondiente al proceso actual, es decir, al proceso en ejecución.
4.4.1. Creación de procesos
La primera función, inicializar_tabla_procesos, inicializa la tabla de procesos marcando todas las entradas como libres. Además, inicializa el campo memoria de los distintos PCBs. Para un PCB concreto este campo indica el segmento de memoria que ocupará el proceso al que se le asigne dicho PCB.
La siguiente función, crearproceso0, inicializa la entrada PCB del proceso que ocupa la entrada 0 de la tabla de procesos. Como hemos comentado en la sección 3.1, este proceso 0 es el propio sistema operativo que, según la entrada PCB, ocupa los primeros 64 KB de memoria (lo que incluye la tabla de vectores de interrupción y la zona de datos de la BIOS).
La función crearproceso, que se describe en las líneas 38–86 del fichero procesos.c, es la más impor-tante de todas. Esta función crea un nuevo proceso para ejecutar el código del programa COM cuyo nombre se le pasa como argumento. El otro parámetro que se le pasa a esta función es el dispositivo de almacenamiento en el que se encuentra el programa a ejecutar.
Lo primero que hace la función crearproceso es buscar una entrada libre en la tabla de procesos. Si no encuentra ninguna la función termina. Si encuentra una entrada libre carga en el segmento de memoria asignado a dicha entrada el programa a ejecutar, haciendo uso de la función cargarCOM (líneas 54–58). Observe que el programa no se carga al principio del segmento sino 0x100 bytes más adelante ya que se supone que ése es el desplazamiento en el que comienza un programa COM dentro de un segmento.
Si el programa se ha podido cargar en memoria entonces la función crearproceso inicializa su pila (líneas 60–72) y su entrada PCB (líneas 74–83) y termina, devolviendo el identificador o PID del nuevo proceso (que se corresponde con su número de entrada en la tabla de procesos).
Lo más interesante es cómo se inicializa la pila ya que debe almacenar un contexto adecuado para así poder ejecutar el nuevo proceso cuando se restaure su contexto. Todos los registros almacenados en la pila van a tener un valor 0 salvo los siguientes:
CS, DS, ES y SS: todos van a tener el mismo valor, que será el segmento de memoria asignado a la entrada PCB seleccionada para el proceso.
IP: su valor inicial es 0x100 para un programa COM.
registro de estado: debe tener activo el bit IF para que se habiliten las interrupciones cuando el programa comience a ejecutarse. Se le asigna el valor 0x0200.
SP: apunta a la cima de la pila creada (vea las líneas 60 y 79). Ya que estamos suponiendo segmentos de 64 KB, la pila crece desde la dirección 0xFFFE hacia abajo.
4.4.2. Planificación de procesos
La planificación de procesos realizada por OSO consiste en una planificación circular con un quantum de 2 unidades de tiempo. Cada unidad de tiempo equivale al periodo de los ticks de reloj. Ya que se producen 18’2 ticks por segundo, podemos decir que el quantum equivale a 2 × 1
1802 = 00110segundos o 110 milisegundos.
La planificación de procesos se realiza en la función planificador (línea 124 del fichero procesos.c). Como vemos, sólo se cambia de proceso cuando el proceso actual está bloqueado (está en el estado BLOQ_TECLADO) o cuando está en ejecución y ha agotado su quantum (su estado es PROCESO_RUN y el campo current->quantum es 0). En estos dos casos se llama a la función round_robin que selecciona de forma circular al siguiente proceso listo tras el proceso actual.
En cuanto a la función round_robin, el único aspecto interesante que podemos comentar es que únicamente selecciona al proceso 0 (que es el propio sistema operativo) cuando no hay ningún otro proceso listo.
Otra función relacionada con la planificación es la función desbloquearprocesos (línea 89 de procesos.c) que se utiliza para poner como listo a cualquier proceso cuyo estado coincida con el que se pasa
como parámetro. Esta función la utilizará la SAI del teclado para desbloquear a todo proceso que se encuentre bloqueado esperando un carácter de teclado (el estado de estos procesos será BLOQ_TECLADO).
Finalmente, la función ceder_CPU (línea 159) la puede utilizar un proceso para ceder la CPU a otro proceso. Esto ocurrirá cuando un proceso se bloquee dentro del núcleo. En ese caso, el proceso no podrá continuar su ejecución hasta que no se produzca el evento que espera, por lo que tendrá que ceder la CPU para que el sistema no se quede parado.
La función ceder_CPU, junto con la función nuevo_proceso, simula una interrupción. Para ello, apila la palabra de estado del procesador (con pushf), el registro CS y el registro IP. Esto último lo hace de forma indirecta llamando a la función nuevo_proceso que actúa como una SAI.
4.4.3. Apropiación de la CPU
Para evitar que un proceso monopolice la CPU hay que desalojar periódicamente al proceso que se encuentre en ella. Para conseguir esto vamos a hacer uso de las interrupciones de reloj que trata la función sai_timer descrita en el apartado 3.2.
En cada interrupción de reloj, la SAI decrementa en una unidad el quantum del proceso en ejecución y llama al planificador. Si el quantum es 0, el planificador cambia de proceso. En caso contrario, deja la CPU al proceso interrumpido.
4.5. Gestión de memoria
En el apartado 4.4.1 hemos visto que cuando se inicializa la tabla de procesos en la función inicializar_tabla_procesosse inicializa también el campo memoria de los distintos PCBs. Como hemos dicho, para un PCB este campo indica el segmento de memoria que ocupará un proceso al que se le asigne dicho PCB.
Como podemos observar en dicha función, al PCB 0 se le asigna el segmento de memoria 0, al PCB 1 el segmento de memoria 0x1000, al PCB 2 el 0x2000, y así sucesivamente hasta el PCB 9, al que se le asigna el segmento de memoria 0x9000. Dicho de otra manera, al PCB 0 se le asignan los primeros 64 KB de memoria, al PCB 1 los siguientes 64 KB y así sucesivamente. En total, los 10 procesos que pueden existir a la vez ocuparán los primeros 640 KB de memoria RAM, que es la memoria disponible en el modo real para la ejecución de programas. Como vemos, la gestión de memoria de OSO es muy sencilla y se basa en un esquema de particiones estáticas del mismo tamaño donde a cada proceso se le asignará la partición correspondiente al PCB que use.
Esta gestión de memoria también permite cierta «protección» entre procesos. OSO sólo es capaz de ejecutar programas COM. Estos programas se ejecutan haciendo uso de un único segmento porque todos los registros de segmento, incluido el de pila, tienen el mismo valor. Ya que el tamaño del desplazamiento dentro de un segmento es de 16 bits, un programa COM no puede direccionar más de 64 KB y, por tanto, no puede acceder a ninguna dirección de memoria fuera del segmento de memoria asignado (siempre, claro está, que no modifique el valor de ningún registro de segmento).
5. Sistema de ficheros
OSO utiliza FAT12 como sistema de ficheros. Este sistema de ficheros es el que utiliza Windows en los dis-quetes. Como hemos comentado en la introducción, al utilizar un sistema de ficheros real podemos utilizar las herramientas que ya existen en sistemas operativos como Windows o Linux.
En esta sección no pretendemos describir de forma detallada cómo funciona FAT12 ya que es un sistema de ficheros ampliamente explicado y documentado. Lo que vamos a hacer es explicar aquellos aspectos de la implementación más interesantes.
El código fuente que implementa el sistema de ficheros en OSO aparece en los ficheros fat.h y fat.c. Estos ficheros se muestran a continuación:
#ifndef FAT H #define FAT H
#define TAMA SECTOR 512
#define NUM DISPOSITIVOS 4 5
struct dispositivo {
unsigned int unidad; unsigned int cabezas;
unsigned int sectores por pista; 10
unsigned int bytes por sector; unsigned long total sectores; unsigned int tipo fat; unsigned int inicio fat;
unsigned int entradas fat; 15
unsigned int inicio raiz; unsigned int entradas raiz; unsigned int inicio clusters; unsigned int sectores por cluster;
}; 20
void inicializar tabla dispositivos(void);
struct dispositivo * abrir dispositivo(unsigned char unidad);
25
int cerrar dispositivo(struct dispositivo * pdis); int cargarCOM(struct dispositivo far * pdis,
char far * programa,
char far * direccion); 30
#endif
/* $Id: fat.h,v 1.3 2003/07/24 15:21:38 piernas Exp $ */
#include "io.h" #include "string.h" #include "fat.h"
/* Estructura de un sector de arranque en un sistema de ficheros FAT */ 5
struct boot sector {
char salto[3]; /* 00h */
char identificacion[8]; /* 03h */
unsigned int bytes por sector; /* 0Bh */
unsigned char sectores por cluster; /* 0Dh */ 10
unsigned int sectores reservados; /* 0Eh */
unsigned char copias fat; /* 10h */
unsigned int entradas raiz; /* 11h */
unsigned int total sectores; /* 13h */
unsigned char formato disco; /* 15h */ 15
unsigned int sectores por fat; /* 16h */
unsigned int sectores por pista; /* 18h */
unsigned int cabezas; /* 1Ah */
unsigned int sectores ocultos; /* 1Ch */
unsigned int pad; 20
unsigned long total sectores long; /* 20h */
unsigned char unidad hd; /* 24h */
char reservado; /* 25h */
char marca; /* 26h */
unsigned long numero serie; /* 27h */ 25
char etiqueta[11]; /* 2Bh */
char cargador[0x1BE−0x3E]; /* 3Eh */ char particiones[512−0x1BE]; /* 1BEh */
}; 30
/* Estructura de datos para guardar información de los distintos * dispositivos abiertos */
struct dispositivo tabla dispositivos[NUM DISPOSITIVOS];
35
/* Inicializa la tabla de dispositivos */
void inicializar tabla dispositivos(void) { int i;
for (i = 0; i < NUM DISPOSITIVOS; i++) 40
tabla dispositivos[i].cabezas = 0; }
/* Abre el dispositivo especificado por “unidad”. Se supone que el dispositivo
* tiene formato FAT12 o FAT16 */ 45
struct dispositivo * abrir dispositivo(unsigned char unidad) { int i;
struct dispositivo * pdis; struct boot sector sector;
long total clusters; 50
for (i = 0; i < NUM DISPOSITIVOS; i++) if (tabla dispositivos[i].cabezas == 0)
break;
if (i == NUM DISPOSITIVOS) 55
return NULL;
if (leersector((char *)§or, 0, 0, 1, unidad)) {
kputs("ERROR: leyendo el sector de arranque\n\r");
return NULL; 60
}
pdis = tabla dispositivos + i;
/* Datos generales del disco */
pdis−>unidad = unidad; 65
pdis−>cabezas = sector.cabezas;
pdis−>sectores por pista= sector.sectores por pista; pdis−>bytes por sector = sector.bytes por sector; pdis−>total sectores= (long) sector.total sectores;
if (pdis−>total sectores == 0) 70
pdis−>total sectores = sector.total sectores long;
/* Directorio raiz */
pdis−>inicio raiz = sector.sectores reservados + sector.sectores por fat * sector.copias fat; 75
pdis−>entradas raiz= sector.entradas raiz;
/* Datos sobre clusters */
pdis−>inicio clusters = pdis−>inicio raiz + (pdis−>entradas raiz * 32) / pdis−>bytes por sector;
pdis−>sectores por cluster = sector.sectores por cluster; 80
/* Datos de la FAT */
total clusters = ((unsigned)pdis−>total sectores − pdis−>inicio clusters) / pdis−>sectores por cluster; pdis−>tipo fat = sector.formato disco;
pdis−>inicio fat = sector.sectores reservados; 85
pdis−>entradas fat = total clusters + 2; return pdis;
}
/* Lee “num” sectores del dispositivo “pdis” empezando en el sector “nsec”. 90
* los sectores leídos se almacenan a partir de la dirección “buffer” */
static int dispositivo leersectores (struct dispositivo far * pdis, unsigned nsec,
unsigned int num,
char far * buffer) { 95
unsigned int sector, cabeza, cilindro; int i;
if ((nsec + num − 1) >= pdis−>total sectores) {
kputs("Intentando acceder mas alla del final del dispositivo\n\r"); 100
kcadvalor("Sector: ", nsec); kcadvalor("Contador: ", num); return −1;
}
105
cilindro = nsec / (pdis−>cabezas * pdis−>sectores por pista); cabeza = (nsec % (pdis−>cabezas * pdis−>sectores por pista)) /
pdis−>sectores por pista;
sector = (nsec % (pdis−>cabezas * pdis−>sectores por pista)) %
pdis−>sectores por pista+ 1; 110
for(i = 0; i < num; i++) {
if (leersector(buffer, cilindro, cabeza, sector, pdis−>unidad)) { kputs("ERROR: E/S\n\r");
return −1; 115
} ++sector;
if (sector > pdis−>sectores por pista) { sector = 1; ++cabeza; 120 if (cabeza >= pdis−>cabezas) { cabeza = 0; ++cilindro; } } 125
buffer += pdis−>bytes por sector; }
return 0; }
130
/* Lee el cluster “cluster” (formado por cualquier número de sectores) del * dispositivo “pdis” y lo almacena en la dirección “buffer” de memoria */
static int dispositivo leercluster(struct dispositivo far * pdis, unsigned int cluster,
char far * buffer) { 135
unsigned sector cluster;
sector cluster = (cluster − 2) * pdis−>sectores por cluster; sector cluster += pdis−>inicio clusters;
140
return dispositivo leersectores(pdis,
sector cluster,
pdis−>sectores por cluster, buffer);
} 145
/* Cierra un dispositivo abierto previamente */
int cerrar dispositivo(struct dispositivo * pdis) {
int entrada = pdis − tabla dispositivos; 150
tabla dispositivos[entrada].cabezas = 0; return 0;
}
155
/* Formato de la entrada de directorio en un sistema FAT */
struct entrada dir {
char nombre[8];
struct { 160
unsigned char r: 1, h: 1, s: 1, e: 1, d: 1, a: 1, pad: 2; } atributos;
char reservado dos[10];
struct { unsigned int s: 5, m: 6, h: 5; 165 } hora; struct { unsigned int d: 5, m: 4, a: 7; } fecha;
unsigned int bloque inicio; 170
long tama; };
/* Devuelve el cluster inicial (cluster) y el tamaño (tama) del fichero
* “nomfich” del directorio raíz. Si el fichero existe, la función devuelve 175
* 0 y -1 en caso contrario. */
static int cluster inicial(struct dispositivo far * pdis, char far * nomfich, unsigned int far * cluster, long far * tama) { struct entrada dir far * dir;
char bloque[TAMA SECTOR]; 180
int i, lon;
char nombre[8], ext[3], far * psubstr; int raiz;
psubstr = kstrchr(nomfich, ’.’); 185
if (psubstr) {
lon = psubstr − nomfich; if (lon > 8)
lon = 8;
kstrncpy(nombre, nomfich, lon); 190
for (i = lon; i < 8; i++) nombre[i] = ’ ’; lon = kstrlen(++psubstr); if (lon > 3)
lon = 3; 195
kstrncpy(ext, psubstr, lon); for (i = lon; i < 3; i++)
ext[i] = ’ ’; } else { 200 lon = kstrlen(nomfich); if (lon > 8) lon = 8;
kstrncpy(nombre, nomfich, lon);
for (i = lon; i < 8; i++) 205
nombre[i] = ’ ’; for (i = 0; i < 3; i++)
ext[i] = ’ ’; }
210
raiz = pdis−>inicio raiz;
if (dispositivo leersectores(pdis, raiz, 1, bloque)) return −1;
dir = (struct entrada dir *) bloque;
215
for (i = 0; i < pdis−>entradas raiz; i++) { if (!kstrncmp(dir−>nombre, nombre, 8)
&& !kstrncmp(dir−>ext, ext, 3)) { *cluster = dir−>bloque inicio;
*tama = dir−>tama; 220
return 0; }
++dir;
if ((char *)dir >= (bloque + pdis−>bytes por sector)) {
if (dispositivo leersectores(pdis, raiz, 1, bloque)) return −1;
dir = (struct entrada dir *) bloque; }
} 230
/* No hemos encontrado el fichero */
*cluster = 0; return −1;
} 235
static int siguiente cluster 12(struct dispositivo far * pdis, int cluster) { unsigned int desplazamiento, aleer, bloque1, bloque2;
char bloque[2 * TAMA SECTOR];
unsigned int siguiente; 240
desplazamiento = ((cluster << 1) + cluster) >> 1; bloque1 = desplazamiento / pdis−>bytes por sector; bloque2 = (desplazamiento + 1) / pdis−>bytes por sector;
245 if (bloque1 == bloque2) aleer = 1; else aleer = 2; 250
bloque1 += pdis−>inicio fat;
if (dispositivo leersectores(pdis, bloque1, aleer, bloque)) return 0;
255
siguiente = *((unsigned int far *)(bloque + desplazamiento % pdis−>bytes por sector)); if (cluster & 0x0001) siguiente >>= 4; else 260 siguiente &= 0x0FFF; if (siguiente > 0xFF0) return 0; 265 return siguiente; }
static int siguiente cluster 16(struct dispositivo far * pdis, int cluster) {
return 0; 270
}
/* Devuelve el cluster que sigue a “cluster” o 0 si “cluster” es el último * cluster de un fichero o si se produce un error al leer de disco. */
static int siguiente cluster(struct dispositivo far * pdis, int cluster) { 275
switch(pdis−>tipo fat) { case 0xF0:
return siguiente cluster 12(pdis, cluster); case 0xF8:
return siguiente cluster 16(pdis, cluster); 280
} return 0; }
/* Carga el fichero “programa” del directorio raíz del dispositivo “pdis” en 285
* dirección de memoria “direccion”. */
int cargarCOM(struct dispositivo far * pdis, char far * programa, char far * direccion) {
unsigned int cluster;
long tama; 290
tama cluster = pdis−>bytes por sector * pdis−>sectores por cluster;
if (cluster inicial(pdis, programa, &cluster, &tama)) 295
return −1; while (cluster) {
dispositivo leercluster(pdis, cluster, direccion);
(long)direccion += tama cluster; 300
cluster = siguiente cluster(pdis, cluster); }
return 0;
} 305
/* $Id: fat.c,v 1.6 2004/06/14 13:49:02 piernas Exp $ */
Antes de comentar las estructuras de datos y funciones más importantes, conviene decir que en estos ficheros se están mezclando dos cosas: la gestión del dispositivo de almacenamiento y la gestión del sistema de ficheros en sí. Comentaremos las funciones que implementan cada una de estas gestiones de forma separada.
5.1. Gestión de los dispositivos de almacenamiento
OSO está preparado para manejar hasta 4 dispositivos de almacenamiento a la vez. Cada dispositivo de al-macenamiento debe tener un sistema de ficheros FAT ya que se va a usar la información que se almacena en el sector de arranque de este sistema de ficheros para conocer la geometría del disco (cilindros, cabezas y sectores por pista) y así poder averiguar la posición exacta del sector o sectores a leer o escribir.
La función inicializar_tabla_dispositivos (línea 37 del fichero fat.c) se invoca al inicio del sistema operativo. Esta función inicializa la estructura tabla_dispositivos en la que se guarda informa-ción para cada dispositivo abierto (es decir, en uso).
Antes de acceder a un dispositivo se debe llamar a la función abrir_dispositivo (línea 46). Esta función busca un hueco en la tabla de dispositivos para el nuevo dispositivo y rellena la entrada correspondiente con los datos que hay en el sector de arranque. En el sector de arranque encontramos datos sobre el propio dispositivo (cabezas, sectores por pista, bytes por sector, tamaño total del dispositivo) y datos sobre el sistema de ficheros FAT (copias de la FAT, sectores que ocupa cada copia de la FAT, tamaño del directorio raíz, sectores por cluster, tipo de FAT, etc.).
En la función abrir_dispositivo también calculamos ciertos valores que no aparecen directamente en el sector de arranque pero que son útiles a la hora de acceder a un sistema de ficheros FAT. Estos valores también se almacenan en la entrada correspondiente de la tabla de dispositivos. Algunos valores calculados son: sector de inicio del directorio raíz, sector de inicio del primer cluster, número total de clusters, etc.
A partir de este momento ya podemos acceder al dispositivo para leer o escribir, haciendo uso del puntero devuelto por la función abrir_dispositivo. En la versión de OSO que estamos describiendo sólo se im-plementa la lectura. Para leer existen dos funciones: dispositivo_leersectores (línea 92 de fat.c) y dispositivo_leercluster(línea 133 de fat.c).
La función dispositivo_leersectores lee un cierto número de sectores a partir de una determinada posición y los almacena en la dirección de memoria pasada como parámetro. Esta función nos presenta el dis-positivo como un array lineal de sectores, desde 0 hasta N − 1. Por tanto, está función tendrá que averiguar en qué cilindro, cabeza y sector dentro de una pista se encuentra cada uno de los sectores a leer, para lo que utilizará la información sobre la geometría del disco que se guarda en la tabla de dispositivos.
La función dispositivo_leercluster hace uso de la función anterior para leer de disco el cluster cuyo número se le pasa como parámetro. Ya que los clusters 0 y 1 no existen en realidad, tendrá que restar 2 al número de cluster pasado para poder calcular la posición exacta del cluster a leer.
Cuando ya no se va a usar más un dispositivo, se debe llamar a cerrar_dispositivo (línea 149) para liberar la entrada ocupada en la tabla de dispositivos.
5.2. Gestión del sistema de ficheros
En la implementación actual, OSO sólo es capaz de gestionar sistemas de ficheros FAT12, aunque el código está preparado para añadir fácilmente la gestión de sistemas de ficheros FAT16.
Otra limitación de OSO es que no reconoce subdirectorios. Sólo trabaja con el directorio raíz del sistema de ficheros. Por tanto, no implementa ningún mecanismo de resolución de rutas.
La gestión del sistema de ficheros que hace OSO se implementa principalmente en dos funciones:
cluster_inicialy siguiente_cluster_12.
La función cluster_inicial recibe el dispositivo y el nombre del fichero a buscar en el directorio raíz de dicho dispositivo y devuelve, en dos parámetros de salida, el cluster inicial y el tamaño del fichero. La función devuelve 0 si encuentra el fichero y -1 en caso contrario.
Para buscar el fichero en el directorio raíz, lo primero que hace cluster_inicial es descomponer la cadena de caracteres del nombre del fichero en los campos «nombre» y «extensión» (líneas 185–209 del fichero fat.c). Ambos campos están separados por el carácter «.». Ya que el campo «nombre» debe tener 8 caracteres, si en la cadena pasada como parámetro hay más de 8 caracteres antes del punto, se toman sólo los 8 primeros caracteres, y si hay menos, se rellena con espacios hasta completar los 8 caracteres. Con el campo «extensión» se hace exactamente lo mismo aunque en este caso para un tamaño de 3 caracteres.
Una vez descompuesto el nombre del fichero en los campos «nombre» y «extensión», se busca entre las entradas del directorio raíz una que tenga los mismos campos «nombre» y «extensión» que los buscados (líneas 211–230). Si encontramos dicha entrada, devolvemos el número de cluster y el tamaño almacenados en la misma. Un aspecto a tener en cuenta es que vamos a suponer que en un sistema de ficheros FAT12 los nombres de fichero se guardan en mayúsculas. Por ello, es importante que el nombre del fichero a buscar ya se encuentre en mayúsculas cuando se llame a la función cluster_inicial.
La función cluster_inicial nos da el número del primer cluster o bloque lógico asignado al fichero. Para obtener la dirección del resto de clusters del fichero se hace uso de la función siguiente_cluster que, a su vez, hace uso de la función siguiente_cluster_12.
En un sistema de ficheros FAT12, cada número de cluster ocupa 12 bits, es decir, 1’5 bytes. Esto hace que un
cluster par y otro impar compartan los 8 bits de un byte. La distribución de los 3 bytes ocupados por un cluster
par (P) y otro impar (I) consecutivos es «PP IP II», donde cada letra representa 4 bits y el byte compartido «IP» almacena los 4 bits más significativos del cluster par y los 4 bits menos significativos del cluster impar.
Así pues, para leer la entrada par tendremos que leer los bytes «PP IP» como un entero (línea 256 de fat.c), lo que hará que se interpreten como el entero «IP PP» (esto es así porque en memoria los bytes se almacenan en el orden little-endian). De ese entero nos interesan los 12 bits menos significativos, los cuales obtendremos aplicando al entero leído un «y-lógico» con el valor 0x0FFF (línea 261 de fat.c).
Si la entrada es impar tendremos que leer los bytes «IP II» también como entero, lo que hará que se interpreten como «II IP». En este caso nos interesan los 12 bits más significativos los cuales obtendremos desplazando a la derecha 4 posiciones el entero leído (línea 259 de fat.c).
Lo único que nos falta es saber en qué posición o desplazamiento se encuentra el primer byte del entero a leer. Podemos observar que para el cluster 0 el primer byte a leer es el 0, para el cluster 1 el byte 1, para el 2 el byte 3, para el 3 el byte 4, y así sucesivamente. Este es el dato que se calcula en la línea 242 del fichero fat.c, que equivale a la operación2Z+Z
2 , es decir, a 105× Z donde se desprecian los decimales.
Finalmente, debemos de tener en cuenta que los 2 bytes a leer se pueden encontrar en dos bloques distintos, uno como último byte de un bloque y el otro como primer byte del siguiente bloque (líneas 243–249 de fat.c).
La última función, cargarCOM, hace uso de las funciones dispositivo_leercluster, cluster_inicialy siguiente_cluster para cargar en una posición de memoria dada el contenido del fichero cuyo nombre se le pasa como parámetro.
6. Gestión de la E/S
El sistema operativo OSO maneja tres dispositivo distintos: un dispositivo de almacenamiento (habitualmente, el disquete), la pantalla y el teclado. Los dos primeros dispositivos se gestionan haciendo uso de los servicios que proporciona la BIOS mientras que el teclado se gestiona directamente accediendo al hardware correspondiente (aunque el teclado también se podría gestionar a través de la BIOS, hemos preferido gestionarlo directamente para así poder bloquear/desbloquear a procesos que lean de teclado, como veremos después).
La gestión de la entrada/salida se implementa en los ficheros io.h e io.c que se muestran a continuación:
#ifndef IO H #define IO H
/* Para salida por pantalla */
void escribe cad(char far * cadena, int longitud); 5
/* Para la lectura de un sector de disco */
unsigned int leersector (unsigned char far *buffer, char pista, char cabeza, char sector, char unidad);
void tick pararmotor(void); 10
/* Para entrada por teclado */
#define NULO ’\0’ void recoger caracter(void);
char obtener caracter(void); 15
#endif
/* $Id: io.h,v 1.6 2004/04/12 17:12:56 piernas Exp $ */
#include "io.h" #include "procesos.h"
/* Escribe una cadena de caracteres por pantalla usando la función teletipo
* de la BIOS */ 5
void escribe cad(char far * cadena, int longitud) { asm { mov ah, 03h mov bh, 0 int 10h 10 mov ah, 13h mov al, 1 mov bl, 07h
mov cx, longitud /* Cuidado, bp se utiliza para acceder a longitud, y se
* modifica justo a continuación. */ 15
les bp, dword ptr cadena int 10h
} }
20
/* Las siguientes funciones se utilizan para leer un sector de disco y * para parar el motor de la disquetera si es necesario */
static unsigned int pararmotor = 0;
unsigned int leersector (unsigned char far *buffer, 25
char cabeza, char sector, char unidad) { int i; 30 for(i = 0; i < 3; i++){ asm {
les bx, dword ptr buffer
mov al , 1 /* 1 sector */ 35
mov ch , pista mov cl , sector mov dh , cabeza mov dl , unidad
mov ah , 02 /* De disco a memoria. */ 40
int 13h
mov ax , 0 /* Devolvemos 0 indicando que la operacion tuvo exito. */
jnc noError /* Si no se produce error salimos del bucle. */
mov ax , −1 /* Devolvemos -1 indicando que la operación falló. */
} 45
} noError:
if (unidad == 0 | | unidad == 1) { asm {cli}
pararmotor = 36; /* Unos dos segundos */ 50
asm {sti} }
}
void tick pararmotor(void) { 55
unsigned char unidad; if (pararmotor > 0) {
pararmotor−−;
if (pararmotor == 0) 60
/* Esto para los motores de las unidades de disquetes */ asm { mov al, 0Ch mov dx, 3F2h 65 out dx, al } } } 70
/* Mapa del teclado. Asocia un código de tecla con un código ASCII */
static char mapateclado[ ]= { 27,’1’,’2’,’3’,’4’,’5’,’6’,’7’,’8’,’9’,’0’,’-’,’=’,8,9, ’Q’,’W’,’E’,’R’,’T’,’Y’,’U’,’I’,’O’,’P’,’<’,’>’,13,−1, 75 ’A’,’S’,’D’,’F’,’G’,’H’,’J’,’K’,’L’,164,’;’,135,−1, ’<’,’Z’,’X’,’C’,’V’,’B’,’N’,’M’,’,’,’.’,39,−1,−1,−1,’ ’, −1,−1,−1,−1,−1,−1,−1,−1,−1,−1,−1,−1,26,’7’,’8’,’9’,’-’, ’4’,’5’,’6’,’+’,’1’,’2’,’3’,’0’,’.’ }; 80
static char ultimo caracter = NULO;
/* Extrae el código de tecla del hardware de teclado, lo convierte en
* carácter y lo almacena en la variable “ultimocaracter” */ 85
void recoger caracter(void) {
int i, c;
/* Extraemos el código de la tecla del hardware de teclado */ 90
asm{
in al, 60h mov i, ax
} 95
/* Le indicamos al hardware del teclado que ya hemos retirado el código de tecla */
asm { in al, 61h xor ax, 80h 100 out 61h, al nop nop xor ax, 80h out 61h, al 105 }
/* Convertimos el código de tecla en código ASCII (si se puede) de acuerdo a tablateclado */ c = (i & 0x80) ? −1 : mapateclado[i−1]; 110 if (c >= 0) { ultimo caracter = c; desbloquearprocesos(BLOQ TECLADO); } 115 }
/* Devuelve el carácter almacenado en la variable “ultimo caracter” */
char obtener caracter(void) {
char c = ultimo caracter; 120
ultimo caracter = NULO; return c;
}
125
/* $Id: io.c,v 1.6 2004/04/14 20:16:40 piernas Exp $ */
6.1. Lectura de un dispositivo de almacenamiento
La única operación de E/S que hace OSO con los dispositivos de almacenamiento es la lectura. Esta operación se implementa en la función leersector (línea 26 del fichero io.c) mediante el servicio 13h de la BIOS. Al usar dicho servicio, la BIOS hace todo el trabajo por nosotros: arranca el motor de la disquetera, espera a que el disco alcance una velocidad adecuada, lee el sector solicitado de disco y lo copia en la dirección de memoria indicada. Después de hacer la lectura, y tras unos segundos, el motor de la disquetera se debe apagar para no «desgastar» el disquete que se pueda encontrar en ella. El motivo por el que el motor no se apaga inmediatamente es que si se hacen varias lecturas seguidas no es necesario arrancar el motor para cada una de ellas, acelerando considerablemente el proceso de lectura.
Generalmente, es la BIOS la que se encarga de apagar el motor de la disquetera tras unos pocos segundos. Para llevar una cuenta del tiempo, la BIOS hace uso de las interrupciones de reloj. El problema en el caso de OSO es que las interrupciones de reloj no ejecutan código de la BIOS sino código del sistema operativo (en concreto, la función sai_timer que se implementa en el fichero sais.c). Esto hace que la BIOS sea incapaz de apagar el motor, algo que tendrá que hacer el propio sistema operativo.
Cada vez que se llama a la función leersector se asigna a la variable pararmotor un valor, en ticks de reloj, para que se pare el motor de la disquetera tras unos 2 segundos. Cada vez que se produce una interrupción de reloj, la SAI del reloj llama a la función tick_pararmotor (línea 53 de io.c) que comprueba si debe parar el motor de la disquetera, lo cual ocurrirá cuando la variable pararmotor llegue a 0.
Una solución distinta a ésta hubiera sido la de ejecutar, desde la SAI del reloj, la función de la BIOS asociada a la interrupción 8. De esta manera la BIOS seguiría «recibiendo» las interrupciones de reloj aunque de manera