Procesos en C: fork, exec, waitpid, tuberías y posix_spawn

Cómo crear procesos en C para lanzar otros programas en Linux: fork, execvp y waitpid, cómo recoger el código de salida, cómo leer lo que escribe el proceso hijo a través de una tubería y la alternativa corta, posix_spawnp. Siempre sin pasar por el intérprete de órdenes, que es la puerta de la inyección de órdenes.

En Linux y los demás sistemas Unix, un programa crea otro proceso en dos pasos: fork lo duplica y, en la copia, exec la sustituye por el programa nuevo. Después, el padre espera con waitpid a que termine y recoge su código de salida. Todo esto es de POSIX: en Windows se usa CreateProcess.

fork, exec y waitpid

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

static int ejecutar(char *const argumentos[])
{
    fflush(stdout);
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return -1;
    }
    if (pid == 0) {
        execvp(argumentos[0], argumentos);
        perror(argumentos[0]);          /* solo llega aquí si exec falló */
        _exit(127);
    }
    int estado;
    if (waitpid(pid, &estado, 0) < 0) {
        perror("waitpid");
        return -1;
    }
    if (WIFEXITED(estado)) return WEXITSTATUS(estado);
    return -1;
}

int main(void)
{
    char *saludo[] = {"echo", "Hola desde un proceso hijo", nullptr};
    char *falla[] = {"ls", "/no/existe", nullptr};
    char *inexistente[] = {"programa-que-no-existe", nullptr};

    printf("echo terminó con %d\n", ejecutar(saludo));
    printf("ls terminó con %d\n", ejecutar(falla));
    printf("el inexistente terminó con %d\n", ejecutar(inexistente));
    return 0;
}
salidaHola desde un proceso hijo
echo terminó con 0
ls: cannot access '/no/existe': No such file or directory
ls terminó con 2
programa-que-no-existe: No such file or directory
el inexistente terminó con 127

El fflush(stdout) antes de fork no es un adorno: si queda texto en el búfer de printf, el hijo hereda una copia, y la salida del padre y la del hijo pueden salir desordenadas o duplicadas, sobre todo cuando la salida va a un archivo. fork devuelve dos veces: 0 en el hijo y el número de proceso (PID) del hijo en el padre. En el hijo, execvp busca el programa en el PATH y lo carga en lugar del actual; si funciona, nunca vuelve. Si falla, el hijo termina con _exit(127), el código que usan los intérpretes para «orden no encontrada» (con _exit y no exit, para no vaciar dos veces los búferes de stdio copiados del padre). WIFEXITED y WEXITSTATUS sacan el código de salida del estado que devuelve waitpid.

Los argumentos van en un array, uno por elemento, y no pasan por ningún intérprete de órdenes: un nombre de archivo con espacios, punto y coma o comillas llega tal cual. Por eso esta forma es la segura, y system("ls " + nombre) es una inyección de órdenes esperando a ocurrir.

Leer lo que escribe el hijo: pipe

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

int main(void)
{
    int tubo[2];
    if (pipe(tubo) < 0) {
        perror("pipe");
        return EXIT_FAILURE;
    }

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return EXIT_FAILURE;
    }
    if (pid == 0) {
        close(tubo[0]);
        dup2(tubo[1], STDOUT_FILENO);
        close(tubo[1]);
        execlp("printf", "printf", "rojo\nverde\nazul\n", (char *) nullptr);
        _exit(127);
    }

    close(tubo[1]);
    FILE *entrada = fdopen(tubo[0], "r");
    if (entrada == nullptr) {
        perror("fdopen");
        return EXIT_FAILURE;
    }
    char linea[128];
    int n = 0;
    while (fgets(linea, sizeof linea, entrada) != nullptr) {
        linea[strcspn(linea, "\n")] = '\0';
        printf("Línea %d del hijo: %s\n", ++n, linea);
    }
    fclose(entrada);
    waitpid(pid, nullptr, 0);
    return 0;
}
salidaLínea 1 del hijo: rojo
Línea 2 del hijo: verde
Línea 3 del hijo: azul

Una tubería (pipe) es un canal con dos extremos: lo que se escribe en tubo[1] se lee por tubo[0]. Tras el fork, el hijo conecta su salida estándar al extremo de escritura con dup2 y el padre lee del otro, convertido en un FILE * con fdopen para poder usar fgets. Cerrar los extremos que cada proceso no usa no es opcional: si el padre no cerrara tubo[1], nunca leería el final del archivo y se quedaría esperando para siempre.

La forma corta: posix_spawnp

#include <stdio.h>
#include <spawn.h>
#include <sys/wait.h>

extern char **environ;

int main(void)
{
    char *argumentos[] = {"sh", "-c", "exit 3", nullptr};
    pid_t pid;
    int error = posix_spawnp(&pid, argumentos[0], nullptr, nullptr, argumentos, environ);
    if (error != 0) {
        fprintf(stderr, "No se pudo lanzar el proceso (%d)\n", error);
        return 1;
    }
    int estado;
    waitpid(pid, &estado, 0);
    printf("El hijo %s con código %d\n", WIFEXITED(estado) ? "terminó" : "murió", WEXITSTATUS(estado));
    return 0;
}
salidaEl hijo terminó con código 3

posix_spawnp hace fork y exec en una sola llamada, y suele ser más eficiente en programas que usan mucha memoria. Sus dos argumentos nulos permiten, entre otras cosas, redirigir la entrada y la salida del hijo. Aquí el hijo es un intérprete de órdenes que termina con el código 3, solo para ver cómo se recoge: en tus programas, usa sh -c únicamente con texto fijo, nunca con datos de fuera.

  • ParaUsa
  • Lanzar un programa y esperarposix_spawnp + waitpid, o fork + execvp
  • Leer su salidapipe + dup2 en el hijo, o popen con una orden fija
  • El código de salidaWIFEXITED y WEXITSTATUS
  • Evitarsystem y popen con datos externos: pasan por /bin/sh

Todo el código de esta página se compiló con GCC 15.3 (C23, -Wall -Wextra, sin avisos) y se ejecutó en Linux, en un contenedor limpio sin acceso a la red, antes de publicarla; las salidas están copiadas de esa ejecución.