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 type
el 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:
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á
limit
limitada por la configuración del sistema. - De acuerdo con el acuerdo,
shell
al iniciar una nueva aplicación, siempre abra los descriptores de tres números de0
,1
, como , , . Se nombran con macros en C, respectivamente , , .2
stdin
stdout
stderr
STDIN_FILENO
STDOUT_FILENO
STDERR_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-node
nodo , en el que se encuentra el bloque de control que registra la información estática del archivo, y es v-node
la 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 filename
nombrado los permisos flags
descritos 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 |
mode
Se 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 0666
habrá .
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 getc
solemos 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á getc
la 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 oldfd
correspondiente a newfd
la entrada. Si newfd
no hay un archivo correspondiente, newfd
cuando se use nuevamente, el archivo correspondiente es la entrada correspondiente al oldfd
archivo . Si newfd
hay un archivo correspondiente, dup2
se cerrará oldfd
antes de newfd
. Si el valor de retorno es negativo, significa falla.
Por ejemplo, antes de llamar
ejecutar sentencia
dup2(4,1)
se volvió así
De hecho, esta es la redirección stdout
de , y todas stdout
las operaciones futuras apuntarán al archivo fd=4
de .
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 fd
del 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:
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, argv
el ú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 system
funció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
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.
2.3 Visualización de funciones
2.3.1 Símbolo del sistema con característica de identidad
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, ThyShell
estarán las palabras, estos son caracteres con la identidad del escritor.
2.3.2 Ejecutar un comando externo sin parámetros
Lo seleccionamos ls
como 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
redirección de entrada
funcionando normalmente
2.3.4 Comandos de tubería
Se pueden canalizar dos comandos juntos
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.
2.3.6 Tamaño del código
ThyShell
Todos los códigos están implementados ThyShell.c
en , 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_name
comprimirá~
El método de implementación específico es llamar getcwd
a la función , puede obtener la ruta actual, con la ayuda getenv
de 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
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.
Puede ver que puede cambiar libremente bajo los permisos de usuario, y el método para lograrlo es usar chdir
la 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, ThyShell
tambié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
ThyShell
Se pueden implementar comandos de canalización múltiple y la demostración específica es la siguiente:
La entrada cat filename | wc -l | less
puede aparecer de la siguiente manera
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 execvp
de , la implementación específica es la siguiente
execvp(command.argv[0], command.argv);