La implementación de minishell en Linux

Autor: Gao Fecha: 2018-12-03
La implementación de minishell en Linux

La Implementación de intérprete de mandatos en Linux

Introducción

¿Qué es un intérprete de mandatos? Es un programa que hace la interfaz en modo texto entre el usuario y el sistema operativo.

En diferentes distribuciones de GNU/Linux vienen instalado ya por defecto Bash. Un programa informático de interpréte de mandato desarrollado por el proyecto GNU.

Muchas veces en el contexto de empresa y no tenemos el entorno gráfico para gestión y administración de recursos, la única posibilidad es mediante el comando en modo texto.

Objectivo

Nuestro objetivo principal de esta implementación es manajer el concepto del proceso. Su creación, su ciclo de vida, y la intercomunicación necesaria entre proceso.

Librería de apoyo

Para facilitar la implementación y también para centrar en nuestro objetivo principal. Hemos usado la librería libparser que su función primordial es convertir una cadena de caracteres en un struct tline que encapsula los comandos que va ejecutar, los argumentos, los redireccionamientos, etc…

Estructuras de datos utilizadas

Estructuras de datos que vienen con la libraría:

typedef struct {
    char * filename;
    int argc;
    char ** argv;
} tcommand;

typedef struct {
    int ncommands;
    tcommand * commands;
    char * redirect_input;
    char * redirect_output;
    char * redirect_error;
    int background;
} tline;

Nuestro:

/* almacenamos las informaciones asociados a los procesos creados por sistema  */
typedef struct JobInfo {
    int id;
    pid_t pgid;
    char * command;
    struct JobInfo * next;
} JobInfo;

El tipo JobInfo contien información sobre un trabajo que son un serie de trabajo connectado con pipes.

Funcionamiento general

El shell debe ser capaz de reconocer y lanzar el programa correspondiente al comando que introduzca el usuario, que es un archivo binario que reside en uno de los directorio que lista en la variable PATH y ejecutarlo con los paramteros que vienen acompañados.

O tambiém una secuencia de comandos conectados enre sí mediante el mecanismo proporcionado por sistema Linux - pipe. Y también establecer los redireccionamientos de entrada y de salida si fuera preciso. Y vamos a tener unos cuantos comando interno que se usa para alterar los valores propio del shell.

Implementación

Comando básico

La funcionalidad básica de shell se hace dentro de un while donde lee un línea de texto que introduzca el usuario y parseamos con la librería llamando a la función tokenize y procesamos la línea de comandos.

char buf[BUFFER_SIZE];
tline * line;

do {
    if (fgets(buf, BUFFER_SIZE, stdin) > 0) {
        /* Leer una linea del taclado */
        line = tokenize(buf);
        if (line == NULL) {
            continue;
        }

        execline(line, buf);
    }
} while (true);

Para hacerlo mas interactivo y también para que el usuario identifique que está detro del shell o en el commando en ejecución. Mostramos una cabecera de shell msh> con la función printf.

void prompt() {
    printf("msh:>");
}

Cuando tenemos el struct tline pasamos a execline donde ejecutamos los comandos correspondientes. La operación más simple sería ejecutar un comando sin redirrecionamiento.

execvp(command->argv[0], command->argv);

Redirecionamiento

A veces el usuario pretende que la entra o la salida de programa sea diferente, por ejemplo, un archivo.

Tenemos que abrir un archivo que especifica la entra o la salida con la función open y luego también redirigirla con la llamada a dup2.

input = open(line->redirect_input, O_RDONLY);

output = open(line->redirect_output, O_WRONLY | O_CREAT | O_TRUNC, DEFAULT_FILE_CREATE_MODE);

Comandos encadenados

También tenemos la necesidad de realizar varios comandos a la vez y comunicando unos con otraos. Con la llamada pipe creamos una tubería que sirve luego en establecer la comunicación entre procesos, y con la llamada dup2 redireccionamos la salida del comando anterior a la entrada de tubería y desde la salida de la tubería a la entrada estandar para el siguiente comando.

pipe(pipeline);

dup2(pipeline[0], STDIN_FILENO);
execvp(command->argv[0], command->argv);

Y para el siguiente comando cogemos el flujo de entrada y crear otra tubería para si tenemos más comando. Todos estos se puede meter dentro un for que iteramos para crear la comunicación entre ellos.

