La familia exec - Procesos e Hilos
Volver a Procesos e Hilos Bloque 4 - Ejecutar programás con exec - Tema 16 de 43

Tema 16

La familia exec

fork crea un proceso duplicando al padre. Pero ¿Como se pasa de "tengo una copia del padre" a "estoy ejecutando otro programa"? La respuesta es exec: una familia de funciónes que reemplazan el programa del proceso actual por otro, Sin cambiar el PID. En este tema veras las seis variantes de exec, Que se reemplaza, Que se conserva, y como se usan en la práctica.

1. Qué es exec

La familia de funciónes exec reemplaza el programa del proceso actual por otro. El proceso no cambia: Sigue teniendo el mismo PID, el mismo PPID, los mismos descriptores de archivo. Pero su código, datos y stack se reemplazan por los del nuevo programa. Si exec tiene exito, nunca retorna: el código que sigue a exec en el mismo proceso no se ejecuta.

exec es la segúnda mitad del patron de unix para crear procesos: fork crea el proceso (Duplicando al padre), y exec reemplaza el programa (cargando el nuevo). Esta separación es una de las decisiones de diseno más importantes de unix, y la veremos en detalle en el tema 17.

Hay seis variantes de exec, Que se diferencian en como se pasan los argumentos y el entorno, y en sí buscan el programa en el PATH. Todas llaman internamente a la syscall execve(), Que es la unica que el núcleo implementa.

exec no crea un proceso nuevo

exec no crea un proceso: reemplaza el programa del proceso actual. El PID no cambia. Si quieres crear un proceso nuevo y ejecutar un programa, primero haces fork (para crear el proceso) y luego exec (para reemplazar el programa). Si llamás a exec sin fork, el programa actual se reemplaza y no hay vuelta atras.

2. La familia exec

Las seis funciónes de la familia exec se declaran en <unistd.h>. Sus nombres siguen un patron: la primera letra indica como se pasan los argumentos (l = lista, v = vector/arreglo), la segúnda indica sí se busca en el PATH (p = path), y la tercera indica sí se pasa un entorno explicito (e = environment).

FuncionArgumentosPATHEntornoDescripcion
execlLista (varargs)NoHerado del padreEjecuta un programa con argumentos en lista.
execvArreglo (argv)NoHerado del padreEjecuta un programa con argumentos en arreglo.
execlpLista (varargs)SiHerado del padreBusca el programa en PATH.
execvpArreglo (argv)SiHerado del padreBusca el programa en PATH.
execveArreglo (argv)NoExplicito (envp)La syscall real. Permite pasar un entorno controlado.
execvpeArreglo (argv)SiExplicito (envp)Busca en PATH y permite entorno explicito (extension GNU).

Todas las variantes llaman internamente a execve(), Que es la unica syscall que el núcleo implementa. Las demás son envolturas de la biblioteca de C Que preparan los argumentos y el entorno antes de llamar a execve.

3. Qué se reemplaza

Cuando exec tiene exito, el núcleo reemplaza completamente el programa del proceso:

El nuevo programa empieza desde su función main() (o desde el entry point del ELF), con los argumentos argc y argv Que se pasaron a exec, y las variables de entorno que se especificaron (o las heredadas).

4. Qué se conserva

Despues de exec, el proceso conserva varios atributos:

AtributoSe conserva?Detalle
PIDSiEl PID no cambia. Es el mismo proceso.
PPIDSiEl padre no cambia.
Descriptores de archivoSi (salvo O_CLOEXEC)Los descriptores abiertos se conservan, excepto los marcados con FD_CLOEXEC.
Directorio de trabajoSiEl cwd no cambia.
Directorio raizSiEl root no cambia.
umaskSiLa umask no cambia.
Senales bloqueadasSiLa máscara de senales bloqueadas se conserva.
Limites de recursosSiLos rlimit se conservan.
ID de usuario y grupoSiLas credenciales no cambian.
Variables de entornoSi (salvo execve)Se conservan, excepto con execve que permite pasar un entorno nuevo.

