Manejador del terminal en el minikernel

Práctica de carácter optativo. El desarrollo satisfactorio de la misma puede proporcionar un punto más en la nota total. Sólo se podrá realizar esta práctica una vez que se ha completado satisfactoriamente la práctica obligatoria. La funcionalidad que se plantea en esta práctica se puede incluir en la versión obligatoria de la práctica o, si ya se ha desarrollado otra práctica optativa, puede juntarse con esta, aunque no obligatoriamente. Idealmente, un alumno podría desarrollar una única versión de la práctica que incluyera toda la funcionalidad planteada en los distintos enunciados.

Descripción de la funcionalidad pedida

En la parte obligatoria se ha desarrollado un módulo básico de gestión de la entrada del terminal. Esta práctica optativa plantea desarrollar un manejador más complejo que ofrezca, dentro de lo que cabe, funciones similares a las presentes en un sistema UNIX convencional. Las caracteristicas principales de este módulo serán las siguientes:

  • En la parte obligatoria de la práctica se usaba un buffer para el terminal con un tamaño muy pequeño (“TAM_BUF_TERM” igual a 8 bytes). Dada la mayor complejidad del módulo, parece razonable usar un tamaño mayor (por ejemplo, de 32 bytes).
  • La entrada está orientada a líneas. Una solicitud de lectura no se satisface hasta que se haya introducido una línea completa.
  • La llamada para leer del terminal es:
    int leer(void *buf, int tam);

    Esta llamada devuelve el número de bytes leídos. El sistema operativo copia la información leída en la zona que comienza en la dirección apuntada por “buf”. El segundo parámetro, “tam”, indica cuántos caracteres solicita leer.

  • El comportamiento de la lectura sigue la misma pauta que en UNIX. Así, si en la llamada se solicitan “S” caracteres y el usuario introduce una línea con “L” caracteres (incluyendo el carácter de fin de línea ”'\n'”), se presentan los dos siguientes casos:
    • Si “S >= L”, se copian los “L” caracteres introducidos incluyendo el de fin de línea.
    • Si “S < L”, se copian los “S” primeros caracteres de la línea. El resto quedan disponibles para las siguientes peticiones de lectura, que se satisfarán inmediatamente sin necesidad de introducir más caracteres de fin de línea. En ningún caso se añade un carácter nulo (”'\0'”) al final del buffer.
  • De manera similar a UNIX, algunos caracteres tendrán asociado un tratamiento especial. Concretamente, se definen los siguientes caracteres especiales:
    • Indicador de fin de línea. Este carácter causa que se complete la línea en curso quedando disponible para su lectura. El propio carácter de fin de línea (”'\n'”) formará parte de la línea.
    • Borrado del anterior carácter tecleado. Este carácter provoca que el anterior deje de formar parte de la línea. Por defecto, se usará el carácter “Crtl-H” (código ASCII 8 en decimal), aunque, como se comentará más adelante, se podrá redefinir.
    • Borrado de la línea. Este carácter provoca la eliminación de todos los caracteres que forman parte de la línea en curso. Por defecto, se usará el carácter “Crtl-U” (código ASCII 21 en decimal), aunque también se podrá redefinir.
    • Carácter de final de entrada de datos. La línea en curso queda disponible para ser leída aunque no se haya tecleado el carácter de fin de línea. En la línea no se incluirá este carácter. Si se pulsa al principio de una línea, “leer” devuelve 0 bytes, lo que, por convención, se usa para indicar que ha terminado la entrada de datos. Por defecto, se usará el carácter “Crtl-D” (código ASCII 4 en decimal), aunque también se podrá redefinir.
    • Carácter que permite abortar un proceso. Por defecto, se usará el carácter “Crtl-B” (código ASCII 2 en decimal), aunque también se podrá redefinir, seguido por un carácter que identifica al proceso que se pretende abortar. El convenio seguido será el mismo que se usa en la función de biblioteca “strtol”:
      • Si el carácter está entre 0 y 9, se corresponde con un proceso con ese identificador.
      • Si el carácter corresponde con una A, se trata del proceso con identificador igual a 10; si es una B es el 11; y así sucesivamente hasta la Z que se refiere al proceso con identificador igual a 35. Si el carácter no corresponde con el identificador de un proceso existente, se ignoran ambos caracteres (el “Crtl-B” y el presunto identificador de proceso).
        Como se estudia en la teoría de la asignatura, la terminación involuntaria es una operación delicada, que se analiza en detalle más adelante.
        Nótese que en este caso no se ha usado el típico de UNIX (“Crtl-C”) ya que queremos que siga activo para matar al minikernel. <note>Nota: En el caso de incluir esta funcionalidad sobre una versión con threads, la operación de abortar se realizará sobre todos los threads del proceso especificado.</note>
    • Carácter de escape. Permite al usuario indicar que el carácter que se tecleará justo a continuación no deberá interpretarse como especial aunque lo sea. Por defecto, se usará el carácter “\”, aunque también se podrá redefinir. Por simplicidad, no se hará eco de estos caracteres especiales, excepto del carácter indicador de fin de línea.
  • Como se comentó previamente, puede redefinirse la asociación entre un carácter y una función especial. Para ello, se ofrecen dos nuevos servicios (hasta cierto punto, similares a las funciones POSIX “tcgetattr” y “tcsetattr”):
    int obtener_car_control(struct car_cont *car);
    int fijar_car_control(struct car_cont *car);

    Siendo el tipo “struct car_cont”:

    struct car_cont {
        char cc[5];
    };

    Cada posición del vector corresponde con un carácter especial siguiendo el criterio:

    • Posición 0: carácter de borrado del último carácter.
    • Posición 1: carácter de borrado de línea.
    • Posición 2: carácter de fin de datos.
    • Posición 3: carácter para abortar procesos.
    • Posición 4: carácter de escape. Nótese que esta definición de tipo debería estar disponible tanto para el sistema operativo como para las aplicaciones. Por tanto, se debería incluir tanto en el archivo de cabecera usado por los programas de usuario (“servicios.h”) como en el usado por el sistema operativo (“kernel.h”).
  • Puesto que no hay una llamada específica para redefinir cada carácter especial, si un programa quiere cambiar un carácter específico, tendrá que obtener primero las definiciones actuales usando “obtener_car_control”, modificar el carácter correspondiente y usar “fijar_car_control” para activar la nueva definición. Así, por ejemplo, el siguiente programa establece que el carácter para abortar es el “Crtl-A” (código ASCII 1 en decimal).
