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: 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).
| Funcion | Argumentos | PATH | Entorno | Descripcion |
|---|---|---|---|---|
execl | Lista (varargs) | No | Herado del padre | Ejecuta un programa con argumentos en lista. |
execv | Arreglo (argv) | No | Herado del padre | Ejecuta un programa con argumentos en arreglo. |
execlp | Lista (varargs) | Si | Herado del padre | Busca el programa en PATH. |
execvp | Arreglo (argv) | Si | Herado del padre | Busca el programa en PATH. |
execve | Arreglo (argv) | No | Explicito (envp) | La syscall real. Permite pasar un entorno controlado. |
execvpe | Arreglo (argv) | Si | Explicito (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:
- Codigo (texto): se descarta el código anterior y se carga el del nuevo programa.
- Datos inicializados y BSS: se reemplazan por los del nuevo programa.
- Heap: se libera y se reinicia.
- Stack: se libera y se crea uno nuevo con los argumentos y el entorno del nuevo programa.
- Manejadores de senales: se restablecen a los valores por omision (los manejadores del programa anterior se pierden).
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:
| Atributo | Se conserva? | Detalle |
|---|---|---|
| PID | Si | El PID no cambia. Es el mismo proceso. |
| PPID | Si | El padre no cambia. |
| Descriptores de archivo | Si (salvo O_CLOEXEC) | Los descriptores abiertos se conservan, excepto los marcados con FD_CLOEXEC. |
| Directorio de trabajo | Si | El cwd no cambia. |
| Directorio raiz | Si | El root no cambia. |
| umask | Si | La umask no cambia. |
| Senales bloqueadas | Si | La máscara de senales bloqueadas se conserva. |
| Limites de recursos | Si | Los rlimit se conservan. |
| ID de usuario y grupo | Si | Las credenciales no cambian. |
| Variables de entorno | Si (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.
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:
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.
#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:
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.
#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.
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.
#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.
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.
#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;
}
$ 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).
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.
#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;
}
$ 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").
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:
$ 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:
#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;
}
$ 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
- exec reemplaza el programa del proceso actual sin cambiar el PID.
- Si exec tiene exito, no retorna. Si falla, retorna -1 y establece errno.
- Se reemplazan: Código, datos, heap, stack, manejadores de senales.
- Se conservan: PID, PPID, descriptores (salvo O_CLOEXEC), cwd, root, umask, limites, credenciales.
- execl: argumentos en lista variable. execv: argumentos en arreglo.
- execlp/execvp: buscan en PATH. execve/execvpe: permiten entorno explicito.
- execve es la syscall real; las demás son envolturas.
- El patron estandar es: fork + exec en el hijo + wait en el padre.
- Si exec falla, el hijo debe terminar con _exit(127).
- strace muestra la syscall execve que todas las variantes llaman.
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
- Compila y ejecuta ejemplo_execl.c. Observa que "Antes de exec" se imprime, pero el código después de exec no se ejecuta.
- Compila y ejecuta fork_exec.c. Prueba con diferentes comandos: ls, cat, echo.
- Compila y ejecuta demo_exec.c. Verifica que las cuatro variantes producen el mismo resultado.
- Usa strace -f ./fork_exec ls para ver la syscall execve.
- Modifica fork_exec.c para que el hijo use execve con un entorno controlado.
- Escribe un programa que use execvp para ejecutar un comando que no existe y verifique que el hijo termina con código 127.