La conservacion de descriptores de archivo es especialmente importante: un programa puede abrir un archivo, hacer fork + exec, y el nuevo programa hereda el descriptor. Esto es la base de la redirección de entrada/salida en la shell.

Los manejadores de senales se pierden

Despues de exec, todos los manejadores de senales se restablecen a sus valores por omision. Si el programa anterior tenia un manejador para SIGTERM, el nuevo programa no lo tiene. Las senales que estaban bloqueadas siguen bloqueadas, pero las pendientes se descartan.

5. execl: lista de argumentos

execl recibe los argumentos como una lista variable (varargs), terminada en NULL:

C - firma de execl

int execl(const char *pathname, const char *arg, ..., /* (char *) NULL */);

El primer argumento es la ruta del programa. El segúndo es el nombre del programa (argv[0], Que por convencion es el nombre del programa). Los siguientes son los argumentos, terminados en NULL.

ejemplo_execl.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 

int main(void)
{
    printf("Antes de exec: PID %d\n", (int) getpid());

    /* Ejecutar "ls -l /tmp" */
    execl("/bin/ls", "ls", "-l", "/tmp", NULL);

    /* Si llegamos aquí, exec fallo */
    perror("execl");
    return 1;
}

execl es comodo cuándo se conocen los argumentos en tiempo de compilacion. Pero es inflexible: no se puede construir la lista de argumentos dinaminimamente.

El NULL final es obligatorio: Sin el, execl no sabe dónde terminan los argumentos y lee memoria invalida. Es un error comun olvidar el NULL.

6. execv: arreglo de argumentos

execv recibe los argumentos como un arreglo (vector) de punteros a cadenas, terminado en NULL:

C - firma de execv

int execv(const char *pathname, char *const argv[]);

El primer argumento es la ruta del programa. El segúndo es un arreglo de cadenas (argv), Dónde argv[0] es el nombre del programa y el último elemento es NULL.

ejemplo_execv.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 

int main(void)
{
    char *args[] = {"ls", "-l", "/tmp", NULL};

    printf("Antes de exec: PID %d\n", (int) getpid());

    execv("/bin/ls", args);

    perror("execv");
    return 1;
}

execv es más flexible que execl porque el arreglo se puede construir dinaminimamente. Es útil cuándo los argumentos se generan en tiempo de ejecución (por ejemplo, leyendo de un archivo o de la entrada del usuario).

7. execvp y execlp: busqueda en PATH

execvp y execlp son como execv y execl, pero buscan el programa en los directorios del PATH Sí el nombre no contiene una barra (/). Esto es lo que hace la shell cuándo ejecutas un comando.

C - firmás de execvp y execlp

int execvp(const char *file, char *const argv[]);
int execlp(const char *file, const char *arg, ..., /* (char *) NULL */);

Si el nombre del programa contiene una barra (/), se usa como ruta absoluta o relativa. Si no contiene barra, se busca en cada directorio del PATH.

ejemplo_execvp.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 

int main(void)
{
    /* Busca "ls" en PATH */
    char *args[] = {"ls", "-l", "/tmp", NULL};
    execvp("ls", args);  /* no necesita "/bin/ls" */

    perror("execvp");
    return 1;
}

execvp es la función más usada en la práctica, porque permite ejecutar programás por nombre (Como la shell) Sin saber su ruta completa. Es lo que usa la shell cuándo ejecutas un comando.

8. execve: con entorno explicito

execve es la syscall real: la unica que el núcleo implementa. Las demás son envolturas de la biblioteca de C. execve permite pasar un entorno (envp) explicito, en lugar de heredar el del padre.

C - firma de execve

int execve(const char *pathname, char *const argv[], char *const envp[]);

El tercer argumento es un arreglo de cadenas con las variables de entorno, terminado en NULL. Cada cadena tiene el formato "NOMBRE=valor". Si se pasa NULL o un arreglo vacio, el programa se ejecuta sin variables de entorno.

ejemplo_execve.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 

