1. Redirigir es preparar referencias
En el tema 8 construimos un cat que lee de stdin cuando corresponde y entrega su contenido por stdout. Su bucle de transferencia no necesita saber si esos descriptores llegan a una terminal, un archivo o una tubería. Esa separación es lo que hace posible la redirección.
Una redirección cambia qué referencia ocupa un número de descriptor del proceso. El programa que después lee de 0 o escribe en 1 utiliza esa conexión preparada; no necesita recibir el nombre del destino como un argumento adicional.
programa < entrada.txt > salida.txt 2> errores.txt
descriptor 0 → apertura de entrada.txt, lectura
descriptor 1 → apertura de salida.txt, escritura
descriptor 2 → apertura de errores.txt, escritura
Los símbolos de redirección los interpreta el shell. En una orden como cat < entrada.txt, cat no recibe necesariamente las palabras «<» y «entrada.txt» como argumentos. Recibe un descriptor 0 ya conectado y ejecuta su lógica habitual.
Preparar una referencia no cambia todos sus alias
Cuando reemplazas la entrada 1, cambias esa entrada de la tabla. Si la entrada 2 había obtenido antes una referencia al destino antiguo de 1, no se vuelve a modificar automáticamente. Es la misma distinción entre número y apertura que estudiamos en el tema 4.
La redirección tampoco transforma el tipo del destino. Un archivo regular sigue siendo posicionable, una tubería sigue siendo un flujo y un dispositivo tiene sus propias operaciones. La convención «escribir el resultado en 1» unifica la interfaz, no todas las garantías de los objetos.
Qué proceso cambia su tabla
En la ejecución ordinaria de una herramienta externa, el shell organiza un proceso con las conexiones requeridas y este ejecuta el programa. El shell interactivo no pierde por ello permanentemente su propia salida. Los builtins y exec del shell tienen reglas particulares de preparación y restauración.
Nuestro lanzador será un ejecutable independiente que modifica su propia tabla y después se sustituye mediante execvp. No necesita hacer otro fork para demostrar esa sustitución. El proceso padre que lo inició conserva sus referencias.
2. dup: conservar otra referencia
#include <unistd.h>
int dup(int oldfd);
int guardado = dup(STDOUT_FILENO);
if (guardado == -1) {
/* Consultar errno e informar el fallo. */
}
Dup crea una entrada en el menor número libre y hace que referencie la misma descripción de archivo abierta que oldfd. No vuelve a abrir una ruta y no copia el contenido del archivo. Devuelve el nuevo número, o -1 si falla.
Si stdout llega a una terminal, guardado conserva una referencia a esa apertura. Después puedes reemplazar stdout por un archivo y seguir manteniendo la referencia anterior en guardado. Esta es la base de guardar y restaurar una conexión.
Estado compartido y marcas independientes
Los duplicados comparten posición y banderas de estado, como O_APPEND cuando corresponde. La marca FD_CLOEXEC pertenece a la entrada de descriptor y en el duplicado creado por dup queda desactivada. Si el original estaba marcado para cerrarse ante exec, no supongas que el nuevo lo está también.
Copiar un entero con int guardado = 1; no obtiene una referencia independiente. Si después sustituyes la entrada 1, esa variable seguirá conteniendo 1 y llegará al destino nuevo. Dup sí solicita otra entrada que mantiene la apertura anterior.
El número devuelto no tiene por qué ser 3
Si 0, 1 y 2 están ocupados y no hay otras entradas, dup(1) suele devolver 3. Si 0 está cerrado, podría devolver 0. Si un programa ya tiene archivos abiertos, el número puede ser mayor. Guarda el resultado; no escribas el programa suponiendo que todos los temporales son siempre 3.
3. dup2: elegir el número de destino
#include <unistd.h>
int dup2(int oldfd, int newfd);
/* Hacer que stdout llegue a la misma apertura que fd. */
if (dup2(fd, STDOUT_FILENO) == -1) {
/* Manejar el error. */
}
Si oldfd es válido y los números son distintos, dup2 cierra la referencia que ocupaba newfd, si la había, e instala allí un duplicado de oldfd. La sustitución de esa entrada se hace atómicamente respecto a la asignación del descriptor. Si tiene éxito devuelve newfd, no necesariamente cero.
| Llamada | Referencia que se toma | Entrada que se reemplaza |
|---|---|---|
dup2(fd, 1) | La de fd. | Salida estándar. |
dup2(fd, 0) | La de fd. | Entrada estándar. |
dup2(1, 2) | La que tiene stdout ahora. | Error estándar. |
dup2(2, 1) | La que tiene stderr ahora. | Salida estándar. |
En 2>&1, el número que aparece a la izquierda es el descriptor que se cambia; el de la derecha aporta la referencia. Su operación conceptual es dup2(1, 2). Confundir los argumentos invierte la redirección.
Por qué no hacer close(newfd) seguido de dup(oldfd)
Entre esas dos llamadas, un manejador de señal o un hilo podría obtener el número que liberaste. Entonces dup elegiría otro y tu programa no habría instalado el descriptor esperado. Dup2 evita la ventana de «cerrar y volver a ocupar» para esa entrada.
Esto no convierte en atómica una secuencia completa de varias redirecciones y aperturas. Solo resuelve la sustitución de newfd. Otros cambios de estado pueden ocurrir antes o después.
El caso oldfd == newfd
Si oldfd es válido y coincide con newfd, dup2 no hace nada y devuelve ese número. No cierra la entrada y no limpia su FD_CLOEXEC. Si oldfd es inválido, la llamada falla incluso cuando ambos enteros coinciden.
Si oldfd es inválido y los números son distintos, dup2 falla sin cerrar newfd por ese motivo. El manual de dup(2) documenta estas reglas y la atomicidad. La diferencia del caso de igualdad será importante al instalar una apertura que ya recibió el descriptor 0 o 1.
4. El orden de las redirecciones
Las redirecciones de Bash se procesan de izquierda a derecha. No son una descripción global que se resuelva al final ignorando el orden. Cada duplicación usa la referencia que existe en el momento de ese paso.
inicio: 1 → terminal 2 → terminal
>salida.txt: 1 → archivo 2 → terminal
2>&1: 1 → archivo 2 → misma apertura de archivo
inicio: 1 → terminal 2 → terminal
2>&1: 1 → terminal 2 → apertura que tenia 1
>salida.txt: 1 → archivo 2 → apertura anterior de terminal
En A se guardan los dos canales en el archivo. En B, solo stdout va al archivo; stderr conserva el destino original de stdout, que podría ser una terminal, una tubería u otro archivo. No se establece una regla futura del tipo «2 seguirá cualquier cambio posterior de 1».
Abrir dos veces el mismo nombre es otra cosa
programa >salida.txt 2>salida.txt obtiene dos aperturas independientes. Ambas pueden empezar en 0 y escribir regiones que se solapen. No equivale a compartir una sola apertura mediante 2>&1.
Incluso si se usa anexado en dos aperturas, siguen siendo estados de apertura diferentes. El hecho de que O_APPEND coloque cada escritura al final puede evitar ese solapamiento de posiciones, pero no convierte ambas aperturas en la misma ni impone un orden de registros de aplicación.
Redirigir no añade tuberías por sí solo
El símbolo | conecta procesos mediante una tubería y prepara descriptores para cada extremo. Las redirecciones explícitas pueden cambiar esas conexiones. En programa 2>&1 | filtro, el destino de stdout preparado para la tubería se duplica hacia stderr. No implica que todos los errores de cualquier comando se mezclen por defecto.
El manual de redirecciones de Bash describe el orden y las formas de duplicación. Aquí construiremos redirecciones de un programa; no implementaremos un intérprete completo ni toda la gestión de una tubería de varios procesos.
5. Un emisor de salida y diagnóstico en C
Para observar las conexiones necesitamos un programa que escriba una secuencia conocida en cada canal. Emitir.c manda resultado\n a stdout y después diagnostico\n a stderr. Usa write, por lo que no depende del momento en que se vacíe un búfer de printf.
#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
static int enviar(int fd, const char *datos, size_t longitud)
{
size_t total = 0;
while (total < longitud) {
ssize_t n = write(fd, datos + total, longitud - total);
if (n > 0) {
total += (size_t)n;
continue;
}
if (n == -1 && errno == EINTR)
continue;
if (n == 0)
errno = EIO;
return -1;
}
return 0;
}
int main(void)
{
const char resultado[] = "resultado\n";
const char diagnostico[] = "diagnostico\n";
int estado = 0;
if (enviar(STDOUT_FILENO, resultado, sizeof resultado - 1) == -1) {
perror("write stdout");
estado = 1;
goto cerrar;
}
if (enviar(STDERR_FILENO, diagnostico, sizeof diagnostico - 1) == -1) {
perror("write stderr");
estado = 1;
}
cerrar:
if (close(STDOUT_FILENO) == -1) {
perror("close stdout");
estado = 1;
}
return estado;
}
Los mensajes son datos deliberados para la demostración. En una herramienta real, una ejecución normal no tiene por qué producir diagnósticos. Aquí permiten reconocer qué referencia usa cada canal. El orden de sus llamadas es stdout y después stderr; no estamos deduciendo un orden universal de las salidas de procesos concurrentes.
Emitir conserva la política ordinaria de SIGPIPE de un programa lanzado en este entorno. Si su salida va a una tubería sin lectores, puede terminar por esa señal. La gestión explícita para convertir ese caso en EPIPE se mostró en mi-cat, en el tema 8.
6. Laboratorio: reproducir las conexiones en Bash
Trabaja en Bash sobre Linux o WSL, dentro de una carpeta nueva. Guarda emitir.c antes de compilar y mantén LAB9 en la misma terminal para los siguientes experimentos. No se necesitan permisos de administrador.
$ LAB9=$(mktemp -d "$HOME/tema9.XXXXXX")
$ cd "$LAB9"
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 emitir.c -o emitir
$ ./emitir >salida.txt 2>errores.txt
$ cat salida.txt
resultado
$ cat errores.txt
diagnostico
Compartir una apertura o conservar la conexión anterior
$ ./emitir >juntos.txt 2>&1
$ cat juntos.txt
resultado
diagnostico
$ ./emitir 2>&1 >solo-salida.txt
diagnostico
$ cat solo-salida.txt
resultado
En la segunda ejecución el diagnóstico aparece en el destino original de stdout. Si ejecutas el laboratorio desde otra herramienta que capture ese canal, lo verá esa captura, no necesariamente una pantalla física.
Dos aperturas independientes pueden sobrescribirse
$ ./emitir >dos-aperturas.txt 2>dos-aperturas.txt
$ cat dos-aperturas.txt
diagnostico
$ wc -c < juntos.txt
22
$ wc -c < dos-aperturas.txt
12
El emisor escribe diez bytes por stdout y doce por stderr. Con dos aperturas desde 0, la segunda secuencia sobrescribe la primera y llega más lejos. La salida es reproducible con este programa secuencial y un archivo regular estable; no generalices el mismo texto final a cualquier programa con sus propios búferes y escritores concurrentes.
Anexar, leer y conectar un filtro
$ printf 'previo\n' >registro.txt
$ ./emitir >>registro.txt 2>&1
$ cat registro.txt
previo
resultado
diagnostico
$ printf 'entrada de prueba\n' >entrada.txt
$ cat <entrada.txt >copia-entrada.txt
$ cmp entrada.txt copia-entrada.txt
$ ./emitir 2>&1 | wc -l
2
El anexado procede de la apertura con O_APPEND, no de dup2. La duplicación posterior comparte esa misma apertura y su estado. Para entrada se obtiene una apertura de lectura y se instala en 0; ningún paso convierte el archivo en una cadena ni interpreta sus líneas.
7. Instalar un descriptor y cerrar el temporal
La receta habitual es abrir, hacer dup2 al número deseado y cerrar el descriptor temporal. Pero esa receta necesita una condición: si open devolvió ya el número de destino, no se debe cerrarlo como si sobrara.
int fd = open("salida.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, STDOUT_FILENO);
close(fd); /* si fd era 1, acabamos de cerrar stdout */
Si stdout estaba cerrado antes de open, la apertura puede recibir 1. Dup2(1, 1) deja la entrada como estaba y el close posterior la elimina. Comprobar todos los retornos es necesario, pero también hay que razonar sobre la igualdad de los números.
O_CLOEXEC añade otra condición
Nos gusta abrir los temporales con O_CLOEXEC para no transmitir referencias sobrantes a otro programa. Cuando dup2 instala un duplicado en otro número, la nueva entrada queda sin FD_CLOEXEC. Sin embargo, si los números coinciden, dup2 no limpia esa marca: el exec cerraría precisamente el canal que queríamos conservar.
La función siguiente distingue ambos casos. Si el descriptor ya está en su destino, limpia FD_CLOEXEC y lo conserva. Si hay que duplicar en otro número, utiliza dup2 y cierra el temporal una vez. Su contrato es consumir el temporal o dejarlo instalado, no devolverlo para que el llamador lo cierre otra vez.
#ifndef REDIRECCION_H
#define REDIRECCION_H
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
static int duplicar_en(int origen, int destino)
{
int resultado;
do {
resultado = dup2(origen, destino);
} while (resultado == -1 && errno == EINTR);
return resultado;
}
static int instalar_y_cerrar(int temporal, int destino)
{
if (temporal == destino) {
int marcas = fcntl(temporal, F_GETFD);
if (marcas == -1)
return -1;
return fcntl(temporal, F_SETFD, marcas & ~FD_CLOEXEC);
}
if (duplicar_en(temporal, destino) == -1) {
int codigo = errno;
(void)close(temporal); /* mantener el error principal */
errno = codigo;
return -1;
}
return close(temporal); /* Linux: sin reintento ciego del cierre */
}
#endif
Guarda esa cabecera junto a los programas. El ayudante devuelve un estado de éxito no negativo o -1; en la rama de igualdad el éxito de fcntl es cero, y en la otra el de close también. No confundir su contrato con el de duplicar_en, que devuelve el número de destino.
Si falla dup2, se conserva su errno y se intenta liberar el temporal. Si falla el cierre después de instalar, la redirección puede estar aplicada aunque el ayudante comunique error. El lanzador no continúa con exec en ese caso. No se ofrece rollback de efectos como la creación o el truncado del archivo.
8. Un lanzador con redirecciones en C
Redirigir.c prepara su propia tabla y ejecuta el programa indicado mediante execvp. Admite cinco modos, para observar cada orden sin construir un analizador de sintaxis de shell.
| Modo | Equivalencia conceptual | Operaciones principales |
|---|---|---|
salida | programa >archivo | Abrir con truncado e instalar en 1. |
anexar | programa >>archivo | Abrir con anexado e instalar en 1. |
entrada | programa <archivo | Abrir de lectura e instalar en 0. |
juntos | programa >archivo 2>&1 | Instalar en 1 y después duplicar 1 hacia 2. |
inverso | programa 2>&1 >archivo | Duplicar 1 hacia 2 antes de abrir e instalar la salida. |
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include "redireccion.h"
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
if (argc < 4) {
fprintf(stderr, "uso: %s MODO ARCHIVO PROGRAMA [ARG ...]\n", argv[0]);
fprintf(stderr, "modos: salida anexar entrada juntos inverso\n");
return 2;
}
int entrada = strcmp(argv[1], "entrada") == 0;
int anexar = strcmp(argv[1], "anexar") == 0;
int juntos = strcmp(argv[1], "juntos") == 0;
int inverso = strcmp(argv[1], "inverso") == 0;
int salida = strcmp(argv[1], "salida") == 0;
if (!entrada && !anexar && !juntos && !inverso && !salida) {
fprintf(stderr, "modo desconocido: %s\n", argv[1]);
return 2;
}
if (inverso && duplicar_en(STDOUT_FILENO, STDERR_FILENO) == -1) {
perror("dup2 stdout hacia stderr");
return 125;
}
int fd;
int destino;
if (entrada) {
fd = open(argv[2], O_RDONLY | O_CLOEXEC);
destino = STDIN_FILENO;
} else {
int banderas = O_WRONLY | O_CREAT | O_CLOEXEC |
(anexar ? O_APPEND : O_TRUNC);
fd = open(argv[2], banderas, 0666);
destino = STDOUT_FILENO;
}
if (fd == -1) {
perror("open redireccion");
return 125;
}
/* El ayudante consume fd o lo deja instalado: no cerrarlo otra vez. */
if (instalar_y_cerrar(fd, destino) == -1) {
perror("instalar redireccion");
return 125;
}
if (juntos && duplicar_en(STDOUT_FILENO, STDERR_FILENO) == -1) {
perror("dup2 stdout hacia stderr");
return 125;
}
execvp(argv[3], &argv[3]);
int codigo = errno; /* execvp solo retorna aqui si fallo */
fprintf(stderr, "execvp [%s]: %s\n", argv[3], strerror(codigo));
return codigo == ENOENT ? 127 : 126;
}
Qué recibe execvp
Argv[3] es el nombre del programa y &argv[3] es el comienzo de la lista de argumentos que recibirá. El arreglo argv original ya termina en un puntero nulo. Execvp busca mediante PATH cuando corresponde y no interpreta una orden completa como si fuera una línea de Bash.
Si quieres ejecutar printf con un argumento, pasa el programa y el argumento como elementos separados. Los comodines y comillas los procesa el shell que inició nuestro lanzador antes de que este reciba argv. El lanzador no vuelve a expandirlos.
El programa nuevo conserva los canales instalados
Exec sustituye la imagen del programa dentro del mismo proceso. Los descriptores que no estén marcados FD_CLOEXEC permanecen abiertos. Los temporales se cerraron o se convirtieron en los canales finales sin esa marca. El manual de execve(2) describe esa herencia a través de la sustitución.
El lanzador no espera después de execvp porque, si tuvo éxito, ya no está ejecutando su propio código. El estado final será el del programa ejecutado. Si no logra preparar la redirección, devuelve 125; si execvp informa ENOENT, 127; para los demás fallos de ejecución, 126. El valor 2 queda para errores de uso detectados antes de modificar los archivos.
Los errores también usan el stderr que exista en ese momento
En modo juntos, un fallo de execvp se escribe en el archivo porque stderr ya fue redirigido allí. En modo inverso irá al destino original de stdout. Si la apertura falla antes de redirigir stderr, el diagnóstico usa la conexión anterior. Es una consecuencia del orden, no una excepción de perror.
9. Laboratorio del lanzador
Guarda redireccion.h y redirigir.c en LAB9 y compila. Los programas se ejecutan con rutas explícitas como ./emitir cuando no están en PATH.
$ cd "$LAB9"
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 redirigir.c -o redirigir
$ ./redirigir salida salida-c.txt ./emitir
diagnostico
$ cat salida-c.txt
resultado
$ ./redirigir juntos juntos-c.txt ./emitir
$ cmp juntos.txt juntos-c.txt
$ ./redirigir inverso inverso-c.txt ./emitir
diagnostico
$ cat inverso-c.txt
resultado
Entrada y anexado
$ ./redirigir entrada entrada.txt cat >entrada-c.txt
$ cmp entrada.txt entrada-c.txt
$ printf 'previo\n' >anexado-c.txt
$ ./redirigir anexar anexado-c.txt printf 'nuevo\n'
$ cat anexado-c.txt
previo
nuevo
En el primer caso el shell exterior captura stdout en entrada-c.txt, mientras el lanzador instala stdin desde entrada.txt. Son preparaciones en momentos distintos. En el segundo se ejecuta la utilidad printf localizada por execvp; no se invoca automáticamente el builtin del shell.
Una apertura correcta no garantiza ejecutar el programa
$ LC_ALL=C ./redirigir juntos fallo-exec.txt ./programa-ausente
$ echo $?
127
$ cat fallo-exec.txt
execvp [./programa-ausente]: No such file or directory
El archivo ya pudo crearse o truncarse aunque el programa final no llegara a ejecutarse. Preparar referencias no es una transacción que revierta el sistema de archivos si falla el último paso. Un nombre existente con permisos de ejecución inadecuados puede producir otro fallo y estado 126.
Probar un stdout cerrado antes de open
$ (exec 1>&-; ./redirigir salida stdout-cerrado.txt ./emitir)
diagnostico
$ cat stdout-cerrado.txt
resultado
$ (exec 0<&-; ./redirigir entrada entrada.txt cat) >stdin-cerrado.txt
$ cmp entrada.txt stdin-cerrado.txt
Los paréntesis aíslan el cierre para no quitar permanentemente los canales de tu sesión interactiva. En el primer escenario open puede devolver 1; en el segundo, 0. El ayudante conserva ese descriptor y limpia su marca de cierre al ejecutar, por lo que el programa nuevo puede utilizarlo.
10. Guardar y restaurar stdout
Una herramienta que solo necesita redirigir temporalmente su propia salida puede guardar la conexión anterior con dup, instalar otra y restaurar después. Esto cambia una referencia del proceso actual; no requiere abrir de nuevo el nombre de la terminal, que podría ni siquiera ser el destino inicial.
El ejemplo guardar-restaurar.c imprime antes, durante y después. El mensaje intermedio va al archivo y los otros dos al destino original de stdout. Utiliza la misma cabecera redireccion.h y vacía stdio antes de cada cambio de conexión.
#define _POSIX_C_SOURCE 200809L
#include "redireccion.h"
#include <stdio.h>
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "uso: %s ARCHIVO\n", argv[0]);
return 2;
}
if (printf("antes de redirigir\n") < 0 || fflush(stdout) == EOF) {
perror("salida inicial");
return 1;
}
int guardado = dup(STDOUT_FILENO);
if (guardado == -1) {
perror("dup stdout");
return 1;
}
int estado = 0;
if (fcntl(guardado, F_SETFD, FD_CLOEXEC) == -1) {
perror("marcar descriptor guardado");
estado = 1;
goto restaurar;
}
int fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0666);
if (fd == -1) {
perror("open archivo");
estado = 1;
goto restaurar;
}
if (instalar_y_cerrar(fd, STDOUT_FILENO) == -1) {
perror("instalar stdout temporal");
estado = 1;
goto restaurar;
}
if (printf("durante la redireccion\n") < 0) {
perror("printf temporal");
estado = 1;
}
restaurar:
if (fflush(stdout) == EOF) {
perror("fflush antes de restaurar");
estado = 1;
}
/* El ayudante consume guardado o lo deja instalado. */
int referencia = guardado;
guardado = -1;
if (instalar_y_cerrar(referencia, STDOUT_FILENO) == -1) {
perror("restaurar stdout");
return 1;
}
clearerr(stdout); /* un posible fallo anterior ya fue comunicado */
if (printf("despues de restaurar\n") < 0 || fflush(stdout) == EOF) {
perror("salida restaurada");
estado = 1;
}
return estado;
}
$ gcc -std=c11 -Wall -Wextra -Wpedantic -O2 guardar-restaurar.c -o guardar-restaurar
$ ./guardar-restaurar temporal.txt
antes de redirigir
despues de restaurar
$ cat temporal.txt
durante la redireccion
$ ./guardar-restaurar intermedio.txt >exterior.txt
$ cat exterior.txt
antes de redirigir
despues de restaurar
La segunda ejecución muestra por qué no se debe restaurar abriendo una supuesta /dev/tty. La conexión original era exterior.txt, no una terminal. El descriptor obtenido por dup conserva la referencia adecuada cualquiera que sea el destino original.
La restauración también puede fallar
El programa intenta restaurar incluso si falló la apertura temporal o el vaciado intermedio. Si no logra instalar la referencia guardada, devuelve error y no imprime el mensaje final como si hubiera regresado al destino correcto. No puede prometer que todos los errores sean reversibles.
El guardado usa una entrada adicional, que también debe liberarse. La propiedad queda explícita cuando se entrega a instalar_y_cerrar. No se mantiene después un segundo close sobre el número antiguo.
11. Búferes de stdio y sustitución con exec
Dup2 modifica la tabla de descriptores del núcleo. No vacía el búfer que printf pueda mantener en la memoria del proceso. Esta diferencia explica resultados que a primera vista parecen violar el orden de las llamadas.
printf("texto pendiente");
/* Todavia podria estar en el buffer de stdout. */
dup2(fd_archivo, STDOUT_FILENO);
fflush(stdout);
/* El vaciado usa la conexion que tiene 1 en este momento. */
Si quieres que el texto anterior vaya a la conexión anterior, vacía y comprueba stdout antes de cambiarla. Lo mismo se aplica antes de restaurar: los datos pendientes de la fase temporal deben entregarse mientras ese destino sigue instalado.
Un salto de línea puede producir un vaciado en ciertos modos de búfer, pero no lo garantiza para todos los destinos. Que una prueba funcione en la terminal no demuestra que funcione igual al redirigir a un archivo. El ejemplo de restauración utiliza fflush explícito por ese motivo.
Exec no hace un exit normal del programa anterior
Una sustitución correcta reemplaza la memoria y el programa; no ejecuta las rutinas normales de salida de stdio del programa anterior. No cuentes con que execvp vaciará mensajes que permanecían en un FILE*. Si deben entregarse, vacíalos antes o usa una operación de escritura apropiada.
Nuestro lanzador no genera mensajes normales por stdout antes de exec. Los errores previos utilizan stderr y, si exec falla, se devuelve un estado por la ruta ordinaria. Las interacciones entre FILE*, fileno, búferes y llamadas directas se desarrollan en el tema 10.
Descriptores transmitidos y temporales sobrantes
La entrada final que debe sobrevivir a exec no lleva FD_CLOEXEC. Los temporales se abren con la marca y se cierran cuando ya no hacen falta. Si dejas una referencia extra de una tubería abierta, puedes impedir que otro proceso observe EOF; si dejas una referencia a un archivo, amplías su vida útil y lo expones al programa nuevo.
FD_CLOEXEC no es una autorización de lectura o escritura. Solo decide si una entrada permanece durante una sustitución correcta. El modo de acceso y la posición siguen perteneciendo a la apertura referenciada.
12. Errores, atomicidad y alcance
| Situación | Resultado o riesgo | Regla práctica |
|---|---|---|
| Dup de un origen inválido. | EBADF. | Comprobar que la referencia siga viva y el retorno no sea -1. |
| Dup2 hacia un número fuera del rango permitido. | Error, en Linux normalmente EBADF. | No inventar números arbitrariamente grandes. |
| Sin espacio para otro descriptor. | Dup puede informar EMFILE. | Revisar propiedad, cierres y límites. |
| Interrupción de dup2. | EINTR; en Linux también existen errores particulares de carreras como EBUSY. | Aplicar la política de reintento definida, no ocultar cualquier fallo. |
| Dup2 válido sobre un destino abierto. | La referencia anterior se sustituye. | Guardar con dup si hace falta restaurarla después. |
| Origen y destino con igual número. | No-op si el origen es válido. | No cerrar esa entrada como temporal ni asumir que se limpió CLOEXEC. |
La sustitución implícita no es un close comprobable aparte
Dup2 cierra implícitamente la referencia anterior de newfd cuando corresponde; los errores de ese cierre no se comunican como un close independiente. Si un requisito exige observarlos, hace falta un diseño adicional. Cerrar a mano antes de dup2 para «ver el error» reintroduce la ventana de asignación que se quería evitar.
Las redirecciones de este tema no ofrecen una garantía de persistencia para los datos previos del destino reemplazado. Las cuestiones de sincronización y reporte de E/S pendiente se estudian junto a la vida útil del recurso y el contrato de la aplicación.
Una secuencia fallida puede tener efectos previos
Si se abrió el archivo con O_TRUNC y luego falla dup2 o execvp, el truncado ya ocurrió. Si el modo inverso cambió stderr y después falla la apertura, el diagnóstico puede aparecer en el canal original de stdout. No se aplica un rollback automático de las redirecciones anteriores.
Los modos de nuestro lanzador no implementan noclobber de Bash ni todas las condiciones de sus redirecciones. Tampoco protegen una redirección destructiva a un archivo que el programa luego intente leer: como vimos en el tema 8, un shell puede truncar una ruta antes de que la herramienta llegue a comprobarla.
Tabla compartida con otros hilos
En un programa multihilo, cambiar stdout cambia la entrada de la tabla que suelen compartir sus hilos. Guardar y restaurar no hace que el efecto quede limitado a la función que llamó a dup2. Otros hilos podrían escribir durante la fase temporal.
Los programas del tema son monohilo. Para una aplicación concurrente se necesita coordinación o una arquitectura que no cambie globalmente el canal de otros usuarios. Dup2 resuelve una sustitución de descriptor; no administra por sí solo la sincronización de todos los escritores.
13. Simulador de referencias y redirecciones
Selecciona una secuencia y reinicia. Cada paso ejecuta una operación conceptual sobre una tabla pequeña. Se muestra la apertura a la que llega cada número, su marca de cierre y los bytes que terminan en el archivo o en la salida original. El archivo de salida comienza con PREVIO para distinguir truncado y anexado.
Duplicar referencias en el orden correcto
| fd | Apertura | Objeto y acceso | Posición | FD_CLOEXEC |
|---|
| Paso | Operación | Consecuencia |
|---|
Las aperturas iniciales de 1 y 2 se representan por referencias distintas que llevan al mismo destino visual «salida original». Los archivos tienen posiciones lógicas; para la terminal no se modela un cursor de archivo. Se omiten búferes de stdio, errores parciales, límites de recursos y procesos concurrentes.
El paso exec cierra entradas marcadas y conserva las otras, sin iniciar programas del equipo. La secuencia de cierre incorrecto muestra deliberadamente el error: dup2(1,1) no arregla CLOEXEC y close(1) elimina el canal. La escritura posterior por 1 falla; no es un comportamiento de la instalación correcta.
Qué predicciones contrastar
- En la secuencia juntos, antes de emitir, 1 y 2 deben llegar a la misma apertura de salida. Sus escrituras avanzan un único cursor.
- En la inversa, 2 conserva la apertura original de 1. Cambiar 1 al archivo no vuelve a cambiar 2.
- En dos aperturas, 1 y 2 llegan al mismo archivo por aperturas distintas en posición 0. El diagnóstico sobrescribe el resultado.
- En anexado, el prefijo PREVIO permanece. El comportamiento viene de la apertura compartida con O_APPEND.
- En guardar y restaurar, el descriptor de reserva mantiene la apertura anterior aunque stdout llegue temporalmente a un archivo.
- En la instalación correcta con open igual a 1, se limpia la marca y no se cierra ese canal antes de exec.
14. Resumen y equivalencias
- Redirigir cambia referencias de la tabla del proceso, no el código que usa stdin, stdout o stderr.
- Dup elige el menor descriptor libre y conserva otra referencia a la misma apertura.
- Dup2 permite escoger qué número reemplazar y evita la ventana de close seguido de dup.
- El origen de dup2 es el primer argumento; el número que se cambia es el segundo.
- Las redirecciones de Bash se procesan de izquierda a derecha y duplican referencias actuales.
- Dos aperturas del mismo archivo no comparten automáticamente su posición.
- Si oldfd y newfd coinciden, dup2 no hace nada: conserva también FD_CLOEXEC.
- Un temporal se cierra después de instalarlo solo si es un número distinto del descriptor final.
- Exec conserva los canales sin marca de cierre; una sustitución correcta no retorna al lanzador.
- Guardar y restaurar exige otra referencia real y controlar los búferes antes de cambiar de conexión.
- La atomicidad de dup2 no convierte varias redirecciones en una transacción ni garantiza persistencia.
| Expresión o intención | Operación conceptual |
|---|---|
>archivo | Abrir de escritura con creación/truncado e instalar en 1. |
>>archivo | Abrir de escritura con creación/anexado e instalar en 1. |
<archivo | Abrir de lectura e instalar en 0. |
2>&1 | dup2(1, 2) |
1>&2 | dup2(2, 1) |
| Conservar stdout para restaurarlo. | guardado = dup(1), comprobar y gestionar su cierre. |
| Restaurar desde una referencia guardada. | Instalar esa referencia en 1 y liberar el temporal adecuado. |
15. Preguntas de repaso y práctica
Responde dibujando la tabla después de cada paso, antes de desplegar las soluciones.
1. ¿Qué argumento de dup2 es el descriptor que cambia?
El segundo, newfd. El primero debe aportar una referencia válida que se instalará allí. Por eso 2>&1 corresponde a dup2(1,2).
2. ¿Dup abre otra vez el nombre del archivo?
No. Crea otra entrada que referencia la misma descripción de archivo abierta. Se comparten posición y estado de apertura, sin resolver de nuevo una ruta.
3. ¿Int guardado = 1 permite restaurar el destino anterior de stdout?
No. Solo conserva el número. Si la entrada 1 se reemplaza, usar ese entero lleva al destino nuevo. Dup obtiene otra referencia que sí mantiene la apertura anterior.
4. ¿Por qué close(1) seguido de dup(fd) es una receta frágil?
Porque otra operación puede ocupar el número liberado entre ambas llamadas y dup puede devolver otro. Dup2 realiza la sustitución del número elegido de forma atómica respecto a esa asignación.
5. ¿Qué ocurre si oldfd y newfd son el mismo descriptor válido?
Dup2 devuelve ese número sin cambiar la entrada. No cierra el descriptor ni limpia FD_CLOEXEC. Si el origen es inválido, la igualdad no convierte la llamada en un éxito.
6. ¿Dup2 de un origen inválido cierra primero un destino válido?
No por ese caso. La llamada falla con EBADF sin cerrar newfd al detectar que oldfd no es válido.
7. ¿Por qué >archivo 2>&1 guarda ambos canales?
Porque primero se instala stdout en el archivo y después stderr duplica esa apertura actual. Ambos números llegan a la misma descripción abierta.
8. ¿Por qué 2>&1 >archivo deja stderr en la conexión anterior?
La duplicación ocurre antes de modificar 1. El descriptor 2 conserva la apertura que tenía stdout en ese momento; no sigue cambios futuros de la entrada 1.
9. ¿>archivo 2>archivo equivale a compartir una apertura?
No. Se abren dos descripciones independientes del mismo objeto. Sus posiciones pueden empezar en 0 y las escrituras pueden sobrescribirse.
10. Si open devuelve 1, ¿debe cerrarse ese número después de dup2(1,1)?
No si se pretende mantener stdout instalado. Sería cerrar precisamente la referencia final. Además, si estaba marcada CLOEXEC hay que limpiarla antes de exec.
11. ¿Un dup ordinario conserva activada la marca FD_CLOEXEC del original?
No. El nuevo descriptor queda sin esa marca. La propiedad es de cada entrada; no forma parte del estado compartido de la apertura.
12. ¿Execvp crea otro proceso y espera a que termine?
No. Sustituye el programa dentro del proceso actual. Si tiene éxito no retorna al código siguiente; si falla, devuelve -1 y permite informar errno.
13. ¿Dónde aparece un error de execvp en el modo juntos?
En el archivo ya conectado a stderr. La llamada que falla no restaura automáticamente la conexión anterior de los diagnósticos.
14. ¿Por qué se hace fflush antes de cambiar stdout?
Porque dup2 cambia la tabla del núcleo, no el búfer de printf. Los bytes pendientes podrían vaciarse hacia el destino posterior si no se entregan antes de sustituir la conexión.
15. ¿Restaurar stdout abriendo /dev/tty siempre recupera el destino original?
No. La salida original podía ser un archivo o una tubería. La referencia obtenida mediante dup permite conservar ese destino real sin adivinar su nombre.
16. ¿La atomicidad de dup2 significa que todo un conjunto de redirecciones se deshace al fallar?
No. La sustitución de una entrada es atómica respecto a su asignación; aperturas, truncados y redirecciones anteriores pueden conservar efectos si falla un paso posterior.
17. ¿Guardar y restaurar dentro de una función afecta solo a su hilo?
No en el modelo habitual de hilos que comparten tabla. Cambiar 1 puede afectar a otros escritores del proceso. Se necesita coordinación o un diseño que evite ese cambio compartido.
18. ¿El programa nuevo necesita saber el nombre del archivo usado para redirigir stdout?
No para escribir por su salida estándar. Recibe el descriptor preparado y usa su interfaz normal. Esa independencia permite reutilizar herramientas en archivos y tuberías.
Para practicar
- Ejecuta emitir con stdout y stderr separados. Después compara ambos órdenes de redirección y dibuja las referencias en cada paso.
- Compara los 22 bytes de la apertura compartida con los 12 de dos aperturas independientes. Usa od para observar qué secuencia quedó en el archivo.
- Compila el lanzador y compara sus modos salida, anexar, entrada, juntos e inverso con las órdenes equivalentes de Bash.
- Ejecuta un programa inexistente en modo juntos e identifica dónde se imprime el diagnóstico y qué archivo pudo crearse antes del fallo.
- Crea un archivo no ejecutable e intenta lanzarlo. Distingue un error de preparación de redirección de un fallo posterior de execvp.
- Repite las pruebas que cierran 0 o 1 dentro de subshells. Explica por qué no debe cerrarse el descriptor final ni conservarse su marca CLOEXEC.
- Ejecuta guardar-restaurar con su stdout original conectado a un archivo exterior. Comprueba que la restauración no presupone una terminal.
- En una copia de guardar-restaurar, elimina los fflush de demostración y observa las diferencias al ejecutar desde una terminal y al redirigir a un archivo. No dependas de que una plataforma vacíe de la misma manera.
- En el simulador compara las aperturas a las que llegan 1 y 2 después de cada orden. Relaciona los identificadores de apertura con los cursores compartidos.
- Recorre la instalación correcta con open igual a 1 y el cierre incorrecto. Identifica por separado el efecto de CLOEXEC, el no-op de dup2 y el close indebido.
- Diseña en papel una conexión de productor a consumidor: qué extremo de una tubería se instala en 1 del productor y cuál en 0 del consumidor, y qué referencias sobrantes deberían cerrarse para permitir EOF.
En el tema 10 estudiaremos FILE*, fopen y los búferes de la biblioteca estándar. Ya puedes distinguir las referencias del núcleo del estado que conserva stdio y explicar por qué cambiar un descriptor no equivale a vaciar una salida pendiente.