Hasta ahora has usado tuberías anónimas, esas que conectan la salida de un comando con la entrada de otro con una simple barra vertical. Y funcionan, claro. Pero tienen un problema: solo existen mientras los procesos están vivos y solo conectan procesos con un mismo padre. La cuestión es que llevaba tiempo dándole vueltas a cómo hacer que dos scripts completamente independientes se hablaran sin tener que montar un fichero temporal, con los problemas de sincronización que eso conlleva. Y como de costumbre, la solución era más sencilla de lo que parecía: los FIFOs y los coprocesos.
Fifos y coprocesos
Los FIFOs y los coprocesos son el siguiente escalón. Te permiten que dos scripts completamente independientes se hablen, o que mantengas una sesión abierta con un comando como si fuera un servicio. En este capítulo vas a ver cómo funcionan, cómo usarlos y, sobre todo, cuándo te conviene cada uno.
¿Qué es un FIFO (named pipe)?
Un FIFO —First In, First Out— es un archivo especial que actúa como tubería con nombre. Cuando haces ls -l, lo ves ahí con una p en los permisos:
prw-r--r-- 1 lorenzo lorenzo 0 jun 11 10:00 mi_fifo
Esa p te dice que es un pipe con nombre. Y el tamaño es siempre cero. No es que esté vacío: es que los datos no se almacenan en el archivo, viajan directamente por la memoria del kernel. El archivo es solo el punto de encuentro.
La diferencia clave con las tuberías anónimas es que un FIFO tiene nombre, persiste hasta que lo borras, y cualquier proceso con los permisos adecuados puede usarlo, venga de donde venga. No necesitan ser padre e hijo, ni compartir shell, ni nada.
Imagina que pones un tubo en tu sistema de archivos. Un proceso escribe por un extremo y otro lee por el otro. Los datos nunca tocan el disco. El archivo es solo la boca del tubo.
mkfifo — Crear tuberías con nombre
El comando para crear un FIFO es sencillo:
# Crear un FIFO
mkfifo mi_pipe
# Con permisos específicos
mkfifo -m 600 mi_pipe_secreto
# Varios a la vez
mkfifo pipe_entrada pipe_salida pipe_log
La única opción relevante es -m para los permisos, igual que con chmod. Y sí, hay trucos para crear FIFOs sin mkfifo usando redirecciones, pero no los uses. mkfifo es POSIX, está en todos sitios y es más claro.
Leer y escribir en un FIFO
Aquí viene lo que más sorprende cuando empiezas con FIFOs: bloquean. Por defecto, cuando abres un FIFO para lectura, el proceso se queda esperando hasta que alguien lo abra para escritura. Y al revés.
# Terminal 1 — lee (se queda esperando)
cat mi_fifo
# Terminal 2 — escribe
echo "Hola desde el otro proceso" > mi_fifo
# Terminal 1 muestra: Hola desde el otro proceso
El orden no importa. El que abre primero espera al segundo.
Escribir en un FIFO
# Esto se bloquea hasta que alguien lea
echo "mensaje" > mi_fifo
# Desde un archivo
cat datos.txt > mi_fifo
# Varias líneas
{
echo "Línea 1"
echo "Línea 2"
echo "Línea 3"
} > mi_fifo
Leer de un FIFO
# Leer todo (bloquea hasta que el escritor cierre)
cat mi_fifo
# Línea por línea
while IFS= read -r linea; do
echo "Recibido: $linea"
done < mi_fifo
# Cantidad fija de bytes
head -c 100 < mi_fifo > salida.bin
Apertura no bloqueante 💡
Si no quieres que se bloquee, abre el FIFO en modo lectura/escritura con <>:
exec 3<> mi_fifo
Esto funciona porque el propio proceso es lector y escritor a la vez, así que el FIFO nunca se queda esperando al otro extremo.
Comunicación bidireccional con dos FIFOs
Un FIFO es unidireccional. Los datos van en una dirección. Para hablar en ambos sentidos necesitas dos FIFOs:
#!/bin/bash
FIFO_IN="/tmp/fifo_entrada"
FIFO_OUT="/tmp/fifo_salida"
mkfifo "$FIFO_IN" "$FIFO_OUT"
exec 3<> "$FIFO_IN"
exec 4<> "$FIFO_OUT"
# Proceso que lee de entrada y escribe resultados
(
while IFS= read -r -u 3 comando; do
echo "Procesado: $comando" >&4
done
) &
echo "Hola" >&4
read -r -u 3 respuesta
echo "Respuesta: $respuesta"
exec 3>&- 4>&-
rm -f "$FIFO_IN" "$FIFO_OUT"
Cada proceso abre su par de FIFOs: uno para enviar, otro para recibir. Es exactamente cómo funcionan los sockets a bajo nivel, pero sin montar toda la infraestructura de red.
coproc — Coprocesos en Bash
Los coprocesos llegaron en Bash 4.0 y son otra forma de comunicación entre procesos, más integrada en el shell. Un coproceso es un comando que lanzas en segundo plano pero con el que estableces un canal bidireccional. Le envías datos y lees sus respuestas como si fuera un servicio.
Sintaxis básica
# Toma el nombre del comando
coproc bc -l
# O con nombre explícito
coproc MI_CALC { bc -l; }
Cuando lanzas un coproceso, Bash crea dos arrays:
COPROC[0]oNOMBRE[0]: descriptor de lectura (para leer su salida)COPROC[1]oNOMBRE[1]: descriptor de escritura (para escribir en su entrada)COPROC_PIDoNOMBRE_PID: el PID del proceso hijo
Ejemplo básico
#!/bin/bash
coproc BC { bc -l; }
echo "2 + 3 * 4" >&"${BC[1]}"
exec {BC[1]}>&-
read -r resultado <&"${BC[0]}"
echo "Resultado: $resultado" # 14
exec {BC[0]}>&-
Leer y escribir múltiples veces
Aquí está el verdadero poder: mantener una sesión abierta.
#!/bin/bash
coproc BC { bc -l; }
echo "2 + 2" >&"${BC[1]}"
echo "3 * 7" >&"${BC[1]}"
echo "scale=2; 10/3" >&"${BC[1]}"
exec {BC[1]}>&-
while IFS= read -r linea <&"${BC[0]}"; do
echo "-> $linea"
done
exec {BC[0]}>&-
Cerrar un coproceso limpiamente
Tienes tres formas, según el caso:
# Cerrar su stdin (el proceso recibe EOF y termina solo)
exec {BC[1]}>&-
# Matarlo directamente
kill "$BC_PID" 2>/dev/null
wait "$BC_PID" 2>/dev/null
# Cerrar ambos descriptores
exec {BC[0]}>&- {BC[1]}>&-
Casos prácticos con coproc
Calculadora persistente con bc
#!/bin/bash
calcula() {
local expr="$1"
echo "$expr" >&"${BC[1]}"
read -r resultado <&"${BC[0]}"
echo "$resultado"
}
coproc BC { bc -l; }
echo "Suma: $(calcula '2+2')"
echo "Pi: $(calcula '4*a(1)')"
echo "Raíz: $(calcula 'sqrt(144)')"
exec {BC[1]}>&- {BC[0]}>&-
Consultas persistentes con sqlite3
#!/bin/bash
DB="/tmp/test.db"
sqlite3 "$DB" "CREATE TABLE IF NOT EXISTS usuarios (
id INTEGER PRIMARY KEY,
nombre TEXT,
edad INTEGER
);"
coproc SQLITE { sqlite3 -header -column "$DB"; }
echo "INSERT INTO usuarios VALUES (1, 'Alice', 30);" >&"${SQLITE[1]}"
echo "INSERT INTO usuarios VALUES (2, 'Bob', 25);" >&"${SQLITE[1]}"
echo "INSERT INTO usuarios VALUES (3, 'Charlie', 35);" >&"${SQLITE[1]}"
echo "SELECT * FROM usuarios;" >&"${SQLITE[1]}"
echo ".quit" >&"${SQLITE[1]}"
exec {SQLITE[1]}>&-
while IFS= read -r linea <&"${SQLITE[0]}"; do
echo "DB: $linea"
done
exec {SQLITE[0]}>&-
rm -f "$DB"
Sesión SSH interactiva
#!/bin/bash
SERVIDOR="usuario@servidor"
coproc SSH { ssh -o LogLevel=quiet -T "$SERVIDOR"; }
echo "uname -a" >&"${SSH[1]}"
echo "df -h /" >&"${SSH[1]}"
echo "uptime" >&"${SSH[1]}"
echo "exit" >&"${SSH[1]}"
exec {SSH[1]}>&-
while IFS= read -r linea <&"${SSH[0]}"; do
echo "REMOTO: $linea"
done
exec {SSH[0]}>&-
Requiere tener las claves SSH configuradas, claro. Si no, te pedirá contraseña y el coproceso no sabe gestionar eso.
Servidor interno con coproc
Puedes escribir tu propio servidor dentro del coproceso:
#!/bin/bash
coproc SERVER {
while IFS= read -r comando; do
case "$comando" in
hola) echo "¡Hola, cliente!" ;;
fecha) echo "Fecha: $(date)" ;;
salir) echo "Adiós"; break ;;
*) echo "Comando no reconocido: $comando" ;;
esac
done
}
echo "hola" >&"${SERVER[1]}"
read -r respuesta <&"${SERVER[0]}"
echo "Server: $respuesta"
echo "fecha" >&"${SERVER[1]}"
read -r respuesta <&"${SERVER[0]}"
echo "Server: $respuesta"
echo "salir" >&"${SERVER[1]}"
exec {SERVER[1]}>&- {SERVER[0]}>&-
¿Cuándo usar cada uno?
| Mecanismo | Para qué sirve |
|---|---|
Sustitución <() | Pasar salida de un comando como archivo a otro (diff <(ls A) <(ls B)) |
| FIFO | Comunicación entre scripts completamente independientes |
| coproc | Sesión interactiva persistente con un comando |
Los FIFOs son para cuando dos scripts separados necesitan hablarse a través del sistema de archivos. coproc es para cuando quieres mantener una conversación con un solo comando dentro del mismo script.
Aplicaciones prácticas con FIFOs 🔥
Chat bidireccional entre dos scripts
Script A:
#!/bin/bash
FIFO_A2B="/tmp/chat_a_to_b"
FIFO_B2A="/tmp/chat_b_to_a"
[[ -p "$FIFO_A2B" ]] || mkfifo "$FIFO_A2B"
[[ -p "$FIFO_B2A" ]] || mkfifo "$FIFO_B2A"
cleanup() { rm -f "$FIFO_A2B" "$FIFO_B2A"; exit 0; }
trap cleanup INT TERM
echo "=== Chat A ==="
exec 3> "$FIFO_A2B"
exec 4< "$FIFO_B2A"
while IFS= read -r -u 4 msg; do
echo "[B]: $msg"
done &
while true; do
read -r -p "A> " texto
echo "$texto" >&3
[[ "$texto" == "salir" ]] && break
done
exec 3>&- 4>&-
cleanup
Script B:
#!/bin/bash
FIFO_A2B="/tmp/chat_a_to_b"
FIFO_B2A="/tmp/chat_b_to_a"
[[ -p "$FIFO_A2B" ]] || mkfifo "$FIFO_A2B"
[[ -p "$FIFO_B2A" ]] || mkfifo "$FIFO_B2A"
cleanup() { rm -f "$FIFO_A2B" "$FIFO_B2A"; exit 0; }
trap cleanup INT TERM
echo "=== Chat B ==="
exec 3< "$FIFO_A2B"
exec 4> "$FIFO_B2A"
while IFS= read -r -u 3 msg; do
echo "[A]: $msg"
done &
while true; do
read -r -p "B> " texto
echo "$texto" >&4
[[ "$texto" == "salir" ]] && break
done
exec 3<&- 4>&-
cleanup
Daemon de logging centralizado
Un patrón clásico: un proceso escucha en un FIFO y todos los demás escriben ahí.
#!/bin/bash
FIFO_LOG="/tmp/daemon_log.fifo"
ARCHIVO_LOG="/tmp/central.log"
ROTATE_SIZE=$((10 * 1024 * 1024))
[[ -p "$FIFO_LOG" ]] || mkfifo "$FIFO_LOG"
trap "rm -f '$FIFO_LOG'" EXIT
echo "Daemon de logging iniciado"
while IFS= read -r linea; do
if [[ -f "$ARCHIVO_LOG" ]] && (( $(stat -c%s "$ARCHIVO_LOG") >= ROTATE_SIZE )); then
mv "$ARCHIVO_LOG" "${ARCHIVO_LOG}.old"
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $linea" >> "$ARCHIVO_LOG"
done < "$FIFO_LOG"
Cualquier script escribe en el FIFO y el daemon lo registra ordenadamente:
echo "[INFO] Backup completado" > /tmp/daemon_log.fifo
echo "[ERROR] Fallo en módulo X" > /tmp/daemon_log.fifo
Distribuidor de tareas paralelas (worker pool)
#!/bin/bash
FIFO_TAREAS="/tmp/pool_tareas"
FIFO_RESULTADOS="/tmp/pool_resultados"
NUM_WORKERS=4
mkfifo "$FIFO_TAREAS" "$FIFO_RESULTADOS"
trap "rm -f '$FIFO_TAREAS' '$FIFO_RESULTADOS'" EXIT
worker() {
local id=$1
exec 3< "$FIFO_TAREAS"
exec 4> "$FIFO_RESULTADOS"
local tarea
while IFS= read -r -u 3 tarea; do
local resultado="Worker[$id] procesó: $tarea"
sleep 0.$((RANDOM % 5))
echo "$resultado" >&4
done
exec 3<&- 4>&-
}
for i in $(seq 1 $NUM_WORKERS); do
worker "$i" &
done
exec 3> "$FIFO_TAREAS"
for tarea in tarea_{1..20}; do
echo "$tarea" >&3
done
exec 3>&-
exec 4< "$FIFO_RESULTADOS"
while IFS= read -r -u 4 resultado; do
echo "$resultado"
done
exec 4<&-
wait
echo "Todas las tareas completadas."
Funciones profesionales
fifo_create — Crear FIFO con validación
fifo_create() {
local path="$1"
local perms="${2:-644}"
if [[ -p "$path" ]]; then
return 0
fi
if [[ -e "$path" ]]; then
echo "Error: '$path' existe pero no es un FIFO" >&2
return 1
fi
mkfifo -m "$perms" "$path" || {
echo "Error: no se pudo crear FIFO en '$path'" >&2
return 1
}
return 0
}
fifo_cleanup — Limpiar FIFOs al salir
fifo_cleanup() {
local fifo
for fifo in "$@"; do
if [[ -p "$fifo" ]]; then
rm -f "$fifo"
fi
done
}
coproc_query_db — Consultar base de datos vía coproceso
coproc_query_db() {
local db="$1"
local query="$2"
local resultado
if [[ -z "${_DB_COPROC_PID:-}" ]] || ! kill -0 "$_DB_COPROC_PID" 2>/dev/null; then
coproc _DB_COPROC { sqlite3 -noheader -list "$db"; }
fi
echo "$query" >&"${_DB_COPROC[1]}"
IFS= read -r resultado <&"${_DB_COPROC[0]}"
echo "$resultado"
}
Script completo: worker_pool_fifo.sh
Este script implementa un pool de workers que procesa tareas en paralelo usando FIFOs:
#!/bin/bash
set -euo pipefail
NUM_WORKERS="${1:-4}"
NUM_TAREAS="${2:-20}"
FIFO_TAREAS="/tmp/worker_pool_${$}_tareas.fifo"
FIFO_RESULTADOS="/tmp/worker_pool_${$}_resultados.fifo"
cleanup() {
rm -f "$FIFO_TAREAS" "$FIFO_RESULTADOS" 2>/dev/null
}
trap cleanup EXIT INT TERM
worker() {
local id="$1"
exec 3< "$FIFO_TAREAS"
exec 4> "$FIFO_RESULTADOS"
local tarea
while IFS= read -r -u 3 tarea; do
local tiempo=$((RANDOM % 3 + 1))
sleep "$tiempo"
case "$tarea" in
ping*) resultado="OK: $tarea responde en ${tiempo}s" ;;
scan*) resultado="OK: $tarea — 42 puertos abiertos" ;;
calc*) resultado="OK: $tarea = $((RANDOM % 1000))" ;;
backup*) resultado="OK: $tarea — ${tiempo}MB respaldados" ;;
*) resultado="OK: $tarea procesada en ${tiempo}s" ;;
esac
echo "$id|$tarea|$resultado" >&4
done
exec 3<&- 4>&-
}
mkfifo "$FIFO_TAREAS" "$FIFO_RESULTADOS"
workers_pids=()
for i in $(seq 1 "$NUM_WORKERS"); do
worker "$i" &
workers_pids+=($!)
done
exec 5> "$FIFO_TAREAS"
for i in $(seq 1 "$NUM_TAREAS"); do
case $((i % 4)) in
0) echo "ping_servidor_$i" >&5 ;;
1) echo "scan_red_$i" >&5 ;;
2) echo "calc_complejo_$i" >&5 ;;
3) echo "backup_diario_$i" >&5 ;;
esac
done
exec 5>&-
exec 6< "$FIFO_RESULTADOS"
recibidos=0
while IFS='|' read -r -u 6 wid tarea resultado; do
echo " → Worker[$wid]: $resultado"
((recibidos++))
if (( recibidos >= NUM_TAREAS )); then
break
fi
done
exec 6<&-
wait
cleanup
chmod +x worker_pool_fifo.sh
./worker_pool_fifo.sh # 4 workers, 20 tareas
./worker_pool_fifo.sh 8 100 # 8 workers, 100 tareas
Errores comunes 😅
El FIFO bloquea y no sabes por qué
El error más frecuente: abres un FIFO para lectura, nadie escribe, y el script se queda colgado sin motivo aparente.
# ❌ Se bloquea eternamente
cat /tmp/mi_fifo
# ✅ Abrir en modo RW para evitar bloqueo
exec 3<> /tmp/mi_fifo
# O asegurar que hay escritor
echo "dato" > /tmp/mi_fifo &
cat /tmp/mi_fifo
El escritor también bloquea
No solo el lector se bloquea. El escritor también espera si no hay nadie leyendo:
# ❌ Escritor bloqueado
echo "dato" > /tmp/mi_fifo
# ✅ Asegurar lector primero
cat /tmp/mi_fifo &
sleep 0.1
echo "dato" > /tmp/mi_fifo
Confundir FIFO con archivo normal
# ❌ Los datos no se acumulan
echo "linea1" > /tmp/mi_fifo
echo "linea2" > /tmp/mi_fifo
cat /tmp/mi_fifo # Solo ves "linea2"
# ✅ Los datos fluyen en tiempo real
# El primer echo se entrega al instante al lector
# El segundo espera a que el lector consuma el primero
No cerrar descriptores del coproceso
# ❌ El coproceso sigue vivo
coproc BC { bc -l; }
echo "2+2" >&"${BC[1]}"
# BC sigue esperando más entrada
# ✅ Cerrar stdin cuando termines
exec {BC[1]}>&-
No usar wait con workers en background
# ❌ El script termina antes que los workers
for i in {1..4}; do
worker "$i" &
done
echo "Todos terminaron" # MENTIRA
# ✅ Esperar a todos
for i in {1..4}; do
worker "$i" &
done
wait
echo "Todos terminaron" # AHORA SÍ
Resumen
| Concepto | Sintaxis | Para qué sirve |
|---|---|---|
| Crear FIFO | mkfifo ruta | Tubería con nombre en el sistema de archivos |
| FIFO con permisos | mkfifo -m 600 ruta | Ídem con permisos específicos |
| Leer FIFO | cat ruta | Bloquea si no hay escritor |
| Escribir FIFO | echo "msg" > ruta | Bloquea si no hay lector |
| FIFO sin bloqueo | exec N<> ruta | Modo lectura/escritura |
| Verificar FIFO | [[ -p ruta ]] | True si es un FIFO |
| Coproceso | coproc [NOM] { cmd; } | E/S bidireccional con un comando |
| Leer coproceso | read var <&"${NOM[0]}" | Leer su salida |
| Escribir coproceso | echo >&"${NOM[1]}" | Escribir en su entrada |
| Cerrar coproceso | exec {NOM[1]}>&- | Cerrar su stdin |
| Comunicación bidireccional | Dos FIFOs | Dos procesos intercambian datos |
| Pool de workers | FIFO tareas + FIFO resultados | Distribución paralela de trabajos |
Los FIFOs y los coprocesos son herramientas que no usas todos los días, pero cuando las necesitas, no hay otra forma limpia de hacerlo. Los FIFOs para comunicación entre scripts independientes a través del sistema de archivos. Los coprocesos para mantener sesiones persistentes con comandos dentro del mismo script.
Más información,
- mkfifo(1) — Página de manual del comando mkfifo
- bash(1) — Coprocesses — Sección de coprocesos en el manual de Bash
- Bash Reference Manual — Coprocesses
- Bash Hackers Wiki — Named Pipes