← Fundamentos de Sistemas Operativos Bloque 8 · Integración y cierre · Tema 32 de 34

Tema 32

La shell: qué ocurre cuando ejecutas un comando

Escribir un comando y pulsar Enter es lo primero que se aprende a hacer en una terminal. Entre ese gesto y la aparición del resultado transcurren uno o dos milisegundos, y en ellos interviene todo lo que estudiamos en el curso. Este tema no presenta conceptos nuevos: sigue un solo comando de principio a fin y, al final, construye una shell propia.

1. Una línea, todo el curso

En el tema 10 seguimos un comando a través de los componentes del sistema, en nueve pasos y sin entrar en ninguno. Prometimos reconstruir ese recorrido cuando tuviéramos con qué. El comando que seguiremos es de los más simples que existen:

terminal
$ cat /etc/hostname
equipo
$ 

Muestra el contenido de un archivo de siete bytes. Lo elegimos porque hace una vez cada cosa: lee de un dispositivo de almacenamiento, escribe en una terminal y termina. En el recorrido participan todos los componentes del sistema:

ParticipanteQué esQué hace aquí
El teclado y la pantallaDispositivosProducen interrupciones; muestran píxeles.
El emulador de terminalUn programa gráficoRecibe las teclas y las entrega a la shell; dibuja lo que los programas escriben.
La shell, bashUn programa de usuarioLee la línea, la interpreta, crea el proceso y espera.
catUn archivo ejecutable, y luego un procesoAbre, lee, escribe y termina.
El enlazador dinámico y la biblioteca de CCódigo compartidoPreparan el programa antes de main; envuelven las llamadas al sistema.
El kernelEl programa que gobierna la máquinaTodo lo demás: atiende cada interrupción y cada pedido de los anteriores.

Hay tres procesos de usuario —el emulador de terminal, la shell y cat— que no se llaman entre sí en ningún momento. Se comunican únicamente a través del kernel, mediante archivos, descriptores y señales. La figura muestra en qué momento trabaja cada uno; el resto del tema la recorre de izquierda a derecha.

Línea de tiempo con tres procesos. El emulador de terminal muestra las teclas y, más tarde, la salida. La shell edita la línea, la interpreta, queda bloqueada en wait y finalmente muestra el indicador. El proceso cat existe solo en el medio: se carga, lee, escribe y termina EMULADOR DE TERMINAL SHELL CAT muestra las teclas muestra la salida edita la línea interpreta bloqueada en wait indica se carga lee escribe sale
La escala no es real: la parte izquierda dura segundos, lo que tardamos en escribir, y todo lo demás, un par de milisegundos.

2. El punto de partida: un sistema en reposo

Antes de la primera tecla, el sistema está quieto, y conviene ver esa quietud con los ojos del curso. La shell mostró su indicador y pidió leer de la terminal. No había nada que leer, de modo que el kernel la puso a dormir:

terminal, desde otra ventana
$ ps -o pid,stat,wchan:20,comm -p 4102
    PID STAT WCHAN                COMMAND
   4102 Ss+  do_select            bash
$ ls -l /proc/4102/fd | head -4
total 0
lrwx------ 1 diego diego 64 oct 12 14:40 0 -> /dev/pts/0
lrwx------ 1 diego diego 64 oct 12 14:40 1 -> /dev/pts/0
lrwx------ 1 diego diego 64 oct 12 14:40 2 -> /dev/pts/0

El estado es S, dormido, y la columna WCHAN indica dentro de qué función del kernel espera: una que aguarda actividad en un descriptor. Sus tres descriptores estándar llevan a /dev/pts/0, el extremo de programas de una pseudoterminal, según el tema 29.

El emulador de terminal está igual, dormido a la espera de un suceso. Casi todos los procesos del sistema lo están. Los procesadores, sin nada que ejecutar, se detuvieron con la instrucción hlt, y el kernel silenció su temporizador, como vimos en el tema 19. Nada ocurrirá hasta que llegue una interrupción.

3. Las teclas