int main(void)
{
    char *args[] = {"env", NULL};
    char *env[] = {
        "PATH=/usr/bin:/bin",
        "HOME=/tmp",
        "MI_VAR=hola",
        NULL
    };

    /* Ejecutar "env" con un entorno controlado */
    execve("/usr/bin/env", args, env);

    perror("execve");
    return 1;
}
terminal - ejecutar ejemplo_execve

$ gcc -std=c17 -Wall -Wextra -o ejemplo_execve ejemplo_execve.c
$ ./ejemplo_execve
PATH=/usr/bin:/bin
HOME=/tmp
MI_VAR=hola

El programa ejecuta env con un entorno que sólo tiene 3 variables. Las variables del padre (USER, SHELL, etc.) no se pasan. Esto es útil para ejecutar programás con un entorno controlado (por ejemplo, para seguridad o para pruebas).

9. execvpe: con entorno y PATH

execvpe combina lo mejor de execvp y execve: busca el programa en PATH y permite pasar un entorno explicito. Es una extension GNU (no esta en POSIX, pero esta disponible en linux con glibc).

C - firma de execvpe

int execvpe(const char *file, char *const argv[], char *const envp[]);

Es la función más completa: busca en PATH y permite entorno explicito. Si necesitas ambas funciónalidades y estas en linux, execvpe es la opcion.

10. El patron fork + exec

El patron más comun en unix es: fork para crear un proceso hijo, y exec en el hijo para ejecutar otro programa. El padre espera al hijo con wait.

fork_exec.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "Uso: %s  [args...]\n", argv[0]);
        return 1;
    }

    pid_t pid = fork();
    if (pid < 0) { perror("fork"); return 1; }

    if (pid == 0) {
        /* Hijo: ejecutar el comando */
        execvp(argv[1], &argv[1]);
        /* Si llegamos aquí, exec fallo */
        perror("execvp");
        _exit(127);  /* Código de error estandar */
    }

    /* Padre: esperar al hijo */
    int status;
    waitpid(pid, &status, 0);
    if (WIFEXITED(status))
        printf("Hijo terminó con código %d\n", WEXITSTATUS(status));
    return 0;
}
terminal - ejecutar fork_exec

$ gcc -std=c17 -Wall -Wextra -o fork_exec fork_exec.c
$ ./fork_exec ls -l /tmp
total 4
drwxr-xr-x 2 ana ana 4096 oct 16 20:00 archivo1
...
Hijo terminó con código 0
$ ./fork_exec comando_inexistente
execvp: No such file or directory
Hijo terminó con código 127

Este programa es una mini shell: ejecuta cualquier comando que se le pase como argumento. El hijo llama a execvp, Que busca el comando en PATH. Si exec falla, el hijo termina con código 127 (el código estandar de "comando no encontrado").

El código 127

Cuando exec falla, el hijo debe terminar con un código de error. El código 127 es el estandar de unix para "comando no encontrado". La shell usa 127 para indicar que el comando no se pudo ejecutar. Es importante usar _exit() y no exit() en el hijo, para evitar vaciar los buferes de stdio.

11. Observar exec con strace

strace muestra la syscall execve que se ejecuta por debajo de todas las variantes de exec:

terminal - strace de exec

$ strace -f ./fork_exec ls /tmp
execve("./fork_exec", ["./fork_exec", "ls", "/tmp"], ...) = 0
...
clone(...) = 960
strace: Process 960 attached
[pid  960] execve("/usr/bin/ls", ["ls", "/tmp"], 0x7ffd... /* 45 vars */) = 0
[pid  960] brk(NULL) = 0x55a...
...
[pid  960] write(1, "archivo1\\n", 9) = 9
[pid  960] exit_group(0) = ?
[pid  960] +++ exited with 0 +++
[pid  959] <... wait4 resumed> ...) = 960
[pid  959] +++ exited with 0 +++

Se ve claramente: fork crea el proceso (clone), y el hijo llama a execve con la ruta resuelta (/usr/bin/ls) y los argumentos. Despues de execve, el proceso ejecuta el código de ls.