#include "servicios.h"
 
int main(){
    struct car_cont defs;
    obtener_car_control(&defs);
    defs.cc[3]='\001';
    fijar_car_control(&defs);
    ...................
}
  • En su modo de operación por defecto, el manejador realizará el eco de cada carácter tecleado (excepto de los especiales, como se especificó previamente). Se proporcionará un nuevo servicio que permita activar y desactivar el eco:
    int eco(int estado);

    Si estado es igual a 0 se desactiva el eco y, si es distinto, de cero se reactiva.

  • Si se llena el buffer asociado al terminal, la rutina de tratamiento de la interrupción ignorará el carácter recibido, excepto si se trata de un carácter especial que no haya que almacenar en el buffer.

Aunque se ha intentado describir cómo debe comportarse la práctica ante las distintas circunstancias que pueden acaecer, para resolver las posibles dudas que puedan surgir, se recomienda probar cuál es el comportamiento de un terminal en un sistema UNIX ante el caso sobre el que se plantea la duda.

Para terminar, hay que resaltar que, aunque los aspectos “estéticos” de la edición en el terminal no son trascendentales en el desarrollo de esta práctica, resulta estimulante intentar implementar algún efecto de esta índole, ya que proporciona una interfaz de usuario más vistosa. Así, por ejemplo, lo importante es asegurar que cuando se teclea un carácter de borrado, no se le entregue a la aplicación el carácter previamente tecleado. Sin embargo, si se consigue además que ese carácter desaparezca de la pantalla, el efecto resultante es más impactante. Una pista: escriba en pantalla la secuencia formada por un carácter “\b”, un espacio en blanco y otro carácter “\b”.

La llamada “leer” es incompatible con la llamada “leer_caracter” desarrollada en la parte obligatoria de la práctica, puesto que una tiene un modo de operación orientado a línea y la otra orientado a carácter. Sin embargo, dado el carácter incremental de la práctica y para seguir dando cobertura a todos los programas de usuario, se recomienda reconvertir la llamada “leer_caracter” en una función de biblioteca que llame a la función “leer”.

Problemática al abortar un proceso

Como se analiza en el tema de procesos de la parte teórica de la asignatura, la terminación involuntaria de un proceso es una operación compleja puesto que hay que asegurarse de que el estado del sistema queda coherente, lo cual puede ser complicado si el proceso estaba en ese momento en modo sistema. Como se explica en la parte teórica, la solución habitual se basa en que sea el propio proceso a abortar el que realice su auto-destrucción y en el uso de una interrupción software de terminación, de carácter no expulsivo, para conseguir que el estado del sistema sea coherente dejando que se complete la llamada en curso, en caso de que la hubiera.

En la parte teórica de la asignatura, se estudia el caso más complejo: la terminación involuntaria en un sistema multiprocesador con un núcleo expulsivo. En la práctica que nos ocupa, se trata de un monoprocesador con un núcleo no expulsivo, lo que permite hacer ciertas simplificaciones. Además, nótese que, dado que el minikernel sólo dispone de una única interrupción software, se usará tanto para planificación como para terminación. La información almacenada en el BCP permitirá distinguir para qué labor se está usando en cada caso.

A continuación, se revisa la problemática vinculada con la operación de abortar un proceso, simplificada para el tipo de sistema en el que se basa el minikernel.

