22

Named Pipes (FIFO) y Coprocesos en Bash

Vistas: 0
Tutorial: Scripts en Bash
Named Pipes (FIFO) y Coprocesos en Bash

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] o NOMBRE[0]: descriptor de lectura (para leer su salida)
  • COPROC[1] o NOMBRE[1]: descriptor de escritura (para escribir en su entrada)
  • COPROC_PID o NOMBRE_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?

MecanismoPara qué sirve
Sustitución <()Pasar salida de un comando como archivo a otro (diff <(ls A) <(ls B))
FIFOComunicación entre scripts completamente independientes
coprocSesió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

ConceptoSintaxisPara qué sirve
Crear FIFOmkfifo rutaTubería con nombre en el sistema de archivos
FIFO con permisosmkfifo -m 600 rutaÍdem con permisos específicos
Leer FIFOcat rutaBloquea si no hay escritor
Escribir FIFOecho "msg" > rutaBloquea si no hay lector
FIFO sin bloqueoexec N<> rutaModo lectura/escritura
Verificar FIFO[[ -p ruta ]]True si es un FIFO
Coprocesocoproc [NOM] { cmd; }E/S bidireccional con un comando
Leer coprocesoread var <&"${NOM[0]}"Leer su salida
Escribir coprocesoecho >&"${NOM[1]}"Escribir en su entrada
Cerrar coprocesoexec {NOM[1]}>&-Cerrar su stdin
Comunicación bidireccionalDos FIFOsDos procesos intercambian datos
Pool de workersFIFO tareas + FIFO resultadosDistribució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,

Deja una respuesta