- Exec reemplaza el programa en ejecución sin cambiar el PID y se usa junto con fork() para cargar nuevos ejecutables en C y entornos Unix.
- En la shell, exec permite sustituir la propia shell o reconfigurar de forma global los descriptores de fichero y las redirecciones.
- En Perl, exec y system gestionan la ejecución de comandos externos con comportamientos distintos, apoyándose en funciones y módulos como Shell.
- Dominar exec y los descriptores de archivo es clave para escribir scripts y programas robustos, controlando a fondo la entrada y salida.
Cuando empiezas a trabajar en serio con la línea de comandos de GNU/Linux, tarde o temprano te cruzas con el misterioso comando exec y las funciones exec de C en programas ejecutables Unix. Al principio su comportamiento desconcierta: a veces el script «desaparece» y deja de ejecutar líneas, otras veces se usa para hacer redirecciones raras de ficheros o para lanzar procesos remotos. Todo suena muy de bajo nivel, pero es justo ahí donde reside su potencia.
En este artículo vamos a desmontar esa complejidad. Verás cómo exec puede significar cosas distintas según hablemos de la shell, de C o incluso de lenguajes como Perl, y cómo encaja con otras piezas básicas como fork(), system, los descriptores de fichero o comandos como find. La idea es que termines con una visión práctica y completa, y que no te tiemble el pulso cuando veas un exec en un script o en código fuente.
Qué es exec en Unix/Linux: idea general
En el núcleo de Unix, exec es el mecanismo para reemplazar el programa que se está ejecutando por otro distinto dentro del mismo proceso. Es decir: el proceso no muere y crea otro, sino que cambia su «imagen de memoria» por la de un nuevo programa. El PID se mantiene, pero el código y los datos pasan a ser los del ejecutable nuevo.
Desde el punto de vista práctico, esto se traduce en que la función exec de C nunca vuelve si todo va bien. La llamada sustituye el programa actual por el nuevo, y sólo verás un valor de retorno si ha ocurrido un error (por ejemplo, que el ejecutable no exista o no tengas permisos).
Este concepto se refleja en varios niveles:
- En C: familia de funciones
exec*(execl,execv,execve,execvp, etc.). - En la shell (Bash, sh, etc.): comando interno
exec, que puede reemplazar la shell por un programa o manipular descriptores de archivos. - En lenguajes como Perl: función
execque se comporta de forma parecida, con matices respecto asystem.
La gracia está en que todos estos usos comparten la misma idea de fondo: dejar de ejecutar lo que se estaba haciendo y, en el mismo proceso, pasar a ejecutar otra cosa, o modificar la forma en que ese proceso se comunica con el exterior mediante descriptores.
La familia exec en C: reemplazar la imagen de proceso