El proceso que se pretende abortar puede estar en distintos estados:

  1. Recién creado.
  2. Listo habiendo estado antes en ejecución.
  3. Listo habiendo estado antes bloqueado.
  4. Bloqueado.
  5. En ejecución.

Abortar el proceso en los dos primeros casos no implica ningún problema. Sin embargo, en los tres últimos puede ser problemático.

En los casos 3 y 4, el proceso está en una llamada al sistema, en el cambio de contexto asociado a un bloqueo. Habría que asegurarse de que el estado de las estructuras de datos del sistema operativo no quedan incoherentes al abortar el proceso dejando la llamada a medias. Antes de abortar el proceso, habría que dejar el estado del sistema coherente (por ejemplo, si el proceso se había bloqueado reservando algún recurso previamente, habría que liberarlo).

Como un ejemplo trivial de este problema de coherencia, supóngase que el sistema operativo lleva un contador de cuántos procesos están bloqueados en un mutex:

mutex_lock(M) {
    .....
    Si M está bloqueado por otro proceso
        M.contador_bloq++;
        Bloquear(M.lista_bloq);
    .....
}

Si se aborta directamente a un proceso bloqueado (o listo habiendo estado antes bloqueado) en un mutex, el contador quedaría incoherente. Se requiere reajustarlo para reflejar que el proceso ya no está bloqueado.

En el caso 5 (proceso en ejecución), puede estar en medio de una llamada al sistema. Si es así, habría que diferir la operación de abortar hasta que termine la llamada, usando para ello la interrupción software para ello.

Como se estudia a la teoría de la asignatura, la solución habitual es que sea el propio proceso el que lleve a cabo su terminación. En los casos 3 y 4 puede realizar las acciones correctoras que considere oportunas antes de abortarse el mismo. Asimismo, para el caso 5, esta estrategia asegura que el proceso termina la llamada al sistema.

Para ello, en la rutina donde se pretende abortar al proceso, sólo se le marcará como “abortado” en su BCP. Además, si el proceso está bloqueado se le desbloqueará, y si está en ejecución se activará una interrupción software.

Antes de hacer un cambio de contexto (ya sea en la operación de bloquear o dentro de la interrupción software), el propio proceso deberá comprobar si está marcado como “abortado” y, si es así, terminar su ejecución, después de haber realizado alguna acción correctora en el caso de que sea precisa. Nótese que no tiene sentido que un proceso se bloquee si está abortado.

A la vuelta de un cambio de contexto (ya sea en la operación de bloquear o dentro de la interrupción software), el propio proceso deberá comprobar si está marcado como “abortado” y, si es así, terminar su ejecución, después de haber realizado alguna acción correctora en el caso de que sea precisa.

A continuación, se muestra esta idea para el ejemplo planteado previamente:

Bloquear(lista) {
    .........................
 
    Si proceso actual abortado
        // el proceso actual está abortado pero en vez de
        // terminar la llamada intenta bloquearse. No debe
        // bloquearse, sino realizar las acciones "correctoras"
        // pertinentes y terminar.
        return -1;
 
    cambio_contexto(...);
 
    Si proceso actual abortado
        // el proceso ha sido abortado mientras estaba bloqueado
        // o listo después del bloqueo. Por tanto, al reanudar
        // su ejecución, debe realizar las acciones "correctoras"
        // pertinentes y terminar.
        return -1;
    .........................    
    return 0;
}
 
mutex_lock(M) {
    .....
    Si M está bloqueado por otro proceso
        M.contador_bloq++;
        if (Bloquear(M.lista_bloq)<0) {
            M.contador_bloq--; // ya no está bloqueado en el mutex
            terminar_proceso();
        }
    .....
}

Por regularidad, esta misma estrategia (que el propio proceso aborte su ejecución) se podría usar con los procesos en el caso 2 e incluso para el caso 1.

Código fuente de apoyo

Para la realización de esta práctica se tomará como base el código desarrollado en la práctica obligatoria del minikernel. Para poder trabajar en esta práctica sin afectar al código de la parte obligatoria y poderla entregar de manera independiente, se debe copiar a un nuevo directorio denominado “minikernel_term.2023”. Desde el directorio base del usuario, se pueden ejecutar los siguientes mandatos para realizar esta copia:

$ mkdir DATSI/SOA/minikernel_term.2023
$ cp -R DATSI/SOA/minikernel.2023/* DATSI/SOA/minikernel_term.2023

Documentación que se debe entregar

El mandato a ejecutar en la máquina <tt>triqui.fi.upm.es</tt> es:

entrega.soa minikernel_term.2023

Este mandato realizará la recolección de los mismos ficheros que en la práctica obligatoria del minikernel.

No existe un corrector automático para esta práctica. El alumno deberá usar sus propios programas de prueba.

Las prácticas entregadas serán corregidas una vez terminado el plazo de entrega, que es el mismo de las prácticas obligatorias.

 
docencia/asignaturas/soa/practicas/minikernel_terminal.txt · Última modificación: 2023/01/30 22:55 por fperez
 
Recent changes RSS feed Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki