Cómo mantener un comando tras desconectar SSH
Si SSH se desconecta, el kernel envía SIGHUP y el proceso termina. Compara nohup, disown, tmux y systemd-run para elegir la opción adecuada.
Por qué el comando termina cuando se desconecta SSH
Para mantener un comando en ejecución después de una desconexión de SSH, el comando debe terminar en un lugar al que no pueda llegar la señal de cierre. Cada método siguiente organiza esto de una forma distinta. Por eso, conviene empezar por el mecanismo.
La sesión de inicio de sesión se ejecuta en una pty (pseudo-terminal), un dispositivo de terminal virtual que sshd crea en el servidor para la sesión. Es el terminal de control de la shell y de todos los comandos que se inician desde ella. Para conocer el resto de este proceso, consulte qué configura SSH al iniciar sesión. Cuando se interrumpe la conexión TCP, sshd cierra su extremo y la pty se destruye. El kernel interpreta esto como el cierre del terminal. Por eso envía SIGHUP al grupo de procesos en primer plano de ese terminal y al líder de la sesión, que es la shell. La acción predeterminada de SIGHUP es finalizar el proceso. El comando estaba en el grupo de procesos en primer plano, por lo que termina.
Los trabajos en segundo plano tampoco son seguros. Un trabajo iniciado con & se ejecuta en su propio grupo de procesos, por lo que el kernel no le envía la señal directamente. Bash sí lo hace. Al recibir SIGHUP, un bash interactivo reenvía SIGHUP a todos los trabajos de su tabla antes de salir. El resultado es el mismo desde el punto de vista del usuario: el trabajo desaparece y el archivo de registro queda detenido a mitad de una línea.
Aquí hay una asimetría que suele causar confusión. Escribir exit no cierra los trabajos en segundo plano, porque bash sólo lo hace cuando la opción huponexit está establecida, y está desactivada de forma predeterminada. Una conexión interrumpida sí los cierra. El trabajo que sobrevivió al cierre ordenado del terminal puede terminar cuando se pierde la conexión wifi.
De aquí se derivan dos consecuencias, que constituyen todo el problema. Un proceso que ignora SIGHUP, o que no tiene ningún terminal de control, no recibirá el cierre. Además, un proceso cuya salida estándar todavía apunta a la pty destruida no tiene dónde escribir: la escritura falla con EIO (error de entrada/salida), y la mayoría de los programas terminan en ese momento. Hay que resolver ambas partes. Muchas recetas sólo resuelven la primera. Por eso se informa de que «nohup no funcionó».
Si la conexión se interrumpe varias veces al día, corrija también ese problema. ServerAliveInterval 60 en ~/.ssh/config evita que una sesión inactiva sea descartada por un tiempo de espera de NAT (traducción de direcciones de red) en algún punto de la ruta. Una sesión que nunca llega a abrirse es un fallo distinto, con causas diferentes. En ese caso resulta importante la diferencia entre conexión rechazada y conexión agotada por tiempo de espera.
¿Qué método mantiene un comando en ejecución después de desconectarse de SSH?
Hay cuatro opciones, ordenadas según la importancia del trabajo.
nohuposetsid: un trabajo puntual que inicia ahora y cuyo registro revisa después. Debe redirigir la salida manualmente.disown: un trabajo que ya inició y que olvidó proteger. Recupera el proceso, pero no puede devolverle la salida.tmuxoscreen: trabajo que necesita supervisar, interrumpir y retomar durante varios días.systemd-runo un archivo de unidad real: cualquier trabajo que deba continuar después de cerrar la sesión, como unrsyncde seis horas o una importación de base de datos durante la noche.
La regla que conviene recordar es la siguiente: si olvidar el trabajo sería un problema, debe pertenecer a systemd y no a tmux. Una ventana de tmux depende de que una persona la recuerde. Una unidad tiene un nombre, un estado, un registro y una política de reinicio que la siguiente persona puede encontrar sin que nadie tenga que indicárselo.
nohup y setsid: inícializar el proceso y desconectarse
nohup ./import.sh > ~/import.log 2>&1 &
echo $! > ~/import.pidnohup establece la disposición de SIGHUP para ignorarla y después ejecuta el comando. Así, la señal de cierre de la terminal llega al proceso y no produce ningún efecto. La redirección debe escribirla usted. Si deja la salida estándar apuntando a la terminal, nohup la redirige automáticamente a nohup.out en el directorio actual. Si no puede hacerlo, usa $HOME/nohup.out y muestra:
nohup: ignoring input and appending output to 'nohup.out'Es fácil perder de vista ese archivo, así que asígnele un nombre. $! contiene el PID (identificador de proceso) del último trabajo en segundo plano. Guardarlo permite comprobar el estado del trabajo después de volver a iniciar sesión.
setsid aborda el mismo problema desde el lado contrario. Ejecuta el comando en una sesión nueva sin terminal de control. Por tanto, no existe ninguna terminal que pueda cerrarse y afectar al proceso.
setsid --fork ./import.sh > ~/import.log 2>&1Use --fork. Sin esa opción, setsid ejecuta setsid() en el mismo proceso cuando el proceso aún no es un líder de grupo de procesos. Esto ocurre dentro de un script de shell y hace que el script quede bloqueado. Con --fork, el comportamiento es el mismo en un script y en el prompt.
Compruebe qué se inició realmente:
ps -o pid,ppid,sid,tty,stat,cmd -p "$(cat ~/import.pid)"Una columna TTY de ? indica que el proceso no tiene una terminal de control. Por tanto, ninguna terminal puede afectarlo al cerrarse. Con nohup, la columna TTY sigue mostrando algo como pts/0 mientras mantiene la conexión y pasa a mostrar ? cuando se destruye el pty. Ambos resultados son correctos. El trabajo sobrevivió.
disown: recuperar un trabajo ya iniciado
Inició un trabajo de dos horas en primer plano y después recordó este problema. No lo finalice para iniciarlo de nuevo.
# press Ctrl-Z to suspend the job first
bg
jobs -l
disown -h %1Ctrl-Z suspende el trabajo, bg lo reanuda en segundo plano y jobs -l muestra su número de trabajo junto a su PID. disown -h %1 marca ese trabajo para que bash no le envíe SIGHUP. Un disown %1 simple elimina el trabajo por completo de la tabla de bash. Esto tiene el mismo efecto cuando se cierra la sesión, pero entonces jobs ya no lo muestra.
Lo que disown no puede hacer es mover la salida. El proceso sigue teniendo la pty como salida estándar y, cuando la pty desaparece, la siguiente escritura devuelve EIO. Por tanto, disown guarda de forma fiable un trabajo silencioso, como una compilación que escribe en un archivo, pero a menudo pierde uno que genera mucha salida. El trabajo sobrevive sin un lugar donde escribir o termina en la siguiente línea de salida.
Existe una herramienta de recuperación para los descriptores de archivo. reptyr mueve un proceso en ejecución a su terminal actual: instálelo con sudo apt install -y reptyr y después ejecute reptyr <pid> desde una ventana de tmux. Funciona mediante ptrace, y Ubuntu incluye kernel.yama.ptrace_scope = 1, que permite realizar seguimientos sólo de sus propios procesos descendientes, por lo que un proceso heredado necesita sudo reptyr <pid>. Úselo como herramienta de emergencia. No base en ella un procedimiento habitual.
tmux: trabajo que debe seguir ejecutándose aunque se cierre la sesión
tmux (multiplexor de terminal) resuelve el problema en otro punto. En lugar de proteger el proceso frente al pty, proporciona al proceso un pty que no pertenece a la sesión SSH. El servidor tmux se ejecuta fuera de esa sesión y controla los terminales de todo lo que se ejecuta dentro de él. La conexión SSH sólo es un visor conectado a ese servidor. Si la conexión se interrumpe, el servidor no lo detecta.
sudo apt update && sudo apt install -y tmux
tmux new -s importInicie el trabajo en esa ventana y pulse Ctrl-b seguido de d para desconectarse de la sesión. Vuelva a iniciar sesión más tarde y retómelo:
tmux ls
tmux attach -t importtmux ls debería mostrar una línea que empiece por import: 1 windows. Si muestra no server running on /tmp/tmux-1000/default, no existe ninguna sesión a la que conectarse, porque nunca se creó o porque algún proceso terminó el servidor.
screen hace lo mismo con otra combinación de teclas. screen -S import crea una sesión y Ctrl-a seguido de d se desconecta de ella. screen -ls muestra las sesiones existentes y screen -r import recupera una. Cualquiera de las dos herramientas sirve. La combinación para desconectarse es la parte que suele olvidarse.
Un multiplexor también es el entorno adecuado para el trabajo interactivo que debe sobrevivir a una interrupción de la conexión. Por eso ejecutar Claude Code en un VPS dentro de tmux es la configuración habitual y permite controlar una sesión del servidor desde un teléfono mediante una red móvil que se reconecta cada pocos minutos.
systemd-run: entregar el trabajo a PID 1
Si un trabajo no debe depender de usted en absoluto, entréguelo al sistema de inicio.
sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/Esto crea una unidad de servicio transitoria llamada bigsync.service. Obtiene su propio cgroup, no tiene un terminal de control y no depende de su sesión de inicio de sesión. El comando devuelve el control inmediatamente e imprime Running as unit: bigsync.service. Puede supervisarlo de cualquiera de estas formas:
systemctl status bigsync
journalctl -u bigsync -f--collect indica a systemd que elimine la unidad cuando termine, incluso si falla. Sin esta opción, una unidad transitoria fallida permanece cargada y su nombre sigue ocupado, por lo que la siguiente ejecución falla con un mensaje que indica que la unidad ya existe. La salida se envía al journal con marcas de tiempo en cada línea. Las entradas del journal sólo sobreviven a un reinicio cuando existe /var/log/journal, así que ejecute sudo mkdir -p /var/log/journal y reinicie systemd-journald si necesita conservarlas.
Como usuario normal, ejecutar systemd-run sin sudo solicita autorización a polkit e imprime ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===. Use sudo para las unidades del sistema.
También puede ejecutar el trabajo con su propio gestor de usuario:
systemd-run --user --unit=bigsync --collect /usr/bin/rsync -aH /srv/data/ /mnt/backup/Esto tiene una particularidad importante. Su gestor por usuario, user@1000.service, normalmente se detiene cuando termina su última sesión y detiene con él todas las unidades de usuario. Active el modo persistente una vez:
loginctl enable-linger "$USER"
loginctl show-user "$USER" --property=LingerEl segundo comando debería imprimir Linger=yes. Con el modo persistente activado, el gestor de usuario se inicia durante el arranque y sigue ejecutándose tanto si ha iniciado sesión como si no. Sin esta opción, systemd-run --user no le aporta ninguna ventaja frente a nohup.
systemd-run --scope es otra cosa. Ejecuta el comando en primer plano y conectado a su terminal, por lo que no resulta útil en este caso.
Para cualquier trabajo que vaya a ejecutar más de una vez, escriba la unidad en un archivo en lugar de crear una unidad transitoria cada vez.
Una unidad permanente para un trabajo que volverá a ejecutar
[Unit]
Description=Nightly data sync
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=deploy
WorkingDirectory=/srv/data
ExecStart=/usr/local/bin/nightly-sync.shGuárdelo como /etc/systemd/system/nightly-sync.service, ejecute sudo systemctl daemon-reload, inícielo con sudo systemctl start nightly-sync y vuelva a leerlo con journalctl -u nightly-sync. Añada un archivo .timer equivalente cuando deba ejecutarse según una programación en lugar de bajo demanda.
Escribir una unidad de servicio systemd y su temporizador explica detalladamente el formato del archivo y la sintaxis de la programación.
Dónde va la salida y por qué desaparece
El orden de las redirecciones importa. > file 2>&1 dirige la salida estándar al archivo y después dirige el error estándar al mismo destino. 2>&1 > file lo hace al revés: el error estándar sigue yendo al terminal, y el terminal es lo que está a punto de desaparecer. Bash también acepta &> file para ambas transmisiones a la vez.
La segunda sorpresa es el almacenamiento en búfer. Cuando la salida estándar es un terminal, la biblioteca C vacía el búfer después de cada línea. Cuando la salida estándar es un archivo, cambia a un búfer por bloques de unos pocos kilobytes, por lo que tail -f ~/import.log no muestra nada durante minutos y el trabajo parece detenido. Fuerce el almacenamiento por líneas con stdbuf -oL ./import.sh > ~/import.log 2>&1 o use la opción propia del programa, como python3 -u o grep --line-buffered.
Evite este patrón:
nohup ./import.sh 2>&1 | tee ~/import.log &nohup protege import.sh y nada más. tee es un proceso independiente dentro de la misma canalización y también termina al recibir la señal de desconexión. import.sh intenta escribir entonces en una tubería sin lector, por lo que recibe SIGPIPE y se detiene. Coloque toda la canalización dentro de setsid bash -c '...' o escriba directamente en el archivo y ejecute tail -f sobre él cuando vuelva a conectarse.
Hay otro detalle específico de rsync. --info=progress2 escribe una secuencia de retornos de carro que se ve correctamente en un terminal y se convierte en una línea enorme en un archivo de registro o en el journal. Para una ejecución desatendida, elimínelo y use --stats en su lugar.
Por qué un trabajo que funciona en el shell falla con systemd o cron
El shell interactivo lee /etc/profile, ~/.profile y ~/.bashrc, por lo que tiene tu PATH, los shims de tu gestor de versiones y tus variables exportadas. Una unidad de systemd no lee ninguno de ellos. cron tampoco: en Debian y Ubuntu, cron ejecuta los trabajos con SHELL=/bin/sh y PATH=/usr/bin:/bin.
En systemd, el síntoma es que systemctl status informa de (code=exited, status=203/EXEC). Esto significa que systemd no pudo ejecutar el archivo, porque la ruta era incorrecta o el archivo no tiene permiso de ejecución. En cron, normalmente es command not found, enviado por el correo local, o no se entrega en absoluto cuando no hay ningún sistema de correo instalado.
Compruebe cuál es el entorno antes de pasar una hora haciendo suposiciones:
sudo systemd-run --collect --wait --unit=envtest /usr/bin/env
journalctl -u envtest --no-pagerEsto muestra el entorno exacto con el que se ejecutará el trabajo. Después, corrija la diferencia. Use rutas absolutas para todo lo que sea suyo, porque systemd resuelve un rsync sin ruta contra una lista fija de rutas del sistema y nunca contra el PATH de su shell. Pase las variables necesarias con -p Environment="KEY=value" en la línea de comandos o con EnvironmentFile=/etc/default/myjob en un archivo de unidad. Cuando un trabajo necesite realmente su entorno de inicio de sesión, ejecútelo como /bin/bash -lc 'my-command' y acepte que el trabajo dependerá de sus archivos de configuración personales.
Qué sigue terminando un trabajo separado
- Un reinicio. Nada de tmux persiste tras un reinicio, porque el servidor es un proceso normal y las sesiones son su estado en memoria. Las actualizaciones del kernel implican reinicios, por lo que un trabajo que no pueda reiniciar fácilmente debe pertenecer a una unidad que pueda
systemctl enable. - El asesino de procesos por falta de memoria.
dmesg -T | grep -i 'killed process'lo muestra, incluido el nombre del proceso que eligió. Una importación grande en un VPS pequeño es un objetivo frecuente. - La limpieza de logind. Si
/etc/systemd/logind.confestableceKillUserProcesses=yes, los procesos restantes se terminan cuando finaliza la última sesión, incluido el servidor de tmux. Compruebe el valor actual conloginctl show --property=KillUserProcessesy excluya a su usuario conloginctl enable-linger "$USER". - Un disco lleno. El trabajo se detiene porque el registro que redirigió llenó el sistema de archivos, no porque usted haya salido. Ejecute
df -hantes de atribuirlo a la señal.
Iniciar un trabajo mediante SSH sin mantener la conexión
ssh vps 'sudo systemd-run --unit=import --collect /usr/local/bin/import.sh'systemd-run devuelve el control en cuanto se inicia la unidad. Por tanto, el comando ssh también termina y el trabajo no queda asociado a la sesión que lo inició. Esta es la opción limpia.
La variante de nohup requiere más cuidado:
ssh vps 'nohup ./import.sh > ~/import.log 2>&1 < /dev/null &'Sin las redirecciones, parece que el comando se bloquea. sshd mantiene abierto el canal mientras cualquier proceso conserve la salida estándar o el error estándar del comando remoto. Un trabajo ejecutado en segundo plano hereda ambos descriptores. nohup por sí solo no lo corrige, porque nohup sólo redirige la salida cuando esa salida es una terminal. En este caso, es una tubería hacia el cliente. Añadir < /dev/null también cierra la entrada estándar. ssh -n realiza la misma función desde el extremo del cliente.
FAQ
¿Por qué se detiene mi comando cuando se cierra la conexión SSH?
El pty (pseudo-terminal) que usaba la sesión se destruye y el kernel envía SIGHUP al grupo de procesos en primer plano de ese terminal. La acción predeterminada para SIGHUP es finalizar el proceso. Los trabajos en segundo plano también terminan porque bash reenvía SIGHUP a todos los trabajos de su tabla antes de salir. Un comando que ignora SIGHUP, como uno iniciado con nohup, o uno que nunca compartió la sesión, como una unidad de systemd, no se ve afectado.
¿Es mejor tmux o systemd-run para un rsync de seis horas?
systemd-run. Una sesión de tmux depende de un proceso servidor que usted inició, por lo que termina con el siguiente reinicio. Además, es invisible para quien no sepa ejecutar tmux ls. Ejecutar sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/ proporciona systemctl status bigsync para el estado y journalctl -u bigsync para la salida. El siguiente administrador puede encontrar ambos sin recibir instrucciones. Use tmux cuando necesite observar la pantalla y escribir en ella.
¿Cómo puedo ver la salida de un trabajo cuya redirección olvidé configurar?
Normalmente no puede, porque esa salida se envió a un terminal que ya no existe. Mientras el proceso siga ejecutándose, puede inspeccionar sus archivos abiertos con sudo ls -l /proc/<pid>/fd o observar sus llamadas al sistema con sudo strace -p <pid>, pero el texto que ya se escribió se ha perdido. reptyr <pid> puede mover el proceso a un terminal nuevo, y en Ubuntu kernel.yama.ptrace_scope = 1 significa que necesita sudo para un proceso que no es hijo suyo. El hábito que evita todo esto consiste en redirigir la salida a un archivo desde el principio y tail -f ese archivo.
¿Sobrevive a un reinicio una sesión de tmux separada?
No. El servidor de tmux es un proceso normal y las sesiones son su estado en memoria, por lo que un reinicio termina ambos. También termina cuando /etc/systemd/logind.conf establece KillUserProcesses=yes y usted cierra la última sesión, algo que loginctl enable-linger "$USER" evita. Para trabajos que deban reanudarse automáticamente después de un reinicio, escriba una unidad de systemd y systemctl enable.
¿Por qué mi script se ejecuta en el shell, pero falla como unidad de systemd?
Una unidad no lee /etc/profile ni ~/.bashrc, por lo que no dispone de sus adiciones a PATH ni de sus variables exportadas. systemctl status mostrando (code=exited, status=203/EXEC) significa que systemd no pudo ejecutar el archivo, así que use una ruta absoluta y compruebe el bit de ejecución. Ejecute sudo systemd-run --collect --wait --unit=envtest /usr/bin/env, vuelva a leerlo con journalctl -u envtest y obtendrá el entorno exacto que recibe el trabajo. Proporcione lo que falte con Environment= o EnvironmentFile=.