Habt Ihr Euch schon einmal gefragt, was eigentlich genau passiert, wenn man unter Linux ein Programm startet? Bestimmt habt Ihr das, und vielleicht ist Euch auch die Antwort nicht g?nzlich unbekannt: Es entsteht ein neuer Prozess. Doch ehe nun die J?nger aus Redmond einwenden werden, da? sich solches auch auf ihrer Lieblingsplattform zu ereignen pflegt, lasset uns also beweisen, da? GNU bisweilen doch UNIX ist und gemeinsam den heiligen Schrein auftun, welcher Linux in Sachen Prozessverwaltung mit all den anderen Derivaten in geschwisterlicher Eintracht den Microsoft’schen Ma?st?ben entr?ckt. Zugegeben, so richtig deutlich wird das wohl erst im zweiten Teil dieser Serie werden. alls werde ich mich redlich bem?hen, Euch m?glichst umfassend aber dennoch halbwegs anschaulich in eine Materie einzuweihen, welche selbst vielen Power-Usern noch als exklusives Insiderwissen gilt. Da? dies beileibe nicht sein muss, soll im folgenden gezeigt werden.
von Harald Stoiber
Zum Prozessbegriff
Kehren wir zum anfangs erw?hnten Programmstart zur?ck. Die unmittelbare Folge desselben kennen wir inzwischen – was aber ist nun dieser Prozess, der da entstehen soll, genau? Er ist streng genommen nichts als ein Verwaltungsrahmen, in den ein Programm oder ein Teil davon seitens des Betriebssystems eingebettet ist. F?r jeden Prozess gibt es eine eindeutige Kennung, die sogenannte Process Identification (kurz PID). Linux verwendet zu diesem Zweck ausschlie?lich positive 32-Bit-Zahlen (null ausgenommen), wobei PID Nummer 1 einem Systemprozess namens “init” vorbehalten ist – dessen Beschreibung hier allerdings zu weit f?hrt. Jedem Prozess sind bestimmte Ressourcen zugeordnet, und er ist auch der Rahmen, in welchem penibel daf?r gesorgt wird, dass bei dessen Aufl?sung – sprich: Prozessende – eben diese Ressourcen wieder zur Nutzung durch andere Prozesse freigegeben werden. Was auch unter Linux geschehen mag, es geschieht stets im Rahmen eines Prozesses. Aus der Kernel-Perspektive ist es daher niemals die CPU-Instruktion an der Adresse 4711, die 5 KB Hauptspeicher anfordert, sondern immer der Prozess, in dessen Kontext diese Instruktion gerade abl?uft. Wenn hier von Kontexten die Rede ist, dann verwundert gewiss nicht der Umstand, dass es beim internen Wechsel von Prozess A zu Prozess B tats?chlich zu einem sogenannten Context-Switching kommt. Ich werde in Teil 3 etwas ausf?hrlicher darauf eingehen, was den Kontext eines Prozesses ausmacht. Momentan – so lohnend und aufschlussreich dergleichen auch sein mag – soll uns jedoch ein ganz anderer Aspekt der Prozessverwaltung wesentlich mehr interessieren. Es geht um die Genesis einer PID und damit um die Frage: Wie entsteht ein neuer Prozess?
Lasst uns Prozesse machen in unserem Bild!
Wem das eine Nuance zu biblisch klingt, der sei unbesorgt: Ich w?hlte diesen Titel nicht aus schierer Religiosit?t analog zum ersten Buch Mose. Vielmehr veranschaulicht er n?mlich ausgesprochen deutlich, wie unter Linux neue Prozesse das Licht des Adressraums erblicken.
Ein Prozess erzeugt einen anderen Prozess ?hnlich wie eine Am?be ihre Zellteilung vollf?hrt. Die Am?be repliziert ihren DNA-Strang, ein Prozess hingegen seine Ablaufumgebung. Einzelheiten zu letzterer werde ich – wie gesagt – sp?ter nachreichen. Verweilen wir noch etwas bei der eigentlichen Entstehung des neuen Prozesses.
Ob ein Prozess seinen Stammhalter wohl mit dem selben Stolz be?ugt, wie dies auch wir Menschen tun? Auch wenn ich soeben zu scherzen pflegte, hat das Anlegen von Prozessen mit einer biologischen Geburt doch etliches gemeinsam – so vieles schlie?lich, dass man tats?chlich vom Parent- beziehungsweise Child-Proze? spricht. Sie gleichen einander wie ein Ei dem anderen und unterscheiden sich im Grunde nur darin, von wem sie abstammen. Dies wird f?r jeden Prozess in der sogenannten Parent-PID festgehalten. Pedanten m?gen mir bitte nachsehen, dass ich leichtfertig von Gleichheit spreche und die Abweichungen bei “pending signals” und Dateisperren hier nicht n?her ausf?hren werde. Auf jeden Fall kann sich kein Prozess von ganz alleine teilen, sondern er ben?tigt dazu die Geburtshelferin namens Gabel
Die Geburtshelferin namens Gabel
Das meinte ich ?brigens ernst! F?r die wundersame Prozessvermehrung zeichnet ein Systemaufruf namens “fork” verantwortlich – welchen man auf Deutsch am besten als “sich verzweigen” ?bersetzt. Bemerkenswert ist hierbei, dass “fork” zwar nur einen Aufrufer hat, daf?r aber nach seinem Ende gleich zu zwei Prozessen zur?ckkehrt – und zwar parallel. Der Child-Proze? wird dabei sehr wohl “im Bild” des Parent erzeugt, wie uns die Metapher aus dem vorigen Kapitel lehrte – anschlie?end jedoch sind beide im Ablauf von einander unabh?ngig. Der Vollst?ndigkeit halber m?chte ich noch erw?hnen, dass der neue Prozess selbstverst?ndlich auch seine eigene PID bekommen hat – aber vermutlich dachtet Ihr Euch das ohnehin bereits.
Um die Funktionsweise von “fork” soll es dann in der n?chsten Folge gehen.
Wenn schlie?lich nach der Verdoppelung den beiden Prozessen die Ablaufkontrolle erstmals beziehungsweise abermals ?bertragen wird, so erfolgt dies jeweils unmittelbar nach jener Anweisung, die den Aufruf von “fork” bewirkte. Denkt daran: Parent und Child liegt noch immer dasselbe Programm zugrunde! Wie wei? ich nun als Proze?, ob ich das Kind bin oder nicht? Erinnert Ihr Euch daran, da? eine g?ltige PID immer gr??er als null sein mu?? Das ist n?mlich auch der Trick an der ganzen Sache: Als Child bekommt man von “fork” den Wert null zur?ck, w?hrend der Parent-Proze? einen Wert gr??er als null erh?lt, welcher nichts anderes ist als die PID des soeben erzeugten Child-Prozesses. Auch ist f?r den Fall vorgesorgt, da? es aus Ressourcenmangel nicht gelang, einen neuen Proze? zu erstellen. Um dem Parent-Proze? die Fehlgeburt zu verk?nden, ?bermittelt ihm “fork” statt der PID einen negativen Wert. Solltet Ihr schon einmal einen einschl?gigen C-Quelltext gelesen haben, dann wi?t Ihr nun endlich, was die obligatorische Abfrage nach dem “fork” zu bedeuten hat. Im Zuge der Gabelung wird der neue Proze? unter anderem mit einer ebenso exakten wie vollst?ndigen Kopie jener Speicherbereiche versehen, die sein Hervorbringer belegte, als er “fork” aufrief. Ihr findet das eine unertr?gliche Speicherverschwendung? Ihr denkt, da? jedes “fork” daher entsprechend langsam sein mu?? Nun, fr?her war es das auch. Doch ersannen findige K?pfe alsbald die L?sung…
Das Copy-on-Write-Verfahren
Auch wenn Euch die folgenden Erl?uterungen vielleicht etwas abschrecken m?gen, ist das Prinzip trotzdem denkbar einfach: Unmittelbar nach dem “fork” teilen sich beide Prozesse noch immer einen einzigen Adre?raum. Lediglich die Verwaltungsstrukturen f?r selbigen wurden dupliziert. Alle beteiligten Speicherbereiche werden in eben diesen Verwaltungsstrukturen f?r beide Prozesse als schreibgesch?tzt gekennzeichnet. Was geschieht nun, wenn entweder Parent oder Child auf den ihnen zugeordneten Speicher etwas schreiben wollen. Dazu mu? ich etwas weiter ausholen:
Die Copy-on-Write-Methode bedient sich eines Leistungsmerkmals moderner Prozessoren, welches auf der Intel-Plattform mit dem 80386 Einzug hielt. Gemeint ist die virtuelle Speicherverwaltung. Damit wurde es zum ersten Mal m?glich, mehrere Adre?r?ume neben einander zu halten, wobei deren logische Speicherbelegung aber auf jeweils beliebige physische Adressen abgebildet werden kann. So kommt es beispielsweise, da? zwei Prozesse ?blicherweise nicht auf denselben physischen (=linearen) Speicherplatz zugreifen, auch wenn es sich aus Programmsicht in beiden F?llen um ein und dieselbe Adresse handelt. Man spricht hier folgerichtig von getrennten Adre?r?umen.
Wie arbeitet Copy-on-Write?
In vorigen Abschnitt haben wir uns mit getrennten Adre?r?umen besch?ftigt. Im Falle des Copy-on-Write ist es ja genau umgekehrt: Der gesamte Adre?raum ?berlappt sich f?r Parent und Child v?llig, wobei Schreibzugriffe der beiden schon auf Prozessorebene unterbunden werden. Was also passiert, wenn einer der Beteiligten nun doch schreiben will? Es kommt zu einer Speicherverletzung – einer Fehlerbedingung, von der ein Unterprogramm des Linux-Kernels unterrichtet wird. Diese Kernel-Routine dupliziert nun den betroffenen Speicherbereich an einer anderen physischen Adresse und sorgt daf?r, da? die vom schreibenden Proze? verwendete logische Adresse auf den neuen Lagerplatz der Daten verweist. Die Kopie des vormals schreibgesch?tzten Speichers unterliegt anschlie?end ?brigens keiner Zugriffsbeschr?nkung mehr, so da? nach der Systemintervention der angeforderte Schreibvorgang wie geplant ?ber die B?hne geht. Auch dem anderen Proze? ist seitens der Adressierungslogik wieder jeder Zugriff erlaubt – allerdings bezieht er sich nach wie vor auf den urspr?nglichen physischen Speicherplatz.
Wem jetzt schon ordentlich der Kopf raucht, der labe sich an folgender popul?rwissenschaftlicher Vereinfachung meiner letzten Ausf?hrungen: Fr?her hat “fork” immer den gesamten Proze?speicher kopiert, w?hrend dies heute nur mehr f?r jene Bereiche geschieht, welche f?r beide Prozesse tats?chlich unterschiedlich sind – und zwar wirklich erst, sobald sie f?r beide Prozesse unterschiedlich werden. Neben der besseren Performance von “fork” selbst schl?gt au?erdem noch die effizientere Speichernutzung wohltuend zu Buche – eigentlich eine gute Sache, findet Ihr nicht?
In der n?chsten und letzten Folge soll es dann um Ablaufumgebungen gehen und der Frage nachgegangen werden, wie eigentlich ein neues Programms gestartet werden kann.
Was umfasst ein Prozess unter Linux?
Jeder Linux-Prozess besteht aus:
- Den aktuellen Inhalten aller CPU-Register Auf Intel’s Prozessoren geh?ren dazu neben allen sonstigen Registern auch die Schattenregister, die f?r jedes Segmentregister dessen vollst?ndigen Deskriptor speichern. Auch die Startadresse des Seitenverzeichnisses ist Teil des Prozesskontext. Weiters mit von der Partie ist nat?rlich auch der Befehlsz?hler, welcher die Stelle markiert, an der sich die Programmausf?hrung gerade befindet.
- Dem momentanen Adressraum Der komplette Umfang des Seitenverzeichnisses samt untergeordneter Seitentabellen wird ebenso je Prozess getrennt gespeichert und auch verwaltet.
- Seiner eigenen Identifikation Im 2.4er-Kernel gilt die PID zwar immer noch als eindeutig unter den Prozessen, aber im Rahmen mancher Systemfunktionen (wie beispielsweise getpid) ist an die Stelle der alten PID nun die Thread-Group-ID getreten. Letztere soll auch unter Linux das unter anderem von Solaris her bekannte Konzept der ganzheitlichen PID erm?glichen, deren einzelne Threads dieser streng untergeordnet sind. Der Prozess als Thread-Group – die Zeiten ?ndern sich.
- Allen offenen File-Deskriptoren Dazu geh?ren neben Handles auf Dateien auch Ger?te, Pipes oder Sockets. Manche dieser Deskriptoren tragen m?glicherweise das Close-on-Exec-Attribut, aber dazu sp?ter etwas mehr.
- S?mtlichen bestehenden Dateisperren
- Den Dateisystemparametern Diese umfassen das aktuelle Root-Directory sowie die umask-Einstellung.
- Den momentanen Resource-Limits Hierunter ist alles zu verstehen, was durch setrlimit bzw. sein Shell-?quivalent ulimit konfigurierbar ist – max. open files, core limit…
- Allf?llige Memory-Mappings Hier geht es haupts?chlich um den Status von Shared-Memory bzw. um vom Prozess verwendete Shared-Object-Libraries.
- Die aktuellen Signal-Parameter Dies betrifft alle Signal-Handler und die Einstellung, welche Signals gegenw?rtig maskiert sind.
Wie wechselt man das Programm?
Nach s?mtlichen Erkl?rungen zu Prozessen, deren Entstehung und deren Eigenschaften soll es jetzt um die allererste Grundfrage der Shell-Programmierung gehen. Es ist ja durchwegs ganz nett, von ein und demselben Programm tausende parallel ablaufende Kopien zu machen – aber wie startet man eigentlich ein neues Programm? Im Gegensatz zu beispielsweise Windows wird unter UNIX im allgemeinen, und Linux im speziellen, beim Starten eines Programms das Anlegen des neuen Prozesses und das Laden des Executable-Image in wirklich diese beiden Schritte unterteilt. Der neue Prozess ensteht durch fork, w?hrend das Image durch einen Systemaufruf aus der exec-Familie (z.B. execve) im Speicher etabliert wird. Der Ablauf ist also: fork, dann im Child-Prozess exec aufrufen. Mit exec wird der gesamte Adressraum gegen einen ausgewechselt, der gerade einmal das neu geladene Programm beherbergt. Man beachte hierbei, dass der Adressraum ein Konstrukt ist, das nur den gegenw?rtigen Prozess, nicht aber seinen Parent-Prozess betrifft. Nicht allein das Image des neuen Programmes wird im frisch erzeugten Adressraum verankert. Zudem ruft exec auch den dynamischen Linker (oder im Falle eines ELF-Image den im Segment “PT_INTERP” genannten Interpreter) auf, um allf?llige Shared-Libraries ebenfalls in jenen Adressraum einzublenden. ?berdies werden alle Datei-Deskriptoren geschlossen, die als “close on exec” markiert wurden. Zumal fork den gesamten Adressraum seines Aufrufers dupliziert, ist das Copy-on-Write-Verfahren ?brigens umso wichtiger, wenn es – wie hier – lediglich um das Erstellen einer neuen PID geht! Weiters sorgt exec daf?r, dass die zur Zeit im Wartezustand (pending) befindlichen Signals verworfen und s?mtliche Signals wieder auf ihren Default-Handler umgeleitet werden. Abschlie?end wird dem zuvor geladenen Programm die Kontrolle ?bertragen. Heureka, ein neues Programm ward gestartet!
Nun gut, das soll f?rs erste alles sein, was ich Euch ?ber Prozesse unter Linux mit auf den Weg geben m?chte. Den Rest kann sich jeder leicht selbst erarbeiten und aus den diversen Man-Pages zusammensuchen.
sehr gute info, aber leider zu verschwurbelt geschrieben. klarer w?r besser!