Alternativas: posix_spawn y vfork - Procesos e Hilos
Volver a Procesos e Hilos Bloque 4 - Ejecutar programás con exec - Tema 19 de 43

Tema 19

Alternativas: posix_spawn y vfork

fork + exec es el patron clasico de unix para crear procesos. Pero no es la unica forma. posix_spawn crea y ejecuta en un sólo paso, Sin duplicar la memoria. vfork es una alternativa historica que evita la copia de memoria. En este tema veras como funciónan, Cuándo convienen, y sus limitaciones.

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.

fork + exec no siempre es la mejor opcion

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>:

C - firma de posix_spawn

#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:

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:

FuncionBusqueda en PATHDescripcion
posix_spawnNoRequiere la ruta completa del programa.
posix_spawnpSiBusca 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:

AtributoFuncionDescripcion
Flagsposix_spawnattr_setflags()Controla que atributos se aplican (resetids, setsigdef, setsigmask, etc.).
Senales por omisionposix_spawnattr_setsigdefault()Senales que se restablecen a sus valores por omision en el hijo.
Mascara de senalesposix_spawnattr_setsigmask()Senales bloqueadas en el hijo.
Schedulerposix_spawnattr_setschedpolicy(), posix_spawnattr_setschedparam()Politica y parámetros de planificacion del hijo.
ID de grupo de procesosposix_spawnattr_setpgroup()Grupo de procesos del hijo.

Los flags más comunes son:

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:

FuncionEquivalenteDescripcion
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:

redirigir_posix_spawn.c

#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:

C - firma de vfork

#include 

pid_t vfork(void);

8. Cómo funcióna vfork

vfork funcióna así:

  1. El padre se suspende: el padre se bloquea hasta que el hijo llame a exec o _exit.
  2. 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).
  3. 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.
  4. 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.

vfork: el padre se suspende, el hijo comparte la memoria del padre, y cuándo el hijo llama a exec o _exit, el padre se desbloquea. Padre (PID 100) pid = vfork() Padre suspendido Hijo (PID 101) vfork() devuelve 0 Hijo ejecuta (comparte memoria) exec() o _exit() padre se desbloquea
vfork: el padre se suspende mientras el hijo se ejecuta. Cuando el hijo llama a exec o _exit, el padre se desbloquea.

9. Por que vfork es peligroso

vfork es peligroso porque el hijo comparte la memoria del padre, incluyendo el stack. Esto significa que:

vfork_peligroso.c

#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;
}
terminal - ejecutar vfork_peligroso

$ 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.

vfork es casí siempre un error

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

Caracteristicafork + execposix_spawnvfork
Creacion de memoriaCOW (tabla de páginas)Sin copia (carga directa)Sin copia (comparte memoria)
Syscalls2 (fork + exec)1 (posix_spawn)2 (vfork + exec)
FlexibilidadAlta (Código arbitrario en el hijo)Media (file_actions y attrp)Baja (Sólo exec o _exit)
Redireccióndup2 en el hijofile_actionsNo recomendada
Cambiar de usuariosetuid en el hijoPOSIX_SPAWN_RESETIDSNo recomendada
Cambiar de directoriochdir en el hijoNo soportadoNo recomendada
SeguridadAlta (proceso aislado)AltaBaja (Memoria compartida)
PortabilidadPOSIXPOSIXPOSIX (pero opcional)
Uso recomendadoCasos generalesSistemás embebidos, alto rendimientoSistemás sin MMU

11. Cuando usar cada uno

La eleccion depende del contexto:

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.

glibc usa posix_spawn internamente

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:

terminal - strace de posix_spawn

$ 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:

terminal - strace de 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:

demo_posix_spawn.c

#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;
}
terminal - ejecutar demo_posix_spawn

$ 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

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

  1. Compila y ejecuta demo_posix_spawn.c. Verifica que funcióna correctamente.
  2. Compila y ejecuta redirigir_posix_spawn.c. Verifica que la salida va al archivo.
  3. Usa strace -f para observar las syscalls que genera posix_spawn.
  4. Compila y ejecuta vfork_peligroso.c. Observa que el padre ve el cambio del hijo.
  5. Escribe un programa que use posix_spawn con file_actions para redirigir stdin y stdout.
  6. Compara el tiempo de fork + exec con posix_spawn usando un benchmark.