La tarea vence el viernes 17 de mayo a las 12:00. El plazo para reuniones conmigo es el lunes 13 de mayo (ver aquí).
En code/test/halt.c encontrarás un programa sencillo de C (no C++) que llama a Halt. El binario es halt, y puedes ejecutarlo bajo Nachos con el comando
nachos -x ../test/haltPara crear el binario de Nachos, haz "make depend" y después "make" en el directorio code/userprog. Para crear el binario de halt, haz solamente "make" en code/test.
Usa el argumento "-d a" con Nachos para encender mensajes de depurificación. Debieras seguir la ejecución de Nachos con halt.(1) Después de la llamada a Initialize en threads/system.cc, main llama a StartProcess en userprog/progtest.cc. StartProcess crea un nuevo espacio de direcciones para el proceso de usuario (ver userprog/addrspace.cc) y inicia la ejecución del proceso con la llamada a machine->Run.
Secciones 2.4, 4, y 6.3 de A Road Map Through Nachos también tienen información sobre la operación de Nachos con los programas de usuario.
Cada proceso de usuario consiste en un thread de kernel y también de un objeto de AddrSpace (address space, o espacio de direcciones). En threads/thread.h encontrarás que la clase Thread tiene un nuevo miembro space (y algunos otros) si USER_PROGRAM es definido. En StartProcess hay también una línea
currentThread->space = space;para asignar a este miembro.
El objeto de AddrSpace es como un process control block (PCB). Nachos usa este objeto para guardar información individual sobre un proceso. Una parte de esta información es la tabla de páginas del proceso.
En la rutina de construcción AddrSpace en addrspace.cc está la inicialización de la tabla de páginas de un proceso nuevo. La asignación de marcos a páginas es muy sencilla. El código asigna marco 0 a página 0, marco 1 a página 1, etc., con
pageTable[i].virtualPage = i; pageTable[i].physicalPage = i;Con esta estrategia de asignación no podemos tener más que un proceso de usuario en el sistema a la vez, ya que un segundo proceso usaría los mismos marcos físicos que el primero.
Una solución es que implementemos una nueva clase para administrar los marcos del sistema. Por ejemplo, podemos tener una clase CoreMap con funciones de membrecía GetFrame y FreeFrame. Si el administrador global de marcos tiene el nombre coreMap, podríamos tener el código siguiente:
pageTable[i].virtualPage = i; pageTable[i].physicalPage = coreMap->GetFrame();Por supuesto, cuando destruyamos el objeto de AddrSpace (cuando el proceso termine), debiéramos liberar los marcos con llamadas a FreeFrame.
Debiéramos llenar con ceros cada marco que asignemos a un proceso. También tenemos que pensar en el caso donde no haya más marcos libres en el sistema. Ya que no tenemos la memoria virtual, la creación del nuevo proceso fracasaría en este caso.
Después de la asignación de memoria a un proceso, tenemos que cargar el binario de proceso. Ahora tenemos un nuevo problema. El cargar es relativamente fácil en el Nachos original ya que cada dirección virtual es igual a la dirección física correspondiente. Pero con el uso de coreMap para asignar marcos no sabemos la relación entre las páginas y los marcos del proceso; los marcos no son necesariamente contiguos, por ejemplo.
Este problema es común en las interacciones entre un proceso y el sistema operativo. Si el sistema operativo tiene una dirección en el espacio de usuario, tiene que traducir la dirección, usando la tabla de páginas del proceso, en una dirección física. Tenemos que añadir una nueva rutina Translate a la clase AddrSpace que traduce una dirección lógica en una dirección física. Entonces, si el sistema operativo quiere (por ejemplo) copiar un string en la dirección virtAddr en el espacio de usuario, puede ejecutar lo siguiente:
physAddr = userSpace->Translate(virtAddr); char *string = &(machine->mainMemory[physAddr]); strcpy(copy, string);Esto no es completamente correcto; el string puede atravesar dos páginas, y si el marco de la segunda página no es contiguo con el de la primera, el sistema operativo escribirá en el marco de otro proceso. Entonces tenemos que copiar cada byte individualmente, con una traducción para la dirección de cada byte. Por supuesto, hay muchas posibilidades para optimizar este proceso.
Para cargar un binario de usuario en Nachos, podemos leer un byte a la vez del archivo y escribir en la dirección física que corresponde a la destinación lógica del byte.
Implementa esta habilidad de cargar y ejecutar un proceso en cualquier parte de la memoria. Tienes que cambiar addrspace.h y addrspace.cc. Puedes implementar también coremap.h y coremap.cc si quieres usar el enfoque precedente para administrar la memoria (la clase BitMap en bitmap.h puede ser de interés para guardar el estado de los marcos del sistema). Asegura que halt aún funciona.
En userprog/syscall.h están las declaraciones para las llamadas de sistema de Nachos. Los programas de usuario en C que llaman al sistema debieran incluir este archivo (por ejemplo, ver el código para halt).
Cuando un proceso de usuario haga una llamada de sistema, la CPU genera una interrupción y el control pasa a Nachos en la rutina ExceptionHandler en userprog/exception.cc. Aquí puedes insertar un enunciado de switch usando el valor de type para llamar tu implementación de la llamada de sistema. Debieras usar mi implementación de exception.cc en
/usr/local/nachos/iic2332/tarea2Necesitarás también el archivo systemcall.h con las declaraciones para las funciones llamadas por ExceptionHandler. Tienes que implementar systemcall.cc.
Para ahora implementa solamente SysCallCreate, SysCallOpen, SysCallWrite, SysCallRead, y SysCallClose, que son las operaciones de I/O (los semánticos están en systemcall.h). Para cada operación, tienes que chequear si todos los argumentos son legales y volver (quizás con un código de error) si no. ¡No debe ser posible que un usuario pueda botar el sistema!
Recuerda también que las argumentos que son direcciones son direcciones en el espacio de usuario. Tienes que traducirlas antes de usarlas. El objeto de AddrSpace corriente (con la tabla de páginas del proceso) está en currentThread->space.
Es más fácil si empiezas con SysCallWrite y el caso donde el output es ConsoleOutput. Para escribir en y leer de la consola puedes usar mi implementación de SynchConsole en synchconsole.h y synchconsole.cc. Usando esta clase el thread de usuario esperará si es necesario. Si usas SynchConsole debieras crear la consola de sistema con el código siguiente, que puedes ubicar en StartProcess:
theConsole = new SynchConsole();Para probar tu implementación tienes que escribir un programa pequeño en C, por ejemplo:
#include "syscall.h"
main() { Write("Test\n", 5, ConsoleOutput); Halt(); }
(Necesitas la llamada explicita a Halt ya que no tienes todavía
una implementación de Exit, y main llamará Exit
como un default cuando vuelva.)
Para compilar este programa, copia el directorio de test de iic2332/tarea2 a tu directorio de userprog. Haz un "make" en tu directorio de code/bin. Finalmente, edita el Makefile en userprog/test, inserta el nombre de tu nuevo archivo, y haz un "make". Puedes correr tu programa con "../nachos -x programa".
Para las llamadas de I/O tienes que mantener una lista de descriptores válidos de archivos abiertos. Esta lista es una parte del objeto de AddrSpace de cada proceso de usuario. Una llamada a Open añade un descriptor nuevo a la lista. Si un proceso sale sin cerrar todos sus archivos abiertos, tienes que cerrarlos durante la destrucción del proceso. También, tienes que manejar el caso en Read, Write, y Close donde un proceso pasa un descriptor ilegal a la llamada de sistema.
Para crear nuevos identificadores (para uso como descriptores, por ejemplo), yo uso un administrador global de identificadores. Si te interesa, puedes usar id.h y id.cc.
Para probar tu implementación, puedes usar los archivos filesysN.c en test.
Exec crea un nuevo thread y un nuevo objeto de AddrSpace. Entonces llama a Fork. El argumento a Fork puede ser una función que toma como argumento el objeto de AddrSpace y que ejecuta el mismo código que está en la segunda mitad de StartProcess.
Exec tiene que volver un identificador único para el nuevo proceso. Con una llamada a Join con este identificador un proceso puede esperar hasta que su subproceso termine y pueda recibir el código de salida del subproceso (que es el argumento a Exit). El diseño de esta funcionalidad es la parte más complicada de la tarea. Tienes que pensar en que información el proceso debe mantener sobre sus subprocesos, y cómo los subprocesos pueden actualizar esta información cuando salgan. La sincronización es importante aquí, y tienes que usar las abstracciones de la primera tarea. Puedes crear nuevas clases y archivos para manipular esta información; los objetos resultantes deben ser nuevos miembros de AddrSpace.
Debieras cambiar StartProcess para reflejar tu nueva implementación. Por ejemplo, StartProcess puede inicializar algunas variables de sistema y entonces llamar al mismo código al que SysCallExec llama. StartProcess inicia el primer proceso del sistema y Exec todos los otros.
Tú puedes probar el nuevo sistema con shell, kshell, consoleA, y consoleB en el directorio test.
El sistema en este estado no usa la expropiación en la planificación de procesos. Copia el archivo tarea2/system.cc al tu directorio de threads para usar la expropiación siempre. Prueba tu sistema baja esta política.
Después de eliminar los archivos de ".o" y otros ejecutables (por ejemplo, nachos y programas de prueba), haz un "tar" de todo el directorio de code con
cd code; tar cvf ../nombre .donde nombre es como en la primera tarea. Copia el archivo al directorio de entrega en iic2332.
Vamos a desempacar tu archivo de tar, hacer un "make depend" y "make" en userprog, y probar tu sistema con los programas en userprog/test y quizás otros también.