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

systemd Type instellen: simple, forking of notify?

Uw unit toont status active terwijl het proces is gestopt? Leer hoe Type= simple, forking, notify en oneshot de main PID bepalen en voorkom foutieve monitoring door systemd.

Waarom rapporteert systemd een unit als actief terwijl het proces is beëindigd

Een systemd-service-unit blijft active zolang het proces dat systemd als hoofdproces beschouwt in leven is, en de Type= in de [Service]-sectie bepaalt welk proces dat is. Als u de verkeerde waarde kiest, eindigt systemd met het monitoren van een shell-wrapper of een kortstondig ouderproces, terwijl de daemon die u wilt beheren binnen dezelfde unit sterft. De unit rapporteert correct over het proces dat het moet monitoren.

Het wijzigen van het herstartbeleid helpt hier niet. Restart= treedt in werking wanneer het hoofdproces wordt beëindigd, dus Restart=always wordt nooit geactiveerd zolang de hoofd-PID (process identifier) toebehoort aan iets dat nog steeds draait. Corrigeer eerst Type=. Wat systemd doet nadat het hoofdproces daadwerkelijk is beëindigd, is een afzonderlijke beslissing, die wordt behandeld in de handleiding voor Restart= en RestartSec=.

Wat bepaalt Type= precies

Elke Type=-waarde beantwoordt tegelijkertijd twee vragen. Wanneer mag systemd deze unit als gestart beschouwen, en welk proces is het hoofdproces.

Het eerste antwoord regelt de volgorde. Een unit die de uwe noemt in After= wacht totdat systemd de uwe als gestart markeert. Een Type= die te vroeg "gestart" rapporteert, zorgt ervoor dat afhankelijke units draaien voordat uw service ze kan beantwoorden.

Het tweede antwoord regelt het toezicht. systemd plaatst elk proces dat een unit start in een cgroup (control group), een kernel-functie die processen groepeert zodat ze gezamenlijk kunnen worden beperkt en beëindigd. De cgroup is de manier waarop systemctl stop opruimt: KillMode= staat standaard op control-group, waardoor het stoppen van een unit een signaal stuurt naar elk proces daarbinnen. De hoofd-PID is specifieker. Dit is het enige proces waarvan het beëindigen de unit stopt, en waarvan de exit-status het resultaat van de unit wordt. Het lezen van de cgroup alsof het de hoofd-PID is, is waar de verwarring begint.

Type=simple rapporteert dat de service is gestart voordat het binaire bestand wordt uitgevoerd

Type=simple is de standaardinstelling wanneer ExecStart= is ingesteld en noch Type=, noch BusName= aanwezig is. systemd maakt het proces aan, beschouwt de unit direct als gestart en behandelt dat proces als de hoofd-PID. Vervolg-units beginnen onmiddellijk, nog voordat het binaire bestand van de service is uitgevoerd.

Dat laatste detail verklaart een veelvoorkomende verrassing. Een typefout in het ExecStart=-pad resulteert nog steeds in een starttaak die slaagt, waarna de foutmelding een moment later volgt wanneer de uitvoering mislukt. systemd registreert dat geval met exitcode 203, die in de eigen tabel EXEC wordt genoemd en wordt gedefinieerd als een fout bij het uitvoeren van het binaire servicebestand. Het feit dat systemctl start zonder foutmelding terugkeert, bewijst dus niet dat uw binaire bestand bestaat.

Gebruik simple voor een programma dat op de voorgrond blijft en zichzelf nooit naar de achtergrond verplaatst. Dit is van toepassing op de meeste moderne daemons en vrijwel alles wat u zelf schrijft.

Type=exec wacht tot het programma daadwerkelijk is gestart

Type=exec is simple met één extra stap. systemd beschouwt de unit pas als gestart nadat zowel de fork als de uitvoering van het binaire bestand zijn geslaagd. Een ontbrekend binair bestand of een User= die niet kan worden opgelost, zorgt er nu voor dat de starttaak zelf faalt, in plaats van dat deze succes rapporteert en even later geruisloos stopt.

Type=exec is geïntroduceerd in systemd 240, dus elke huidige serverdistributie beschikt hierover. Ubuntu 24.04 levert systemd 255 en Debian 13 levert systemd 257, per augustus 2026. Controleer uw versie met systemctl --version.

De prijs hiervoor is één extra synchronisatiestap bij het opstarten. Het voordeel is een eerlijke exit-status van systemctl start. Voor een programma op de voorgrond heeft exec de voorkeur boven simple.

Type=forking, en hoe de main PID verloren gaat

Type=forking vertelt systemd dat het proces in ExecStart= een child-proces zal forken en daarna bewust zal afsluiten. systemd wacht tot dat eerste proces is afgesloten en markeert de unit pas daarna als gestart. Het achtergebleven child-proces is de daemon.

De moeilijkheid is de identiteit. Het proces dat systemd startte is verdwenen, dus moet systemd bepalen welke overlevende het hoofdproces is. Stel PIDFile= in op het bestand dat de daemon schrijft, normaal gesproken een pad onder /run, en systemd leest de PID daaruit. systemd controleert ook of de PID in dat bestand verwijst naar een proces dat al bij deze service hoort, zodat een verouderd bestand dat naar een ongerelateerd proces wijst, wordt geweigerd in plaats van vertrouwd.

Zonder PIDFile= is GuessMainPID= van toepassing, en deze staat standaard op yes. De gok is alleen betrouwbaar wanneer de service zich stabiliseert in één enkel proces. De handleiding stelt de beperking duidelijk: als de daemon uit meer dan één proces bestaat, kan de gok onjuist zijn en stopt de foutdetectie met werken. Een unit kan ook eindigen met een main PID van 0, wat betekent dat systemd helemaal niets heeft om te superviseren.

De meeste daemons die forken hebben ook een schakelaar die ze op de voorgrond houdt. Gebruik die schakelaar met Type=exec en verwijder de regel PIDFile=. Minder bewegende delen betekent minder manieren om de PID te verliezen.

Type=oneshot voor taken die eindigen

Type=oneshot verwacht dat het proces wordt uitgevoerd en wordt afgesloten. systemd markeert de unit pas als gestart nadat deze is afgesloten, wat oneshot de juiste vorm maakt voor alles waar een andere unit op moet wachten. Het is ook de impliciete standaard wanneer een unit noch Type=, noch ExecStart= specificeert.

Twee gedragingen zijn specifiek voor oneshot. Het is het enige type dat meer dan één ExecStart=-regel accepteert, en deze regels worden in volgorde uitgevoerd. De start-timeout is standaard ook uitgeschakeld, dus een oneshot die blijft hangen wacht voor eeuwig, tenzij u zelf TimeoutStartSec= instelt.

Nadat het proces is afgesloten, keert de unit terug naar de status inactief. RemainAfterExit=yes houdt deze active zonder dat er een proces draait. Dat is de bewuste versie van het symptoom bovenaan deze pagina, en het is correct wanneer de taak van de unit was om een status achter te laten in plaats van iets draaiende te houden: het laden van een firewall-regelset of het opstarten van een container-stack. Het is het patroon achter een Docker Compose-stack die terugkeert na een herstart, waarbij de unit het compose-commando uitvoert, afsluit en actief blijft omdat de containers die het startte langer leven dan de unit zelf. Een oneshot-unit is ook wat een planning activeert, wat de andere helft is van het uitvoeren van een taak op een systemd-timer in plaats van cron.

Type=notify laat de service aangeven wanneer deze gereed is

Type=notify verlegt de beslissing naar de service. systemd houdt de starttaak open totdat het proces READY=1 verstuurt via een Unix-socket waarvan het pad in de omgevingsvariabele NOTIFY_SOCKET staat. De C-interface is sd_notify(3) en veel servers ondersteunen deze reeds.

Dit is het nauwkeurige antwoord op de vraag "is het gestart". simple en exec rapporteren dat de service is gestart voordat deze de configuratie heeft gelezen of de listening socket heeft geopend, waardoor een afhankelijke unit te vroeg kan starten en de eerste verbinding kan mislukken. notify rapporteert dat de service is gestart op het moment dat de service zelf aangeeft gereed te zijn.

systemd accepteert dit bericht alleen van het hoofdproces, wat NotifyAccess=main betekent, en Type=notify impliceert dit. Als het bericht afkomstig is van een child-proces of een helper, stel dan NotifyAccess=all in. Een shellscript kan systemd-notify --ready aanroepen, maar dit draait als een afzonderlijk proces van korte duur, waardoor NotifyAccess=all nodig is en systemd mogelijk geen bericht kan toewijzen waarvan de afzender al is afgesloten. Een service die het protocol zelf spreekt, is betrouwbaarder.

Twee gerelateerde instellingen zijn het vermelden waard. Type=notify-reload, beschikbaar sinds systemd 253, breidt dezelfde handshake uit naar herlaadacties, zodat systemctl reload pas terugkeert wanneer de service meldt dat het herladen is voltooid in plaats van direct na het verzenden van het signaal. WatchdogSec= vraagt een notify-service om met een bepaald interval een keep-alive-bericht te sturen, waarbij systemd een gemiste deadline als een fout beschouwt.

Type=dbus en Type=idle

Type=dbus wacht totdat de service een naam registreert op D-Bus, de message bus die systeem- en desktopservices gebruiken om met elkaar te communiceren. Dit vereist BusName= en wordt de standaard zodra BusName= is ingesteld. Gebruik dit alleen voor een service die daadwerkelijk een busnaam registreert.

Type=idle gedraagt zich als simple, maar stelt het uitvoeren van het programma uit totdat taken in de wachtrij zijn afgehandeld, met een limiet van vijf seconden. Dit is bedoeld om te voorkomen dat console-uitvoer tijdens het opstarten vermengd raakt met statusberichten. Het is geen tool voor het bepalen van de volgorde en hoort niet thuis bij een normale service.

Waarom een wrapper-script systemd op de verkeerde PID laat vastlopen

Dit is de structuur die het oorspronkelijke symptoom veroorzaakt.

[Service]
Type=simple
ExecStart=/opt/app/run.sh
#!/bin/bash
export APP_ENV=production
/opt/app/bin/server --config /etc/app.yaml &
/opt/app/bin/exporter --port 9101

systemd registreert de shell als de hoofd-PID. De shell blijft actief terwijl exporter op de voorgrond draait. Als server crasht, merkt de shell dit niet op. De hoofd-PID blijft dus actief, de unit is nog steeds active en Restart= heeft niets om op te reageren. Beide processen bevinden zich de gehele tijd in de cgroup van de unit, waardoor systemctl stop nog steeds correct opruimt. Het toezicht is verbroken, niet de opschoning.

De oplossing hangt af van het aantal langlopende processen dat de unit daadwerkelijk bevat.

Als er één proces is, vervang de shell dan door dat proces.

#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yaml

exec vervangt de shell door het opgegeven programma en behoudt dezelfde PID. De PID die systemd heeft geregistreerd, behoort nu dus tot de daemon. Nog beter is het om de wrapper te verwijderen. Environment= en EnvironmentFile= dragen de variabelen over en ExecStartPre= voert de configuratiestap uit, zodat systemd de daemon direct kan starten en de PID door de constructie zelf kent.

Als er twee processen zijn, vertegenwoordigt geen enkele PID de unit. Splits ze in twee units en orden ze met After= en Wants=. Eén unit per proces is de configuratie die systemd goed kan bewaken, en het is de enige manier waarop elk proces zijn eigen herstartgedrag krijgt.

Wat wijzigingen in ExitType=cgroup inhouden

