1. Por que buscar alternativas
fork + exec es el patron clasico de unix, pero tiene un costo: fork duplica el espacio de direcciónes (con COW, pero aun así crea una tabla de páginas y marca las páginas como sólo lectura). En sistemás embebidos, en sistemás con memoria limitada, o en programás que crean muchos procesos, este costo puede ser significativo.
Además, fork + exec requiere dos syscalls (fork y exec) y un cambio de contexto entre ellas. En sistemás dónde la latencia es critica, esto puede ser un problema.
Por eso, POSIX definio posix_spawn(), una función que combina fork y exec en un sólo paso, Sin duplicar la memoria. Y Linux tiene vfork(), una alternativa historica que evita la copia de memoria.
Aunque fork + exec es flexible y potente, no es la unica forma de crear procesos. En sistemás con restricciones de memoria o tiempo, posix_spawn puede ser más eficiente. Y en sistemás antiguos (Sin COW), vfork era la unica forma de crear procesos sin copiar la memoria.
2. posix_spawn: crear y ejecutar en un paso
posix_spawn() es una función de POSIX Que crea un proceso nuevo y ejecuta un programa en el, en un sólo paso. No duplica la memoria del padre: el nuevo proceso carga el programa directamente, Como sí se hubiera llamado a exec. Es más eficiente que fork + exec en sistemás dónde la copia de memoria es costosa.
posix_spawn es especialmente útil en sistemás embebidos, Dónde la memoria es limitada y fork (incluso con COW) puede ser demasiado costoso. Tambien es útil en programás que crean muchos procesos y quieren evitar el overhead de fork.
La función se declara en <spawn.h>:
#include
int posix_spawn(pid_t *pid, const char *path,
const posix_spawn_file_actions_t *file_actions,
const posix_spawnattr_t *attrp,
char *const argv[], char *const envp[]);
Los parámetros son:
pid:puntero dónde se guarda el PID del hijo creado.path:ruta del programa a ejecutar (no busca en PATH; usar posix_spawnp para eso).file_actions:acciones a realizar sobre los descriptores de archivo del hijo (redirección, cierre, etc.).attrp:atributos del hijo (flags, senales, scheduler, etc.).argv:argumentos del programa.envp:variables de entorno del programa.
Si file_actions o attrp son NULL, se usan los valores por omision (Sin acciones, atributos por omision).
3. La firma de posix_spawn
posix_spawn devuelve 0 Sí tiene exito, o un código de error (no errno) Sí falla. El PID del hijo se guarda en *pid. A diferencia de fork, no hay "dos retornos": posix_spawn retorna una sola vez, en el padre.
Hay dos variantes:
| Funcion | Busqueda en PATH | Descripcion |
|---|---|---|
posix_spawn | No | Requiere la ruta completa del programa. |
posix_spawnp | Si | Busca el programa en PATH (Como execvp). |
posix_spawn es más simple de usar que fork + exec para casos simples, pero menos flexible: no permite ejecutar código arbitrario en el hijo antes de exec (Como redirigir con dup2, cambiar de usuario, etc.). Las redirecciónes se hacen con file_actions, y los atributos con attrp.
4. Atributos de posix_spawn
Los atributos del hijo se configuran con posix_spawnattr_t. Se inicializa con posix_spawnattr_init() y se destruye con posix_spawnattr_destroy().
Los atributos que se pueden configurar son:
| Atributo | Funcion | Descripcion |
|---|---|---|
| Flags | posix_spawnattr_setflags() | Controla que atributos se aplican (resetids, setsigdef, setsigmask, etc.). |
| Senales por omision | posix_spawnattr_setsigdefault() | Senales que se restablecen a sus valores por omision en el hijo. |
| Mascara de senales | posix_spawnattr_setsigmask() | Senales bloqueadas en el hijo. |
| Scheduler | posix_spawnattr_setschedpolicy(), posix_spawnattr_setschedparam() | Politica y parámetros de planificacion del hijo. |
| ID de grupo de procesos | posix_spawnattr_setpgroup() | Grupo de procesos del hijo. |
Los flags más comunes son:
POSIX_SPAWN_RESETIDS:restablece los IDs de usuario y grupo del hijo a los del padre (útil para programás setuid).POSIX_SPAWN_SETSIGDEF:restablece las senales especificadas a sus valores por omision.POSIX_SPAWN_SETSIGMASK:establece la máscara de senales del hijo.POSIX_SPAWN_SETSCHEDULER:establece la politica de planificacion del hijo.POSIX_SPAWN_SETPGROUP:establece el grupo de procesos del hijo.
5. Acciones de archivos
Las acciones de archivos (posix_spawn_file_actions_t) permiten preparar los descriptores de archivo del hijo antes de exec, Sin necesidad de llamar a dup2 o close manualmente. Se inicializa con posix_spawn_file_actions_init() y se destruye con posix_spawn_file_actions_destroy().
Las acciones disponibles son:
| Funcion | Equivalente | Descripcion |
|---|---|---|
posix_spawn_file_actions_addopen() | open() + dup2() | Abre un archivo y lo asigna a un descriptor especÃfico. |
posix_spawn_file_actions_adddup2() | dup2() | Duplica un descriptor a otro. |
posix_spawn_file_actions_addclose() | close() | Cierra un descriptor. |
Estas acciones se ejecutan en el hijo antes de exec, en el orden en que se agregaron. Permiten redirigir entrada/salida, cerrar descriptores, etc., Sin necesidad de código en el hijo.
6. Redirección con posix_spawn
La redirección con posix_spawn se hace con file_actions. Por ejemplo, para redirigir stdout a un archivo:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
#include
int main(void)
{
pid_t pid;
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);
/* Redirigir stdout al archivo */
posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO,
"/tmp/salida.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
/* Cerrar stderr (opcional) */
posix_spawn_file_actions_addclose(&actions, STDERR_FILENO);
char *args[] = {"echo", "Hola desde posix_spawn", NULL};
char *env[] = {NULL};
int ret = posix_spawnp(&pid, "echo", &actions, NULL, args, env);
if (ret != 0) {
fprintf(stderr, "posix_spawn: %s\n", strerror(ret));
return 1;
}
posix_spawn_file_actions_destroy(&actions);
waitpid(pid, NULL, 0);
/* Mostrar el archivo */
system("cat /tmp/salida.txt");
return 0;
}
posix_spawn_file_actions_addopen abre el archivo y lo asigna al descriptor especificado (STDOUT_FILENO en este caso). Es equivalente a open + dup2 + close, pero se hace en el hijo antes de exec, Sin código en el hijo.
Las file_actions son más limitadas que la preparacion manual con dup2: no permiten cambiar de directorio, cambiar de usuario, o ejecutar código arbitrario en el hijo. Pero cubren los casos más comunes (redirección, cierre de descriptores).
7. vfork: la alternativa historica
vfork() es una variante de fork que no copia la memoria del padre. En lugar de crear una copia del espacio de direcciónes, el hijo comparte la memoria del padre (incluyendo el stack) hasta que llama a exec o _exit. Es una alternativa historica para sistemás sin COW.
vfork se creó en los anos 70, Cuándo los sistemás no tenian COW y fork copiaba toda la memoria ficamente. vfork evitaba esta copia compartiendo la memoria del padre, lo que hacia la creacion de procesos mucho más rápida.
Hoy en dia, con COW, vfork es innecesario: fork es casí igual de rápido. Pero vfork sigue existiendo por compatibilidad y porque en algunos casos (Sistemás embebidos sin MMU) es la unica forma de crear procesos.
La firma de vfork es la misma que fork:
#include
pid_t vfork(void);
8. Cómo funcióna vfork
vfork funcióna así:
- El padre se suspende: el padre se bloquea hasta que el hijo llame a exec o _exit.
- El hijo comparte la memoria del padre: incluyendo el stack. El hijo puede leer la memoria del padre, pero no debe modificarla (excepto las variables que necesita para exec).
- El hijo llama a exec o _exit: Cuándo el hijo llama a exec, se carga el nuevo programa y el padre se desbloquea. Cuando el hijo llama a _exit, el proceso termina y el padre se desbloquea.
- El padre continua: el padre se desbloquea y continua ejecutandose.
El punto clave es que el padre esta suspendido mientras el hijo se ejecuta. Esto significa que el hijo puede usar el stack del padre sin problemás (porque el padre no esta usando su stack). Pero sí el hijo modifica variables del padre, el padre vera los cambios cuándo se desbloquee.
9. Por que vfork es peligroso
vfork es peligroso porque el hijo comparte la memoria del padre, incluyendo el stack. Esto significa que:
- El hijo no puede modificar variables del padre: Sí lo hace, el padre vera los cambios cuándo se desbloquee, lo que puede causar comportamientos inesperados.
- El hijo no puede llamar a funciónes que usen el stack: porque el stack es compartido con el padre. Llamar a printf, malloc, o cualquier función que use el stack puede corromper el estado del padre.
- El hijo debe llamar a exec o _exit inmediatamente: no puede hacer otra cosa. Cualquier operación que no sea exec o _exit es peligrosa.
- El hijo no puede retornar de la función que llamo a vfork: porque el return usaria el stack del padre, Que esta suspendido.
#define _POSIX_C_SOURCE 200809L
#include
#include
int main(void)
{
int x = 42;
pid_t pid = vfork();
if (pid < 0) { perror("vfork"); return 1; }
if (pid == 0) {
/* PELIGROSO: modificar variable del padre */
x = 100;
/* PELIGROSO: llamar a printf (usa el stack) */
printf("Hijo: x = %d\n", x);
_exit(0);
}
/* El padre ve x = 100, no 42 */
printf("Padre: x = %d\n", x);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o vfork_peligroso vfork_peligroso.c
$ ./vfork_peligroso
Hijo: x = 100
Padre: x = 100
El hijo modifico x a 100, y el padre ve 100 en lugar de 42. Esto demuestra que vfork comparte la memoria. En un programa real, esto puede causar bugs sutiles y difíciles de diagnosticar.
En código moderno, vfork casí nunca se usa. Con COW, fork es casí igual de rápido y mucho más seguro. vfork sólo se usa en sistemás sin MMU (Dónde no hay COW) o en casos muy especÃficos dónde se necesita maxima eficiencia.
10. Comparacion: fork+exec vs posix_spawn vs vfork
| Caracteristica | fork + exec | posix_spawn | vfork |
|---|---|---|---|
| Creacion de memoria | COW (tabla de páginas) | Sin copia (carga directa) | Sin copia (comparte memoria) |
| Syscalls | 2 (fork + exec) | 1 (posix_spawn) | 2 (vfork + exec) |
| Flexibilidad | Alta (Código arbitrario en el hijo) | Media (file_actions y attrp) | Baja (Sólo exec o _exit) |
| Redirección | dup2 en el hijo | file_actions | No recomendada |
| Cambiar de usuario | setuid en el hijo | POSIX_SPAWN_RESETIDS | No recomendada |
| Cambiar de directorio | chdir en el hijo | No soportado | No recomendada |
| Seguridad | Alta (proceso aislado) | Alta | Baja (Memoria compartida) |
| Portabilidad | POSIX | POSIX | POSIX (pero opcional) |
| Uso recomendado | Casos generales | Sistemás embebidos, alto rendimiento | Sistemás sin MMU |
11. Cuando usar cada uno
La eleccion depende del contexto:
- fork + exec: el caso general. flexible, seguro, y con COW suficientemente rápido para la mayoria de los casos. Usar cuándo se necesita preparar al hijo de forma compleja (redirección, cambio de usuario, etc.).
- posix_spawn: Cuándo se quiere evitar el overhead de fork, o en sistemás embebidos sin COW. Usar cuándo la preparacion del hijo es simple (redirección basica, atributos estandar).
- vfork: Sólo en sistemás sin MMU (Dónde no hay COW ni fork real). No usar en sistemás modernos.
En la práctica, fork + exec es la opcion por omision. posix_spawn se usa en sistemás embebidos y en programás que crean muchos procesos. vfork es raro y desaconsejado.
En versiones recientes de glibc, system() y popen() usan posix_spawn internamente en lugar de fork + exec, porque es más eficiente. Esto es transparente para el programador.
12. Observar con strace
strace muestra las syscalls que posix_spawn genera por debajo. En linux, posix_spawn usa clone() + execve(), Como fork + exec:
$ strace -f ./redirigir_posix_spawn
...
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, ...) = 970
strace: Process 970 attached
[pid 970] openat(AT_FDCWD, "/tmp/salida.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 3
[pid 970] dup2(3, 1) = 1
[pid 970] close(3) = 0
[pid 970] close(2) = 0
[pid 970] execve("/usr/bin/echo", ["echo", "Hola desde posix_spawn"], ...) = 0
...
Se ve que posix_spawn crea un proceso (clone) y en el hijo ejecuta las file_actions (open, dup2, close) y luego execve. Es equivalente a fork + preparacion + exec, pero en un sólo paso.
Para vfork, strace muestra la syscall vfork:
$ strace ./vfork_peligroso
vfork() = 971
--- SIGCHLD ... ---
wait4(-1, ...) = 971
...
13. Un programa que demuestra posix_spawn
El siguiente programa usa posix_spawn para ejecutar un comando y mostrar su PID:
#define _POSIX_C_SOURCE 200809L
#include
#include
#include
#include
#include
int main(void)
{
pid_t pid;
char *args[] = {"echo", "Hola desde posix_spawn", NULL};
char *env[] = {NULL};
/* Usar posix_spawnp para buscar en PATH */
int ret = posix_spawnp(&pid, "echo", NULL, NULL, args, env);
if (ret != 0) {
fprintf(stderr, "posix_spawn: %s\n", strerror(ret));
return 1;
}
printf("Hijo creado con PID %d\n", (int) pid);
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 demo_posix_spawn demo_posix_spawn.c
$ ./demo_posix_spawn
Hola desde posix_spawn
Hijo creado con PID 980
Hijo terminó con código 0
14. Resumen
- posix_spawn crea y ejecuta un proceso en un sólo paso, Sin duplicar la memoria.
- posix_spawn usa file_actions para redirigir descriptores y attrp para configurar atributos.
- posix_spawnp busca el programa en PATH (Como execvp).
- vfork comparte la memoria del padre hasta que el hijo llama a exec o _exit.
- vfork es peligroso: el hijo no puede modificar variables del padre ni llamar a funciónes que usen el stack.
- Con COW, fork es casí igual de rápido que vfork, por lo que vfork es innecesario en sistemás modernos.
- fork + exec es la opcion por omision; posix_spawn se usa en sistemás embebidos y de alto rendimiento.
- glibc usa posix_spawn internamente en system() y popen().
15. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Qué es posix_spawn?
Una función de POSIX Que crea un proceso nuevo y ejecuta un programa en el, en un sólo paso, Sin duplicar la memoria del padre.
2. Qué ventaja tiene posix_spawn sobre fork + exec?
No duplica la memoria del padre (ni siquiera con COW). Es más eficiente en sistemás con memoria limitada o en programás que crean muchos procesos.
3. Qué son las file_actions en posix_spawn?
Acciones que se ejecutan en el hijo antes de exec: abrir archivos (addopen), Duplicar descriptores (adddup2), cerrar descriptores (addclose). Equivalente a dup2 y close.
4. Qué es vfork?
Una variante de fork que comparte la memoria del padre (incluyendo el stack) hasta que el hijo llama a exec o _exit. El padre se suspende mientras el hijo se ejecuta.
5. Por que vfork es peligroso?
Porque el hijo comparte la memoria del padre. Si modifica variables del padre, el padre vera los cambios. Si llama a funciónes que usan el stack, puede corromper el estado del padre.
6. Cuando se debe usar vfork?
Solo en sistemás sin MMU (Dónde no hay COW ni fork real). En sistemás modernos, fork es suficientemente rápido y mucho más seguro.
7. Qué diferencia hay entre posix_spawn y posix_spawnp?
posix_spawnp busca el programa en PATH (Como execvp). posix_spawn requiere la ruta completa.
8. Qué limitacion tiene posix_spawn frente a fork + exec?
No permite ejecutar código arbitrario en el hijo antes de exec (Como cambiar de directorio, cambiar de usuario, etc.). Solo file_actions y attrp.
9. Cómo se observa posix_spawn con strace?
Se ve la syscall clone() seguida de las file_actions (open, dup2, close) y execve(). Es equivalente a fork + preparacion + exec.
10. Por que glibc usa posix_spawn en system() y popen()?
Porque es más eficiente que fork + exec, especialmente en sistemás dónde la copia de memoria es costosa.
Para prácticar
- Compila y ejecuta demo_posix_spawn.c. Verifica que funcióna correctamente.
- Compila y ejecuta redirigir_posix_spawn.c. Verifica que la salida va al archivo.
- Usa strace -f para observar las syscalls que genera posix_spawn.
- Compila y ejecuta vfork_peligroso.c. Observa que el padre ve el cambio del hijo.
- Escribe un programa que use posix_spawn con file_actions para redirigir stdin y stdout.
- Compara el tiempo de fork + exec con posix_spawn usando un benchmark.