Shell simple de Linux

1. Conocimiento relacionado con Shell

1.1 Comandos externos y comandos incorporados

Creo que el shell en realidad puede verse como un front-end para el procesamiento de cadenas y un back-end para llamar a otros archivos. El front-end analiza la cadena ingresada por el usuario y luego pasa el resultado analizado al back-end, lo que permite que el back-end llame a otros archivos. Los archivos aquí se refieren a "comandos externos". La razón de este nombre es precisamente porque la función de implementación de comandos externos no está dentro del shell. Es precisamente porque el shell no necesita implementar comandos externos, por lo que la dificultad de esta gran tarea no es alta. La función de los comandos internos correspondientes se implementa dentro del shell (es decir, debe implementarse en el código del shell). Afortunadamente, los trabajos grandes no requieren la implementación de comandos internos.

En cuanto a si un comando es un comando interno o un comando externo, puede usar typeel comando para verificar, por ejemplo, ingresar

type cd

tendrá salida

cd 是 shell 内建

La descripción es un comando interno, y cuando se ingresa

type less

La salida es

less 是 /usr/bin/less

La descripción es un comando externo.

1.2 Descriptores de archivos

Bajo la filosofía de diseño de "todo es un archivo", lo que llamamos "redireccionamiento" es el proceso de reemplazar los archivos estándar de entrada y salida con archivos de nuestra propia elección. Aquí implicará algún conocimiento del sistema operativo, como sigue:

inserte la descripción de la imagen aquí
La figura anterior es un diagrama esquemático completo de la relación entre procesos y archivos. En el extremo izquierdo hay una tabla de descriptores de archivos única para cada proceso. Su esencia es una tabla de recuperación de archivos. Podemos usar descriptores de archivos (file descriptor, fd) para recuperar las entradas correspondientes.

Aquí primero se presenta la naturaleza de fd:

  • Cada proceso tiene su propio espacio de incremento fd. Los enteros positivos ocupados por fds cerrados pueden reutilizarse. La cantidad de archivos que un solo proceso puede abrir al mismo tiempo está limitlimitada por la configuración del sistema.
  • De acuerdo con el acuerdo, shellal iniciar una nueva aplicación, siempre abra los descriptores de tres números de 0, 1, como , , . Se nombran con macros en C, respectivamente , , .2stdinstdoutstderrSTDIN_FILENOSTDOUT_FILENOSTDERR_FILENO

Las entradas que recuperamos apuntan a algo llamado entrada de la tabla de archivos, que todavía no es un archivo real. Puede considerarse como el estado del archivo, que registra información como nuestros permisos en este archivo, el desplazamiento actual de lectura y escritura. Es fácil pensar que dos entradas de archivo diferentes pueden corresponder al mismo archivo, pero existen diferencias en la información de estado, como permisos y compensaciones. Esta tabla de entrada de archivos es compartida por todos los procesos.

​La entrada del archivo contendrá un puntero al v-nodenodo , en el que se encuentra el bloque de control que registra la información estática del archivo, y es v-nodela correspondencia uno a uno entre cada archivo y cada uno.

En resumen, cuando programamos en lenguaje C, o usamos fd(el más esencial) o usamos FILE*para manipular archivos (debería ser el paquete provisto por C). Aquí usamos fd, porque generalmente lo usamos para implementar redirección y conductos, y los parámetros de las llamadas al sistema relacionadas son descriptores de archivos.

1.3 Apertura y cierre de expedientes

En el proceso de usuario se puede implementar a través de llamadas al sistema, para la apertura de archivos tenemos

int open(char *filename, int flags, int mode);

usable. Esta función abrirá el archivo filenamenombrado los permisos flagsdescritos por , tenemos una serie de macros, y soporte y operación

macro significado
O_RDONLY solo lectura
O_MAL solo escribe
O_RDWR legible y escribible
O_CREAR Si el archivo no existe, cree un archivo truncado (vacío)
O_TRUNC Si el archivo ya existe, truncarlo
O_APPEND Antes de cada operación de escritura, establezca la posición del archivo al final

modeSe especifica el bit de permiso de acceso del nuevo archivo, y también hay una definición de macro, pero no se expandirá. Generalmente, no 0666habrá .

Esta función devuelve el descriptor de archivo para el archivo abierto fd.

Cuando necesitamos cerrar un archivo, podemos hacer esto

int close(int fd);

1.4 Leer y escribir archivos

​ Esto en realidad no tiene nada que ver con la implementación del shell, pero es la primera vez que lo entiendo, déjame registrarlo, es decir, haremos una llamada al sistema cada vez que leamos y escribamos archivos, pero esto sin duda es un alto costo, porque se usa con frecuencia en modo usuario y modo kernel.