ExitType= werd toegevoegd in systemd 250. De standaardwaarde is main: de unit wordt als gestopt beschouwd zodra het hoofdproces wordt beëindigd. Met ExitType=cgroup wordt de unit als actief beschouwd zolang er een proces in de bijbehorende cgroup actief is.

[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcher

Dit lost een specifiek probleem op. Een launcher die het eigenlijke werk start en vervolgens afsluit, zou onder ExitType=main door systemd als gestopt worden beschouwd, waarna de overgebleven processen worden beëindigd. Met ExitType=cgroup volgt de unit de gehele groep.

Wees duidelijk over wat dit niet oplost. ExitType=cgroup houdt een unit actief zolang er ten minste één proces leeft; een unit die twee daemons beheert, blijft dus actief nadat een van beide is gestopt. Het lost het launcher-scenario op. Het verandert een unit niet in een supervisor van meerdere onafhankelijke processen. ExitType= kan bovendien niet worden gecombineerd met Type=oneshot.

De cgroup is ook de plek waar resource-accounting plaatsvindt. Limieten zoals MemoryMax= en CPUQuota= zijn daarom van toepassing op elk proces dat door de unit is gestart, ongeacht wat Type= zegt over het hoofd-PID. Dat aspect wordt behandeld in het beperken van het geheugen- en CPU-gebruik van een service met systemd.

Hoe u het proces vindt dat systemd daadwerkelijk in de gaten houdt

Doorloop deze stappen op de unit die u aan het debuggen bent, in de juiste volgorde. Lees wat systemd heeft geladen, bekijk wat het volgt en vergelijk dit vervolgens met de procestabel.

systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.service

systemctl cat toont het unit-bestand samen met elke drop-in die erop van toepassing is, zodat u leest wat systemd daadwerkelijk heeft geladen in plaats van het bestand waarvan u denkt dat u het heeft bewerkt. systemctl show toont de effectieve waarden, inclusief de standaardinstellingen die u nooit expliciet heeft genoteerd. Noteer de waarde van MainPID voordat u verdergaat.

systemd-cgls --unit=app.service
ps -o pid,ppid,stat,etime,args -p "$(systemctl show -p MainPID --value app.service)"

systemd-cgls somt elk proces in de cgroup van de unit op. De regel ps beschrijft het specifieke proces dat systemd beheert. Lees deze twee samen. Een MainPID van 0 betekent dat systemd geen proces heeft om te monitoren. Een MainPID die verwijst naar een shell terwijl de cgroup ook uw daemon bevat, wijst op het eerder genoemde wrapper-scenario. Een cgroup met meer processen dan verwacht betekent dat er een launcher of een forking daemon bij betrokken is.

systemctl status app.service
journalctl -u app.service -b

systemctl status toont de statusregel en de cgroup-boom tegelijkertijd, waardoor deze vaak beide vragen in één keer beantwoordt. journalctl -u, beperkt tot de huidige boot met -b, toont de start- en stopgebeurtenissen die systemd voor de unit heeft geregistreerd, inclusief de exitcodes die het heeft waargenomen. Als de daemon naar een eigen logbestand schrijft in plaats van naar de journal, lees dat bestand dan ook, aangezien systemd alleen kan registreren wat het bereikt.

Wanneer u Type= wijzigt, moet u de configuratie herladen en de service herstarten.

systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.service

systemd-analyze verify parseert het bestand en rapporteert instellingen die niet geaccepteerd kunnen worden. daemon-reload zorgt ervoor dat systemd de unit-bestanden opnieuw inleest vanaf de schijf. Een gewijzigde Type= wordt niet toegepast op een unit die al draait, dus een herstart is vereist, niet optioneel.

Test vervolgens de wijziging. Pak de PID van het proces dat voor u van belang is uit systemd-cgls en beëindig dit proces. Voer direct daarna systemctl is-active app.service uit. Als Type= correct is, verlaat de unit de actieve status. Als de unit actief blijft, houdt systemd nog steeds iets anders in de gaten.

Welk systemd service Type= moet u gebruiken

  • Een programma dat op de voorgrond blijft draaien: Type=exec.
  • Een programma dat notificaties voor gereedheid ondersteunt: Type=notify, en notify-reload als het ook herlaadacties bevestigt.
  • Een daemon die zichzelf naar de achtergrond verplaatst: Type=forking met PIDFile=, of de bijbehorende foreground-schakelaar met Type=exec.
  • Een script dat werk uitvoert en afsluit: Type=oneshot, plus RemainAfterExit=yes wanneer het doel was om een status achter te laten.
  • Een launcher die afsluit terwijl de onderliggende processen blijven draaien: Type=simple met ExitType=cgroup.

Als u niet zeker weet welk type een daemon van derden vereist, lees dan eerst het meegeleverde unit-bestand. Het uitvoeren van systemctl cat op een unit die door de distributie is meegeleverd, toont het Type= dat de upstream-ontwikkelaar heeft gekozen; die keuze is door meer mensen getest dan uw eigen configuratie.

FAQ

Waarom blijft mijn systemd-unit actief als het proces is gestopt?

Omdat het proces dat systemd als hoofdproces beschouwt, nog steeds actief is. systemd controleert één PID per service, gekozen op basis van Type=, in plaats van elk proces in de cgroup van de unit. Een wrapper-script gestart met Type=simple is hiervan meestal de oorzaak: de shell is het hoofd-PID, waardoor de unit actief blijft wanneer een daemon die door de shell op de achtergrond is gestart, wordt afgesloten. Voer systemctl show -p MainPID app.service uit, bekijk vervolgens de cgroup van de unit met systemd-cgls --unit=app.service en vergelijk beide.

Wat is het verschil tussen Type=simple en Type=exec?

Type=simple beschouwt de unit als gestart zodra systemd het proces heeft aangemaakt, nog voordat het binaire bestand is uitgevoerd. Een onjuist pad in ExecStart= resulteert daarom in een succesvolle starttaak, gevolgd door een foutmelding. Type=exec wacht tot de uitvoering is geslaagd, zodat een fout direct door de starttaak zelf wordt gerapporteerd. Beide behandelen hetzelfde proces als het hoofd-PID. Type=exec vereist systemd 240 of nieuwer.

Heb ik nog steeds PIDFile= nodig bij Type=forking?

Ja, wanneer de daemon er een schrijft. Zonder dit bestand valt systemd terug op GuessMainPID=; dit is een schatting en is alleen betrouwbaar voor een service die uit één enkel proces bestaat. Wanneer de schatting onjuist of onmogelijk is, werken foutdetectie en automatisch herstarten niet meer voor die unit. Wijs PIDFile= naar het exacte pad waar de daemon naar schrijft, meestal onder /run.

Wanneer moet ik RemainAfterExit=yes gebruiken?

Wanneer het doel van de unit was om de systeemstatus te wijzigen in plaats van een proces draaiende te houden. Een Type=oneshot-unit die firewallregels laadt of een containerstack start, wordt afgesloten zodra het werk is voltooid. Zonder RemainAfterExit=yes wordt de unit inactief, waardoor systemctl stop niets meer heeft om te stoppen en er geen mogelijkheid is om een ExecStop=-opschoonactie uit te voeren. Met deze instelling blijft de unit actief zonder processen, wat in dit geval de bedoeling is.

Vereist het wijzigen van Type= een daemon-reload?

Ja, en ook een herstart van de unit. systemctl daemon-reload zorgt ervoor dat systemd de unit-bestanden op de schijf opnieuw inleest, maar een draaiend exemplaar behoudt de Type= waarmee het is gestart. Voer sudo systemctl daemon-reload uit en vervolgens sudo systemctl restart app.service voordat u gaat testen; anders observeert u nog steeds het oude supervisiegedrag.