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.
También te puede interesar
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 esperar
posix_spawnp+waitpid, ofork+execvp - Leer su salida
pipe+dup2en el hijo, opopencon una orden fija - El código de salida
WIFEXITEDyWEXITSTATUS - Evitar
systemypopencon 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.