Cuando ejecutas un comando en la shell, ese proceso ocupa la terminal: bloquea el prompt, recibe las teclas que escribes y muere si pulsas Ctrl+C. Eso es el foreground. El background es lo contrario: el proceso corre de forma independiente, la shell recupera el prompt de inmediato y tú puedes seguir trabajando. La distinción parece trivial hasta que tienes que compilar algo largo, monitorizar logs y editar un fichero al mismo tiempo en una sola terminal.
Lo que hace posible todo esto es el sistema de jobs de la shell. Un job es la unidad de control que bash (o zsh) asigna a cada pipeline o comando que lanzas. No es un concepto del kernel —el kernel solo conoce procesos y grupos de procesos—, sino una abstracción de la shell que mapea sobre grupos de procesos (PGID). Cuando la shell pone un job en foreground, entrega el control del terminal a ese grupo de procesos; cuando lo manda al background, el grupo sigue ejecutándose pero ya no controla el terminal.
¿Cuándo usas esto? En trabajo interactivo: cuando lanzaste algo en foreground y necesitas la terminal urgentemente, cuando quieres correr varias tareas en paralelo sin abrir otra sesión, o cuando un proceso tarda más de lo esperado y quieres liberarte. Para procesos que deben sobrevivir al cierre de sesión, nohup y disown cubren casos puntuales, pero si estás pensando en producción, systemd es la respuesta correcta y tmux o screen son la respuesta correcta para sesiones interactivas.
Lo que se rompe cuando no entiendes esto: dejas un proceso en foreground accidentalmente suspendido con Ctrl+Z y te preguntas por qué el servidor web no responde; lanzas algo con & suponiendo que sobrevivirá al cerrar la terminal y no lo hace porque recibe SIGHUP; usas nohup en producción donde debería haber una unit de systemd y pierdes el control sobre reinicios, logs y dependencias.
# Lanzamos una descarga larga en foreground para poder suspenderla después
curl -L https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-12.5.0-amd64-netinst.iso \
-o /tmp/debian.iso
# En este punto la terminal está bloqueada. Pulsamos Ctrl+Z:
# [1]+ Stopped curl -L https://...
# La shell imprime el número de job (1) y el estado (Stopped).
# El proceso ha recibido SIGTSTP y está pausado en memoria.
# Vemos el estado de todos los jobs de esta sesión de shell
jobs -l
# -l añade el PID además del número de job
# El job aparece como Stopped. Lo mandamos al background para que continúe
bg %1
# curl retoma la descarga. La terminal queda libre.
# Lanzamos algo directamente en background desde el principio
sleep 300 &
# La shell imprime: [2] 48271 — número de job y PID
jobs -l
# [1] 47803 Running curl -L https://...
# [2] 48271 Running sleep 300
# Traemos la descarga de curl al foreground para ver el progreso
fg %1
# Ahora curl vuelve a controlar la terminal. Ctrl+C lo mataría.
# Ctrl+Z lo suspende de nuevo si queremos.
# Supongamos que queremos cerrar esta terminal pero que sleep 300 siga vivo.
# disown lo desliga de la lista de jobs: la shell no le enviará SIGHUP al salir.
disown %2
# Después de esto, jobs ya no muestra el job 2, pero el proceso sigue vivo.
# Puedes encontrarlo con: pgrep -a sleep
# Alternativa: si ya sabes desde el principio que el proceso debe sobrevivir
# al cierre de sesión, usa nohup. Redirige stdout/stderr porque la terminal
# dejará de existir y escribir en ella sería un error.
nohup python3 /opt/scripts/procesar_datos.py >> /var/log/procesar.log 2>&1 &
# nohup intercepta SIGHUP y lo ignora antes de que llegue al proceso.
# El & es necesario; nohup por sí solo no manda el proceso al background.
Lo que está pasando en cada paso
Ctrl+Z no mata el proceso: envía SIGTSTP al grupo de procesos en foreground. El kernel lo pausa completamente —no consume CPU, no avanza— y la shell lo registra en su tabla de jobs con estado Stopped. Es reversible; el proceso mantiene todo su estado en memoria.
bg %1 envía SIGCONT al grupo de procesos del job 1. El proceso retoma la ejecución exactamente donde estaba, pero ahora sin el control del terminal. Si intenta leer de stdin, recibirá SIGTTIN y se volverá a suspender automáticamente —es el kernel protegiéndote de lecturas concurrentes del terminal.
jobs -l consulta la tabla interna de la shell, no el kernel directamente. Por eso solo ves los jobs de esa sesión de bash. Si abres otra terminal y haces jobs, la lista estará vacía aunque los mismos procesos sigan corriendo. Para ver procesos del sistema completo, usas ps o pgrep.
disown %2 elimina el job de esa tabla interna. La implicación concreta: cuando la shell recibe SIGHUP al cerrarse (lo cual ocurre cuando cierras la terminal o termina la sesión SSH), ya no sabe que ese proceso existe y no se lo reenvía. El proceso sigue corriendo bajo el mismo PPID hasta que ese padre muere, momento en el que es adoptado por el PID 1 (systemd en Debian moderno).
nohup funciona diferente: no actúa sobre la tabla de jobs sino sobre el propio proceso, poniendo SIGHUP en la máscara de señales ignoradas antes del exec. Esto significa que aunque la shell sí le envíe SIGHUP, el proceso simplemente lo descarta. La redirección explícita >> /var/log/procesar.log 2>&1 en el ejemplo es importante: nohup redirige stdout a nohup.out por defecto si no lo haces tú, lo cual es un fichero que aparece donde no lo esperas.
Para el script de Python del ejemplo, si ese proceso debe correr de forma continua o reiniciarse tras un fallo, lo correcto es una unit de systemd con Type=simple y Restart=on-failure. nohup aquí es una solución de emergencia, no de diseño. Y si lo que necesitas es mantener una sesión de terminal completa con múltiples ventanas a través de desconexiones SSH, tmux resuelve exactamente eso de forma limpia, sin que tengas que redirigir nada manualmente.
N° 63