Come mantenere un comando attivo dopo SSH
Quando SSH si disconnette, il kernel invia SIGHUP e il comando termina. Confronta nohup, disown, tmux e systemd-run per scegliere il metodo adatto.
Perché il comando termina quando la connessione SSH si interrompe
Per mantenere un comando in esecuzione dopo l'interruzione di SSH, il comando deve trovarsi in una condizione in cui il segnale di disconnessione non possa raggiungerlo. Ogni metodo descritto di seguito ottiene questo risultato in modo diverso, quindi è utile partire dal meccanismo.
La sessione di accesso viene eseguita su una pty (pseudo-terminal), cioè un dispositivo terminale virtuale che sshd crea sul server per la sessione. È il terminale di controllo della shell e di ogni comando avviato dalla shell. Per una descrizione completa di questo percorso, vedere cosa configura SSH quando si accede. Quando la connessione TCP si interrompe, sshd chiude la propria estremità e la pty viene distrutta. Il kernel interpreta l'evento come una disconnessione del terminale, quindi invia SIGHUP al gruppo di processi in primo piano di quel terminale e al leader della sessione, cioè la shell. L'azione predefinita per SIGHUP consiste nel terminare il processo. Il comando apparteneva al gruppo di processi in primo piano, quindi termina.
Neppure i job in background sono al sicuro. Un job avviato con & appartiene a un proprio gruppo di processi, quindi il kernel non gli invia direttamente il segnale. Lo fa Bash. Quando riceve SIGHUP, una bash interattiva reinvia SIGHUP a ogni job presente nella propria tabella prima di terminare. Dal punto di vista dell'utente, il risultato è identico: il job scompare e il file di log si interrompe a metà riga.
Qui esiste un'asimmetria che spesso crea confusione. La digitazione di exit non disconnette i job in background, perché Bash esegue questa operazione solo quando è impostata l'opzione huponexit, disattivata per impostazione predefinita. Una connessione interrotta, invece, li disconnette. Il job che è sopravvissuto alla chiusura ordinata del terminale può comunque terminare quando la connessione Wi-Fi si interrompe.
Ne derivano due conseguenze, che costituiscono l'intero problema. Un processo che ignora SIGHUP, oppure che non ha alcun terminale di controllo, non viene disconnesso. Inoltre, un processo il cui output standard punta ancora alla pty distrutta non ha più una destinazione per la scrittura: la scrittura fallisce con EIO (errore di input/output) e la maggior parte dei programmi termina a quel punto. È necessario risolvere entrambi gli aspetti. Molte procedure risolvono solo il primo, motivo per cui si legge spesso che «nohup non ha funzionato».
Se la connessione si interrompe più volte al giorno, è opportuno correggere anche questo problema. ServerAliveInterval 60 in ~/.ssh/config impedisce che una sessione inattiva venga eliminata dal timeout di un NAT (network address translation) presente lungo il percorso. Una sessione che non viene mai aperta è un problema diverso, con cause diverse; in questo caso è utile la differenza tra connessione rifiutata e connessione scaduta.
Quale metodo mantiene in esecuzione un comando dopo la disconnessione SSH?
Quattro risposte, ordinate in base all'importanza del lavoro.
nohuposetsid: un'attività una tantum che avvii subito e per cui consulterai il log in seguito. Il reindirizzamento dell'output è a tuo carico.disown: il lavoro che hai già avviato e che hai dimenticato di proteggere. Recupera il processo, ma non può restituirti l'output.tmuxoscreen: un'attività che devi monitorare, interrompere e riprendere nell'arco di più giorni.systemd-runo un vero file di unità: qualsiasi attività che deve sopravvivere alla sessione di accesso, ad esempio unrsyncdi sei ore o un'importazione del database eseguita durante la notte.
La regola da ricordare è questa: se dimenticarti del lavoro sarebbe un problema, il lavoro appartiene a systemd, non a tmux. Una finestra tmux è qualcosa che una persona deve ricordarsi di gestire. Un'unità ha un nome, uno stato, un log e una policy di riavvio che la persona successiva può individuare senza che nessuno debba spiegarglielo.
nohup e setsid: avvia il processo e disconnettiti
nohup ./import.sh > ~/import.log 2>&1 &
echo $! > ~/import.pidnohup imposta la disposizione di SIGHUP su ignore, quindi esegue il comando. In questo modo il kernel invia il segnale di hangup, che non produce alcun effetto. Il redirect deve essere specificato da te. Se lasci lo standard output collegato al terminale, nohup lo reindirizza automaticamente in nohup.out nella directory corrente, usando $HOME/nohup.out come fallback, e stampa:
nohup: ignoring input and appending output to 'nohup.out'È facile perdere di vista questo file, quindi assegnagli un nome esplicito. $! contiene il PID (identificatore del processo) dell'ultimo job in background. Salvarlo consente di controllare il job dopo aver effettuato nuovamente l'accesso.
setsid risolve lo stesso problema agendo dal lato opposto. Esegue il comando in una nuova sessione senza terminale di controllo. Non esiste quindi alcun terminale che possa interromperlo.
setsid --fork ./import.sh > ~/import.log 2>&1Usa --fork. Senza questa opzione, setsid richiama setsid() internamente quando il processo non è già process group leader. Questo accade negli shell script e lascia lo script bloccato. Con --fork il comportamento è identico sia nello script sia al prompt.
Verifica il risultato effettivo:
ps -o pid,ppid,sid,tty,stat,cmd -p "$(cat ~/import.pid)"Una colonna TTY con valore ? indica che il processo non ha un terminale di controllo. Nulla può quindi interromperlo. Con nohup la colonna TTY mostra ancora un valore simile a pts/0 mentre la connessione resta attiva e diventa ? quando il pty viene distrutto. Entrambi i risultati sono corretti. Il job è sopravvissuto.
disown: recuperare un job già avviato
Hai avviato un job di due ore in primo piano e poi hai ricordato questo problema. Non terminarlo per avviarlo di nuovo.
# press Ctrl-Z to suspend the job first
bg
jobs -l
disown -h %1Ctrl-Z sospende il job, bg lo riprende in background e jobs -l stampa il numero del job accanto al relativo PID. disown -h %1 contrassegna il job in modo che bash non gli invii SIGHUP. Il semplice disown %1 rimuove completamente il job dalla tabella di bash. Anche questo impedisce gli effetti del hangup, ma in seguito jobs non lo elenca più.
disown non può spostare l'output. Il processo mantiene il pty come output standard e, quando il pty viene rimosso, la scrittura successiva restituisce EIO. Di conseguenza, disown salva in modo affidabile un job silenzioso, ad esempio una compilazione che scrive in un file, ma spesso non riesce con un job che produce molto output. Il job sopravvive senza una destinazione per l'output oppure termina alla riga successiva che deve scrivere.
Esiste uno strumento di recupero per i descrittori di file. reptyr trasferisce un processo in esecuzione sul terminale corrente: installalo con sudo apt install -y reptyr, quindi esegui reptyr <pid> da una finestra tmux. Funziona tramite ptrace e Ubuntu include kernel.yama.ptrace_scope = 1, che consente di tracciare soltanto i propri processi discendenti. Per un processo ereditato è quindi necessario sudo reptyr <pid>. Usalo come strumento di emergenza. Non basare su di esso una procedura ordinaria.
tmux: il lavoro da tenere sotto controllo e riprendere in seguito
tmux (multiplexer di terminale) risolve il problema a un livello diverso. Invece di proteggere il processo dal pty, assegna al processo un pty che non appartiene alla sessione SSH. Il server tmux viene eseguito al di fuori della sessione e gestisce i terminali di tutto ciò che contiene. La connessione SSH è soltanto un visualizzatore collegato al server. Se la connessione cade, il server non se ne accorge.
sudo apt update && sudo apt install -y tmux
tmux new -s importAvvia il job in quella finestra, quindi premi Ctrl-b seguito da d per scollegarti. Accedi di nuovo in seguito e riprendi il lavoro:
tmux ls
tmux attach -t importtmux ls dovrebbe stampare una riga che inizia con import: 1 windows. Se stampa no server running on /tmp/tmux-1000/default, non esiste alcuna sessione a cui collegarsi, perché non è mai stata creata oppure un processo ha terminato il server.
screen svolge la stessa funzione con una combinazione di tasti diversa. screen -S import crea una sessione, quindi Ctrl-a seguito da d si scollega da essa. screen -ls elenca le sessioni esistenti e screen -r import ne riapre una. Entrambi gli strumenti sono adatti in questo caso. Il tasto di scollegamento è il dettaglio che viene più spesso dimenticato.
Un multiplexer è anche l'ambiente corretto per il lavoro interattivo che deve sopravvivere a una perdita della connessione. Per questo eseguire Claude Code su un VPS all'interno di tmux è la configurazione standard. È inoltre ciò che rende gestire una sessione del server da un telefono pratico su una rete mobile che si riconnette ogni pochi minuti.
systemd-run: affidare il job a PID 1
Per un job che non deve dipendere in alcun modo da te, affidalo al sistema init.
sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/Questo crea una unità di servizio transitoria denominata bigsync.service. Ottiene un cgroup dedicato, non ha un terminale di controllo e non dipende dalla tua sessione di accesso. Il comando restituisce subito il prompt e stampa Running as unit: bigsync.service. Puoi monitorarla in uno dei seguenti modi:
systemctl status bigsync
journalctl -u bigsync -f--collect indica a systemd di rimuovere l'unità al termine, anche se l'esecuzione è fallita. Senza questa opzione, un'unità transitoria fallita resta caricata e il suo nome continua a essere occupato. L'esecuzione successiva fallisce quindi con un messaggio che indica che l'unità esiste già. L'output viene scritto nel journal, con un timestamp su ogni riga. Le voci del journal sopravvivono a un riavvio solo quando esiste /var/log/journal. Esegui quindi sudo mkdir -p /var/log/journal e riavvia systemd-journald se vuoi conservarle.
Come utente normale, eseguire systemd-run senza sudo richiede l'autorizzazione tramite polkit e stampa ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===. Usa sudo per le unità di sistema.
Puoi anche eseguire il job tramite il tuo user manager:
systemd-run --user --unit=bigsync --collect /usr/bin/rsync -aH /srv/data/ /mnt/backup/Questa configurazione presenta un problema. Il tuo user manager, user@1000.service, normalmente si arresta quando termina l'ultima sessione e arresta con sé tutte le unità utente. Abilita il lingering una volta sola:
loginctl enable-linger "$USER"
loginctl show-user "$USER" --property=LingerIl secondo comando dovrebbe stampare Linger=yes. Con il lingering abilitato, il tuo user manager si avvia al boot e continua a funzionare indipendentemente dal fatto che tu abbia effettuato l'accesso. Senza questa impostazione, systemd-run --user non offre alcun vantaggio rispetto a nohup.
systemd-run --scope è un comando diverso. Esegue il comando in primo piano, collegato al tuo terminale, quindi non è utile in questo caso.
Per qualsiasi attività che eseguirai più di una volta, definisci l'unità in un file invece di digitare ogni volta un'unità transitoria.
Unità permanente per un job da eseguire nuovamente
[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.shSalva il contenuto come /etc/systemd/system/nightly-sync.service, esegui sudo systemctl daemon-reload, quindi avviala con sudo systemctl start nightly-sync e rileggila con journalctl -u nightly-sync. Aggiungi un file .timer corrispondente quando deve essere eseguita secondo una pianificazione anziché su richiesta.
Scrivere un'unità di servizio systemd e il relativo timer illustra in dettaglio il formato dei file e la sintassi delle pianificazioni.
Dove finisce l’output e perché scompare
L’ordine dei reindirizzamenti è importante. > file 2>&1 indirizza lo standard output al file e poi indirizza lo standard error nello stesso punto. 2>&1 > file esegue le operazioni nell’ordine opposto: lo standard error continua a essere inviato al terminale, che è proprio ciò che sta per scomparire. Bash accetta anche &> file per entrambi i flussi contemporaneamente.
Il secondo aspetto inatteso è il buffering. Quando lo standard output è un terminale, la libreria C scarica ogni riga. Quando lo standard output è un file, passa a un buffer a blocchi di alcuni kilobyte, quindi tail -f ~/import.log non mostra nulla per diversi minuti e il processo sembra bloccato. Forza il line buffering con stdbuf -oL ./import.sh > ~/import.log 2>&1 oppure usa l’opzione prevista dal programma, ad esempio python3 -u o grep --line-buffered.
Evita questo schema:
nohup ./import.sh 2>&1 | tee ~/import.log &nohup protegge import.sh e nient’altro. tee è un processo distinto nella stessa pipeline e termina comunque quando riceve il segnale di hangup. import.sh sta quindi scrivendo in una pipe senza lettore, perciò riceve SIGPIPE e si arresta. Inserisci l’intera pipeline in setsid bash -c '...' oppure scrivi direttamente nel file ed esegui tail -f sul file quando ti riconnetti.
Un ultimo dettaglio riguarda specificamente rsync. --info=progress2 scrive un flusso di ritorni carrello che appare corretto in un terminale, ma diventa un’unica riga enorme in un file di log o nel journal. Per un’esecuzione non interattiva, rimuovilo e usa invece --stats.
Perché un job che funziona nella shell non riesce con systemd o cron
La shell interattiva legge /etc/profile, ~/.profile e ~/.bashrc, quindi dispone di PATH, degli shim del gestore delle versioni e delle variabili esportate. Un'unità systemd non legge nessuno di questi elementi. Anche cron non li legge: su Debian e Ubuntu, cron esegue i job con SHELL=/bin/sh e PATH=/usr/bin:/bin.
Con systemd, il sintomo è systemctl status che restituisce (code=exited, status=203/EXEC). Questo significa che systemd non ha potuto eseguire affatto il file, perché il percorso era errato oppure il file non è contrassegnato come eseguibile. Con cron, il sintomo è di solito command not found, consegnato tramite la posta locale, oppure non viene consegnato affatto quando non è installato alcun sistema di posta.
Verifica l'ambiente effettivo prima di passare un'ora a fare ipotesi:
sudo systemd-run --collect --wait --unit=envtest /usr/bin/env
journalctl -u envtest --no-pagerQuesto comando stampa l'ambiente esatto con cui verrà eseguito il job. Correggi quindi la differenza. Usa percorsi assoluti per tutto ciò che gestisci direttamente, perché systemd risolve un rsync semplice usando un elenco fisso di percorsi di sistema e non usando mai il PATH della shell. Passa le variabili necessarie con -p Environment="KEY=value" nella riga di comando oppure con EnvironmentFile=/etc/default/myjob in un file di unità. Quando un job richiede realmente il tuo ambiente di login, eseguilo come /bin/bash -lc 'my-command' e considera che il job dipende ora dai tuoi dotfile.
Cosa può ancora terminare un job scollegato
- Un riavvio. tmux non conserva nulla durante un riavvio, perché il server è un processo ordinario e le sessioni sono il suo stato in memoria. Gli aggiornamenti del kernel richiedono riavvii, quindi un job che non puoi riavviare facilmente deve appartenere a un'unità che puoi
systemctl enable. - L'out-of-memory killer.
dmesg -T | grep -i 'killed process'lo mostra, incluso il nome del processo scelto. Un'importazione di grandi dimensioni su un VPS di piccole dimensioni è un bersaglio frequente. - La pulizia eseguita da logind. Se
/etc/systemd/logind.confimpostaKillUserProcesses=yes, i processi rimasti vengono terminati quando termina la tua ultima sessione, incluso il server tmux. Controlla l'impostazione corrente conloginctl show --property=KillUserProcessesed escludi il tuo utente conloginctl enable-linger "$USER". - Un disco pieno. Il job si arresta perché il log a cui hai reindirizzato l'output ha riempito il filesystem, non perché ti sei disconnesso. Esegui
df -hprima di attribuire la causa al segnale.
Avviare un job tramite SSH senza mantenere la connessione
ssh vps 'sudo systemd-run --unit=import --collect /usr/local/bin/import.sh'systemd-run restituisce il controllo non appena l'unità è stata avviata, quindi anche il comando ssh termina e il job non è collegato alla sessione che lo ha avviato. Questa è la variante corretta.
La variante con nohup richiede maggiore attenzione:
ssh vps 'nohup ./import.sh > ~/import.log 2>&1 < /dev/null &'Senza i reindirizzamenti, il comando sembra bloccarsi. sshd mantiene aperto il canale finché un processo dispone ancora dell'output standard o dell'errore standard del comando remoto, e un job eseguito in background eredita entrambi. nohup da solo non risolve il problema, perché nohup reindirizza l'output soltanto quando tale output è un terminale; in questo caso è invece una pipe verso il client. L'aggiunta di < /dev/null chiude anche il flusso di input. ssh -n svolge la stessa funzione dal lato client.
FAQ
Perché il comando si interrompe quando cade la connessione SSH?
Il pty (pseudo-terminale) usato dalla sessione viene eliminato e il kernel invia SIGHUP al gruppo di processi in primo piano associato a quel terminale. L'azione predefinita di SIGHUP consiste nel terminare il processo. Anche i job in background vengono terminati, perché prima di uscire bash reinvia SIGHUP a ogni job presente nella propria tabella. Un comando che ignora SIGHUP, ad esempio uno avviato con nohup, oppure un comando che non ha mai condiviso la sessione, come un'unità systemd, non viene influenzato.
Per un rsync di sei ore, è meglio tmux o systemd-run?
systemd-run. Una sessione tmux dipende da un processo server avviato dall'utente, quindi termina al riavvio successivo, ed è invisibile a chi non sa eseguire tmux ls. L'esecuzione di sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/ fornisce systemctl status bigsync per lo stato e journalctl -u bigsync per l'output, entrambi facilmente reperibili dall'amministratore successivo senza istruzioni aggiuntive. Usa tmux per le attività in cui devi osservare lo schermo e inserire comandi.
Come posso vedere l'output di un job per il quale ho dimenticato di configurare il reindirizzamento?
In genere non è possibile, perché l'output è stato inviato a un terminale che non esiste più. Mentre il processo è ancora in esecuzione, puoi esaminare i file aperti con sudo ls -l /proc/<pid>/fd oppure monitorare le chiamate di sistema con sudo strace -p <pid>, ma il testo già scritto è perduto. reptyr <pid> può spostare il processo su un terminale nuovo e, in Ubuntu, kernel.yama.ptrace_scope = 1 indica che sono necessari sudo per un processo che non è un proprio processo figlio. Per evitare tutto questo, reindirizza l'output in un file all'avvio e tail -f quel file.
Una sessione tmux scollegata sopravvive a un riavvio?
No. Il server tmux è un processo ordinario e le sessioni sono memorizzate nella sua memoria, quindi un riavvio termina entrambi. Il server termina anche quando /etc/systemd/logind.conf imposta KillUserProcesses=yes e l'utente esegue il logout dall'ultima sessione; loginctl enable-linger "$USER" impedisce questo comportamento. Per le attività che devono riprendere automaticamente dopo un riavvio, scrivi un'unità systemd e systemctl enable.
Perché lo script funziona nella shell ma non come unità systemd?
Un'unità non legge /etc/profile o ~/.bashrc, quindi non dispone né delle aggiunte a PATH né delle variabili esportate. Se systemctl status mostra (code=exited, status=203/EXEC), significa che systemd non è riuscito a eseguire il file; usa quindi un percorso assoluto e controlla il bit di esecuzione. Esegui sudo systemd-run --collect --wait --unit=envtest /usr/bin/env, rileggi il risultato con journalctl -u envtest e otterrai l'ambiente esatto del job. Fornisci ciò che manca con Environment= o EnvironmentFile=.