​ Las funciones que getcsolemos se llaman funciones de lectura y escritura en búfer. Lo que dijo es que leerá la información del tamaño completo del búfer después de abrir el archivo, y luego seguirá getcla llamada de, una por una desde el búfer hacia el exterior. Entrega, hasta que no haya más, es más conveniente volver a llamar al sistema para llenar todo el búfer.

1.5 Redirección

Con el conocimiento anterior, podemos introducir la redirección.La función que usamos es

int dup2(int oldfd, int newfd);

Esta función dice que copie la entrada del descriptor oldfdcorrespondiente a newfdla entrada. Si newfdno hay un archivo correspondiente, newfdcuando se use nuevamente, el archivo correspondiente es la entrada correspondiente al oldfdarchivo . Si newfdhay un archivo correspondiente, dup2se cerrará oldfdantes de newfd. Si el valor de retorno es negativo, significa falla.

Por ejemplo, antes de llamar

inserte la descripción de la imagen aquí

ejecutar sentencia

dup2(4,1)

se volvió así

inserte la descripción de la imagen aquí

De hecho, esta es la redirección stdoutde , y todas stdoutlas operaciones futuras apuntarán al archivo fd=4de .

1.6 Canalización

La canalización también se basa en el entendimiento previo

int pipe(int fd[2]);

Devuelve si tiene éxito 0, de lo contrario, devuelve -1.

Cuando lo consiga, modificará el contenido fddel array , según lo estipulado: fd[0] → r; fd[1] → w. Leer y escribir datos en el archivo de canalización es en realidad leer y escribir el búfer del núcleo. No open, pero manual close.

Su implementación tiene un diagrama esquemático:

inserte la descripción de la imagen aquí

1.7 Llamada de comandos externos

Podemos usar la siguiente función

int execvp(const char* command, char* argv[]);

Cabe señalar que esta función generalmente no regresa, es decir, la instrucción posterior a ella no se ejecutará, por lo que si se ejecuta, informará de un error. Además, argvel último elemento debe ser NULL.

Por ejemplo, digamos que queremos ingresar el siguiente comando

ls -a -l ~

Entonces los parámetros correspondientes deben ser

command = "ls";
argv = {
    
    "ls", "-a", "-l", "~", NULL};

2. Funciones básicas

2.1 Análisis de la demanda

​ Al principio tenía mucho miedo, porque sentía que el shell estaba relacionado con el sistema operativo, por lo que podría ser por un conocimiento insuficiente del sistema operativo que no podía escribirlo. Más tarde, con una investigación profunda, descubrí que no es tan difícil escribir un shell. Un shell simple que no implementa redirección, canalizaciones, comandos integrados y comandos en segundo plano se puede escribir en más de 100 líneas. En De hecho, su esencia se puede resumir Implementa una systemfunción .

Para la redirección, de hecho, solo necesita identificar el símbolo de redirección <,>,>>por separado y luego registrar el archivo redirigido. Antes de llamar al comando externo, realice la operación de redirección del archivo y luego llámelo.

Para el comando de canalización, debido a que el título solo requiere realizar la conexión de canalización de dos comandos, se pueden mantener dos variables de comando, y luego se identifica la posición |de y luego se corta en dos comandos en consecuencia, y luego respectivamente Se realiza la redirección y se cumple el requisito. Pero los "dos comandos" no son generales, por lo que los expandí a una conexión de cualquier comando de tubería, el efecto se demostrará más adelante y el principio de implementación se presentará más adelante.

2.2 flujo de programa de shell

inserte la descripción de la imagen aquí

Debido a que el proceso de análisis es relativamente complicado, es demasiado engorroso mostrarlo en el diagrama de flujo general. Por lo tanto, se dibuja un diagrama de flujo secundario para describir el proceso de análisis.

inserte la descripción de la imagen aquí

2.3 Visualización de funciones

2.3.1 Símbolo del sistema con característica de identidad

inserte la descripción de la imagen aquí

Se puede ver que cuando se inicia mi shell, el nombre del shell se imprimirá primero Thysrael Shell, y luego en el lado izquierdo del símbolo del sistema en cada línea, ThyShellestarán las palabras, estos son caracteres con la identidad del escritor.

2.3.2 Ejecutar un comando externo sin parámetros

inserte la descripción de la imagen aquí

Lo seleccionamos lscomo objeto de prueba y descubrimos que se puede ejecutar.