En programación de sistemas con C en Linux, la familia de funciones exec es la herramienta estándar para cargar otro programa dentro del proceso actual. Suele aparecer junto con fork(), que se encarga de crear el proceso hijo.
La definición general es que exec sustituye el espacio de memoria del proceso (código, datos, pila, etc.) por el de un programa distinto. El sistema operativo prepara esa nueva imagen y salta a su punto de entrada como si se hubiera arrancado desde cero, pero reaprovechando el mismo PID.
Las variantes más comunes son:
int execl(const char *path, const char *arg, ...);int execv(const char *path, char * const argv[]);int execve(const char *path, char * const argv[], char * const envp[]);int execvp(const char *file, char * const argv[]);
En todas ellas, path es la ruta absoluta al ejecutable, salvo en execvp, donde puedes pasar simplemente el nombre y se buscará en los directorios definidos en la variable de entorno PATH. Si el ejecutable se distribuye como formato AppImage, la llamada sigue aplicando. Los argumentos del nuevo programa se pasan como lista variable (execl) o como array (execv y compañía).
Es importante entender que estas funciones devuelven -1 sólo si han fallado (no se ha llegado a ejecutar el programa nuevo). En el caso normal, la ejecución no vuelve al punto de la llamada, porque el código del proceso ya es otro.
Relación entre fork() y exec(): crear procesos y ejecutar programas
En Linux se acostumbra a diferenciar con claridad entre dos ideas: crear procesos y ejecutar programas. No es lo mismo un proceso que un ejecutable, aunque se relacionen.
Para crear un proceso nuevo partiendo de uno existente se utiliza fork(). Esta llamada hace, simplificando, una copia casi idéntica del proceso padre: mismo código, mismo estado de variables, mismos ficheros abiertos, etc. El hijo recibe un valor de retorno 0 en fork(), mientras que el padre recibe el PID del hijo. Si algo sale mal, ambos verán un -1.
Esta copia no es tan bruta como suena gracias a técnicas como Copy-On-Write (COW), con las que el sistema operativo pospone la copia real de las páginas de memoria hasta que alguno de los procesos intenta modificarlas. Así se evita replicar grandes cantidades de memoria para inmediatamente tirarlas si luego se llama a exec().
Lo habitual es que el padre se encargue de crear procesos hijos con fork() y delegar en ellos el trabajo real. El hijo, una vez creado, ejecuta exec() para cargar el programa que realmente nos interesa. De este modo, el padre puede seguir creando más hijos o esperar a que alguno termine usando wait(), que lo bloquea hasta que el hijo finaliza.
Juntar ambas piezas suele dar lugar a patrones como:
- El padre llama a
fork(). - En el proceso hijo, se invoca a una de las funciones
exec*con el programa deseado, como por ejemplols -l. - El padre monitoriza o sincroniza la finalización del hijo mediante
wait().
Este modelo clásico tiene algunas ineficiencias cuando se hace una copia grande del proceso sólo para inmediatamente reemplazarla con exec()</code. De ahí la importancia del COW y de tener claro el papel de cada llamada en el ciclo de vida de los procesos.
El comando exec en la shell: reemplazar la shell y jugar con descriptores
Dentro de Bash y otras shells, exec es un built-in con dos grandes usos: sustituir la shell por otro comando y modificar la configuración de los descriptores de fichero de esa shell.
El caso más directo es cuando usas exec seguido de un comando. Por ejemplo:
exec wall "Mensaje para todos los usuarios"
Aquí, la shell que ejecuta ese exec deja de existir y es reemplazada por el comando wall. Todo lo que viniera después de esa línea en el script no se ejecutará, porque literalmente ya no queda shell que lo interprete.
Sin embargo, el uso más interesante en scripting suele ser el otro: usar exec con redirecciones para reconfigurar los descriptores de archivo de manera global para la sesión o el script. Por ejemplo, puedes hacer que la salida estándar vaya a /dev/null:
exec 1>/dev/null
Desde ese momento, cualquier orden que escribas en esa shell no mostrará nada por pantalla, porque su descriptor 1 (stdout) apunta a /dev/null. Lo potente es que esto afecta a toda la shell, no sólo a un comando concreto.
También puedes abrir un descriptor adicional asociado a un fichero:
exec 3>>/tmp/salida.log
De este modo, el descriptor 3 se convierte en un canal de salida a /tmp/salida.log. Todo lo que redirijas con >&3 irá a ese archivo, mientras que el resto de la salida estándar seguirá en la pantalla, o donde la tengas configurada.
Es importante recordar que la shell mantiene la posición en el archivo asociado al descriptor. Si escribes varias veces, el puntero avanza; si intentas leer a continuación desde ese descriptor, puede que ya estés al final del fichero y no obtengas nada. Cuando quieras cerrar el descriptor, basta con:
exec 3>&-
Lo que indica a la shell que deje de usar ese canal. Estos trucos son muy útiles para estructurar logs en scripts complejos o separar distintos tipos de salida en archivos diferentes sin tener que redirigir cada comando individualmente.
Ejemplo avanzado de exec en shell: redirecciones globales
En scripts medianamente elaborados es frecuente guardar el estado original de los descriptores y después redirigirlos temporalmente para volcar la salida a un log, un fichero temporal o incluso a /dev/null.
Un patrón típico consiste en copiar primero la salida estándar a un descriptor auxiliar y luego redirigir la salida estándar a un archivo:
exec 3>&1
exec 1>/tmp/salida.txt
Con este fragmento, el descriptor 3 apunta a la salida que antes tenía el descriptor 1 (la terminal normalmente), y a continuación se hace que el descriptor 1 se conecte al archivo /tmp/salida.txt. A partir de ahí, todo lo que se imprima por stdout de los comandos del script irá a ese fichero.
Más adelante, cuando ya no quieras seguir registrando en el archivo, puedes restaurar la situación inicial devolviendo la salida estándar al descriptor 3 y cerrando este último:
exec 1>&3
exec 3>&-
Tras estas líneas, los comandos vuelven a mostrar su salida por la pantalla, y se libera el descriptor auxiliar. Es una solución limpia para encapsular secciones de un script que deben loguearse sin llenar el resto de comandos de redirecciones.
También es posible redirigir la entrada estándar de la shell a un archivo, de forma que todas las lecturas de stdin vengan de ahí, lo que puede resultar útil en automatizaciones específicas o en pruebas donde quieras alimentar a un script con datos pregrabados.
El comando read y su relación con los descriptores
Dentro del ecosistema de la shell, read es la orden encargada de leer desde la entrada estándar y almacenar el resultado en variables. Por defecto, lee hasta encontrar un salto de línea, pero su comportamiento se puede afinar con varias opciones.
La forma más sencilla es:
read MIVAR
Con esto, la cadena que escribas por teclado hasta pulsar Enter se guarda en MIVAR. Es bastante habitual añadir la opción -r para que las barras invertidas no se interpreten como caracteres de escape:
read -r MIVAR
Hay opciones especialmente útiles como -p, que permite mostrar un texto de aviso (prompt), y -s, que impide que lo que teclees tenga eco en pantalla. Eso es justo lo que se emplea cuando quieres pedir una contraseña dentro de un script:
read -s -p "Contraseña: " PASSWD
En este caso, no verás los caracteres que escribes, igual que cuando ejecutas su o sudo. Además, read está muy conectado con la variable especial IFS, que define qué caracteres se consideran separadores de campos (por defecto tabulador, espacio y salto de línea).
Si haces algo como:
IFS="-"; read A B C
La orden interpretará el carácter guion como separador, repartiendo los campos leídos entre las variables A, B y C. Si hay más variables que campos, las últimas quedarán vacías; si hay más campos que variables, la última acumulará todos los restantes.
Y, por supuesto, todo esto se combina con las redirecciones que configures con exec: si has redirigido stdin a un archivo, read obtendrá sus datos de ahí en lugar del teclado. Esa flexibilidad es uno de los pilares de los scripts de shell bien diseñados.
exec en Perl: diferencias con system y uso avanzado
En Perl, el concepto de exec está muy alineado con el comportamiento en C y en la shell: sirve para reemplazar el proceso actual por otro programa. Sin embargo, Perl también ofrece system, que a menudo se confunde con exec aunque no hagan lo mismo.
La función system lanza un comando externo y espera a que termine, devolviendo el control al script. En cambio, exec sustituye completamente el proceso Perl por el programa llamado y no vuelve al script, salvo en caso de error. Un ejemplo muy básico sería:
exec 'cat -n /etc/passwd';
die "no se pudo ejecutar cat: $!";
Si la llamada a exec tiene éxito, nunca se ejecutará el die, porque el intérprete de Perl habrá sido reemplazado por el binario cat. Si falla (por ejemplo, porque no existe cat), entonces sí se ejecuta el die mostrando el mensaje de error y el valor de $!.
Un detalle muy útil es que, al igual que con system, puedes invocar exec con una lista de argumentos para evitar que se interpreten metacaracteres de la shell. Por ejemplo:
exec '/bin/echo', 'Los argumentos son: ', @ARGV;
Si ejecutas ese script como:
./exec.pl 'uno' '| ls ' '| who > quien'
Verás que los argumentos que incluyen | o redirecciones no activan la shell, sino que se pasan tal cual a /bin/echo. La salida sería algo tipo:
Los argumentos son: uno | ls | who > quien
Es decir, ningún comando se ha llegado a ejecutar realmente, sólo se han mostrado los textos.
Usar exec en Perl para preparar un entorno de ejecución
Más allá de los ejemplos sencillos, Perl se presta a construir scripts que preparan de forma sofisticada el entorno de trabajo antes de ejecutar otro programa. Un caso típico es cuando vas a lanzar tareas en máquinas remotas vía SSH y quieres recrear cierto contexto del proceso local.
Un enfoque habitual es tener una subrutina que construye dinámicamente un script temporal y luego lo ejecuta. En ese script, se pueden fijar el directorio de trabajo, las variables de entorno o el comportamiento de las señales, y finalmente se usa system o exec según si queremos volver al envoltorio o no.
Por ejemplo, se puede almacenar en una cadena multi-línea un pequeño programa que:
- Cambia al directorio original desde el que se invocó el script principal.
- Restaura las variables de entorno a partir de un listado serializado.
- Ejecuta el comando real con
system, comprobando su estado de salida. - Elimina el script temporal y termina explícitamente con
exit(0).
Antes de guardar ese programa en el fichero temporal, se sustituyen marcadores de texto (el directorio, el comando a ejecutar, el nombre del script, etc.) por sus valores reales. Con esta técnica, puedes encapsular toda la lógica de preparación de entorno y tener un único punto de entrada que se limite a «envolver» cualquier ejecución remota.
Este tipo de solución aprovecha a fondo la capacidad de Perl para manipular cadenas, ficheros y procesos, y casa muy bien con las funciones exec y system cuando se necesitan comportamientos muy específicos en sistemas distribuidos.
El módulo Shell de Perl y la llamada implícita a comandos
Otro aspecto curioso relacionado con la ejecución de comandos externos en Perl es el módulo Shell. Este módulo permite llamar a programas del sistema como si fueran funciones Perl, gracias al uso de AUTOLOAD.
Imagina que cargas el módulo así:
use Shell qw(echo cat ps cp);
A partir de ese momento, cuando escribes algo como:
$foo = echo("howdy", "funny", "world");
Lo que ocurre por debajo es que Perl no encuentra una subrutina llamada echo y dispara AUTOLOAD del módulo. Este mecanismo:
- Identifica el nombre del comando a partir del nombre completo de la subrutina (
Shell::echo). - Construye dinámicamente una función real que ejecutará ese comando del sistema.
- Reenvía la llamada a la nueva función, pasando los argumentos recibidos.
Si inspeccionas la ejecución con el depurador de Perl, verás cómo los argumentos se organizan en @_ y se construye el comando a lanzar. El resultado de ejecutar echo se recoge y se almacena en $foo, de forma que:
print $foo;
Mostrará algo del estilo howdy funny world con el formato que genere el echo del sistema. Este enfoque es cómodo, pero conviene no abusar de él porque puede dificultar la lectura del código y la gestión de errores.
El comando exec en shell como herramienta de redirección avanzada
Como hemos visto, el comando exec en la shell no sólo sirve para sustituir la shell, sino que se usa mucho para abrir, cerrar o duplicar descriptores de fichero. Esta capacidad permite crear patrones muy poderosos en scripts.
Por ejemplo, podrías querer redirigir permanentemente la salida de todo el script a un archivo de log sin tener que hacer >>log.txt en cada línea. Algo así como:
exec >/var/log/mi_script.log 2>&1
Con esto, tanto la salida estándar como la de error irán al mismo fichero desde el comienzo del script. A partir de ahí, cualquier comando que falle dejará rastro en el log sin ensuciar la terminal.
En otros casos, te puede interesar asociar descriptores específicos a ficheros temporales para ir guardando datos que luego procesarás más adelante. Al cerrar esos descriptores con exec n>&-, liberas recursos y evitas fugas de manejadores.
Es cierto que este tipo de construcciones no se ve en scripts básicos, pero cuando empiezas a escribir utilidades de administración serias se vuelven casi imprescindibles para controlar bien qué se ve en pantalla, qué se guarda y cómo se estructura la información.
En definitiva, dominar exec como herramienta de redirección avanzada te da un nivel de control sobre la E/S del shell scripting que marca la diferencia entre scripts de «andar por casa» y herramientas robustas de producción.
Al final de todo este recorrido, la idea clave es que exec, tanto en C, como en la shell o en Perl, gira siempre en torno a dos ejes: sustituir el programa en ejecución y gestionar con precisión las entradas y salidas. Entender cómo se combinan con fork(), system, read o módulos como Shell te permite diseñar soluciones más limpias, eficientes y seguras, ya sea para lanzar procesos remotos, encapsular entornos de ejecución complejos o simplemente escribir scripts que hagan justo lo que tú quieres, sin sorpresas.
Redactor apasionado del mundo de los bytes y la tecnología en general. Me encanta compartir mis conocimientos a través de la escritura, y eso es lo que haré en este blog, mostrarte todo lo más interesante sobre gadgets, software, hardware, tendencias tecnológicas, y más. Mi objetivo es ayudarte a navegar por el mundo digital de forma sencilla y entretenida.
