Zombieplaag! – Waar komen ze vandaan en hoe bestrijd je ze?
- August 1, 2013
- 0
Zombies zijn processen die reeds zijn geëindigd, maar waarvan nog niet alle procesboekhouding door de kernel is opgeruimd. Je zou kunnen zeggen: deze processen zijn niet meer onder ons, maar hun geest waart nog rond. In dit artikel beschrijven we waarom de procesboekhouding van een geëindigd proces door de kernel wordt bewaard en wat de implicaties hiervan zijn voor de gezondheid van je systeem.
PROCESBEHEER
Om goed te kunnen begrijpen hoe zombies ontstaan, moeten we eerst weten hoe processen worden gecreëerd en hoe processen weer eindigen. In de levenscyclus van een proces spelen vier system calls een grote rol:
System call fork
Met deze system call kan een bestaand proces een nieuw proces creëren. Dat nieuwe proces (kind) wordt een nagenoeg identieke kloon van het bestaande proces (ouder), ook wat de procesboekhouding betreft. Natuurlijk krijgt het kind wel een uniek process-ID (pid).
System call exit
Door aanroep van deze system call geeft een proces aan te willen eindigen. Als enige parameter geeft de aanroeper een exit code mee waarmee hij naar de buitenwereld laat weten of alles goed is verlopen (waarde 0) of niet (waarde . 0).
System call wait
Deze system call gebruikt het ouderproces om te wachten op het eindigen (of een andere toestandswisseling) van één van zijn kindprocessen. Zodra een kindproces eindigt, komt het ouderproces los uit deze system call en deze krijgt daarmee het pid en de exit code van het geëindigde kindproces toegespeeld.
System call exec
Met deze system call kan een proces een andere executable file ‘inladen’. We noemen deze system call voor de volledigheid in dit rijtje; hij speelt verder geen specifieke rol bij het ontstaan van zombies.
Het gebruikelijke synchronisatiescenario tussen ouder- en kindproces verloopt zoals aangegeven in figuur 1 (in verticale richting verloopt de tijd). De ouder creëert een kind (fork) en zal kort daarna willen wachten op het einde van dat kind (de ouder blokkeert in system call wait). Het kind wordt geboren (fork), doet zijn ding en eindigt weer (exit), waarna de ouder wordt doorgestart. In de uitloop van de system call wait (aangeroepen door de ouder) worden het pid en de exit code uit de procesboekhouding van het kind gehaald, en overgedragen aan het ouderproces. Daarnaast worden ook statistische gegevens uit de procesboekhouding van het kind overgeheveld naar de procesboekhouding van de ouder; deze gegevens kunnen later door de ouder worden opgevraagd. Pas aan het einde van de system call wait wordt de procesboekhouding van het kind volledig verwijderd! Dit scenario vind je ook terug in de werking van de shell, die voor het uitvoeren van een commando een kind start (fork) en vervolgens wacht totdat het commando zijn werk heeft gedaan (wait). Daarna vraagt de shell door middel van een prompt om het volgende commando. Als je de commandoregel afsluit met een &-symbool, dan start de shell een , maar wacht hij niet op de afloop daarvan (onderdrukt de system call wait).
1. Het gebruikelijke scenario
VERWEESDE PROCESSEN
Een kindproces kan prima blijven functioneren als zijn ouder eindigt. Op het moment dat de ouder stopt, wordt het ouderschap van de verweesde kinderen altijd overgenomen door het init-proces (pid 1), zoals aangegeven in figuur 2. In de procesboekhouding van de verweesde kinderen wordt het ppid daadwerkelijk veranderd naar 1. Het init-proces zelf is niet op de hoogte van het feit dat hij (opnieuw) stiefouder is geworden. Natuurlijk kent het initproces de verplichtingen die het ouderschap met zich meebrengt, maar het zal nooit spontaan de system call wait aanroepen die het proces zal blokkeren totdat een kind eindigt. Immers, het init-proces moet aanspreekbaar blijven voor andere belangrijke taken binnen het systeem. Toch zal de system call wait voor ieder geëindigd kind moeten worden aangeroepen om uiteindelijk de procesboekhouding van dat kind te verwijderen. Gelukkig wordt een ouder ook op de hoogte gesteld van een geëindigd kind via een signal (SIGCHLD). De ouder kan dan reageren met een aanroep van de system call wait en weet dat deze aanroep niet blokkeert, omdat er al een geëindigd kind klaarstaat. Als het init-proces het pid dat de system call wait terugmeldt, herkent als dat van een eigen kind, dan zal hij de juiste acties ondernemen. Als het teruggemelde pid van een ondergeschoven weesproces blijkt, wordt het kind genegeerd. Ook in het laatste geval is de procesboekhouding wel netjes opgeruimd.
2. Verweesd scenario
DE ZOMBIES KOMEN
In figuur 3 vind je een derde scenario van het samenspel tussen ouder en kind: de ouder creëert een kind (fork), maar heeft vervolgens niet de behoefte om te wachten op de afloop van dat kind (wait). Op het moment dat het kindproces eindigt (exit), zal de kernel de gebruikelijke opschoning uitvoeren: open bestanden worden alsnog gesloten, alle geheugengebieden van het proces worden vrijgegeven et cetera. Een deel van de procesboekhouding blijft echter bestaan! Als de ouder in de (verre) toekomst de system call wait aanroept, dan moeten het pid en de exit code van het kind daarin nog bewaard zijn gebleven. In de periode tussen het eindigen van het kind (exit) en het opvragen van de kindgegevens door de ouder (wait), zal de procesboekhouding van het kind in de uitvoer van het commando ps zichtbaar zijn als een zombieproces met de toestand Z en de toevoeging <defunct> (letterlijk: overleden/ter ziele); zie Listing 1. In deze uitvoer zie je dat het geheugen van een zombie al is vrijgegeven (waarde 0 in kolom SZ). Ook het processorverbruik (kolom TIME) zal niet verder oplopen.
LISTING 1
$ ps -l F S UID PID PPID … SZ … TIME CMD
0 S 1049 12822 12821 2275 00:00:00 bash
0 S 1049 30469 12822 402 00:00:00 appx
1 Z 1049 30470 30469 0 00:00:00 appx <dfunct>
1 Z 1049 30471 30469 0 00:00:00 appx <defunct>
0 R 1049 30473 12822 1076 00:00:00 ps
3. Zombiescenario
ZIJN ZOMBIES SCHADELIJK?
Feitelijk eindigt ieder proces als zombie, omdat er altijd enige tijd verstrijkt tussen het einde van het kind (exit) en de doorstart van de ouder die op het kind wacht (wait). Echter, de zombie bestaat dan slechts een fractie van een seconde, waardoor je mazzel moet hebben om hem te zien. Als een ouder lang wacht met de system call wait of als een ouder domweg ‘vergeet’ voor ieder in het verleden gecreëerd kind een system call wait te doen, dan ontstaan zombies met een permanenter karakter. Zombies hebben echter geen nadelige invloed op de systeemprestaties: ze gebruiken geen geheugen (behalve enkele Kbytes voor de procesboekhouding) en ze gebruiken geen processortijd (er is geen code meer om uit te voeren). Toch kunnen grote hoeveelheden zombies in de weg gaan zitten als bepaalde drempelwaarden worden bereikt: Maximaal pid-nummer (default 32768) Als het totale aantal threads in het systeem de maximale pidwaarde bereikt (zie de file /proc/ sys/kernel/pid_max), dan kunnen geen nieuwe processen (threads) meer worden gestart. Iedere zombie houdt een pid in gebruik. Maximum aantal threads op systeemniveau Als het totale aantal threads in het systeem het maximale aantal toegestane threads bereikt (zie de file /proc/sys/kernel/threadsmax), dan mogen geen nieuwe processen (threads) meer worden gestart. Iedere zombie telt mee als een bestaande thread. Maximum aantal threads per gebruiker Als het totale aantal threads voor een gebruiker het maximale aantal toegestane threads voor die gebruiker bereikt (zie de uitvoer van het commando ulimit -Su), dan mag die gebruiker geen nieuwe processen (threads) meer starten. Deze restrictie geldt niet voor de root-gebruiker. Merk op dat de drie genoemde drempelwaarden betrekking hebben op het aantal threads, niet op het aantal processen. Ieder proces bestaat minstens uit één, maar mogelijk uit diverse threads, dus een limiet wordt vaak eerder bereikt dan je verwacht! En áls zo’n limiet wordt bereikt, zal je shell de melding No more processes geven bij het starten van een commando of kunnen applicaties crashen als ze een extra thread willen creëren.
ZOMBIEBESTRIJDING
Als bepaalde ouderprocessen structureel zombiekinderen laten bestaan, dan is er in ieder geval sprake van een programmeerfout die moet worden opgelost. Mochten de zombies in de tussentijd toch in de weg zitten, dan kun je overwegen om ze te ruimen. Het heeft echter geen zin om de zombie zelf te ‘killen’, want het gaat om een proces dat al is geëindigd. De enige remedie is het eindigen van het ouderproces (het desnoods ‘killen’) als dat vanuit het oogpunt van beschikbaarheid toelaatbaar is. Als het ouderproces eindigt, wordt het ouderschap van de zombies overgedragen aan het init-proces (zie figuur 2), dat alsnog het SIGCHLD-signal ontvangt en daarop reageert met een system call wait per zombie.
MYSTICITEIT
Mocht je incidenteel een (langdurige) zombie ontwaren op je systeem, dan is dat geen ramp. Zombies zijn zelfs niet altijd te voorkomen. Maar zodra er permanent hordes zombies verschijnen, ga dan na wie de ouder is en voer een goed gesprek met de verantwoordelijke programmeur. Zo kun je mysticiteit op je systeem verder reduceren. – alle 3 de afbeeldingen (die met de pijlen) opnieuw tekenen in Illustrator. Dan ziet het er strakker uit. Ook zijn de lettertypes nu totaal verschillend, doordat ze allemaal anders uitgerekt zijn. Beetje netjes en strak maken dus. – Ik heb ook het idee dat de listing op pagina 21 niet helemaal goed wordt weergegeven. Ziet die er wellicht beter uit als we de listing verdelen over 2 kolommen (zoals we ook gedaan hebben bij Java)? Dit laat ik graag aan jou over.
Gerlof Langeveld werkt al zeventien jaar als docent/ consultant bij AT Computing. Hij doceert onder andere de masterclass ‘Linux performance analyse en tuning’, waarin performance-aspecten van procesbeheer aan de orde komen, en de cursus ‘Linux system programming’, die dieper ingaat op het gebruik van (genoemde) system calls.