for (i = 0; i < line->ncommands; i++) {
    pipe(pipeline)
    output = pipeline[1];
    
    /* ejecutar el comando */
    execute(&(line->commands[i]), input, output);

    input = pipeline[0];
}

Eso sería la idea inicial de la secuencia de operaciones que hay que hacer, en la implemención real habría que tener en cuenta más cosa, pj. los redireccionamientos.

Background

Tenemos también la necesidad de distinguir entre los procesos background y los procesos foreground. Lo que hacemos es asginar un id de grupo a los commandos de la misma linea con la función setpgid. Y llamamos a tcsetpgrp con el valor de grupo si debe ejecutar en el foreground y al terminar volvemos a llamar tcsetpgrp y ponemos el shell en foreground. Este procemiento es la forma más estandar de hacer y evitar problema de interferencia con el shell y otros procesos. Evitamos la situación de proceso zombie y también nos permite separar la señal que se envia.

Debug

Para mejorar la experiencia de la depuración hemos usado el Macro #ifndef DEBUG. Siendo la difultad que depurar en el contexto de multiproceso y la posible inferencia entre ellos. Hemos puesto sentencia

**fprintf(stderr, mensaje de seguimiento);**

Con eso conseguimos tener presente las ultimas operaciones que ha tomado nuestro minishell.

Sobre todo nos ayudaría mucho si podemos conseguir obtener el estado de proceso. Hemos creado uana función debug_wait que delega la llamada a waitpid pero podemos imprimir informaciones necesarios que queremeos.

Comando interno

cd

Aparte de ejecutar los archivos binarios que hay en el sistema. Shell tiene una serie de estado perdeneciente a shell asimismo una serie comando para poder modificar cuyos estados, se conoce como comando interno. cd es uno de ello que modifica la ruta actual del programa. chdir es una interfaz proporcionada por sistema que permite cumplir dicho deber.

chdir(line->commands[0].argv[1]);

Si el usuario no especifica con los parametros usamos la variable HOME como directorio de llegada.

chdir(getenv("HOME"));

jobs

Acerca de la gestión de proceso background y foreground. Tenemos que ser capaz de mostrar por pantalla lista de tareas que se está ejecutando en el backgrounp y también.

Tenemos definido struct JobInfo previamente mencionado, que es una linkedlist que sirve para guardar la lista de procesos que están en ejecución. Cada vez que ejecutamos una línea de comando almacenmos también su informaciones relativas en la lista.

insert_job(&job_list, new_job(current, strdup(command)));

Al recibir el comando jobs lo que hacemos es recorrer la lista siguiendo el puntero de next hasta que acabe la lista.

current = job_list;
while (current != NULL) {
    print_job(current, i);
    current = current->next;;
}

fg

Cuando el usuario necesitará traer un proceso en background ejecutará fg + id. El id sería el numero de aparece cuando muestra con jobs.

if (current != NULL && current->id == id) {
    pgid = current->pgid;
    tcsetpgrp(STDIN_FILENO, pgid);
    debug_wait(pgid, 0);
    tcsetpgrp(STDIN_FILENO, shell_pgid);
}

Por brevedad no hemos concluido cómo gestionamos la memoria dinámica del shell. Basicamente lo que hacemos es cuando un proceso pasa a foreground o bién que ha terminado libreramos la memeria asosiada a él.

Gestión de señal

El shell es capaz de ejecutar más de un comando en el background. El shell mismo no debe morir ni los procesos background ni el shell. Tenemos que ignoramos tanto SIGINT como SIGQUIT. Lo conseguiremos con signal.

signal(SIGINT, SIG_IGN);
signal(SIGQUIT, SIG_IGN);

Hemos puesto ese en la inicialización de shell.

Más detalle

Aquí arriba solo hemos comentado las ideas y la implementación basica de cómo va cada cosa. La gestión real también tenemos que tratar a los posibles errores que sucede mientras ejecutamos la función, la gestión de memoria dinámica, y el control sobre las señales, etc…

Conclusión

Este trabajo centramos prinpicalmente en el tema de proceso y mecanismo de gestión entre ellos. El problema principal que tuvimos fue gestión la tubería que olvidemos cerrar la tubería en el proceso padre. Que muchas veces no sabemos qué está ocurriendo siendo la dificultad de la depuración es muy alta.