La primera tecla, la c, pone todo en movimiento. Su camino hasta aparecer en pantalla es más largo de lo que parece:

  1. El controlador del teclado genera una interrupción. Un procesador despierta, guarda lo poco que estaba haciendo, pasa a modo kernel y salta, a través de la tabla de vectores, al manejador. interrupción
  2. El driver del teclado lee el código de la tecla y lo entrega al subsistema de entrada, que lo deja disponible en un archivo de /dev/input y despierta a quien lo esté esperando. modo kernel
  3. El entorno gráfico recibe el suceso, determina qué ventana tiene el foco y se lo comunica al emulador de terminal. modo usuario
  4. El emulador escribe el carácter c en su extremo de la pseudoterminal, con una llamada write. llamada al sistema
  5. El kernel pasa el carácter al otro extremo, /dev/pts/0, y despierta a la shell, que dormía esperando entrada. modo kernel
  6. La shell lee el carácter, lo añade a la línea que está armando y lo escribe en su salida, para que se vea. llamadas al sistema
  7. El emulador lee ese eco de su extremo y dibuja la letra en la ventana. modo usuario

Una letra en pantalla supone una interrupción, varios cambios de contexto entre tres procesos y una decena de llamadas al sistema. Es el recorrido de la tecla del tema 17, con todos los intermediarios que entonces omitimos. Y ocurre dos veces por tecla, porque soltarla produce otra interrupción.

El paso 6 precisa algo que dijimos en el tema 29. Allí vimos que el kernel puede retener la línea y hacer él mismo el eco. Bash prefiere encargarse: mientras se escribe un comando, pone la terminal en un modo en que cada tecla le llega de inmediato y sin eco, y es él quien muestra los caracteres. Por eso funcionan las flechas, el historial y el completado con el tabulador, que el kernel no sabría hacer. Antes de ejecutar un comando, Bash devuelve la terminal al modo normal, de manera que el programa lanzado la encuentre como espera.

4. Enter: la shell interpreta la línea

Las dieciocho pulsaciones recorren el mismo camino. La última, Enter, le indica a la shell que la línea está completa. Lo que sigue ocurre por entero dentro de la shell, en modo usuario: es trabajo sobre texto.

PasoQué hace la shellEn nuestra línea
SepararDivide la línea en palabras y operadores.Dos palabras: cat y /etc/hostname.
ExpandirSustituye variables, comodines y otras construcciones por su valor.Nada que expandir.
Reconocer redireccionesAparta los >, < y |, que no son argumentos.No hay.
Clasificar el comandoDetermina qué es la primera palabra: un alias, una función, un comando interno o un programa.Un programa.
LocalizarloBusca un archivo ejecutable con ese nombre en los directorios de la variable PATH./usr/bin/cat

La expansión merece un ejemplo, porque corrige una idea equivocada muy extendida. Cuando se escribe ls *.c, el programa ls no recibe ningún asterisco:

terminal
$ strace -e trace=execve ls *.c 2>&1 | head -1
execve("/usr/bin/ls", ["ls", "abrir.c", "hijo.c", "lanzar.c", "mapa.c"], 0x7ffd… /* 36 vars */) = 0

La shell reemplazó el patrón por la lista de archivos que coinciden antes de crear ningún proceso. Los programas reciben siempre argumentos ya expandidos, y por eso todos interpretan los comodines del mismo modo: ninguno los interpreta.

La clasificación decide si hará falta un proceso nuevo. El comando type muestra el resultado:

terminal
$ type cat cd echo ll
cat is /usr/bin/cat
cd is a shell builtin
echo is a shell builtin
ll is aliased to `ls -alF'
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Los comandos internos los ejecuta la propia shell, sin crear ningún proceso. Algunos lo son por rapidez, como echo. Otros, porque no podrían ser otra cosa: cd debe cambiar el directorio de la shell, y un programa aparte cambiaría el suyo, como vimos en el tema 28.

cat es un programa. Para encontrarlo, la shell recorre en orden los directorios de PATH y pregunta al kernel, en cada uno, si existe allí un archivo ejecutable con ese nombre. Lo encuentra en el cuarto. La primera vez anota la ubicación, y las siguientes ya no busca.

5. Nace un proceso

Con la ruta del programa en la mano, la shell crea un proceso. Es el fork del tema 26, que la biblioteca traduce a la llamada clone:

  1. La shell llama a fork. Entra al kernel. llamada al sistema
  2. El kernel reserva una estructura de proceso y le asigna el siguiente PID libre. modo kernel
  3. Copia las credenciales, el directorio actual y la tabla de descriptores. El hijo queda con los mismos descriptores 0, 1 y 2, que llevan a la terminal. modo kernel
  4. Duplica el espacio de direcciones sin copiar memoria: crea tablas de páginas que apuntan a los mismos marcos, marcados como de solo lectura. modo kernel
  5. Pone al hijo en la cola de procesos listos y vuelve. A la shell le devuelve el PID del hijo; al hijo, cuando le toque ejecutarse, le devolverá cero. modo kernel

Durante un instante hay dos procesos bash idénticos. El padre no tiene más que hacer por ahora: llama a wait y queda dormido. El hijo, en cambio, todavía ejecutando código de la shell, hace unos preparativos antes de convertirse en otro programa:

Es el intervalo entre fork y exec cuya utilidad señalamos en los temas 26 y 28: ya es otro proceso, y aún puede prepararse el terreno.

6. El proceso cambia de programa

El hijo llama a execve("/usr/bin/cat", ["cat", "/etc/hostname"], entorno). Es la carga que describimos en el tema 9, y ahora podemos leerla completa:

  1. El kernel recorre la ruta hasta el inodo de /usr/bin/cat, y comprueba que el proceso tenga permiso de ejecución sobre él. archivos y permisos
  2. Lee los primeros bytes del archivo y reconoce el número mágico de ELF. formato
  3. Descarta el espacio de direcciones actual: la copia de la shell desaparece. memoria
  4. Crea uno nuevo, mapeando los segmentos del ejecutable con sus permisos: el código, de lectura y ejecución; los datos, de lectura y escritura. No lee el archivo: anota qué parte corresponde a cada rango. memoria
  5. Mapea también el enlazador dinámico, cuyo nombre figura en la cabecera del ejecutable. memoria
  6. Prepara una pila nueva y copia en ella los argumentos y las variables de entorno. memoria
  7. Fija el contador de programa en el punto de entrada del enlazador dinámico, y vuelve a modo usuario. cambio de modo

Las direcciones de todo lo mapeado se eligieron al azar, por el mecanismo del tema 27. Y hay cosas que execve no toca: el PID, el padre, las credenciales y la tabla de descriptores siguen siendo los mismos. El proceso es el que creó fork, con otro contenido.

La primera instrucción del programa nuevo no llega a ejecutarse. La página que la contiene no tiene todavía un marco asignado, y la CPU genera un fallo de página. El kernel lo atiende trayendo esa página del archivo —que con toda probabilidad está en la caché de disco, porque cat es un programa de uso frecuente— y la instrucción se repite. Es el primero de alrededor de un centenar de fallos de página que este proceso sufrirá en su milisegundo de vida, todos resueltos sin que lo note, como en el tema 18.

7. Antes de main

El proceso se ejecuta en modo usuario, pero todavía no es cat quien tiene el control. Es el enlazador dinámico, que debe completar el programa con las bibliotecas que necesita. Es la parte de la traza que identificamos en el tema 16 como los actos segundo y tercero:

Unas veinticinco llamadas al sistema —openat, mmap, mprotect— antes de que se ejecute la primera línea escrita por los autores de cat. De las cuatro decenas y media de llamadas que hará este proceso, la mayoría pertenece a esta fase.

8. El programa hace su trabajo

Por fin, main. El trabajo de cat es el bucle de copiar.c, del tema 14, precedido de una apertura. En la traza del tema 16 eran cinco líneas:

la parte central de la traza de cat
openat(AT_FDCWD, "/etc/hostname", O_RDONLY) = 3
read(3, "equipo\n", 131072)             = 7
write(1, "equipo\n", 7)                 = 7
read(3, "", 131072)                     = 0
close(3)                                = 0

La apertura es el recorrido del tema 28. El kernel parte del inodo raíz, busca etc en su tabla, obtiene otro inodo, busca hostname en él. En cada directorio comprueba el permiso de atravesarlo, y en el archivo, el de lectura, contra las credenciales del proceso: las del usuario que escribió el comando, heredadas a través de la shell. Crea una entrada de archivo abierto con posición cero y devuelve el descriptor libre más bajo, el 3.

La lectura depende de dónde estén los datos:

Si el bloque está en la cachéSi no está
El kernelCopia los bytes de la caché al búfer del programaPide el bloque al driver del disco y pone al proceso a dormir
El procesoNo deja la CPUPasa al estado bloqueado; otro proceso usa el procesador
Interviene el hardwareNoEl controlador lee, copia a memoria por DMA y avisa con una interrupción
DuraciónMenos de un microsegundoDecenas de microsegundos, o milisegundos en un disco giratorio

La segunda columna es la secuencia completa del tema 8, con la interrupción del tema 17 y el cambio de contexto voluntario del tema 19. En cualquiera de los dos casos, el programa no percibe diferencia: llamó a read y recibió 7.

Pidió 131 072 bytes y obtuvo siete. No es un error: es el límite del que hablamos en el tema 14. Por eso hará luego una segunda lectura, que devuelve cero, para saber que el archivo terminó.

9. La salida llega a la pantalla

cat llama a write(1, "equipo\n", 7). No sabe qué hay en el descriptor 1. Lo que hay es /dev/pts/0, y los siete bytes emprenden el camino inverso al de las teclas:

  1. El kernel busca el descriptor 1 en la tabla del proceso, llega a un archivo de dispositivo de caracteres y pasa el pedido a la función de escritura del driver de terminales. tabla de operaciones
  2. La disciplina de línea procesa la salida. Convierte el salto de línea en el par «retorno de carro y avance de línea» que una terminal necesita. modo kernel
  3. Los bytes quedan en el búfer del otro extremo de la pseudoterminal, y el kernel despierta al emulador de terminal, que dormía esperándolos. write devuelve 7. modo kernel
  4. El emulador de terminal lee los bytes. Interpreta los caracteres de control, decide en qué fila y columna va cada letra y calcula su dibujo con el tipo de letra elegido. modo usuario
  5. Entrega la imagen de su ventana al entorno gráfico, que la combina con las demás ventanas. modo usuario
  6. El driver de video envía el resultado a la tarjeta gráfica, que lo muestra en el siguiente refresco de la pantalla. modo kernel

Hay una asimetría que conviene notar. Para cat, la escritura terminó en el paso 3: recibió su 7 y siguió adelante. Las letras aparecen en pantalla después, cuando el emulador recibe su turno de procesador, y es muy probable que para entonces cat ya no exista. «Escribir en la pantalla» significa, para un programa, dejar bytes en un búfer del kernel.

Como el descriptor 1 es una terminal, la biblioteca de C habría usado el búfer por líneas del tema 15. cat ni siquiera pasa por ella: llama a write directamente, con todo lo que leyó.

10. El final

La segunda lectura devuelve cero. cat cierra el descriptor 3 y retorna de main. El código de arranque llama a exit, que vacía los búferes de la biblioteca y hace la llamada al sistema exit_group:

  1. El kernel cierra los descriptores que quedaban abiertos. modo kernel
  2. Libera el espacio de direcciones. Los marcos que solo usaba este proceso vuelven a estar disponibles; los de la biblioteca siguen en uso por los demás, y los del archivo ejecutable permanecen en la caché de disco. modo kernel
  3. Guarda el código de salida, 0, en la estructura del proceso, que pasa al estado zombi. modo kernel
  4. Envía SIGCHLD al padre y lo despierta de su espera. modo kernel
  5. Llama al planificador, porque el proceso que estaba en la CPU dejó de existir. modo kernel

La shell recibe el procesador y su llamada wait retorna, con el PID del hijo y su código de salida. En ese momento el kernel elimina la estructura del proceso: el zombi duró unos microsegundos. La shell entonces:

El sistema volvió al estado de la sección 2. Los procesadores no tienen nada que hacer y se detienen. Entre la pulsación de Enter y este instante pasaron entre uno y dos milisegundos.

11. Verlo ocurrir

El recorrido puede observarse casi completo. La parte de la shell se ve conectando strace a ella desde otra terminal, antes de escribir el comando:

terminal 2, mientras en la terminal 1 se ejecuta cat /etc/hostname
$ sudo strace -f -e trace=clone,execve,wait4,exit_group -p 4102
strace: Process 4102 attached
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3c1a2b4a10) = 9301
strace: Process 9301 attached
[pid  4102] wait4(-1,  <unfinished ...>
[pid  9301] execve("/usr/bin/cat", ["cat", "/etc/hostname"], 0x5c7e0d4f1b20 /* 36 vars */) = 0
[pid  9301] exit_group(0)               = ?
[pid  9301] +++ exited with 0 +++
<... wait4 resumed>[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WSTOPPED|WCONTINUED, NULL) = 9301
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=9301, si_uid=1000, si_status=0, …} ---

Son las secciones 5, 6 y 10 en cinco líneas: crear, esperar, reemplazar, terminar, despertar. Quitando el filtro se ve además la lectura de cada tecla y la búsqueda en PATH. La parte de cat es la traza del tema 16. Con las dos, y algunas cuentas, se puede dimensionar lo que cuesta un comando:

terminal
$ time cat /etc/hostname
equipo

real	0m0.002s
user	0m0.000s
sys	0m0.002s
$ strace -c cat /etc/hostname 2>&1 | tail -1
100.00    0.000000           0        47         1 total
Para mostrar siete bytes…Cantidad aproximada
Interrupciones del teclado36: dos por cada una de las dieciocho pulsaciones
Procesos creados1
Llamadas al sistema de catUnas 50, de las cuales 5 son su trabajo propio
Archivos abiertos por cat4: el índice de bibliotecas, la biblioteca de C, la configuración regional y el que pedimos
Fallos de páginaAlrededor de 100
Procesos de usuario que intervienen3, además del entorno gráfico
Tiempo, desde EnterDe 1 a 2 milisegundos

Las cifras varían de un equipo a otro, pero el orden de magnitud es ese. La desproporción entre el resultado y el esfuerzo es la medida de lo que el sistema operativo hace por cada programa. El autor de cat escribió un bucle con read y write; no escribió nada sobre teclados, discos, pantallas, memoria ni otros programas.

12. Variaciones

Con pequeños cambios en la línea, el recorrido se modifica en puntos concretos. Cada variación corresponde a algo que ya estudiamos:

Se escribeQué cambiaTema
cd /tmpEs un comando interno. No hay fork ni exec: la shell llama a chdir en su propio proceso.28
cat /etc/hostname > copiaEntre fork y exec, el hijo abre copia y lo coloca en el descriptor 1 con dup2. cat escribe en un archivo, y la sección 9 no ocurre.28
cat /etc/hostname | wc -cLa shell crea una tubería y dos procesos. La salida de uno es la entrada del otro, y los dos se ejecutan a la vez.28
sleep 100 &La shell no llama a wait: muestra el indicador enseguida. Recogerá al hijo cuando termine.26
Ctrl+C durante la ejecuciónLa disciplina de línea envía SIGINT al grupo que está en primer plano: el del comando, no el de la shell.29
noexisteLa búsqueda en PATH falla. La shell informa command not found y deja 127 en $?, sin crear ningún proceso.26
./notas.txtexecve falla con EACCES, porque el archivo no tiene permiso de ejecución. El código es 126.30
./script.shEl kernel encuentra #! en lugar del número mágico de ELF, y ejecuta el intérprete indicado en esa línea, pasándole el archivo.9
./segfaultUna excepción termina con el proceso. wait informa a la shell que murió por una señal, y la shell escribe el mensaje.18
sudo cat /etc/shadowEl programa ejecutado es sudo, que tiene el bit setuid. Comprueba la autorización y ejecuta cat con otra identidad.30

El caso de la tubería deja ver cómo se combinan las piezas. Para cat /etc/hostname | wc -c, la shell pide al kernel una tubería y recibe dos descriptores. Crea dos hijos: en el primero pone el extremo de escritura en el descriptor 1; en el segundo, el de lectura en el 0. Cada uno ejecuta su programa, y ninguno sabe que el otro existe. cat cree escribir en su salida; wc cree leer de su entrada. Cuando cat termina, su extremo se cierra, y la lectura de wc devuelve cero: es el fin de los datos.

13. Una shell propia

En el tema 3 anotamos, entre las decisiones de diseño de Unix, que la shell es un programa común, que cualquiera puede reemplazar. Tenemos todo para comprobarlo. El siguiente programa es una shell: muestra un indicador, lee una línea, la divide en palabras y ejecuta el comando.

minishell.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#define MAX_ARGS 32

int main(void)
{
    char linea[1024];
    char *args[MAX_ARGS];

    for (;;) {
        printf("mini$ ");
        fflush(stdout);                         /* el indicador no termina en salto de línea */

        if (fgets(linea, sizeof(linea), stdin) == NULL)
            break;                              /* fin de la entrada: Ctrl+D */

        /* 1. separar la línea en palabras */
        int n = 0;
        for (char *p = strtok(linea, " \t\n"); p != NULL && n < MAX_ARGS - 1; p = strtok(NULL, " \t\n"))
            args[n++] = p;
        args[n] = NULL;

        if (n == 0)
            continue;                           /* línea vacía */

        /* 2. comandos internos: se ejecutan en este mismo proceso */
        if (strcmp(args[0], "exit") == 0)
            break;
        if (strcmp(args[0], "cd") == 0) {
            if (chdir(n > 1 ? args[1] : getenv("HOME")) == -1)
                perror("cd");
            continue;
        }

        /* 3. programas: crear, reemplazar, esperar */
        pid_t pid = fork();
        if (pid == -1) {
            perror("fork");
            continue;
        }
        if (pid == 0) {
            execvp(args[0], args);              /* busca en PATH y ejecuta */
            perror(args[0]);
            _exit(127);
        }

        int estado;
        waitpid(pid, &estado, 0);

        if (WIFSIGNALED(estado))
            printf("[finalizado por la senal %d]\n", WTERMSIG(estado));
        else if (WEXITSTATUS(estado) != 0)
            printf("[codigo de salida %d]\n", WEXITSTATUS(estado));
    }
    return 0;
}
terminal
$ gcc -Wall -Wextra -g minishell.c -o minishell
$ ./minishell
mini$ cat /etc/hostname
equipo
mini$ pwd
/home/diego/so
mini$ cd /tmp
mini$ pwd
/tmp
mini$ ls /noexiste
ls: cannot access '/noexiste': No such file or directory
[codigo de salida 2]
mini$ /home/diego/so/segfault
Antes de escribir
[finalizado por la senal 11]
mini$ noexiste
noexiste: No such file or directory
[codigo de salida 127]
mini$ exit
$ 

Cincuenta líneas, y ejecuta cualquier programa del sistema. Cada parte del código tiene su tema:

En el códigoCorresponde aTema
fflush(stdout) después del indicadorEl búfer por líneas: sin esa llamada, el texto sin salto de línea no aparecería.15
fgets devuelve NULLCtrl+D: la disciplina de línea hace que read devuelva cero.14, 29
cd tratado aparteEl directorio actual es de cada proceso.28
fork, execvp, waitpidCrear, reemplazar, esperar.26
_exit(127) y no returnSi exec falla, el hijo es una copia de la shell: no debe vaciar búferes que no son suyos.15, 26
WIFSIGNALEDDistinguir un final voluntario de uno por señal.18

Lo que le falta respecto de Bash es, casi todo, el trabajo sobre texto de la sección 4. No expande comodines ni variables, de modo que ls *.c le pasa a ls un asterisco literal. No entiende comillas, redirecciones ni tuberías. Y no hace los preparativos de la sección 5: no crea grupos de procesos ni maneja señales, por lo que Ctrl+C la termina a ella junto con el comando en curso. Los ejercicios proponen añadir algunas de estas cosas. Ninguna requiere nada que no hayamos visto.

La parte que habla con el kernel, en cambio, está completa, y cabe en diez líneas. La interfaz que Unix definió en los años setenta para ejecutar programas es así de pequeña.

14. El mapa completo

Para cerrar, el recorrido entero en una tabla, con el tema donde se estudió cada paso. Es también un índice del curso, ordenado no por materia sino por el orden en que las cosas ocurren:

MomentoQué ocurreTemas
En reposoLa shell duerme en una lectura; los procesadores están detenidos.13, 19, 26
Una teclaInterrupción, driver, subsistema de entrada.8, 17, 29
El ecoEmulador, pseudoterminal, shell; cambios de contexto entre los tres.19, 29
EnterLa shell separa, expande y busca en PATH.10, 28
Crearfork: estructura de proceso, copia al escribir, descriptores heredados.26, 27, 28
Reemplazarexecve: permisos, formato ELF, espacio de direcciones nuevo.9, 27, 30
Cargar a pedidoFallos de página resueltos desde la caché de disco.7, 18
EnlazarEl enlazador dinámico mapea la biblioteca de C.9, 15, 16
AbrirRecorrido de la ruta, permisos, descriptor.28, 30
LeerLlamada al sistema, cambio de modo, caché o disco.12, 13, 14, 7, 8
EscribirDriver de terminal, disciplina de línea, emulador, video.14, 29
Terminarexit, liberación de recursos, zombi, SIGCHLD.15, 26
Recogerwait retorna; la shell muestra el indicador y vuelve a dormir.26
Durante todo el recorridoEl temporizador interrumpe; el planificador reparte los procesadores; el kernel contabiliza.19, 31
Antes de todoEl firmware, el cargador y el sistema de inicio dejaron el kernel en marcha y la sesión abierta.20, 21, 22

En el tema 1 hicimos este mismo ejercicio con un programa de una línea, y lo resolvimos en seis pasos, casi todos con palabras que no podíamos explicar. La distancia entre aquella lista y esta tabla es el contenido del curso.

Queda aplicarlo. En el tema 33 construimos un programa completo, un inspector del sistema, que usa las llamadas al sistema y los archivos de /proc para informar qué está haciendo la máquina.

15. Resumen

16. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué tres procesos de usuario intervienen al ejecutar cat /etc/hostname, y cómo se comunican?

El emulador de terminal, la shell y cat. No se llaman entre sí: se comunican a través del kernel, mediante la pseudoterminal, los descriptores heredados y la notificación de la terminación del hijo.

2. ¿En qué estado está la shell mientras espera que se escriba un comando?

Dormida, bloqueada en una llamada al sistema que espera entrada de la terminal. No consume procesador.

3. ¿Quién muestra en pantalla las letras de un comando mientras se lo escribe en Bash?

Bash. Durante la edición pone la terminal en un modo sin eco y recibe cada tecla de inmediato; él mismo escribe los caracteres, y el emulador de terminal los dibuja. Así puede ofrecer historial y completado.

4. En ls *.c, ¿quién reemplaza el asterisco por los nombres de archivo?

La shell, antes de crear el proceso. ls recibe como argumentos los nombres ya expandidos.

5. ¿Por qué cd no puede ser un programa?

Porque debe cambiar el directorio actual de la shell, que es un dato de ese proceso. Un programa aparte cambiaría el suyo y terminaría, sin ningún efecto.

6. ¿Cómo encuentra la shell el archivo ejecutable de cat?

Recorre en orden los directorios de la variable PATH y comprueba en cada uno si existe un archivo ejecutable con ese nombre. Usa el primero que encuentra, y recuerda la ubicación.

7. ¿Qué hace el proceso hijo entre fork y exec?

Se coloca en un grupo de procesos propio y lo declara en primer plano en la terminal, restablece el tratamiento de las señales y aplica las redirecciones que tenga la línea.

8. ¿Qué conserva un proceso después de execve, y qué pierde?

Conserva el PID, el padre, las credenciales, el directorio actual y los descriptores abiertos. Pierde todo su espacio de direcciones: código, datos, heap y pila se reemplazan por los del programa nuevo.

9. ¿Por qué la primera instrucción del programa nuevo provoca un fallo de página?

Porque execve no lee el ejecutable: solo anota qué parte del archivo corresponde a cada rango de direcciones. La página se trae, normalmente de la caché de disco, en el primer acceso.

10. ¿Quién ejecuta las primeras llamadas al sistema del proceso, antes de main?

El enlazador dinámico, que localiza y mapea las bibliotecas compartidas, y luego el código de inicialización de la biblioteca de C.

11. Cuando write devuelve 7 a cat, ¿ya está el texto en la pantalla?

No necesariamente. Los bytes están en el búfer de la pseudoterminal. El emulador de terminal los leerá y dibujará cuando reciba su turno de procesador.

12. ¿Por qué cat hace una segunda lectura, si ya obtuvo los siete bytes del archivo?

Porque recibir menos bytes de los pedidos no indica el fin del archivo. Solo una lectura que devuelve cero lo confirma.

13. ¿Cómo se entera la shell de que el comando terminó?

Estaba bloqueada en wait. Al terminar el hijo, el kernel le envía SIGCHLD y la despierta; la llamada retorna con el PID del hijo y la forma en que terminó.

14. ¿Qué cambia en el recorrido si se añade > copia al comando?

Entre fork y exec, el hijo abre el archivo y lo coloca en el descriptor 1. El programa escribe en el archivo sin saberlo, y nada llega a la terminal.

15. En minishell.c, ¿por qué el hijo termina con _exit si execvp falla?

Porque en ese punto el hijo es todavía una copia de la shell, con una copia de sus búferes de stdio. exit los vaciaría, y lo que estuviera pendiente saldría dos veces. _exit termina sin tocarlos.

Para practicar

  1. Reproduce el experimento de la sección 11 con dos terminales. Repítelo con cd /tmp, con echo hola y con ls | wc -l, y explica qué llamadas aparecen en cada caso.
  2. Conecta strace a tu shell sin filtros y pulsa una sola tecla. ¿Qué llamadas hace la shell? Identifica la lectura de la tecla y la escritura del eco.
  3. Ejecuta strace -f -e trace=newfstatat,access,execve bash -c 'noexiste' y observa la búsqueda en los directorios de PATH.
  4. Clasifica con type estos comandos: pwd, kill, ls, exit, test, time. ¿Cuáles existen además como programas? Compruébalo con type -a.
  5. Mide con strace -c las llamadas al sistema de cat /etc/hostname y cuenta cuántas ocurren antes del openat del archivo.
  6. Añade a minishell.c el comando interno pwd, usando la función getcwd. Luego, que muestre el directorio actual en el indicador.
  7. Añade a minishell.c la redirección de salida: si la penúltima palabra es >, la última es el archivo. Usa lo que hace redirigir.c, del tema 28.
  8. Haz que minishell ejecute en segundo plano los comandos que terminan en &. ¿Qué pasa con esos hijos cuando terminan, si nadie los espera? Compruébalo con ps.
  9. Ejecuta sleep 100 en minishell y pulsa Ctrl+C. ¿Qué proceso termina? Haz que la shell ignore SIGINT con signal(SIGINT, SIG_IGN) y que el hijo restablezca el tratamiento por defecto antes de execvp.
  10. Elige otro comando simple, como date o ls, y escribe su recorrido completo siguiendo las secciones 3 a 10, señalando en qué difiere del de cat.