2.3.3 Soporte de redirección de E/S

La redirección de la salida estándar, puede ver si está >o >>está funcionando normalmente

inserte la descripción de la imagen aquí

redirección de entrada

inserte la descripción de la imagen aquí

funcionando normalmente

2.3.4 Comandos de tubería

Se pueden canalizar dos comandos juntos

inserte la descripción de la imagen aquí

2.3.5 Combinación de tuberías y redirección

Se puede ver que no hay problema con la combinación de redirección de entrada o redirección de salida y canalización.

inserte la descripción de la imagen aquí

2.3.6 Tamaño del código

ThyShellTodos los códigos están implementados ThyShell.cen , con un total de 322 líneas, lo que cumple con los requisitos de la pregunta.

2.4 Implementación y llamadas al sistema

3. Funciones avanzadas

3.1 Imprimir y comprimir la ruta

En el símbolo del sistema, imprimí la ruta e implementé la compresión de la ruta, es decir, cuando el directorio de inicio aparece en la ruta, se /home/user_namecomprimirá~

inserte la descripción de la imagen aquí

El método de implementación específico es llamar getcwda la función , puede obtener la ruta actual, con la ayuda getenvde la función, puede obtener la ruta del directorio de inicio actual y luego puede comparar y comprimir, el código de implementación específico es el siguiente

void print_prompt()
{
    
    
    char *path = getcwd(NULL, 0);
    const char *home = getenv("HOME");
    if (strstr(home, path) == 0)
    {
    
    
        path[0] = '~';
        size_t len_home = strlen(home);
        size_t len_path = strlen(path);
        memmove(path + 1, path + len_home, len_path - len_home);
        path[len_path - len_home + 1] = '\0';
    }
    printf("ThyShell \033[0;32m%s\033[0m $ ", path);
    free(path);
}

3.2 salir del comando incorporado

El efecto es el siguiente

inserte la descripción de la imagen aquí

El método específico es considerar salir como un comando y luego hacer un juicio antes de llamar al comando externo. Si cumple con el requisito, saldrá directamente. El código de implementación es el siguiente

int builtin_command(Command command)
{
    
    
    if (!strcmp(command.argv[0], "quit"))
    {
    
    
		quit();
    }
    else if (!strcmp(command.argv[0], "cd"))
    {
    
    
        if (chdir(command.argv[1]) != 0)
        {
    
    
            fprintf(stderr, "Error: cannot cd :%s\n", command.argv[1]);
        }
        return 1;
    }

    return 0;
}

3.3 El comando integrado cd

La demostración del efecto es la siguiente.

inserte la descripción de la imagen aquí

Puede ver que puede cambiar libremente bajo los permisos de usuario, y el método para lograrlo es usar chdirla función . El código específico se encuentra en la sección 3.2.

3.4 Detección de errores

Además de la operación de funciones normales, ThyShelltambién tiene una función de detección de anomalías, que puede detectar anomalías de bifurcación, anomalías de espera y anomalías de sintaxis. La implementación específica es envolver la función de llamada del sistema, que no solo garantiza la función normal, sino también hace que el código sea conciso, la implementación específica es la siguiente

void unix_error(char *msg)
{
    
    
	fprintf(stderr, "%s: %s\n", msg, strerror(errno));
	exit(0);
}

pid_t Fork()
{
    
    
	pid_t pid;
	if ((pid = fork()) < 0)
	{
    
    
		unix_error("Fork error");
	}
	return pid;
}

void Wait(pid_t pid)
{
    
    
	int status;
	waitpid(pid, &status, 0);
	if (!WIFEXITED(status))
	{
    
    
		printf("child %d terminated abnormally\n", pid);
	}
}

3.5 Comandos de canalización múltiple

ThyShellSe pueden implementar comandos de canalización múltiple y la demostración específica es la siguiente:

inserte la descripción de la imagen aquí

La entrada cat filename | wc -l | lesspuede aparecer de la siguiente manera

inserte la descripción de la imagen aquí

Indica que la función es normal.

La implementación específica puede hacer referencia al diagrama de flujo, la idea es abstraer la línea de comando en un nivel separado y la línea de comando puede incluir uno o más comandos. Cuando aparece un comando de canalización, la canalización debe abrirse y luego redirigirse.

3.6 Comandos con parámetros

Esto se puede lograr al analizar la línea de comando, y los parámetros se pasarán como los parámetros execvpde , la implementación específica es la siguiente

execvp(command.argv[0], command.argv);

Supongo que te gusta

Origin blog.csdn.net/living_frontier/article/details/129965789
Recomendado
Clasificación