SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor

Commando blijven draaien na verbreken SSH verbinding

Voorkom dat uw proces stopt wanneer de SSH verbinding wegvalt. Leer hoe u SIGHUP vermijdt met nohup, disown, tmux of systemd-run voor een stabiele uitvoering op de server.

Waarom uw commando stopt wanneer de SSH-verbinding verbreekt

Om een commando te laten draaien na een verbroken SSH-verbinding, moet het commando op een plek terechtkomen waar het hangup-signaal het niet kan bereiken. Elke onderstaande methode is een andere manier om dit te regelen, dus begin bij het mechanisme.

Uw login draait op een pty (pseudo-terminal), een virtueel terminalapparaat dat sshd op de server aanmaakt voor uw sessie. Dit is de controlerende terminal van uw shell en van elk commando dat u vanuit die shell start. Als u de rest van dat pad wilt weten, behandelt wat SSH opzet wanneer u inlogt dit. Wanneer de TCP-verbinding wegvalt, sluit sshd zijn kant en wordt de pty vernietigd. De kernel behandelt dit als een terminal die ophangt, dus stuurt het SIGHUP naar de foreground process group van die terminal en naar de session leader, wat uw shell is. De standaardactie voor SIGHUP is het beëindigen van het proces. Uw commando bevond zich in de foreground process group, dus uw commando stopt.

Achtergrondtaken zijn ook niet veilig. Een taak die is gestart met & bevindt zich in zijn eigen process group, dus de kernel stuurt het signaal niet direct. Bash doet dit wel. Bij het ontvangen van SIGHUP stuurt een interactieve bash SIGHUP door naar elke taak in zijn tabel voordat deze afsluit. Vanuit uw perspectief ziet het resultaat er hetzelfde uit: de taak is verdwenen en het logbestand stopt halverwege de regel.

Er is hier een asymmetrie die voor verwarring zorgt. Het typen van exit zorgt er niet voor dat uw achtergrondtaken worden afgesloten, omdat bash dit alleen doet wanneer de optie huponexit is ingeschakeld, en deze staat standaard uit. Een verbroken verbinding sluit ze wel af. De taak die overleefde toen u de terminal netjes afsloot, kan alsnog stoppen wanneer de wifi wegvalt.

Hieruit volgen twee consequenties, en die vormen het gehele onderwerp. Een proces dat SIGHUP negeert, of dat helemaal geen controlerende terminal heeft, wordt niet afgesloten. En een proces waarvan de standaarduitvoer nog steeds naar de vernietigde pty wijst, heeft nergens om naar te schrijven: de schrijfactie faalt met EIO (input/output error), en de meeste programma's sluiten op dat punt af. U moet beide helften oplossen. Veel recepten lossen alleen de eerste op, wat de reden is dat mensen rapporteren dat "nohup niet werkte".

Als uw verbinding meerdere keren per dag wegvalt, los dat dan ook op. ServerAliveInterval 60 in ~/.ssh/config voorkomt dat een inactieve sessie wordt weggegooid door een NAT (network address translation) timeout ergens op het pad. Een sessie die helemaal niet opent is een ander defect met andere oorzaken, wat het punt is waar het verschil tussen connection refused en connection timed out van belang is.

Welke methode houdt een commando actief na het verbreken van de SSH-verbinding?

Vier antwoorden, gerangschikt op basis van de ernst van de taak.

  • nohup of setsid: een eenmalige taak die u nu start en waarvan u achteraf het logbestand leest. U leidt de output zelf om.
  • disown: de taak die u al gestart bent en vergeten bent te beschermen. Dit redt het proces. Het kan de output echter niet meer voor u terughalen.
  • tmux of screen: werk dat u moet monitoren, onderbreken en waarnaar u over meerdere dagen wilt terugkeren.
  • systemd-run of een echt unit file: alles wat uw aanmelding moet overleven, zoals een zes uur durende rsync of een database-import die de hele nacht duurt.

De regel die het onthouden waard is: als het vergeten van de taak een probleem zou vormen, hoort de taak bij systemd, niet bij tmux. Een tmux-venster is iets wat een mens moet onthouden. Een unit heeft een naam, een status, een logbestand en een herstartbeleid dat de volgende persoon kan vinden zonder dat dit expliciet hoeft te worden verteld.

nohup en setsid: starten en loskoppelen

nohup ./import.sh > ~/import.log 2>&1 &
echo $! > ~/import.pid

nohup stelt de afhandeling van SIGHUP in op negeren en voert vervolgens uw commando uit, waardoor het hangup-signaal van de kernel geen effect heeft. De redirect moet u zelf opgeven. Als u de standaarduitvoer naar de terminal laat wijzen, leidt nohup deze voor u om naar nohup.out in de huidige map, met $HOME/nohup.out als fallback, en geeft het volgende weer:

nohup: ignoring input and appending output to 'nohup.out'

Het is gemakkelijk om dat bestand uit het oog te verliezen, dus geef het zelf een naam. $! bevat het PID (process identifier) van de laatste achtergrondtaak; door dit op te slaan kunt u de taak controleren nadat u opnieuw bent ingelogd.

setsid benadert hetzelfde probleem vanuit een andere hoek. Het voert het commando uit in een nieuwe sessie zonder controlerende terminal, waardoor er geen terminal bestaat die het proces kan afsluiten.

setsid --fork ./import.sh > ~/import.log 2>&1

Gebruik --fork. Zonder deze optie roept setsid op zijn plaats setsid() aan wanneer het proces nog geen procesgroepleider is, wat gebeurt binnen een shell-script, waardoor uw script geblokkeerd blijft. Met --fork is het gedrag in een script hetzelfde als bij de prompt.

Controleer wat u daadwerkelijk heeft bereikt:

ps -o pid,ppid,sid,tty,stat,cmd -p "$(cat ~/import.pid)"

Een TTY-kolom van ? betekent dat het proces geen controlerende terminal heeft, dus niets kan het proces afsluiten. Onder nohup toont de TTY-kolom nog steeds iets als pts/0 zolang u verbonden blijft, en dit verandert in ? zodra de pty is vernietigd. Beide resultaten zijn in orde. De taak is blijven draaien.

disown: een reeds gestarte taak redden

U bent een taak van twee uur gestart in de voorgrond en herinnert zich vervolgens dit probleem. Beëindig het proces niet om opnieuw te beginnen.

# press Ctrl-Z to suspend the job first
bg
jobs -l
disown -h %1

Ctrl-Z pauzeert de taak, bg hervat deze op de achtergrond en jobs -l toont het taaknummer naast het PID. disown -h %1 markeert die taak zodat bash er geen SIGHUP naar verstuurt. Een simpele disown %1 verwijdert de taak volledig uit de tabel van bash, wat hetzelfde effect heeft op hangup, maar daarna toont jobs de taak niet meer.

Wat disown niet kan, is de uitvoer verplaatsen. Het proces houdt de pty vast als standaarduitvoer, en wanneer de pty verdwijnt, geeft de volgende schrijfactie EIO terug. Daarom redt disown betrouwbaar een stille taak, zoals een compilatie die naar een bestand schrijft, maar gaat het vaak mis bij een taak met veel uitvoer. De taak overleeft het ofwel zonder plek om te printen, of sterft bij de volgende regel uitvoer.

Er is een reddingstool voor de file descriptors. reptyr verplaatst een draaiend proces naar uw huidige terminal: installeer het met sudo apt install -y reptyr en voer vervolgens reptyr <pid> uit vanuit een tmux-venster. Het werkt via ptrace, en Ubuntu levert kernel.yama.ptrace_scope = 1, wat het tracen van alleen uw eigen afstammelingen toestaat, dus een proces dat u heeft geërfd vereist sudo reptyr <pid>. Beschouw dit als een noodoplossing. Bouw hier geen routine op.

tmux: werk dat u moet monitoren en waarnaar u wilt terugkeren

tmux (terminal multiplexer) lost het probleem op een andere plek op. In plaats van uw proces te beschermen tegen de pty, geeft het uw proces een pty die niet toebehoort aan uw SSH-sessie. De tmux-server draait buiten die sessie en beheert de terminals van alles wat daarbinnen actief is. Uw SSH-verbinding is slechts een viewer die ermee verbonden is. Verbreek de verbinding en de server merkt er niets van.

sudo apt update && sudo apt install -y tmux
tmux new -s import

Start de taak in dat venster en druk vervolgens op Ctrl-b gevolgd door d om los te koppelen (detach). Log later opnieuw in en hervat het werk:

tmux ls
tmux attach -t import

tmux ls zou een regel moeten tonen die begint met import: 1 windows. Als het no server running on /tmp/tmux-1000/default weergeeft, is er geen sessie om aan te koppelen, omdat deze ofwel nooit is aangemaakt of omdat iets de server heeft beëindigd.

screen doet hetzelfde werk met een andere toetscombinatie. screen -S import maakt een sessie aan, en Ctrl-a gevolgd door d koppelt los. screen -ls toont wat er bestaat en screen -r import haalt een sessie terug. Beide tools zijn hier prima geschikt. De toets voor loskoppelen is wat mensen vaak vergeten.

Een multiplexer is ook de juiste plek voor interactief werk dat een wegvallend signaal moet overleven. Daarom is Claude Code draaien op een VPS binnen tmux de standaardconfiguratie, en dat is wat een serversessie aansturen vanaf een telefoon bruikbaar maakt op een mobiel netwerk dat elke paar minuten opnieuw verbinding maakt.

systemd-run: de taak overdragen aan PID 1

Voor een taak die op geen enkele wijze van u afhankelijk mag zijn, draagt u deze over aan het init-systeem.

sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/

Dit creëert een tijdelijke service-unit genaamd bigsync.service. Deze krijgt een eigen cgroup, geen controlerende terminal en geen relatie met uw inlogsessie. Het commando keert direct terug en toont Running as unit: bigsync.service. Monitor het proces met een van de volgende commando's:

systemctl status bigsync
journalctl -u bigsync -f

--collect instrueert systemd om de unit te verwijderen zodra deze is voltooid, ook bij een foutmelding. Zonder deze vlag blijft een gefaalde tijdelijke unit geladen en blijft de naam bezet, waardoor een volgende uitvoering faalt met de melding dat de unit al bestaat. De output wordt naar de journal geschreven, inclusief tijdstempels op elke regel. Journal-items blijven alleen behouden na een herstart als /var/log/journal bestaat; voer daarom sudo mkdir -p /var/log/journal uit en herstart systemd-journald als u dit wenst.

Als normale gebruiker vraagt het aanroepen van systemd-run zonder sudo om autorisatie via polkit en wordt ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units === getoond. Gebruik sudo voor systeem-units.

U kunt de taak ook uitvoeren onder uw eigen user manager:

systemd-run --user --unit=bigsync --collect /usr/bin/rsync -aH /srv/data/ /mnt/backup/

Hier zit een valkuil. Uw user manager, user@1000.service, stopt normaal gesproken wanneer uw laatste sessie wordt beëindigd, en neemt daarbij alle user-units mee. Schakel 'lingering' eenmalig in:

loginctl enable-linger "$USER"
loginctl show-user "$USER" --property=Linger

Het tweede commando hoort Linger=yes te tonen. Met 'lingering' ingeschakeld start uw user manager bij het opstarten en blijft deze actief, ongeacht of u bent ingelogd. Zonder deze instelling biedt systemd-run --user geen voordeel ten opzichte van nohup.

systemd-run --scope is iets anders. Dit voert het commando uit op de voorgrond, gekoppeld aan uw terminal, en helpt hier dus niet.

Voor taken die u vaker dan eens uitvoert, schrijft u de unit weg in plaats van telkens een tijdelijke unit te typen.

