Como manter um comando após desconectar do SSH
A queda do SSH envia SIGHUP e encerra o processo. Compare nohup, disown, tmux e systemd-run para escolher a opção certa para cada tarefa.
Por que o seu comando termina quando a ligação SSH é interrompida
Para manter um comando em execução depois de uma interrupção da ligação SSH, o comando tem de acabar num local que o sinal de desligamento não consiga alcançar. Cada método abaixo organiza isso de uma forma diferente. Por isso, comece pelo mecanismo.
A sua sessão de login é executada num pty (pseudo-terminal), um dispositivo de terminal virtual que sshd cria no servidor para a sua sessão. Esse é o terminal de controlo da sua shell e de todos os comandos que inicia a partir dela. Para conhecer o restante percurso, consulte o que o SSH configura quando inicia sessão. Quando a ligação TCP termina, sshd fecha a sua extremidade e o pty é destruído. O kernel interpreta isso como o desligamento do terminal. Por isso, envia SIGHUP ao grupo de processos em primeiro plano desse terminal e ao líder da sessão, que é a sua shell. A ação predefinida de SIGHUP é terminar o processo. O seu comando estava no grupo de processos em primeiro plano. Por isso, o comando termina.
Os trabalhos em segundo plano também não são seguros. Um trabalho iniciado com & fica no seu próprio grupo de processos, por isso o kernel não lhe envia o sinal diretamente. A bash envia-o. Ao receber SIGHUP, uma bash interativa reenvia SIGHUP a todos os trabalhos na sua tabela antes de terminar. Para si, o resultado parece igual: o trabalho desaparece e o ficheiro de log termina a meio de uma linha.
Existe aqui uma assimetria que causa confusão. Escrever exit não desliga os seus trabalhos em segundo plano, porque a bash só faz isso quando a opção huponexit está definida, e essa opção está desativada por predefinição. Uma ligação interrompida desliga-os. O trabalho que sobreviveu ao fecho correto do terminal ainda pode terminar quando a ligação Wi-Fi cai.
Isto tem duas consequências, que constituem o tema completo. Um processo que ignora SIGHUP, ou que não tem qualquer terminal de controlo, não será desligado. Além disso, um processo cuja saída padrão ainda aponta para o pty destruído deixa de ter para onde escrever: a operação de escrita falha com EIO (erro de entrada/saída), e a maioria dos programas termina nesse momento. É necessário resolver as duas partes. Muitas instruções resolvem apenas a primeira. É por isso que as pessoas dizem que "nohup não funcionou".
Se a sua ligação cai várias vezes por dia, corrija também esse problema. ServerAliveInterval 60 em ~/.ssh/config impede que uma sessão inativa seja descartada por um timeout de NAT (tradução de endereços de rede) algures no percurso. Uma sessão que nem sequer é aberta é uma falha diferente, com causas diferentes. Nesse caso, é importante compreender a diferença entre ligação recusada e ligação que excedeu o tempo limite.
Qual método mantém um comando em execução depois de uma desconexão SSH?
Quatro opções, ordenadas pela importância do trabalho.
nohupousetsid: um trabalho pontual que inicia agora e cujo log consulta depois. Redireciona a saída manualmente.disown: o trabalho que já iniciou e que se esqueceu de proteger. Recupera o processo. Não consegue devolver a saída.tmuxouscreen: trabalho que precisa de monitorizar, interromper e retomar ao longo de vários dias.systemd-runou um ficheiro de unidade real: qualquer trabalho que tenha de continuar depois de terminar a sua sessão, como umrsyncde seis horas ou uma importação de base de dados durante a noite.
Vale a pena memorizar esta regra: se seria um problema esquecer o trabalho, este deve pertencer ao systemd, não ao tmux. Uma janela do tmux depende de alguém se lembrar dela. Uma unidade tem um nome, um estado, um log e uma política de reinício que a pessoa seguinte consegue encontrar sem precisar de instruções.
nohup e setsid: iniciar e desligar-se
nohup ./import.sh > ~/import.log 2>&1 &
echo $! > ~/import.pidnohup define a disposição de SIGHUP como ignorar e depois executa o seu comando. Assim, quando o kernel envia o hangup, nada acontece. O redirecionamento fica a seu cargo. Se deixar a saída padrão ligada ao terminal, nohup redireciona-a para nohup.out no diretório atual, usando $HOME/nohup.out como alternativa, e mostra:
nohup: ignoring input and appending output to 'nohup.out'É fácil perder o rasto desse ficheiro, por isso atribua-lhe um nome. $! contém o PID (identificador do processo) do último trabalho em segundo plano. Guardá-lo permite verificar o trabalho depois de voltar a iniciar sessão.
setsid resolve o mesmo problema pelo lado oposto. Executa o comando numa sessão nova, sem terminal controlador. Assim, não existe um terminal que possa interrompê-lo.
setsid --fork ./import.sh > ~/import.log 2>&1Use --fork. Sem essa opção, setsid chama setsid() no local sempre que o processo ainda não é líder de um grupo de processos. É isso que acontece dentro de um script de shell, e o script fica bloqueado. Com --fork, o comportamento é o mesmo num script e na linha de comandos.
Verifique o resultado efetivo:
ps -o pid,ppid,sid,tty,stat,cmd -p "$(cat ~/import.pid)"Uma coluna TTY de ? significa que o processo não tem terminal controlador. Portanto, nada pode interrompê-lo. Em nohup, a coluna TTY continua a mostrar algo como pts/0 enquanto permanece ligado, e passa a ? quando o pty é destruído. Ambos os resultados são normais. O trabalho sobreviveu.
disown: recuperando um job já iniciado
Você iniciou um job de duas horas em primeiro plano e só depois se lembrou deste problema. Não o termine nem o inicie novamente.
# press Ctrl-Z to suspend the job first
bg
jobs -l
disown -h %1Ctrl-Z suspende o job, bg retoma-o em segundo plano e jobs -l mostra o número do job junto do respetivo PID. disown -h %1 marca esse job para que o bash não lhe envie SIGHUP. O disown %1 simples remove completamente o job da tabela do bash. Isso tem o mesmo efeito quando a sessão termina, mas depois jobs já não o lista.
O que disown não consegue fazer é mover a saída. O processo continua a ter o pty como saída padrão. Quando o pty desaparece, a escrita seguinte devolve EIO. Por isso, disown guarda de forma fiável um job silencioso, como uma compilação que escreve num ficheiro, mas muitas vezes perde um job que gera muita saída. O job sobrevive sem ter onde escrever ou termina na linha de saída seguinte.
Existe uma ferramenta de recuperação para os descritores de ficheiros. reptyr move um processo em execução para o terminal atual. Instale-a com sudo apt install -y reptyr e execute reptyr <pid> dentro de uma janela do tmux. Ela funciona através de ptrace. O Ubuntu inclui kernel.yama.ptrace_scope = 1, que permite rastrear apenas os seus próprios processos descendentes. Por isso, um processo que tenha herdado precisa de sudo reptyr <pid>. Trate-a como uma ferramenta de emergência. Não crie um procedimento habitual com base nela.
tmux: trabalho que precisa de ser monitorizado e retomado
O tmux (multiplexador de terminais) resolve o problema num ponto diferente. Em vez de proteger o processo contra o pty, fornece ao processo um pty que não pertence à sessão SSH. O servidor tmux é executado fora dessa sessão e controla os terminais de tudo o que está dentro dele. A ligação SSH é apenas um visualizador ligado ao servidor. Se a ligação for interrompida, o servidor não deteta a interrupção.
sudo apt update && sudo apt install -y tmux
tmux new -s importInicie o trabalho nessa janela e prima Ctrl-b seguido de d para se desligar. Inicie sessão novamente mais tarde e retome-o:
tmux ls
tmux attach -t importtmux ls deve apresentar uma linha que começa por import: 1 windows. Se apresentar no server running on /tmp/tmux-1000/default, não existe nenhuma sessão à qual se possa ligar, porque a sessão nunca foi criada ou porque algo terminou o servidor.
screen faz o mesmo com uma combinação de teclas diferente. screen -S import cria uma sessão e Ctrl-a seguido de d desliga-se dela. screen -ls lista o que existe e screen -r import retoma uma sessão. Qualquer uma das ferramentas é adequada neste caso. A combinação de teclas para desligar é a parte que as pessoas esquecem.
Um multiplexador também é o local adequado para trabalho interativo que tem de sobreviver a uma interrupção da ligação. Por isso, executar o Claude Code num VPS dentro do tmux é a configuração normal. É também o que torna controlar uma sessão do servidor a partir de um telemóvel utilizável numa rede móvel que restabelece a ligação a cada poucos minutos.
systemd-run: entregue o trabalho ao PID 1
Para um trabalho que não deve depender de si, entregue-o ao sistema init.
sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/Isto cria uma unidade de serviço transitória chamada bigsync.service. A unidade recebe o seu próprio cgroup, não tem terminal de controlo e não mantém qualquer relação com a sua sessão de login. O comando devolve o controlo imediatamente e apresenta Running as unit: bigsync.service. Monitorize-a com uma destas opções:
systemctl status bigsync
journalctl -u bigsync -f--collect indica ao systemd que deve remover a unidade quando terminar, mesmo que tenha falhado. Sem esta opção, uma unidade transitória que falhou permanece carregada e o respetivo nome continua ocupado. A execução seguinte falha com uma mensagem a indicar que a unidade já existe. A saída é enviada para o journal, com marcas de data e hora em cada linha. As entradas do journal só sobrevivem a um reboot quando /var/log/journal existe. Para ativar essa persistência, execute sudo mkdir -p /var/log/journal e reinicie systemd-journald.
Como utilizador normal, chamar systemd-run sem sudo solicita autorização ao polkit e apresenta ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===. Use sudo para unidades do sistema.
Também pode executar o trabalho no seu próprio gestor de utilizador:
systemd-run --user --unit=bigsync --collect /usr/bin/rsync -aH /srv/data/ /mnt/backup/Isto tem uma particularidade importante. O seu gestor por utilizador, user@1000.service, normalmente para quando termina a última sessão e, com ele, para todas as unidades do utilizador. Ative o lingering uma vez:
loginctl enable-linger "$USER"
loginctl show-user "$USER" --property=LingerO segundo comando deve apresentar Linger=yes. Com o lingering ativo, o seu gestor de utilizador inicia no boot e continua em execução, esteja ou não autenticado. Sem esta opção, systemd-run --user não oferece qualquer vantagem em relação a nohup.
systemd-run --scope é algo diferente. Executa o comando em primeiro plano, ligado ao seu terminal, por isso não resolve este caso.
Para qualquer tarefa que vá executar mais de uma vez, escreva a unidade num ficheiro em vez de criar uma unidade transitória manualmente a cada execução.
Uma unidade permanente para uma tarefa que voltará a executar
[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.shGuarde esse conteúdo como /etc/systemd/system/nightly-sync.service, execute sudo systemctl daemon-reload, inicie-a com sudo systemctl start nightly-sync e consulte-a novamente com journalctl -u nightly-sync. Adicione um ficheiro .timer correspondente quando a tarefa dever ser executada segundo um agendamento, em vez de ser iniciada manualmente.
Escrever uma unidade de serviço systemd e o respetivo timer descreve integralmente o formato dos ficheiros e a sintaxe dos agendamentos.
Para onde vai a saída e por que desaparece
A ordem dos redirecionamentos é importante. > file 2>&1 direciona a saída padrão para o ficheiro e depois direciona o erro padrão para o mesmo local. 2>&1 > file faz o contrário: o erro padrão continua a ser enviado para o terminal, e é o terminal que está prestes a desaparecer. O Bash também aceita &> file para os dois fluxos em simultâneo.
A segunda surpresa é o armazenamento intermédio. Quando a saída padrão é um terminal, a biblioteca C envia cada linha imediatamente. Quando a saída padrão é um ficheiro, muda para um buffer de blocos com alguns kilobytes, pelo que tail -f ~/import.log não mostra nada durante minutos e o processo parece bloqueado. Force o armazenamento por linha com stdbuf -oL ./import.sh > ~/import.log 2>&1 ou use a opção do próprio programa, como python3 -u ou grep --line-buffered.
Evite este padrão:
nohup ./import.sh 2>&1 | tee ~/import.log &nohup protege import.sh e nada mais. tee é um processo separado no mesmo pipeline e também termina quando recebe o hangup. import.sh passa então a escrever num pipe sem leitor, recebe SIGPIPE e para. Coloque o pipeline inteiro dentro de setsid bash -c '...' ou escreva diretamente no ficheiro e execute tail -f sobre ele quando voltar a ligar-se.
Há mais um detalhe específico de rsync. --info=progress2 escreve um fluxo de retornos de carro que parece correto num terminal, mas se torna uma linha enorme num ficheiro de log ou no journal. Numa execução não interativa, remova-o e use --stats.
Por que uma tarefa que funciona na sua shell falha no systemd ou no cron
A sua shell interativa lê /etc/profile, ~/.profile e ~/.bashrc. Por isso, tem o seu PATH, os shims do seu gestor de versões e as suas variáveis exportadas. Uma unidade do systemd não lê nenhum desses ficheiros. O cron também não lê nenhum deles: no Debian e no Ubuntu, o cron executa as tarefas com SHELL=/bin/sh e PATH=/usr/bin:/bin.
No systemd, o sintoma é systemctl status a indicar (code=exited, status=203/EXEC). Isto significa que o systemd nem sequer conseguiu executar o ficheiro, porque o caminho estava errado ou o ficheiro não está marcado como executável. No cron, normalmente é command not found, entregue por correio local ou não entregue de todo quando não existe um sistema de correio instalado.
Confirme o ambiente antes de passar uma hora a tentar adivinhar:
sudo systemd-run --collect --wait --unit=envtest /usr/bin/env
journalctl -u envtest --no-pagerIsto mostra o ambiente exato com que a tarefa será executada. Corrija depois a diferença. Use caminhos absolutos para tudo o que for seu, porque o systemd resolve um rsync sem caminho contra uma lista fixa de caminhos do sistema e nunca contra o PATH da sua shell. Passe as variáveis necessárias com -p Environment="KEY=value" na linha de comandos ou com EnvironmentFile=/etc/default/myjob num ficheiro de unidade. Quando uma tarefa precisar realmente do seu ambiente de início de sessão, execute-a como /bin/bash -lc 'my-command' e aceite que a tarefa passa a depender dos seus ficheiros de configuração pessoais.
O que ainda encerra um job destacado
- Um reboot. Nada do tmux persiste após um reboot, porque o servidor é um processo normal e as sessões são o seu estado na memória. As atualizações do kernel exigem reboots, portanto um job que não possa ser reiniciado facilmente deve pertencer a uma unidade que possa ser
systemctl enable. - O OOM killer.
dmesg -T | grep -i 'killed process'mostra essa situação, incluindo o nome do processo escolhido. Uma importação grande numa VPS pequena é um alvo frequente. - A limpeza do logind. Se
/etc/systemd/logind.confdefinirKillUserProcesses=yes, os processos restantes são encerrados quando a sua última sessão termina, incluindo o servidor tmux. Consulte a definição atual comloginctl show --property=KillUserProcessese isente o seu utilizador comloginctl enable-linger "$USER". - Um disco cheio. O job para porque o log redirecionado encheu o sistema de ficheiros, não porque saiu. Execute
df -hantes de atribuir a causa ao sinal.
Iniciar um trabalho por SSH sem manter a ligação ativa
ssh vps 'sudo systemd-run --unit=import --collect /usr/local/bin/import.sh'systemd-run retorna assim que a unidade é iniciada. Por isso, o comando ssh também retorna, e o trabalho não mantém ligação com a sessão que o iniciou. Esta é a forma correta.
A versão com nohup requer mais atenção:
ssh vps 'nohup ./import.sh > ~/import.log 2>&1 < /dev/null &'Sem os redirecionamentos, parece que o comando fica bloqueado. sshd mantém o canal aberto enquanto qualquer processo ainda tiver a saída padrão ou o erro padrão do comando remoto. Um trabalho executado em segundo plano herda ambos. O nohup, por si só, não resolve o problema, porque nohup só redireciona a saída quando essa saída é um terminal. Neste caso, é um pipe de volta para o cliente. Adicionar < /dev/null também fecha o lado da entrada. ssh -n faz o mesmo no lado do cliente.
FAQ
Por que o meu comando para quando a ligação SSH cai?
O pty (pseudo-terminal) que a sua sessão estava a utilizar é destruído, e o kernel envia SIGHUP ao grupo de processos em primeiro plano desse terminal. A ação predefinida de SIGHUP é terminar o processo. Os trabalhos em segundo plano também terminam, porque o bash reenvia SIGHUP para todos os trabalhos da sua tabela antes de sair. Um comando que ignora SIGHUP, como um iniciado com nohup, ou um comando que nunca partilhou a sua sessão, como uma unidade systemd, não é afetado.
O tmux ou o systemd-run é melhor para um rsync de seis horas?
systemd-run. Uma sessão tmux depende de um processo servidor que iniciou, por isso termina no próximo reboot e fica invisível para quem não souber executar tmux ls. Executar sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/ fornece systemctl status bigsync para o estado e journalctl -u bigsync para a saída. O administrador seguinte encontra ambos sem precisar de instruções. Use tmux para trabalhos em que precisa de acompanhar o ecrã e introduzir comandos.
Como vejo a saída de um trabalho que me esqueci de redirecionar?
Normalmente, não pode, porque essa saída foi enviada para um terminal que já não existe. Enquanto o processo ainda estiver em execução, pode inspecionar os ficheiros abertos com sudo ls -l /proc/<pid>/fd ou acompanhar as chamadas de sistema com sudo strace -p <pid>, mas o texto já escrito perdeu-se. reptyr <pid> pode mover o processo para um terminal novo, e no Ubuntu kernel.yama.ptrace_scope = 1 significa que precisa de sudo para um processo que não é seu filho. O procedimento que evita tudo isto é redirecionar a saída para um ficheiro no início e tail -f esse ficheiro.
Uma sessão tmux desanexada sobrevive a um reboot?
Não. O servidor tmux é um processo normal e as sessões são o seu estado mantido em memória, por isso um reboot termina ambos. O servidor também termina quando /etc/systemd/logind.conf define KillUserProcesses=yes e termina a sua última sessão, algo que loginctl enable-linger "$USER" impede. Para trabalhos que têm de voltar a arrancar automaticamente depois de um reboot, escreva uma unidade systemd e systemctl enable-a.
Por que o meu script funciona na shell, mas falha como unidade systemd?
Uma unidade não lê /etc/profile nem ~/.bashrc, por isso não tem as suas adições PATH nem as variáveis exportadas. systemctl status a mostrar (code=exited, status=203/EXEC) significa que o systemd não conseguiu executar o ficheiro, por isso use um caminho absoluto e verifique o bit de execução. Execute sudo systemd-run --collect --wait --unit=envtest /usr/bin/env, leia o resultado com journalctl -u envtest e obterá o ambiente exato que o seu trabalho recebe. Forneça o que estiver em falta com Environment= ou EnvironmentFile=.