Creando una shell de Linux (I)


Una de las asignaturas de la carrera que más he disfrutado ha sido la de Sistemas Operativos, con la que he aprendido muchísimo sobre el funcionamiento, ya no sólo del SO, sino de los lenguajes de programación de bajo nivel (particularmente C). Una de las prácticas más divertidas nos pedía implementar una Shell, es decir, un intérprete de comandos (el famoso “terminal”) y un pequeño conjunto de operaciones.

A priori puede parecer supercomplejo, pero en cuanto has hecho un par de métodos, se abre ante ti el enorme potencial de cualquier núcleo. En el caso que vamos a ejemplificar, estoy operando sobre un “no demasiado moderno” Ubuntu 10.04 y podemos ver la versión del núcleo de la siguiente forma:
$ uname -sr
Linux 2.6.32-74-generic

Cuando necesite consultar alguna llamada al sistema o alguna estructura de datos que desconozca, lo mejor será utilizar las manpages. Así por ejemplo, podría entender lo que es fork de la siguiente manera:
$ man fork
Y eso me muestra la página de manual de la llamada fork sin tener siquiera que abandonar la línea de comandos, lo cual es supercómodo.

Una vez aclarados estos pequeños menesteres, vamos a crear el primer esqueleto del programa.
Básicamente un shell es un bucle, en el que se nos muestra el prompt (por ejemplo &> o cualquier otra combinación de caracteres) para indicar que esta esperando la entrada de una orden. La introducimos, la ejecuta, y de nuevo vuelta a esperar la siguiente orden; a no ser que esa orden termine la ejecución.
Simple.

Así que vamos a incluir la librería estandar de I/O para poder escribir y leer:
#include <stdio.h>

Y creamos también el método “main”. En su cabecera, se suele especificar un número entero (argc) y un array de char (argv). Se refieren a el número de argumentos con el que se ha invocado el programa y la cadena de texto con la que fue invocado. Por ejemplo, cuando yo desde el terminal hago un sencillo:
$ ls /home/emilio
El entero vale 2 (el comando y la ruta) y el array de char almacena toda la cadena, es decir “ls /home/emilio”. Si a posteriori necesitamos separar las palabras que vienen en la cadena, será nuestra misión.

void main (int argc, char * argv[])
{

while (1)
{
printf("\n#->");
}

}

Dentro del método main estaremos por supuesto creando un bucle infinito donde lo único que se hace por ahora es escribir un retorno de carro (es decir, pasar a una línea nueva) mediante “\n” y el indicador #-> para que el usuario sepa que puede escribir ahí.

Si lo compilas y lo ejecutas, obtendrás este bucle infinito. La compilación se invoca con gcc indicando con el flag “-o” el nombre del ejecutable que queremos generar:
$ gcc shell.c -o shell
Y como siempre, lo puedes ejecutar así:
$ ./shell

Por ahora vamos a implementar los procedimientos que leen lo que el usuario introduce, sin hacer ninguna operación, y una única orden, que sea la de salida.

Para leer la entrada utilizaré la función fgets (puedes consultarla, acuérdate, con “man fgets”). Su interfaz es la siguiente:
char *fgets(char *s, int size, FILE *stream);
Así que lee lo que le llegue por el flujo *stream, desde la posición s hasta la posición size – 1, porque hay que recordar que la máquina empieza a contar posiciones desde el cero.
Por eso voy a definir una constante a la que voy a llamar MAXSTR que se corresponde con el tamaño máximo de lo que lea el shell; y una variable “input” donde podamos después almacenar lo leído. Fíjate que el tamaño de la cadena ya lo reservo con “malloc” (y esto evitará unos Buffer Overflow muy chulos).
Para comprobar que esto funciona, puedo hacer que el programa me haga eco de lo que yo le escribo, simplemente volcando la variable “input”.

Como puedes ver en el pantallazo, el compilador nos avisa que el uso implicito de malloc no es nada ortodoxo, pero lo dejaré así por comodidad. También puedes ver la prueba de cómo se comporta.
Ojo con esa línea en blanco antes de saltar al siguiente prompt. En la cadena “input” también va el ENTER que pulsas tras introducir algo y por eso se reproduce de nuevo.
También se puede ver cómo por ahora solo puedo cortar la ejecución con Ctrl+C.

Vamos con el “troceado” de la cadena, para separar lo que serán comandos, de lo que serán argumentos, etc.
En el main crearemos una variable (fuera del bucle para no consumir memoria de forma estúpida) llamada “cmdargs” que es un array de chars. Como en C lo que se almacena es un puntero a una dirección de memoria, podemos hacer que cada uno de estos chars sea en realidad aquel donde podamos leer un comando y sus argumento por separado.
Y lo que es el troceado en si mismo, lo haremos llamando a una función que he creado aparte llamada readcmdargs, para que sea fácil de entender y no vuelva el código de main muy farragoso.
En ella, tomo la cadena “input” y mediante la función strtok busco caracteres como el espacio, el fin de cadena o el tabulador. Si no hay ninguno, la función nos devuelve NULL, lo que quiere decir que el usuario no ha tecleado nada y nuestra funcion devolverá 0; y sino, contaremos cuantos trozos y los iremos almacenando en un array.

Una vez que la función acaba, main comprueba si ha habido cero trozos (es decir, no se había escrito nada) o si había algo. Y en ese caso por ahora solo escribe que hará lo que nosotros deseemos…

La forma en la que lo he programado sigue arrojando warnings, pero por ahora podemos seguir.
Ahora que podemos trabajar de forma lógica con lo que ha escrito el usuario, podemos comparar si lo que ha escrito es una orden que nosotros tengamos implementada. Para ello, voy a olvidarme del “else” en el “if” del main (pues si no se ha escrito nada, nada se hará) y directamente llamo a una función que voy a definir a continuación, llamada cmdcall, encargada de ver qué comando ha escrito el usuario y a continuación llamar a la funcion correspondiente.
Por ahora solo voy a permitir dos comandos, que son precisamente los de terminación, bajo los nombres “quit” y “exit”.
Como en este caso, la terminación no requiere de ningún paso extra, directamente puedo invocar la operacion exit() de C:

Comprobemos que funciona…

ÉXITO!!! 😀

Esto es todo por ahora, pero pronto implementaremos algunas operaciones más para ir dotando a nuestra shell de más funcionalidades y empezar a ver llamadas del sistema muy interesantes 🙂

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s