Een permanente unit voor een taak die u opnieuw zult uitvoeren
[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.sh

Sla dit op als /etc/systemd/system/nightly-sync.service, voer sudo systemctl daemon-reload uit, start de unit vervolgens met sudo systemctl start nightly-sync en lees de status uit met journalctl -u nightly-sync. Voeg een bijbehorend .timer-bestand toe als de taak volgens een schema moet draaien in plaats van op verzoek.

Het schrijven van een systemd service-unit en bijbehorende timer behandelt het bestandsformaat en de syntax voor planningen volledig.

Waar de output naartoe gaat en waarom deze verdwijnt

De volgorde van omleidingen is van belang. > file 2>&1 wijst de standaarduitvoer naar het bestand en wijst vervolgens de standaardfout naar dezelfde locatie. 2>&1 > file doet dit in omgekeerde volgorde: de standaardfout blijft naar de terminal gaan, en de terminal is hetgeen dat op het punt staat te verdwijnen. Bash accepteert ook &> file voor beide stromen tegelijk.

De tweede verrassing is buffering. Wanneer de standaarduitvoer een terminal is, leegt de C-bibliotheek de buffer bij elke regel. Wanneer de standaarduitvoer een bestand is, schakelt deze over naar een blokbuffer van enkele kilobytes, waardoor tail -f ~/import.log minutenlang niets laat zien en de taak dood lijkt. Forceer regelgebaseerde buffering met stdbuf -oL ./import.sh > ~/import.log 2>&1, of gebruik de eigen schakeloptie van het programma, zoals python3 -u of grep --line-buffered.

Vermijd dit patroon:

nohup ./import.sh 2>&1 | tee ~/import.log &

nohup beschermt alleen import.sh en niets anders. tee is een afzonderlijk proces in dezelfde pijplijn en sterft nog steeds bij een hangup. import.sh schrijft vervolgens naar een pipe zonder lezer, waardoor het SIGPIPE ontvangt en stopt. Plaats de gehele pijplijn binnen setsid bash -c '...', of schrijf direct naar het bestand en voer tail -f uit zodra u opnieuw verbinding maakt.

Nog een detail specifiek voor rsync. --info=progress2 schrijft een stroom van carriage returns die er op een terminal correct uitziet, maar in een logbestand of in de journal verandert in één enorme regel. Gebruik voor een onbeheerde uitvoering liever --stats.

Waarom een taak die werkt in uw shell faalt onder systemd of cron

Uw interactieve shell leest /etc/profile, ~/.profile en ~/.bashrc, waardoor deze beschikt over uw PATH, uw version manager shims en uw geëxporteerde variabelen. Een systemd-unit leest geen van deze bestanden. Cron leest ze evenmin: op Debian en Ubuntu voert cron taken uit met SHELL=/bin/sh en PATH=/usr/bin:/bin.

Het symptoom onder systemd is dat systemctl status de melding (code=exited, status=203/EXEC) geeft. Dit betekent dat systemd het bestand helemaal niet kon uitvoeren, omdat het pad onjuist was of het bestand niet als uitvoerbaar is gemarkeerd. Onder cron is dit meestal command not found, dat wordt afgeleverd via lokale e-mail, of nergens wordt afgeleverd als er geen mailsysteem is geïnstalleerd.

Stel vast wat de omgeving is voordat u een uur besteedt aan gissen:

sudo systemd-run --collect --wait --unit=envtest /usr/bin/env
journalctl -u envtest --no-pager

Dit drukt de exacte omgeving af waarin uw taak zal draaien. Dicht vervolgens het gat. Gebruik absolute paden voor alles wat van uzelf is, omdat systemd een kale rsync oplost tegen een vaste lijst met systeempaden en nooit tegen de PATH van uw shell. Geef de variabelen die u nodig heeft door met -p Environment="KEY=value" op de opdrachtregel, of met EnvironmentFile=/etc/default/myjob in een unit-bestand. Wanneer een taak daadwerkelijk uw login-omgeving nodig heeft, voer deze dan uit als /bin/bash -lc 'my-command' en accepteer dat de taak nu afhankelijk is van uw dotfiles.

Wat een detached job nog steeds beëindigt

  • Een reboot. Niets in tmux blijft behouden na een herstart, omdat de server een gewoon proces is en de sessies zich in het werkgeheugen bevinden. Kernel-updates vereisen reboots; een taak die u niet eenvoudig opnieuw kunt opstarten, hoort daarom in een unit die u beheert met systemctl enable.
  • De out-of-memory killer. dmesg -T | grep -i 'killed process' toont dit aan, inclusief de naam van het proces dat werd beëindigd. Een grote import op een kleine VPS is een veelvoorkomend doelwit.
  • logind cleanup. Als /etc/systemd/logind.conf de waarde KillUserProcesses=yes heeft, worden uw achtergebleven processen beëindigd zodra uw laatste sessie eindigt; dit geldt ook voor de tmux-server. Controleer de huidige instelling met loginctl show --property=KillUserProcesses en stel uw gebruiker vrij met loginctl enable-linger "$USER".
  • Een volle schijf. De taak stopt omdat het logbestand waarnaar u omleidde het bestandssysteem heeft gevuld, niet omdat u de sessie verliet. Voer df -h uit voordat u het signaal de schuld geeft.

Een taak starten via SSH zonder verbonden te blijven

ssh vps 'sudo systemd-run --unit=import --collect /usr/local/bin/import.sh'

systemd-run keert direct terug zodra de unit is gestart, waardoor ook het ssh-commando afsluit en de taak geen verbinding meer heeft met de sessie die deze heeft opgestart. Dit is de schone methode.

De nohup-versie vereist meer aandacht:

ssh vps 'nohup ./import.sh > ~/import.log 2>&1 < /dev/null &'

Zonder de omleidingen lijkt dit proces te hangen. sshd houdt het kanaal open zolang een proces nog gebruikmaakt van de standaarduitvoer of standaardfout van het externe commando, en een taak op de achtergrond erft beide. Alleen nohup lost dit niet op, omdat nohup uitvoer alleen omleidt wanneer die uitvoer naar een terminal gaat; in dit geval is het een pipe terug naar uw client. Het toevoegen van < /dev/null sluit ook de invoerzijde. ssh -n voert dezelfde taak uit vanaf de clientzijde.

FAQ

Waarom stopt mijn commando wanneer de SSH-verbinding wordt verbroken?

De pty (pseudo-terminal) die uw sessie gebruikte, wordt vernietigd en de kernel stuurt SIGHUP naar de foreground-procesgroep op die terminal. De standaardactie voor SIGHUP is het beëindigen van het proces. Achtergrondtaken stoppen ook, omdat bash SIGHUP naar elke taak in zijn tabel stuurt voordat het afsluit. Een commando dat SIGHUP negeert, zoals een commando gestart met nohup, of een commando dat uw sessie nooit heeft gedeeld, zoals een systemd-unit, blijft ongewijzigd.

Is tmux of systemd-run beter voor een rsync van zes uur?

systemd-run. Een tmux-sessie is afhankelijk van een serverproces dat u zelf bent gestart; het eindigt dus bij de volgende reboot en is onzichtbaar voor iedereen die niet weet dat hij tmux ls moet uitvoeren. Het draaien van sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/ geeft u systemctl status bigsync voor de status en journalctl -u bigsync voor de output, die de volgende beheerder beide kan vinden zonder dat dit expliciet hoeft te worden vermeld. Gebruik tmux voor werk waarbij u het scherm moet bekijken en erin moet typen.

Hoe zie ik de output van een taak waarvan ik vergeten ben de output om te leiden?

Meestal kan dit niet, omdat die output naar een terminal ging die niet meer bestaat. Terwijl het proces nog draait, kunt u de geopende bestanden inspecteren met sudo ls -l /proc/<pid>/fd of de systeemaanroepen bekijken met sudo strace -p <pid>, maar de tekst die al is geschreven, is verloren. reptyr <pid> kan het proces naar een nieuwe terminal verplaatsen, en Ubuntu's kernel.yama.ptrace_scope = 1 betekent dat u sudo nodig heeft voor een proces dat niet uw eigen kindproces is. De gewoonte die dit alles voorkomt, is om aan het begin de output naar een bestand om te leiden en dat bestand te tail -f.

Overleeft een losgekoppelde tmux-sessie een reboot?

Nee. De tmux-server is een gewoon proces en de sessies zijn de status in het geheugen; een reboot beëindigt dus beide. Het proces sterft ook wanneer /etc/systemd/logind.conf de waarde KillUserProcesses=yes instelt en u uitlogt van uw laatste sessie, wat loginctl enable-linger "$USER" voorkomt. Voor werk dat na een reboot automatisch moet terugkeren, schrijft u een systemd-unit en systemctl enable deze.

Waarom draait mijn script wel in de shell, maar faalt het als systemd-unit?

Een unit leest geen /etc/profile of ~/.bashrc, dus het beschikt noch over uw PATH-toevoegingen, noch over uw geëxporteerde variabelen. systemctl status met de melding (code=exited, status=203/EXEC) betekent dat systemd het bestand helemaal niet kon uitvoeren; gebruik daarom een absoluut pad en controleer de executable-bit. Voer sudo systemd-run --collect --wait --unit=envtest /usr/bin/env uit, lees het terug met journalctl -u envtest, en u heeft de exacte omgeving die uw taak krijgt. Voeg wat ontbreekt toe met Environment= of EnvironmentFile=.

#ssh#tmux#nohup#systemd#long-running-jobs