12. Un programa que demuestra exec

El siguiente programa ejecuta varios comandos usando diferentes variantes de exec:

demo_exec.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 
#include 

static void ejecutar(const char *desc, void (*func)(void))
{
    printf("--- %s ---\n", desc);
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); return; }
    if (pid == 0) { func(); perror("exec"); _exit(127); }
    wait(NULL);
    printf("\n");
}

static void con_execl(void) { execl("/bin/echo", "echo", "Hola desde execl", NULL); }
static void con_execv(void) { char *a[] = {"echo", "Hola desde execv", NULL}; execv("/bin/echo", a); }
static void con_execlp(void) { execlp("echo", "echo", "Hola desde execlp", NULL); }
static void con_execvp(void) { char *a[] = {"echo", "Hola desde execvp", NULL}; execvp("echo", a); }

int main(void)
{
    ejecutar("execl", con_execl);
    ejecutar("execv", con_execv);
    ejecutar("execlp", con_execlp);
    ejecutar("execvp", con_execvp);
    return 0;
}
terminal - ejecutar demo_exec

$ gcc -std=c17 -Wall -Wextra -o demo_exec demo_exec.c
$ ./demo_exec
--- execl ---
Hola desde execl

--- execv ---
Hola desde execv

--- execlp ---
Hola desde execlp

--- execvp ---
Hola desde execvp

El programa demuestra las cuatro variantes más comunes de exec. Todas producen el mismo resultado: ejecutar "echo" con un mensaje. La diferencia esta en como se pasan los argumentos y sí se busca en PATH.

13. Resumen

14. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. Qué hace exec?

Reemplaza el programa del proceso actual por otro. El PID no cambia. Si tiene exito, no retorna.

2. Qué se reemplaza y que se conserva en exec?

Se reemplazan código, datos, heap, stack y manejadores de senales. Se conservan PID, PPID, descriptores (salvo O_CLOEXEC), cwd, root, umask, limites y credenciales.

3. Cual es la diferencia entre execl y execv?

execl recibe los argumentos como una lista variable (varargs). execv recibe un arreglo (argv). execv es más flexible para argumentos dinaminimos.

4. Cual es la diferencia entre execv y execvp?

execvp busca el programa en PATH Sí el nombre no contiene /. execv requiere la ruta completa.

5. Para que sirve execve?

Para ejecutar un programa con un entorno explicito (envp), en lugar de heredar el del padre. Es la syscall real que todas las demás llaman.

6. Por que exec no retorna sí tiene exito?

Porque el código del proceso se reemplaza completamente. El código que sigue a exec en el mismo proceso ya no existe.

7. Qué código debe usar el hijo sí exec falla?

127 (comando no encontrado). Debe usar _exit() y no exit() para evitar vaciar buferes.

8. Qué syscall usa exec internamente?

execve. Todas las variantes de exec son envolturas de la biblioteca de C Que preparan los argumentos y llaman a execve.

9. Los manejadores de senales se conservan en exec?

No. Todos los manejadores se restablecen a sus valores por omision. Las senales bloqueadas siguen bloqueadas, pero las pendientes se descartan.

10. Por que los descriptores se conservan en exec?

Para permitir la redirección de entrada/salida. Un programa puede abrir un archivo, hacer fork + exec, y el nuevo programa hereda el descriptor. Los descriptores marcados con O_CLOEXEC se cierran automaticamente.

Para prácticar

  1. Compila y ejecuta ejemplo_execl.c. Observa que "Antes de exec" se imprime, pero el código después de exec no se ejecuta.
  2. Compila y ejecuta fork_exec.c. Prueba con diferentes comandos: ls, cat, echo.
  3. Compila y ejecuta demo_exec.c. Verifica que las cuatro variantes producen el mismo resultado.
  4. Usa strace -f ./fork_exec ls para ver la syscall execve.
  5. Modifica fork_exec.c para que el hijo use execve con un entorno controlado.
  6. Escribe un programa que use execvp para ejecutar un comando que no existe y verifique que el hijo termina con código 127.