Vous êtes sur la page 1sur 291

ALBRECHT ACHILLES

Betriebssystem
e

4^ Springer

eXamen.press

eXamen.press ist eine Reihe, die


Theorie und Praxis aus aUen
Bereichen der Informatik fr die
Hochschulausbildung vermittelt.

AlbrechtAchilles

Betriebssystem
e
Mit 31 Abbildungen

^ Springer

Albrecht Achilles Fachhochschule Dortmund


Fachbereich Informatik Postfach io 50 18
44047 Dortmund

achilles@fh-dortmund.de

Bibliografische Information der Deutschen Bibliothek


Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen
Nationalbibliografie; detaiUierte bibliografische Daten sind im Internet
ber
abrufbar.

http://dnb.ddb.de

ISSN 1614-5216
ISBN-10 3-540-23805-0 Springer Berlin Heidelberg NewYork
ISBN-13 978-3-540-23805-8 Springer Berlin Heidelberg
NewYork

Dieses Werk ist urheberrechtlich geschtzt. Die dadurch begrndeten Rechte,


insbesondere die der bersetzung, des Nachdrucks, des Vortrags, der
Entnahme von Abbildungen und Tabellen, der Funksendung, der
Mikroverfilmung oder der Vervielfltigung auf anderen Wegen und der
Speicherung
in
Datenverarbeitungsanlagen
bleiben,
auch
bei
nur
auszugsweiser Verwertung, vorbehalten. Eine Vervielfltigung dieses Werkes
oder von Teilen dieses Werkes ist auch im Einzelfall nur in den Grenzen der
gesetzlichen Bestimmungen des Urheberrechtsgesetzes der Bundesrepublik
Deutschland vom 9. September 1965 in der jeweils geltenden Fassung
zulssig. Sie ist grundstzlich vergtungspflichtig. Zuwiderhandlungen
unterliegen den Strafbestimmungen des Urheberrechtsgesetzes.
Springer ist ein Unternehmen von Springer Science+Business Media
springer.de
Springer-Verlag Berlin Heidelberg 2006 Printed in Germany
Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen
usw. in diesem Werk berechtigt auch ohne besondere Kennzeichnung nicht zu
der Annahme, dass solche Namen im Sinne der Warenzeichen- und
Markenschutzgesetzgebung als frei zu betrachten wren und daher von
jedermann benutzt werden drften. Text und Abbildungen wurden mit grter
Sorgfalt erarbeitet. Verlag und Autor knnen jedoch fr eventuell verbliebene
fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung
noch irgendeine Haftung bernehmen.
Satz: Druckfertige Daten des Autors
Herstellung: LE-TeX Jelonek, Schmidt & Vckler GbR,
Leipzig Umschlaggestaltung: KnkelLopka
Werbeagentur, Heidelberg Gedruckt auf surefreiem
Papier 33/3142/YL - 5 4 3 2 1 0

Meiner Frau Sabine gewidmet

Vorwort

Das vorliegende Buch richtet sich an Studenten sowie Interessierte, die sich
einen berblick verschaffen wollen, wie ein Betriebssystem konkret aufgebaut
ist. Dabei wird vorausgesetzt, dass ein Verstndnis fr Datenstrukturen und fr
Programmierung - vorzugsweise C-Programmierung - vorhanden ist. Da heute
bei Studenten hufiger Kenntnisse in Objekt-orientierten Sprachen als in C
anzutreffen sind, werden die Beispiele grndlich dargestellt, so dass ein
Nachvollziehen auch ohne konkrete C-Kenntnisse mglich sein sollte.
Obwohl in einigen Teilen auch sehr Architektur-nahe Programmierung - und
somit Assembler - einflieen muss, wenn man ein Betriebssystem
implementieren will, sind diese Bereiche weitgehend ausgelassen und nur beim
Bootvorgang an einer Stelle erwhnt, ohne auf Assembler-Programmierung
selbst einzugehen. Werden Architektur-spezifische Aspekte erwhnt, so
geschieht das auf Grundlage der x86-Architektur. Dem liegt die Vermutung zu
Grunde, dass die meisten Leser damit am Besten vertraut sind.
Gerade Linux mit seinem zugnglichen Quelltext ist fr eine derartige
Einfhrung in Betriebssysteme hervorragend geeignet. Die krzlich erschienene
Kernel-Version 2.6 verndert unter anderem das Scheduling so stark, dass die
Bedeutung von Linux sowohl im Desktop- als auch im Server-Bereich noch
wachsen wird. Hinzu kommt die frhzeitige Untersttzung der 64 Bit
Architektur der neuen Generation von AMD- und Intel-Prozessoren, die gerade
den Server-Markt beeinflussen werden. Neben den mehr theoretischen Aspekten
gibt es somit auch aus wirtschaftlichen Gesichtspunkten die Notwendigkeit, sich
intensiver mit Linux zu befassen.
Es wurde der Versuch unternommen, Probleme und Fragen aufzuwerfen und
zu zeigen, wie Linux damit konkret umgeht. Einige der aufgeworfenen Fragen
werden im Text bewusst nicht beantwortet, sie sollen vielmehr den Leser zu
eigenen Recherchen veranlassen. Jedes Kapitel wird zunchst mit einer
grundstzlichen Betrachtung eingeleitet, erst danach wird die zugehrige
Implementierung von Linux betrachtet und aus dem globalen Kontext heraus
verstndlich. An einer Reihe von Stellen wird auch gezeigt, wie der Anwender
sich die Eigenschaften von Linux zu nutze machen kann, indem kleine

VIII Vorwort

Beispielprogramme dargestellt werden, die auf die bei der Implementierung


besprochenen System Calls zugreifen. Die Programme knnen - wie das
Betriebssystem Linux selbst - mit dem gcc-Compiler bersetzt werden. Der
Quellcode der Beispiele wird im Internet auf meiner Homepage zur Verfgung
gestellt, siehe Anhang Interessante WWW-Adressen.
Es konnten nicht alle Aspekte von Linux dargestellt werden, der
Schwerpunkt wurde auf die Bereiche

Prozesse und Threads


Scheduling
Speicherverwaltung
Synchronisation
Interrupts
Filesysteme (Dateisysteme) und Plattenverwaltung und
Kommunikation zwischen Prozessen

gelegt. System Calls sowie die Vorgnge beim Bootvorgang werden zum besseren
Verstndnis knapp dargestellt. Da es sich hierbei um besonders Hardwarenahe
Aspekte handelt, die Assembler-Programmierung erfordern, kann dadurch ein
Eingehen auf den Assembler weitestgehend vermieden werden.
Den Kenner der Materie mag auf den ersten Blick verwundern, dass Shared
Memory auch im Kapitel Speicher und die IPC-Semaphoren auch im Kapitel
Synchronisation besprochen werden, gehren sie doch in den eigenstndigen
Bereich der IPC-Objekte. Die Kernel-Strukturen fr IPC-Objekte werden - wie
zu erwarten - in dem entsprechenden Abschnitt 9.3 IPC - Inter Process
Communication betrachtet, die gewhlte Aufteilung ist Ausdruck der
angebotenen Funktionalitt. Ebenso erstaunen mag die Zusammenfassung von
Pipes, IPC und Sockets in dem Kapitel Kommunikation zwischen Prozessen,
auch dies eine Folge hnlicher Funktionalitt aus Sicht des Anwenders, auch
wenn die Wege, die der Kernel beschreitet, sehr unterschiedlich aussehen.
Die Komplexitt und Vernetzungen innerhalb eines Betriebssystems werden
- bei allen vorgenommenen Vereinfachungen - nicht verdeckt.
Kapitelbergreifende Querverweise sollen anregen, nicht nur sequentiell zu
lesen, sondern auch von Zeit zu Zeit vor- und zurckzublttern. Aus diesem
Grunde sind auch das Glossar, das bei neuen oder nicht direkt erluterten
Begriffen sofort zu Rate gezogen werden sollte, sowie der Index sehr ausfhrlich
gehalten. Bei der Tiefe der Darstellung werden in der Regel Aspekte, die fr die
Anwendungsprogrammierung wichtig sind, detaillierter dargestellt als andere.
Problematisch erweist sich bei solchen Texten immer die Verwendung von
Fachbegriffen. Hier werden weitgehend die gngigen englischen Begriffe
benutzt, obwohl dadurch die sprachliche Qualitt leidet. Auch ansonsten habe
ich sprachlich einen Duktus hnlich einer Vorlesung gewhlt, um den Leser
immer wieder direkt anzusprechen.
Hinsichtlich der Typographie werden Dateinamen, Namen von Funktionen,
Argumenten, Kommandos usw. durchgngig in Schreibmaschinenschrift

Vorwort

IX

dargestellt, also beispielsweise write() fr die entsprechende Funktion.


Pfadbezeichnungen zu Dateien des Quellcodes werden grundstzlich relativ zum
Directory beschrieben, in dem sich der Linux-Quelltext befindet, so lautet der im
Text gewhlte Pfad zum Beispiel kernel/exit.c, wohingegen der komplette Pfad
/usr/src/linux-2.6.0-testll/kernel/exit.c heien knnte.
Neben weiterfhrender Literatur werden auch wichtige Links im Internet im
Anhang Interessante WWW-Adressen aufgefhrt. Darunter befindet sich
insbesondere ein Link, der die Programmiersprache C sehr ausfhrlich darstellt.
Meinen besonderen Dank mchte ich folgenden Personen aussprechen:

Herrn Frank Schmidt vom Springer-Verlag, der mich in langen Gesprchen


zu diesem Vorhaben ermunterte;
meinen Kollegen an der Fachhochschule, die mir das Schreiben ermglichten;
meinen vier ehemaligen Studenten Karl-Wilhelm Rips, Gunter Siepmann,
Norbert Witte und Paul Zlonicky - alle inzwischen in anspruchsvollen
Positionen - sowie meinem Freund, Informatik-Begeisterten und Musiker,
Herrn Peter Krger, die sich alle intensiv der Korrektur widmeten.

Vor allem danken mchte ich meiner Familie, die mir Zeit fr dieses
Vorhaben lie und manchmal sicher unter meinem Druck gelitten hat.

Dortmund, Juni 2005

Albrecht Achilles

Inhaltsverzeichnis

Einfhrung...................................................................................................... 1
1.1 WasisteinBetriebssystem?......................................................................... 1
1.1.1
Betriebssystemkern.................................................................... 2
1.1.2
Systemmodule............................................................................. 2
1.1.3
Dienstprogramme ...................................................................... 3
1.1.4
Unterschiedliche Arten von Betriebssystemen......................... 3
1.2 Computer-Hardware.................................................................................. 5
1.2.1
CPU............................................................................................. 5
1.2.2
Busse........................................................................................... 7
1.2.3
Speicher....................................................................................... 8
1.2.4
Ein- und Ausgabegerte............................................................. 9
1.3 System Calls und Interrupts................................................................... 11
1.3.1
Der eigentliche Aufruf.............................................................. 11
1.3.2
Interrupts.................................................................................. 12
1.4 Ressourcen.................................................................................................. 13
1.5 Zusammenfassung................................................................................... 14

System Calls.................................................................................................. 15
2.1 Von der Anwendung zum System Call.................................................... 15
2.2 Der Handler fr System Calls................................................................. 16
2.3 Direkte Aufrufe........................................................................................ 17
2.4 Zusammenfassung................................................................................... 18

Prozesse und Threads ............................................................................... 19


3.1 Grundlagen............................................................................................... 19
3.1.1
Linux Process Control Block.................................................... 20
3.1.2
WeiterfhrendeKonzepte.......................................................... 20
3.1.3
Die Prozess-ID.......................................................................... 24
3.1.4
Sessions und Process Groups................................................... 25
3.2 Erzeugen von Prozessen.......................................................................... 25
3.2.1
Der ursprngliche fork()-Aufruf............................................... 25

XII Inhaltsverzeichnis

3.2.2
AusfhhreneinesanderenProgramms:exec() ............................. 26
3.2.3
Prozesse und Vererbung........................................................... 28
3.3 Beenden eines Prozesses......................................................................... 30
3.3.1
exit() und wait()......................................................................... 30
3.3.2
Synchronisation........................................................................ 32
3.4 Leichtgewichtige Prozesse.................................................................... 33
3.4.1
Der neue fork()-System Call..................................................... 33
3.4.2
Die vfork()-Variante.................................................................. 33
3.4.3
Implementierungmitclone() ..................................................... 34
3.4.4
Threads...................................................................................... 35
3.5 Zusammenfassung................................................................................... 38
4

Scheduling..................................................................................................... 39
4.1 Grundlagen.............................................................................................. 39
4.1.1
Priorittsgesteuert.................................................................... 42
4.1.2
Round Robin.............................................................................. 42
4.1.3
Priorittsgesteuert mit Feedback............................................ 42
4.1.4
Lnge der Zeitscheibe............................................................... 43
4.1.5
Mehrprozessor-Systeme ........................................................... 43
4.1.6
Realtime.................................................................................... 44
4.2 Linux Scheduling..................................................................................... 44
4.2.1
PriorittsarraysundScheduling-Algorithmus.......................... 46
4.2.2
Der Wechsel der Priorittsarrays............................................. 47
4.2.3
Prozesswechsel.......................................................................... 47
4.3 Realtime................................................................................................... 48
4.3.1
Realtime mit FIFO.................................................................... 49
4.3.2
Realtime mit Round Robin....................................................... 49
4.3.3
Realtime System Calls.............................................................. 49
4.4 Timesharing............................................................................................. 51
4.4.1
Dynamische Prioritten und Zeitscheibenberechnung .. . 51
4.4.2
Timesharing System Calls....................................................... 52
4.4.3
Neue Prozesse .......................................................................... 52
4.5 Load Balancing........................................................................................ 52
4.6 Zusammenfassung................................................................................... 53

Speicherverwaltung........................................................................................ 55
5.1 Grundlagen.............................................................................................. 55
5.1.1
Segmentierung.......................................................................... 57
5.1.2
Paging........................................................................................ 57
5.1.3
Virtueller Speicher ................................................................... 58
5.2 Ziele fr Linux.......................................................................................... 60
5.2.1
Vielzahl von Hardware-Plattformen........................................ 60
5.2.2
Inhomogener Speicher.............................................................. 60
5.2.3
NUMA-Architekturen............................................................... 61
5.2.4
Page Cache................................................................................ 61

Inhaltsverzeichnis XIII

5.3

5.4

5.5

5.6
5.7

5.8
5.9
6

5.2.5
pdflush: Zurckschreiben vernderter Pages.......................... 62
5.2.6
Slab: Verwaltung von Kernel-Objekten................................... 62
5.2.7
Frame-Allocation zum sptesten Zeitpunkt............................ 62
Prozess-Adressraum ................................................................................ 63
5.3.1
Memory Deskriptor................................................................... 64
5.3.2
Die Speicherbereiche................................................................ 64
5.3.3
System Calls.............................................................................. 66
Pagetable ................................................................................................. 68
5.4.1
Page locking.............................................................................. 70
5.4.2
PSE............................................................................................ 72
Paging....................................................................................................... 72
5.5.1
Pagefaults.................................................................................. 72
5.5.2
Zones und NUMA..................................................................... 73
5.5.3
Anforderung zusammenhngender Frames............................ 75
Page Cache................................................................................................ 75
Swapping.................................................................................................. 78
5.7.1
Kernel Caches .......................................................................... 78
5.7.2
pdflush....................................................................................... 78
5.7.3
Auswahlstrategie...................................................................... 79
5.7.4
kswapd....................................................................................... 80
5.7.5
Verwaltung des Cache-Bereichs............................................... 80
Slab Layer................................................................................................. 82
Zusammenfassung................................................................................... 83

Synchronisation .............................................................................................. 85
6.1 Grundlagen............................................................................................... 85
6.1.1
Race Conditions........................................................................ 85
6.1.2
Anstze zur Synchronisation................................................... 87
6.1.3
Contention und Scalability von Sperren................................. 89
6.2 Deadlock................................................................................................... 89
6.3 Kernel Synchronisation........................................................................... 91
6.3.1
Atomare Operationen............................................................... 91
6.3.2
Spinlocks................................................................................... 91
6.3.3
Semaphoren.............................................................................. 92
6.3.4
Reader-/Writer-Locks................................................................ 93
6.3.5
Big Kernel Lock........................................................................ 94
6.4 Synchronisation in Benutzerprogrammen.............................................. 94
6.4.1
Signale....................................................................................... 95
6.4.2
Semaphoren.............................................................................. 99
6.5 Zusammenfassung..................................................................................105

XIV Inhaltsverzeichnis

Interrupts......................................................................................................107
7.1 Grundlagen.............................................................................................107
7.1.1
Erkennung eines Interrupts....................................................108
7.1.2
Geschwindigkeit versus Umfang.............................................109
7.1.3
Der Interrupt-Handler ............................................................109
7.2 Implementierung....................................................................................110
7.2.1
Datenstrukturen .....................................................................110
7.2.2
Registrierung...........................................................................112
7.2.3
Interrupt Requests ..................................................................113
7.3 Interrupt Control....................................................................................115
7.4 Bottom Half.............................................................................................117
7.4.1
Veraltete Anstze ....................................................................117
7.4.2
Soft-IRQ....................................................................................117
7.4.3
Tasklets....................................................................................121
7.4.4
Bearbeitung von Soft-IRQs und Tasklets bei hoher Last 124
7.4.5
Work Queues............................................................................125
7.4.6
Warteschlangen........................................................................129
7.5 Zusammenfassung..................................................................................130

Dateisysteme und Plattenverwaltung.........................................................133


8.1 Grundlagen.............................................................................................133
8.1.1
Untersttzung von Dateitypen................................................133
8.1.2
Von der Lochkarte zur Platte..................................................134
8.1.3
Directory und Dateioperationen.............................................135
8.1.4
Implementierungen fr Directories........................................137
8.1.5
Zugriffsmethoden.....................................................................137
8.1.6
Bereitstellung von Speicherplatz............................................138
8.1.7
Directory...................................................................................140
8.1.8
Datenschutz und Datensicherheit...........................................142
8.1.9
Unterschiedliche Filesysteme..................................................143
8.1.10 Festplattenzugriffe.................................................................143
8.2 Virtual File System (VFS)......................................................................144
8.2.1
VFS-Datenstrukturen fr Dateien.........................................144
8.2.2
VFS-Datenstrukturen fr Partitionen....................................151
8.2.3
Suche nach einer Datei............................................................154
8.2.4
Filedeskriptoren: Geffnete Dateien.......................................158
8.2.5
Die Verbindung zum Speicher.................................................160
8.2.6
Filesysteme...............................................................................161
8.3 System Calls............................................................................................162
8.4 Beispiel: read()........................................................................................167
8.4.1
Lesen ohne Readahead............................................................173
8.4.2
Lesen mit Readahead..............................................................176
8.5 Elevator...................................................................................................180
8.6 Extended Filesystem 2............................................................................183
8.6.1
Der Superblock.........................................................................186

Inhaltsverzeichnis XV

8.6.2
Die Inode .............................................. 189
8.6.3
Indirektion und Finden der Blcke.........................................194
8.6.4
Bereitstellung von Speicherplatz............................................195
8.7 Zusammenfassung..................................................................................197
9

Kommunikation zwischen Prozessen............................................................199


9.1 Grundlagen..............................................................................................199
9.2 Pipes.........................................................................................................200
9.2.1
Der prinzipielle Aufbau...........................................................200
9.2.2
Beispiel zur Benutzung einer Pipe........................................201
9.2.3
Nicht-blockierender Zugriff................................................... 203
9.2.4
Schlieen einer Pipe................................................................205
9.2.5
Named Pipes (FIFO) ...............................................................205
9.3 IPC - Inter Process Communication.......................................................206
9.3.1
IPC-Grundlagen ......................................................................207
9.3.2
Message Queues.......................................................................208
9.3.3
Benutzung von Message Queues.............................................209
9.3.4
Semaphoren.............................................................................212
9.3.5
Shared Memory........................................................................214
9.3.6
Der System Call sys_ipc().....................................................215
9.4 Sockets.....................................................................................................215
9.4.1
Schichtenmodell.......................................................................216
9.4.2
System Calls.............................................................................221
9.4.3
Implementierung.....................................................................225
9.5 Zusammenfassung..................................................................................235

10 Der Bootvorgang.............................................................................................237
10.1 Grundlagen.........................................................................................237
10.2 Vom BIOS zum Bootmanager............................................................238
10.3 Der Kernel ..........................................................................................239
10.4 Die Runlevel........................................................................................242
10.5 Module ................................................................................................245
10.6 Zusammenfassung..............................................................................246
A

Kompilieren des Kernels...............................................................................247

Lineare Listen in Linux.................................................................................251

Glossar............................................................................................................257

Interessante WWW-Adressen...............................................................................277
Literaturverzeichnis ............................................................................................279
Sachverzeichnis.....................................................................................................281

Einfhrung

1.1 Was ist ein Betriebssystem?


Ein Versuch, den Begriff Betriebssystem zu definieren, scheitert sehr schnell.
Wichtig sind jedoch die Beobachtungen, die wir bei diesem Versuch machen
knnen:

Auf ein und demselben Rechner knnen unterschiedliche Betriebssysteme


laufen:
So kann auf einem gngigen PC mit x86-Prozessor sowohl Linux - ein Unixhnliches Betriebssystem - als auch Windows eingesetzt werden.
Auf Rechnern unterschiedlicher Hardware kann das gleiche Betriebssystem
arbeiten:
So kann Linux auf einem PC, aber auch auf leistungsstarken Servern
diverser Hersteller implementiert sein.

Im ersten Falle erleben wir bei der Benutzung bei gleicher Hardware groe
Unterschiede, im zweiten Falle trotz unterschiedlicher Hardware nahezu
gleiches Verhalten der Rechner.
Diese Beobachtung fhrt uns zu anderen Fragen:

Wie hngen Hardware und Betriebssystem zusammen?


Kann man einzelne Aufgaben eines Betriebssystems eingrenzen und
beschreiben?
Wie erleben wir als Benutzer ein Betriebssystem?

Die folgende Abb. zeigt den geschichteten Aufbau: die Hardware besteht aus
Sicht des Betriebssystems zunchst aus der Menge der Maschineninstruktionen.
Diese werden heute blicherweise durch einen Mikrocode der CPU definiert und
abgearbeitet. Dieser Mikrocode greift physisch auf die Schaltkreise zu. Die
Schaltkreise knnen auch komplexe Gerte wie z.B. ein PlattenController sein.

1.1 Was ist ein Betriebssystem?

1 Einfhrung

ein virtuelles
Filesystem, dessen sehen
Zugriffe
auf die
die Hardware,
jeweiligen Module
Die Anwendungsprogramme
nicht
sondernabgebildet
das
werden,
die dannDieses
ihrerseits
auf
die konkrete
Hardwareder
zugreifen.
Betriebssystem.
stellt
somit
eine Erweiterung
Hardware dar, die
Moduleden
knnen
bei einigen Betriebssystemen
sogar
dynamisch
im laufenden
einerseits
Anwendungsprogrammen
die Mittel
bereitstellt,
in abstrakter
Betrieb
angefordert
und geladen
sowiedie
wieder
entladendazu
werden,
sodie
dass
der vom
Weise auf
die Hardware
zuzugreifen,
andererseits
dient,
Ressourcen
Betriebssystem
Platz minimiert wird. Bei Linux muss z.B. zu Beginn
zu berwachen bentigte
und zu steuern.
nur auf diejenige Partition zugegriffen werden knnen, auf der das
Betriebssystem installiert ist. Diese ist z.B. ext2-formatiert. Die Untersttzung
fr das FiBu
ext2-Filesystem
muss dann fest in den Kernel
einkompiliert sein, da
Anwendungssoftware
Browser
ansonsten das Betriebssystem die bentigten Informationen mangels Kenntnis
Shell \ Compiler
Linker^oader
des Filesystems
nicht jlesen
kann.2 Steht auf dem Rechner auch Dienstprogramme
eine Partition fr
Windows zur Verfgung, die mit NTFS formatiert ist, soSystem
wird bei
Calls
Zugriff auf
Betriebssystemkem
Betriebssystem
Kem
diese Partition
das bentigte Systemmodul automatisch
dazugeladen.
1.1.3

Filesystem

Netzwerk

Dienstprogramme

Systemmodule

Maschineninstruktion

Auch dieser en
Teil der Software ist im weitesten Sinne dem Bereich Betriebssystem
Hardware
zuzuordnen, mssen Shell, Compiler usw. doch die
Besonderheiten des jeweiligen
Mikrocode
Betriebssystems bercksichtigen. So muss ein Compiler wissen, auf welcher
Gerte/Schaltkreise
Adresse das Betriebssystem
den Anfang eines Programms erwartet.
Entsprechend mssen sich Linker und Loader an die entsprechenden
Konventionen
halten.
Abb. 1.1. Schichten
eines Computersystems
Die Shell kann nur Konstrukte bereitstellen, die das jeweilige
Betriebssystem untersttzt. Eine Shell, die Batch-Verarbeitung im Hintergrund
anbietet, macht in einem Betriebssystem wie dem alten DOS wenig Sinn.
Was diese Dienstprogramme jedoch von den anderen Teilen des
1.1.1
Betriebssystemkern
Betriebssystems unterscheidet, sind folgende Eigenschaften:
Das Betriebssystem
ist blicherweise
selbst in mehrereaufrufen,
Schichtensofern
aufgeteilt.
Jeder Benutzer
kann diese Dienstprogramme
er dieIn
vielen heutigen
Betriebssystemen
muss
zwischen
dem
Kern
und
Systemmodulen
entsprechenden Rechte besitzt - die (ladbaren) Systemmodule hingegen
unterschieden
Der Kern enthlt die
zentralen
Aufgaben,
die nicht mehr
werdenwerden.
vom Betriebssystemkern
selbst
aufgerufen
oder vom
weiter unterteilt
werden
knnen.
Dazu
gehren
z.B.
die
Prozessverwaltung,
die
Systemadministrator manuell geladen.
Speicherverwaltung
oder
auch
elementare
Einund
Ausgaben.
Sie lassen sich leicht gegen andere Dienstprogramme fr das gleiche

Betriebssystem austauschen - bei Systemmodulen ist das erheblich


1.1.2schwieriger
Systemmodule
und kann ausschlielich vom Systemadministrator
vorgenommen werden.
Die Aufgabe der Systemmodule liegt darin, komplexe Aufgaben bereitzustellen.
Hierzu gehren unter anderem der Zugriff auf Filesysteme1 oder
1.1.4
Unterschiedliche
Artenaus
von
Betriebssystemen
Netzwerksoftware.
Durch die Verlagerung
dem
Kern in diese Module wird
ermglicht, dass ein und dasselbe Betriebssystem auf unterschiedliche
Bislang
haben
wir Betriebssysteme
der unterschiedlichen
Hardware betrachtet.
Filesysteme
zugreifen
kann: es kannals
je Erweiterung
nach Bedarf mit
Wenn
wir
jedoch
die
Frage
nach
den
einzelnen
Aufgaben
eines
Betriebssystems
Modulen installiert werden. So ist es mglich, Linux auf das
Filesystem
EXT2
stellen,
kommen
wir
schnell
auf
wichtige
Unterschiede
zu
sprechen.
oder NTFS oder VFAT zugreifen zu lassen. Darber hinausgehend knnen diese
Module sogar zeitgleich installiert werden, so dass unterschiedlich formatierte
1
2
Partitionen
gleichzeitig
knnen.
Der Kern
dazu
blicherweise
ist ein bearbeitet
Zugriff auf werden
ext2 zur
Zeit Standard
undenthlt
somit fest
im Kernel

integriert. Jedoch muss bemerkt werden, dass diese Darstellung so zu kurz greift, weil
moderne Bootmanager sowie die Inital Ramdisk an dieser Stelle auer Acht gelassen
1
synonym: Dateisysteme
werden.

1 Einfhrung

So lassen sich zunchst folgende groe Anforderungsrichtungen


unterscheiden:
Batch: Die Aufgaben knnen routinemig ohne Eingriff eines Benutzers
durchgefhrt werden. Dabei handelt es sich blicherweise um langwierige
Aufgaben, die groe Datenmengen bearbeiten mssen oder hohe Rechenzeit
erfordern. Hierzu gehren unter anderem statistische Auswertungen,
Erstellung von Standard-Berichten, Datenbank-Server usw.
Timesharing: Viele Benutzer knnen nahezu gleichzeitig auf den Rechner
zugreifen und ihre Arbeiten erledigen (Multiuser-Betrieb), so z.B. Edieren
und interaktives Testen von Programmen, Datenbankzugriffe,
BrowserBenutzung, ...
Transaktions-orientiert: Eine Vielzahl kleiner Aufgaben muss in krzester
Zeit bewltigt werden, so zum Beispiel Bankberweisungen,
Buchungssysteme, usw. Dabei muss die Bearbeitung folgende Kriterien
erfllen: Atomaritt, Konsistenz, Isolation und Dauerhaftigkeit (englisch
ACID), d.h. die Bearbeitung muss entweder vollstndig ablaufen oder
keinerlei nderung hinterlassen.
Echtzeit: Bei der berwachung von chemischen oder technischen Prozessen
muss das System in einer durch das jeweilige Problem vorgegebenen Zeit auf
das Auftreten von Ereignissen reagieren. Dabei kann zwischen harten und
weichen Echtzeitsystemen unterschieden werden:
Harte Echtzeitsysteme:
Die Reaktionszeit darf bei manchen Prozessen auf keinen Fall
berschritten werden, weil sonst ernsthafte Schden auftreten knnen
(harte Echtzeitsysteme). Reagiert ein Autopilot in einem Flugzeug zu
spt auf das Erreichen einer kritischen Fluglage, kann es zum Absturz
kommen.
Weiche Echtzeitsysteme:
Bei anderen Systemen, so bei Audio oder Multimedia-Systemen, sind
gewisse Toleranzen hinsichtlich der Abweichung von der Reaktionszeit
erlaubt. Diese Systeme nennt man weiche Echtzeitsysteme.
Neben diesen groen Richtungen gibt es auch Hardware-Anforderungen, die
Betriebssysteme unterscheiden lassen:

Hohe Ein- und Ausgabe-Bandbreite und Verwaltung riesiger


Datenmengen: diese Anforderungen findet man blicherweise bei
greren Rechensystemen3. Groe Server mssen hufig diese
Anforderung erfllen.
Server: Server laufen blicherweise auf groen Rechnersystemen,
Workstations oder groen PCs. Typisches Kennzeichen ist, dass viele
Benutzer ber ein Netz gleichzeitig auf definierte Dienste zugreifen
knnen, z.B. Druckdienste, Datenbankzugriff, Fileserver usw.

Frher sprach man von Mainframes oder Grorechnern, doch sind diese Begriffe
heute eher negativ belegt, obwohl heutige groe Server in diese Kategorie fallen.
3

1.2 Computer-Hardware

Workstations: Die wichtige Anforderung besteht darin, interaktive


Schnittstellen fr einen einzelnen Benutzer zur Verfgung zu stellen.
Tastatur, Maus, grafische Oberflche und Lautsprecher mssen
zumindest untersttzt werden. Heute zhlt in der Regel auch eine
entsprechende Netzanbindung zu den Aufgaben.
Spezielle Hardware: Hierzu zhlen Multiprozessor-Betriebssysteme,
aber auch embedded (verkleinerte) Betriebssysteme fr PDAs
(Personal Digital Assistent) oder Spezialhardware.

1.2

Computer-Hardware

Wenn Betriebssysteme in gewisser Weise als Erweiterung der Hardware


angesehen werden knnen, dann sind Betriebssystem und Hardware sehr eng
verzahnt. Abbildung 1.1 auf S. 2 zeigt, dass das Betriebssystem auf den
Maschineninstruktionen aufsetzt. Am anderen Ende der Hardware stehen
Schaltkreise bzw. physische Gerte. Aus diesem Grunde wollen wir uns einen
berblick ber den Aufbau eines Rechners verschaffen. Als Beispiel soll die
Architektur eines PCs herangezogen werden. Wir wissen, dass der PC ber eine
CPU, ber Speicher, eine Grafikkarte usw. verfgt. Doch wie arbeiten diese
Bestandteile zusammen? Was lsst sich ber ihren Aufbau, ihre Funktion sagen?
Abbildung 1.2 skizziert den Aufbau eines heutigen PCs. Die gestrichelten
Komponenten sind nicht notwendigerweise vorhanden. Die Linien stellen Busse
dar, die die einzelnen Komponenten miteinander verbinden. Die Strichstrke der
Busse soll die unterschiedlichen Geschwindigkeiten verdeutlichen, mit der Daten
ber den jeweiligen Bus ausgetauscht werden knnen.
Die Architektur eines greren Rechensystems ist in der Regel wesentlich
komplexer, auch wenn Komponenten wie CPU, Cache, Speicher, Busse, Controller
usw. dort ebenfalls zu finden sind.

1.2.1

CPU

Ein Programm besteht aus einer Folge von Maschineninstruktionen, die in


aufeinanderfolgenden Speicheradressen abgelegt sind. Aufgabe der CPU ist es,
die Programme Schritt fr Schritt umzusetzen.
Bei unserer folgenden, stark vereinfachten Betrachtung legen wir die x86Architektur zu Grunde. Die CPU ist in Rechenwerke und verschiedene Register
unterteilt. Eines dieser Register ist der Befehlszhler (Program Counter), der auf
die Adresse4 der gerade auszufhrenden Instruktion zeigt. Daraufhin liest die
CPU diese Maschineninstruktion aus der entsprechenden Stelle im Speicher und
fhrt sie aus. Handelt es sich nicht um eine Operation, die eine
Der Speicher kann Byte-weise adressiert werden.

1 Einfhrung

Abb. 1.2. Architektur eines PCs

Die Darstellung ist stark vereinfacht: die PCI-Bridge besteht heute aus zwei Teilen, der
North-Bridge, die fr die Speicherzugriffe und die Grafikkarte zustndig ist, und die damit
verbundene South-Bridge, die die langsameren Verbindungen wie PCI, USB, UDMA/ATA
usw. bedient.
Die Strichstrke der Busse versinnbildlicht die Ubertragungsgeschwindigkeit.

Verzweigung des Kontrollflusses5 bewirkt, so wird nach der Ausfhrung der


Befehlszhler erhht. Da Instruktionen ein, zwei oder sogar mehrere Byte lang
sein knnen, wird der Befehlszhler genau um die Anzahl der Byte erhht, die
die gerade ausgefhrte Instruktion einnimmt. Anschlieend wird die nchste
Instruktion eingelesen. Handelt es sich hingegen um eine Verzweigung des
Kontrollflusses, so wird die ermittelte Adresse der danach auszufhrenden
Instruktion in den Befehlszhler geladen.
Allgemeine Register dienen zur Aufnahme von Daten und Adressen. Diese
Register knnen direkt adressiert und manipuliert werden.
Weitere wichtige Register sind

Stackregister: Dieses dient zum Adressieren eines Stacks. Mit Hilfe


eines Stacks lsst sich z.B. die Wertbergabe bei Prozeduraufrufen
implementieren.
Coderegister: Hiermit wird das Segment adressiert, das den derzeit
ausgefhrten Maschinencode enthlt.
Datensegmentregister: Jedes dieser Register dient dazu, ein
Datensegment zu adressieren.

Z.B. Sprungbefehl oder Prozeduraufruf.

1.2 Computer-Hardware

GDTR, LDTR, IDTR: Register, die beim Aufbau von Pagetables und Interrupt-Deskriptor-Tabellen bentigt werden.
Statusregister: Vergleiche fhren dazu, dass einzelne Bits dieses
Registers gesetzt werden. Diese Werte werden benutzt, um bei
bestimmten Maschineninstruktionen eine Verzweigung auszulsen.

Moderne CPUs sind komplexer aufgebaut als diese kurze Beschreibung


vermuten lsst; in dem betrachteten Kontext wird aber keine detailliertere
Betrachtung bentigt.
Das Betriebssystem muss an die jeweilige CPU angepasst sein. Welche
Register tatschlich zur Verfgung stehen und wie sie eingesetzt werden, ist von
der CPU abhngig. Wir mssen also erwarten, dass zumindest ein Teil der
Betriebssystem-Software sehr CPU-nah in Assembler programmiert ist.
Mit diesen berlegungen knnen wir verstehen, was passiert, wenn eine
einzige Anwendung zu einem Zeitpunkt auf einem Rechner luft. Aber in unserer
tglichen Arbeit am PC haben wir in der Regel mehrere Anwendungen
gleichzeitig laufen. Wie kann die CPU diese Anwendungen gleichzeitig
bedienen? Wie kann der Begriff Anwendung aus Sicht eines Betriebssystems
beschrieben werden? Was geschieht, wenn eine neue Anwendung gestartet wird,
wenn eine laufende Anwendung beendet wird. Diese - und weitere - Probleme
werden im Kap. 3 angesprochen.
1.2.2

Busse

Tatschlich kommuniziert die CPU nicht mit dem Speicher oder der Grafikkarte
direkt, sondern ber die PCI-Bridge bzw. ber die North- und South- Bridge6, die
den entsprechenden Datenstrom je nach Anweisung entweder auf den Speicher
oder auf den PCI-, USB-Bus usw. abbilden. Auf der anderen Seite ist die CPU
ber einen sehr schnellen Bus mit dem internen Cache verbunden.
Die PCI-Bridge ist mit einem Bus mit der CPU verbunden, mit einem
anderen Bus mit dem Speicher. Als dritter Bus verlsst der PCI-Bus diesen
Baustein. Diese Lsung ermglicht es, dass der Speicher auch direkt mit den
externen Controllern Daten austauschen kann, ohne dass die CPU dabei aktiv
beteiligt sein muss.
In den PCI-Bus sind diverse Controller eingesteckt, so die Grafik-Karte, in
der Regel eine Netzwerkkarte und ein USB-Controller, an dem unter anderem
Maus, Tastatur, Laufwerk und Scanner angeschlossen werden knnen. Neben
weiteren dort ggf. eingehngten Bussen wie SCSI und Firewire gibt es noch die
ISA-Bridge. Diese dient dazu, den bergang in den ltesten PC-Bus
vorzunehmen, der aus Grnden der Kompatibilitt zu alter Hardware noch
vorhanden ist. Hier werden parallele Drucker angeschlossen.

Die derzeitige Variante besteht im Einsatz von North- und South-Bridge; zur
Vereinfachung der Darstellung wird die ltere Version der PCI-Bridge herangezogen.
6

1 Einfhrung

Ebenfalls an der ISA-Bridge angesehlossen ist der IDE-Bus, der Platten und
CD- bzw. DVD-Laufwerke untersttzt.
Das Betriebssystem muss die Busse kennen und ansprechen knnen, damit
sich die angeschlossenen Gerte beim Start entsprechend konfigurieren lassen.

Auslagerungsprozess
durch "Alterung"

Abb. 1.3. Speicherhierarchie

Gepunktet sind diejenigen Speicherschichten, die beim PC in der Regel nicht vorhanden
sind.

1.2.3

Speicher

Der Speicher nimmt Daten und auszufhrende Programme auf. Zum Speicher
zhlt nicht nur der Hauptspeicher des Rechners, tatschlich bildet der Speicher
eine durch Busse verbundene Hierarchie (vgl. Abb. 1.3). Diese beginnt bei den
ganz schnellen CPU-Registern, auf die die CPU ohne Verzug zugreifen kann,
fhrt ber den internen und externen Cache, den eigentlichen Hauptspeicher,
nicht flchtigen elektronischen Speicher7, schnelle und langsame Magnetplatten,
Magnetbnder bis hin zu CDs und DVDs. Der Grund liegt im
Preis/Leistungsverhltnis: je schneller der Speicher, desto teurer ist er.
Es gibt Betriebssysteme im Bereich groer Rechenanlagen, die automatisch
und fr den Benutzer transparent diese Speicherhierarchie ausnutzen,

Nur in groen Rechenanlagen vorhanden

1.2 Computer-Hardware

um Daten, die lngere Zeit nicht benutzt wurden, in die langsameren Schichten
altern zu lassen. Erfolgt jedoch ein Zugriff auf diese Daten, so werden sie unabhngig von der Schicht, in der sie sich gerade befinden - so schnell wie
mglich in den Vordergrund, d.h. in den Hauptspeicher geholt.
Bei der tglichen Benutzung unseres PCs lassen wir mehrere Programme
gleichzeitig laufen - z.B. einen Kalender, einen Editor, einen Compiler usw. Der
ausfhrbare Code und die Daten werden im Speicher gehalten. Damit ergeben
sich sofort eine Reihe von Fragen:

Wie verwaltet das Betriebssystem den Hauptspeicher?


Wie schtzt das Betriebssystem die im Hauptspeicher befindlichen Teile
einer Anwendung gegen bergriffe durch andere Anwendungen?

Auf diese Fragen wird im Kap. 5 eingegangen.


Betrachten wir nicht nur den Hauptspeicher, sondern die Speicherhierarchie,
dann stellen sich entsprechende Fragen fr die Verwaltung der Platten.
Genauere Antworten hierauf gibt das Kap. 8.
1.2.4

Ein- und Ausgabegerte

Abbildung 1.2 und die zugehrige Diskussion im Abschn. 1.2.2 deutet eine Reihe
von Ein- und Ausgabegerten an. So finden wir z.B. die Grafik-Karte und als
zugehriges Gert den Monitor, oder den USB-Controller und angedeutet die
Maus bzw. Tastatur. Alle diese Karten und Kontroller sind mit spezialisierten
kleinen Rechnern ausgestattet.
Dieser zweigeteilte Aufbau ist typisch: damit die CPU nicht die gesamte
Arbeit selbst erledigen muss, werden intelligente Chips oder Platinen - oder
sogar ganze Vorrechner - bereitgestellt, die einen eigenen Befehlssatz besitzen.
Wie das Betriebssystem die Hardware vor den Anwendungsprogrammen
verbirgt, so verdecken die Controller die Gerteschnittstellen vor dem
Betriebssystem. Damit andererseits das Betriebssystem nicht auf die Vielzahl
unterschiedlicher Controller und deren unterschiedliche Befehlsstze angepasst
werden muss, werden zu jedem Controller Treiber fr die untersttzten
Betriebssysteme geliefert. Diese Treiber mssen in das Betriebssystem integriert
werden. Je nach Betriebssystem knnen Treiber statisch bei der Kompilation des
Kernels eingebunden oder dynamisch whrend der Laufzeit ge- oder entladen
werden. Dynamisches Laden und Entladen ist fr Hotplug erforderlich, d.h. fr
Gerte, die whrend der Laufzeit angeschlossen bzw. entfernt werden drfen.
Betrachten wir folgende Situation: eine Anwendung mchte einen Datensatz
von einer Platte lesen. Dazu schickt die CPU dem IDE-Controller unter
Verwendung des Treibers einen I/O-Befehl8, und der Controller leistet dann

Genauer: der Befehl wird in ein Register des Controllers geschrieben. Diese Register
knnen entweder in den Speicher eingeblendet sein, d.h. Kommunikation mit dem
Controller wird zu einem normalen Speicherzugriff, oder durch spezielle I/O-Befehle
(Input/Output = Ein-/Ausgabe) adressiert werden.
8

1 Einfhrung

10

die in der Regel komplexe zeitaufwndige Arbeit, ohne dass die CPU weiter
eingreifen muss. In diesem Falle mssen zunchst der Zylinder und Block auf der
Platte bestimmt werden, in dem der Datensatz steht, der Plattenarm muss
positioniert und dann gewartet werden, bis der Block bei der Rotation unter dem
Lesemechanismus des Plattenarms erscheint. Danach kann gelesen werden.
Jetzt hat der Controller die Daten - und nun?
Wie kommt die Anwendung an die Daten? Drei wesentliche Techniken
werden eingesetzt:

Busy waiting: Der Treiber wartet in einer Endlosschleife darauf, dass


der Controller in einem Register anzeigt, dass die Daten zum Auslesen
bereit stehen. Danach werden die Daten ausgelesen, an die gewnschte
Stelle im Speicher geschrieben und die Anwendung erhlt wieder die
Kontrolle und kann mit ihrer Arbeit fortfahren.
Interrupt-gesteuert: Der Treiber initialisiert die Aufgabe und wartet
dann auf einen Interrupt (Unterbrechung) durch den Controller. Anstatt
nun - wie im vorigen Vorgehen - die CPU zu beschftigen, wartet der
Treiber darauf, durch den Interrupt geweckt zu werden. Das
Betriebssystem bekommt die Kontrolle und kann bis zum Eintreffen des
Interrupts andere Anwendungen bedienen.
Wenn der Controller mit seiner Arbeit fertig ist und den erwarteten
Interrupt erzeugt, wird die CPU in ihrer momentanen Arbeit unterbrochen,
ihr Zustand wird gerettet9 und die Unterbrechungsroutine wird aufgerufen.
Diese holt in unserem Falle die Daten vom Controller und legt sie an
entsprechender Stelle im Speicher ab. Anschlieend wird der alte Zustand
der CPU wieder hergestellt und die unterbrochene Arbeit fortgesetzt.
DMA (Direct Memory Access): dazu wird ein spezieller Baustein
bentigt, der Daten direkt, ohne Verwendung der CPU, zwischen
Speicher und Controller transportieren kann. Dieser Baustein muss mit
den entsprechenden Informationen initialisiert werden: Speicherstelle,
Anzahl der Bytes, Controller, Richtung. Nach Beendigung erzeugt der
DMA-Baustein einen Interrupt.
Die erste Methode (busy waiting) ist am einfachsten zu verwirklichen, belastet
aber den Prozessor. Gleichzeitige Bearbeitung von verschiedenen Anwendungen
wird dadurch stark behindert.
Die zweite Methode setzt zustzliche Hardware voraus: einen
InterruptController sowie eigene Leitungen fr die Versendung des Interrupts.
Dafr wird die CPU frei, whrend der Wartezeit der einen Anwendung andere
Anwendungen zu bedienen.
Die dritte Methode erfordert noch mehr Hardware-Aufwand: zustzlich zu
einem Interrupt-Controller wird ein DMA-Baustein bentigt. Der Vorteil liegt
darin, dass die CPU nun auch nicht mehr bentigt wird, um Daten zwischen
Speicher und Controller zu transportieren.
Das dient dazu, dass anschlieend die unterbrochene Arbeit fortgesetzt werden kann.

1.3 System Calls und Interrupts

11

ber Ein-/Ausgabe bzgl. Dateien wird in Kap. 8 weiter nachgedacht, System


Calls und Interrupts werden im Abschn. 1.3 und in den Kapiteln 2 und 7 nher
betrachtet.

1.3

System Calls und Interrupts

Das Betriebssystem stellt Dienste (Funktionen) bereit, die es den Anwendungen


ermglichen, die gewnschte Funktionalitt zu bekommen. Es stellt sich die
Frage, wie eine Anwendung darauf zugreifen kann. Zu Beginn der ersten
Rechner mit Betriebssystemen - und die Geschichte wiederholte sich mit dem
Aufkommen der PCs im DOS - waren im Speicher feste Einsprungadressen fr
die Funktionen des Betriebssystems vorgesehen. Die Anwendung fhrte eine
call-Anweisung auf die jeweilige Adresse aus, nachdem die Register mit den
bentigten Parametern gefllt waren. Am Ende der jeweiligen
Betriebssystemroutine wurde eine ret-Anweisung (Return) ausgefhrt, die
Anwendung konnte mit ihrer Arbeit weitermachen.
Der Nachteil dieses Vorgehens liegt auf der Hand: ohne Speicherschutz
konnten die Funktionen des Betriebssystems von fehlerhaften (oder auch
bswilligen) Anwendungen berschrieben werden und anschlieend eine andere
Funktionalitt aufweisen.
Fr eine sichere Benutzung wurde deshalb schon frh in der Entwicklung
der Betriebssysteme ein Konzept entwickelt, das Anwendungen den Zugriff auf
die Funktionalitt erlaubt, obwohl die Kontrolle ausschlielich beim
Betriebssystem liegt. Dies setzt eine geeignete Hardware-Erweiterung voraus:
der Speicherbereich, in den das Betriebssystem geladen wird, muss gegen
Zugriffe von Anwendungen geschtzt werden und der Prozessor muss zumindest
zwei Privilegien unterscheiden knnen: den User Mode (normaler Modus), in
dem die Anwendungen laufen, und den Kernel Mode (privilegierter Modus), in
dem die Betriebssystemroutinen ablaufen. Dabei kann insbesondere der Zugriff
auf Gerte, d.h. I/O-Aufrufe, nur im privilegierten Modus erfolgen.
1.3.1

Der eigentliche Aufruf

Anwendungen laufen immer im User Mode und mssen sich deshalb der System
Calls bedienen, um auf die angebotene Funktionalitt des Betriebssystems
zuzugreifen. Das prinzipielle Vorgehen beim System Call bei den verschiedenen
Betriebssystemen ist hnlich:

Die Argumente werden in Registern oder in einem Speicherblock in


vorgeschriebener Weise abgelegt.
Sodann wird, je nach Architektur der Hardware, ein spezieller
Systemaufruf oder ein Interrupt erzeugt. Genau dabei wird der
Prozessor in den Kernel Mode versetzt und das Betriebssystem erhlt
die Kontrolle.
Das Betriebssystem prft dann, welcher System Call aufgerufen werden
soll, und ruft diesen auf.

1 Einfhrung

12

Der System Call kann eine Reihe von berprfungen vornehmen, bevor
er die eigentliche Arbeit leistet: Soll eine Datei geffnet werden, so kann
der System Call zunchst prfen, ob der Benutzer auch dazu berechtigt
ist. Auf diese Weise kann das Betriebssystem Benutzerrechte
berwachen.
Nach Abarbeitung des System Calls kann das Betriebssystem einer
anderen Anwendung die Kontrolle bergeben - dies wird in der Regel bei
heute blichen Betriebssystemen geschehen, wenn erst noch auf Daten
von einer Ein-/Ausgabe gewartet werden muss - oder zu der Anwendung
zurckkehren, die den System Call ausgelst hat. In jedem Falle
schaltet der Prozessor mit der bergabe der Kontrolle an eine
Anwendung in den User Mode zurck.

1.3.2

Interrupts

Wir haben Interrupts bereits auf S. 10 kennengelernt. Sie spielen eine groe
Rolle bei der Bearbeitung. So dienen sie nicht nur zum Signalisieren des Endes
einer I/O-Anforderung, sie werden auch eingesetzt, um Fehler abzufangen und
darauf zu reagieren: Division durch Null ist nur ein Beispiel von vielen.
Aber auch andere wichtige Aufgaben werden damit gelst. Zum Beispiel
benutzen viele heutige Betriebssysteme den Speicher so, dass nicht die gesamte
Anwendung auf einmal in den Hauptspeicher geladen werden muss, sondern nur
ein Teil. Erzeugt die CPU dann eine Adresse, auf die die Anwendung zugreifen
will, so prft die Hardware 1
1. ob es sich um eine Adresse handelt, die zum Adressraum der
Anwendung gehrt. Ist diese nicht der Fall, so wird ein Illegal Address
Interrupt erzeugt. Das Betriebssystem beendet daraufhin die Anwendung
mit einer Fehlermeldung,
2. ob diese Adresse bereits im Hauptspeicher geladen ist. Wenn nicht, wird
ein Pagefault Interrupt erzeugt, der dazu fhrt, dass das
Betriebssystem den entsprechenden Teil der Anwendung an geeigneter
Stelle in den Hauptspeicher ldt.
Nachdem nun die Adresse im Hauptspeicher abgebildet ist, kann darauf
zugegriffen werden.
Wir haben gesehen, wie Anwendungen bearbeitet werden knnen, deren
Speicherbedarf grer als der Hauptspeicher ist. Nheres dazu in Kap. 5.
Ein weiterer Einsatz von Interrupts ist fr viele heutige Betriebssysteme
wichtig. Denken wir fr einen kurzen Moment daran, dass wir in der Regel
mehrere Anwendungen zugleich auf unserem Rechner laufen lassen. Wie kann
das sicher funktionieren?
Auf damaligen Grorechnern war sicheres Multitasking (gleichzeitige
Bearbeitung vieler Anwendungen) lngst Standard, als mit Windows 3.1 auf dem

1.4 Ressourcen

13

PC diese Mglichkeit erffnet wurde.10 * Die Lsung in Windows 3.1 lautete: durch
Aufruf von System Calls gab eine Anwendung freiwillig die Kontrolle ab und
ermglichte es anderen Anwendungen, die CPU zu bekommen. Was passiert aber,
wenn ein Programm aus Versehen oder absichtlich eine Endlosschleife enthlt,
die keine System Calls beinhaltet? Deshalb schlug man auch bei den PCBetriebssystemen den Weg ein, der viele Jahre zuvor schon bei Grorechnern
beschritten worden war: die Hardware-Uhr erzeugt in regelmigen kurzen
Abstnden einen Timer-Interrupt, dadurch erhlt das Betriebssystem die
Kontrolle und kann einer Anwendung die CPU entziehen, um sie einer anderen
zuzuteilen. Dabei entsteht nun folgendes Problem: wie kann eine Anwendung,
nachdem ihr die CPU entzogen wurde, richtig weiterarbeiten, wenn sie die CPU
wieder zugeteilt bekommt. Um das zu verstehen, mssen wir genauer ber den
Begriff Anwendungen nachdenken. Offensichtlich gehrt mehr dazu, als nur
den Programmcode in den Speicher zu laden und die CPU diesen dann
abarbeiten zu lassen. Solange die CPU fr die Anwendung ttig ist, mag diese
Sicht einigermaen ausreichen, doch was ist, wenn sie entzogen wird? Damit die
Anwendung an genau der Stelle weitermachen kann, an der die CPU entzogen
wurde, mssen Informationen aufbewahrt werden: Der Zustand der CPURegister muss gerettet werden und bei erneuter Zuteilung mssen die Register
wieder mit der geretteten Information initialisiert werden. Programmcode,
Speicherplatz fr die CPU-Register, Datensegmente, Prioritten, Ressourcen
(vgl. Abschn. 1.4) und Rechte fasst man im Begriff Prozess zusammen. Der
Algorithmus, der vom Betriebssystem verwendet wird, um nach einem derartigen
Interrupt denjenigen Prozess auszuwhlen, der die CPU bekommen soll, wird als
Scheduling bezeichnet. Kap. 3 betrachtet Prozesse detaillierter, Kapitel 4 geht
auf das Scheduling ein.

1.4

Ressourcen

Ein Prozess bentigt in der Regel mehr als nur Hauptspeicher und - wenn er
aktiv ist - die CPU. Tatschlich greift er auch auf Gerte, Dateien usw. zu.
All diese Objekte, die ein Rechner zur Verfgung stellt, werden mit dem
Begriff Ressource belegt. So kann es Ressourcen geben, die mehrfach im System
vorhanden sind - beispielsweise mehrere Plattenpartitionen, zwei DVDLaufwerke - und Ressourcen, die aus wirtschaftlichen oder Nutzer-bedingten
Ansprchen nur einmal zur Verfgung stehen - CPU (es sei denn, wir haben ein
Mehrprozessor-System), Plotter11 usw.
Neben der Anzahl vorhandener Exemplare einer Ressource ist von groer
Bedeutung, ob man die Ressource einem Prozess entziehen kann. Wie wir im
Es sei angemerkt, dass das Betriebssystem OS/2 bereits echtes Multitasking auf dem
PC eingefhrt hatte, jedoch wurde dieses Betriebssystem bei Weitem nicht so bekannt wie
Windows.
n
Natrlich kann es in einem Rechner mehrere geben. Aber in der Regel wird man in
einem PC nur einen vorfinden.
10

1 Einfhrung

14

vorigen Abschnitt gesehen haben, kann das Betriebssystem einem Prozess die
CPU entziehen, um sie einem anderen Prozess zuzuteilen. Doch gilt das nicht fr
alle Ressourcen: Wenn ein Prozess gerade Daten auf den DVD-Writer sichert und
das Betriebssystem in dieser Zeit dem Prozess den DVD-Writer entzieht, dann
wre die DVD unwiederbringlich zerstrt.
Besondere Bedeutung bekommt der Begriff Ressource im Abschn. 6.2.

1.5

Zusammenfassung

Wichtige Konzepte aus dem Gebiet Betriebssysteme sind angerissen worden:

Die enge Verknpfung von Hardware und Betriebssystem,


System Calls und Interrupts, um Dienste des Betriebssystems
anzufordern,
der Begriff Prozess als Zusammenfassung aller momentan bentigten
Ressourcen, Rechte, Prioritten usw., um eine Anwendung zu
bearbeiten,
Timer-Interrupt und Scheduling als Methode des Betriebssystems,
Prozesse gleichzeitig zu bearbeiten,
prinzipielle Vorgehensweisen beim I/O,
Hauptspeicherverwaltung, um die Grenzen des physischen
Hauptspeichers zu berwinden,
das Filesystem sowie die
Modularisierung des Systems.

Die folgenden Kapitel sollen dazu dienen, verschiedene eben betrachtete Aspekte
zu vertiefen, indem sie jeweils einen besonderen Schwerpunkt herausgreifen. In
jedem Kapitel werden zunchst allgemeine Vorgehensweisen vorgestellt, die man
als grundlegende Prinzipien fr die Implementierung des jeweiligen Aspekts
bezeichnen knnte. Doch konkrete Implementierungen weichen blicherweise ein
wenig von diesen Prinzipien ab. Die sich anschlieenden Abschnitte zeigen
unterschiedlich stark detailliert, wie die konkrete Implementierung in Linux
aussieht.
Ein besonderes Problem fr Linux stellt sich dadurch, dass Linux nicht auf
eine Hardware-Plattform ausgerichtet ist, sondern auf einer groen Zahl
unterschiedlicher Plattformen performant laufen soll. Hier erweist sich die eben
erwhnte enge Verknpfung zwischen Hardware und Betriebssystem als
besondere Schwierigkeit. Linux packt dieses Problem so an, dass der Quellcode in
einen allgemeinen Hardware-unabhngigen Teil und in Hardware-spezifische
Teile gegliedert wird. In diesem Buch werden wir uns auf die x86-Architektur
beschrnken, da diese blicherweise zur Verfgung stehen wird.
Um hinreichend performant zu sein, greift Linux in den Hardwarespezifischen Code-Teilen zum Trick, besonders zeitkritische oder besonders
Hardware-abhngige Funktionen in Assembler zu programmieren. Dies wird
durch den verwendeten C-Compiler gut untersttzt. Die im folgenden gewhlte

2
System Calls

2.1

Von der Anwendung zum System Call

System Calls stellen fr Benutzer-Prozesse die einzige Schicht zum Zugriff auf
die Funktionalitt des Betriebssystems und damit zur Benutzung der Hardware
dar (vgl. S. 6). Dadurch wird es berhaupt erst mglich, ein Betriebssystem so zu
entwickeln, dass es auf unterschiedlichen Hardware-Plattformen installiert
werden kann, und die Besonderheiten von Peripheriegerten vor dem Anwender
zu verbergen, so dass auf derselben Plattform diverse Peripheriegerte ohne
nderung des Benutzerprogramms eingesetzt werden knnen. Weiterhin dienen
System Calls der Sicherheit, indem alle bergebenen Parameter einschlielich
aller Rechte zentral sorgfltig geprft werden. Nur dadurch ist es mglich, eine
vernnftige Vergabe der Ressourcen und eine Stabilitt des Systems zu
garantieren.
In den Anwendungsprogrammen ist von System Calls nichts zu sehen. Da
wird eine Datei mit fopen() geffnet. Doch vom Anwendungsprogramm bis zur
Ausfhrung passiert Einiges: Nach dem bersetzen des Programms muss es
noch mit einer C-Bibliothek (englisch: Library) - glibc - gelinkt werden, bevor es
ausgefhrt werden kann. Die Aufgabe dieser Bibliothek ist es, die Standard-CAufrufe in System Calls umzusetzen. Dies - sowie die folgenden Schritte - wird in
Abschn. 2.2 beschrieben.
Bei einem System Call handelt es sich nicht um einen normalen
Funktionsaufruf, da die Bearbeitung im Kernel und somit in einer Umgebung
stattfindet, die nur im Kernel Mode erreicht werden kann (vgl. Abschn. 1.3). Aus
diesem Grunde ersetzt die Bibliothek den Namen des gewnschten System Calls
durch eine Nummer - die Nummerierung kann der Datei include/asmi386/unistd.h entnommen werden. Nachdem diese Nummer in ein geeignetes
Register (eax bei der x86-Architektur) abgelegt worden ist und die zu
bergebenden Parameter entsprechend den Konventionen aufbereitet worden
sind, wird ein Software-Interrupt (int $0x80 = 128. SoftwareInterrupt) erzeugt.
Dadurch schaltet der Prozessor automatisch in den Kernel Mode und ruft die
ber die Nummer adressierte Handler-Routine auf.

2 System Calls

16

2.2

Der Handler fr System Calls

Der System Call Handler ist in Assembler geschrieben und somit von der
jeweiligen Architektur abhngig. Die folgenden Bemerkungen beziehen sich auf
die x86-Architektur. Auch wenn das Prinzip das gleiche ist, unterscheiden sich
die Details einschlielich der Dateinamen usw. bei den anderen Architekturen
von dieser Darstellung. Die entscheidenden Dateien befinden sich im Verzeichnis
arch/i386/kernel1.
Die Datei entry.S enthlt die Routine system_call. Die Aufgabe dieser
Routine besteht darin, die Register zu retten, an Hand des entsprechenden
Registers den gewnschten System Call aufzurufen* 2, die Rckgabe des
Returncodes (Rckgabewert) vorzubereiten und den Kernel Mode zu verlassen,
damit die Arbeit fortgesetzt werden kann.
An Hand des fopen()-Aufrufs eines Anwendungsprogramms (vgl. Listing 2.1)
sollen nun die Schritte aufgezeigt werden, die das Betriebssystem unternimmt.
Das Programm ffnet eine Datei mittels fopen() und endet dann.
#include <stdio.h>
main (int argc, char *argv[])
{ int fid, rc;
rc =
fopen("testal.c","r");
exit(0);
Listing 2.1. Programm mit fopen()

fopen() wird innerhalb der Bibliothek glibc - oder hnlicher Bibliotheken umgewandelt in den Aufruf open(). Mit Hilfe des Dienstprogramms strace, das es
ermglicht, System Calls beim laufenden Programm zu protokollieren, kann man
sehen, dass wirklich open() aufgerufen wird. Die letzten Zeilen der Ausgabe sind
im Listing 2.2 zu finden.
open("testal.c", 0_RD0NLY)
exit_group(0)

= 3
= ?

Listing 2.2. Ausgabe von strace ./demo

*Der Pfad zum Directory, in dem der Linux-Quelltext gespeichert ist, wird
grundstzlich nicht aufgefhrt.
Dies erfolgt in der x86-Architektur ber das in arch/i386/kernel/entry.S
enthaltene Array entry_call_table.
2

2.3 Direkte Aufrufe

17

Frage: Wie mssen Sie strace mit diesem Programm zusammen einsetzen und
wie knnen Sie die gesamten Ausgaben dieses Programms interpretieren?
Der Aufruf von open() wird in der Bibliothek (Library) mit int $0x80 so
durchgefhrt, dass system_call() mit der Nummer 5 und den brigen
Argumenten aufgerufen wird. Der Handler ruft auf Grund der bergebenen
Nummer die Routine sys_open()3 auf. Sie sucht mittels get_unused_fd() den
nchsten freien Filedeskriptor und ruft dann die Funktion filp_open() und
df_install() auf, um das eigentliche ffnen vorzunehmen. Die Funktion
sys_open() gibt entweder einen Fehlercode (Integer < 0) oder einen Filedeskriptor
(Integer > 0) an system_call() zurck.
Im Fehlerfalle werden zur besseren Lesbarkeit symbolische Konstanten
benutzt, die in den Dateien errno-base.h bzw. errno.h4 definiert sind.
strace greift auf den System Call ptrace() zurck, ptrace() dient aber nicht
nur zur Verfolgung von System Calls, er wird allgemein benutzt zum Auslesen
und Verndern von Werten im Adressraum eines Prozesses. Ein Debugger wie
gdb ist auf diesen System Call angewiesen.

2.3

Direkte Aufrufe

System Calls werden von glibc untersttzt. Das gilt aber nur fr bekannte
System Calls. Soll ein neuer System Call ausgetestet und implementiert werden,
so fehlt diese Untersttzung noch. Es muss deshalb einen Weg geben, die AufrufKonventionen auf einfach nutzbare Weise bereitzustellen.
Der bei Linux eingeschlagene Weg benutzt Makros, mit denen die WrapperRoutinen automatisch erzeugt werden. Diese Routinen sind natrlich ebenfalls
Hardware-spezifisch. Soll der open()-Aufruf auf diese Weise ohne Einsatz von
glibc verwendet werden, so muss auf das entsprechende Linux-Makro
zurckgegriffen werden. Der Aufruf von open() lautet:
long open(const char *filename, int flags, int mode)

Im Listing 2.3 wird der Aufruf in das entsprechende Makro _syscallnr5


umgesetzt. Die zum Namen des Makros gehrende Zahl nr gibt die Anzahl der
Argumente an.
#define NR_open 5
_syscall3(long, NR_open, const char *, filename, int, flags,
int, mode)
Listing 2.3. Umsetzung von fopen() in System Call

Definiert in fs/open.c.

Beide Dateien sind in include/asm-generic enthalten.


Das Makro ist in include/asm-i386/unistd.h
definiert.
4
5

18

2 System Calls

Die Definition des Makros ist im Listing 2.4 dargestellt. Der Compiler ersetzt
den Aufruf durch den entsprechenden Assembler-Code. Dieser ist so gestaltet,
dass in der x86-Architektur die richtigen Register mit den Argumenten geladen
werden, der Software-Interrupt ausgefhrt wird und ber das Makro
__syscall_return der Rckgabewert zurckgegeben wird.
#define _syscall3(type,name,typel,argl,type2,arg2,type3,arg3)
\ type name(typel argl,type2 arg2,type3 arg3) \
long res ; \
: "=a" (_
_res) \
: "0" (__NR_##name),"b" ((long)(argl)),"c" ((long)(arg2)), \
"d" ((long)(arg3))); \
__syscall_return(type,__res); \

Listing 2.4. syscall-Wrapper mit 3 Argumenten Anmerkung: der


Backslash (\) dient dazu, die Zeile fortzusetzen, da die MakroAnweisung #define in
einer Zeile geschrieben werden muss.

2.4

Zusammenfassung

Der Weg vom Funktionsaufruf bis zum Aufruf der gewnschten Kernel Funktion
im Kernel einschlielich Umschaltung in den Kernel Mode wurde am Beispiel
von fopen() betrachtet. Dabei wurde mit strace ein Werkzeug zum
dynamischen Verfolgen der Kernel-Aufrufe kennengelernt.
Mit Hilfe von Makros steht ein Mechanismus fr den Aufruf neuer System
Calls bereit, fr die es noch keine Bibliotheksaufrufe gibt.

3
Prozesse und Threads

3.1

Grundlagen

Grundlegend fr das Verstndnis von Betriebssystemen ist der Begriff Prozess,


der sptestens dann bentigt wird, wenn ein System MultitaskingBetrieb
untersttzen soll.
Eine vereinfachende Darstellung lautet: Ein Prozess ist ein Programm in
Bearbeitung. Diese Aussage beschreibt jedoch nicht, welche Informationen ein
Prozess enthalten muss. Ein Prozess besteht aus mehreren Speichersegmenten:
dem Programmcode, dem bzw. den Datensegmenten, Stack und Heap-Bereichen.
Hinzu kommen die Ressourcen, die ein Prozess beansprucht, geffnete Dateien
sowie Hardware, die exklusiv belegt werden muss, wie etwa ein DVD-Writer.
Natrlich gehren auch die CPU-Register dazu - wenn der Prozess gerade
rechnet, d.h. die CPU besitzt. Doch was geschieht, wenn ein anderer Prozess
die CPU belegt? Die Registerinhalte des ersten Prozesses mssen aufbewahrt
werden, damit spter der Zustand des ersten Prozesses rekonstruiert werden kann und
er an der Stelle, an der er unterbrochen wurde, seine Arbeit wieder aufnehmen
kann.
Der Process Control Block (PCB) dient der Aufnahme aller dieser
Informationen. Je nach Betriebssystem - und ggf. Hardware - ist der PCB
unterschiedlich ausgelegt. In der Regel enthlt er unter anderem zustzlich zu
den eben aufgefhrten noch folgende Eintrge:
Die Prozess-ID (PID):
Diese identifiziert die verschiedenen Prozesse eindeutig,
die Prioritt, mit der der Prozess ausgefhrt werden soll:
Prozesse mit hherer Prioritt werden Prozessen mit geringerer Prioritt
vorgezogen,
Rechte, die der Prozess besitzt:
Diese bestimmen, ob das Betriebssystem den Zugriff auf eine angeforderte
Ressource erlauben darf oder nicht,

3 Prozesse und Threads

20

Grenzen bzgl. der angeforderten Ressourcen:


Wie viel CPU-Zeit der Prozess verbrauchen darf,
wie viele Dateien er gleichzeitig ffnen darf,
welche Datenmengen er in die Dateien schreiben darf,
wie viel Speicher er anfordern darf,

die tatschlich verbrauchten Ressourcen:


- Die bislang verbrauchte CPU-Zeit,
- die Anzahl der derzeit geffneten Dateien,

Der Prozess besitzt einen einzigen Pfad, auf dem die Instruktionen durch die
CPU abgearbeitet werden. Im englischen Sprachgebrauch nennt man das einen
Thread (Instruktionsfaden).
3.1.1

Linux Process Control Block

Die Struktur des PCB findet man unter dem Namen task_struct1.
Zu Beginn dieser Struktur (vgl. S. 21) stehen Informationen zum Scheduling,
darauf wird in Kap. 4 eingegangen.
Die Kommentare zeigen die Stellen, an denen die Prozess-ID, die
Benutzerund Group-Informationen (S. 21) und an denen die CPU-Register (S. 23)
gespeichert werden. Die Festlegung des Speicherbereichs fr die CPU-Register
ist natrlich vllig Hardware-abhngig; deshalb verwundert es auch nicht, dass
die Definition dieser Struktur im Prozessor-abhngigen Teil zu finden sind: include/asmi386/processor.h.

Die Ressourcen umfassen neben dem Zugriff auf den Speicher (S. 21) auch
Informationen ber das Filesystem, geffnete Dateien und IPC-Strukturen1 2.
Es sind ferner Verweise3 zu den Kindern, zum Eltern-Prozess und zu dessen
Kindern vorhanden (vgl. S. 22).
3.1.2

Weiterfhrende Konzepte

Kurz nach den Pointern zu den Kindern usw. enthlt der PCB Eintrge, die dazu
dienen, auf das Ende spezieller Kinder zu warten. Darauf wird im Abschn. 3.3
Bezug genommen.
Danach folgen Eintrge, die mit einem besonderen Aspekt des Scheduling zu
tun haben: Real-Time-Scheduling. Der Real Time Scheduler wird im Abschn. 4.3
nher vorgestellt.

Definiert in der Include-Datei include/linux/sched.h.

Vgl. Kap. 9.

In C handelt es sich um Pointer (Zeiger), weswegen ich im folgenden den Begriff


Pointer verwenden werde.
3

3.1 Grundlagen 21
struct task_struct {
/* === Informationen zum Scheduling === */
volatile long state;
/* -1
unrunnable,
0 runnable,
>0 stopped
struct thread__info *thread_info;
*/
atomic_t usage;
unsigned long flags;
/* per process flags
unsigned long
ptrace;
int lock_depth;

defined below */

/* Lock depth */

int prio, static_prio; struct list_head


run_list; prio_array_t *array;

unsigned long
sleep_avg; long
interactive_credit;
unsigned long
timestamp; int
unsigned long policy; cpumask_t cpus_allowed;
unsigned int time_slice, first_time__slice;

struct list_head tasks;


struct list_head ptrace_children;
struct list_head ptrace_list;
struct mm_struct *mm, *active_mm; /* Speicher */
/* === Codes fr Ende, Prozess-ID, Elternprozess, Kinder usw
*/
struct linux_binfmt *binfmt;
int exit_code, exit_signal;
int pdeath_signal;
/* Signal bei Elterntod */
unsigned long personality;
int did_exec: 1 ;
Prozess-ID */ Prozesspid_t pid;
/*
Group */
pid-t __pgrp;
/*
pid_t tty_old_pgrp;
pid_t session;
pid_t tgid;
/* boolean value for session group leader */
int leader;

Listing 3.1. PCB unter Linux (1. Teil)

22

3 Prozesse und Threads


/* pointers to (original) parent process, youngest child,
* younger sibling, older sibling, respectively.
* (p->father can be replaced with p->parent->pid)
*/
struct task_struct *real_parent;
struct task_struct *parent; /* Eltern Prozess */ struct
list_head children;
/* Liste der Kinder */
struct list_head sibling;
/* Geschwisterprozesse */
struct task_struct *group_leader; /* threadgroup leader */
/* PID/PID hash table linkage.
*/ struct pid_link
pids[PIDTYPE_MAX];
wait_queue_head_t wait_chldexit; /* for wait4()
*/ struct completion *vfork_done; /* for vfork()
*/
int user *set_child_tid;
/* CLONE_CHILD_SETTID */
int user *clear_child_tid;
/* CLONE_CHILD_CLEARTID */
unsigned long rt_priority;
/* Real Time Scheduling */
unsigned long it_real_value, it_prof_value,
it_virt_value; unsigned long it_real_incr, it_prof_incr,
it_virt_incr; struct timer_list real_timer;
struct list_head posix_timers; /* POSIX.lb IntervalTimers */
unsigned long utime, stime, cutime, cstime; unsigned long
nvcsw, nivcsw, cnvcsw, cnivcsw;
/* context switch counts*/
u64 start_time;
/* mm fault and swap info: this can arguably be seen as
* either mm-specific or threadspecific */
unsigned long min_flt, maj_flt, nswap, cmin_flt,
cmaj _flt, cnswap;
/* === Benutzer- und Group-Informationen === */
uid_t uid,euid,suid,fsuid; gid_t
gid,egid,sgid,fsgid; int ngroups;
gid_t
groups[NGROUPS];
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
int keep_capabilities:l; struct user_struct *user;
/* === Beschrnkungen === */
struct rlimit
rlim[RLIM_NLIMITS]; unsigned
short used_math; char comm[16] ;

Listing 3.2. PCB unter Linux (2. Teil)

3.1 Grundlagen
/* === Ressourcen === */
int link_count, total_link_count;
struct tty_struct *tty;
/* NULL if no tty */
struct sysv_sem sysvsem;
/* IPC */
struct thread_struct thread; /* CPU-Register
struct fs_struct *fs;
usw.*/ /* Filesystem */
struct files_struct *files;
/* geffnete Dateien */
struct namespace *namespace; /* Namespace */
struct signal_struct *signal; /* Offene Signale, Handler */
struct sighand_struct *sighand;
sigset_t blocked, real_blocked;
struct sigpending pending;
unsigned long sas_ss_sp; size_t
sas_ss_size; int (*notifier)
(void *priv); void
*notifier_data; sigset_t
*notifier_mask;
void *security;
/* Thread group tracking */ u32
parent_exec_id; u32
self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty */
spinlock_t alloc_lock;
/* Protection of proc_dentry:
* nesting proc_lock, dcache_lock,
* write_lock_irq(&tasklist_lock);

*/

spinlock_t proc_lock;
/* context-switch lock */
spinlock_t switch_lock;

/* journalling filesystem info


*/ void *journal_info;
/* VM state */
struct reclaim_state *reclaim_state;
struct dentry *proc_dentry;
struct backing_dev_info *backing_dev_info;
struct io_context *io_context;
unsigned long ptrace_message;
siginfo_t *last_siginfo;

/* For ptrace use. */

Listing 3.3. PCB unter Linux (3. Teil)

23

3 Prozesse und Threads

24

Auf Seite 22 befindet sich ein Eintrag vom Typ user_struct. Dieser verweist
auf eine Struktur, die Informationen ber den Eigentmer des Prozesses enthlt.
Zur Zeit sind diese Informationen beschrnkt auf die Anzahl an Prozessen sowie
die Anzahl geffneter Dateien, doch ist damit zu rechnen, dass in einem spteren
Release mehr Informationen gesammelt werden.
Ein besonderes Problem stellen die unterschiedlichen Unix-Varianten dar.
Natrlich mchte man nicht jedes Programm, das auf einer anderen
UnixVariante luft, fr Linux neu anpassen und kompilieren mssen, da dies
insbesondere aus kommerzieller Sicht wenig berzeugt. Linux benutzt zu diesem
Zweck das Konzept der Personality; vor der Prozess-ID befindet sich ein Eintrag
mit gleichem Namen. Mittels personality kann fr eine Reihe von Unix-Varianten
erreicht werden, dass Ausfhrungsumgebungen bereitgestellt werden, innerhalb
derer solche Prozesse lauffhig sind.
3.1.3

Die Prozess-ID

Prozesse werden ber die Prozess-ID identifiziert. Viele Zugriffe auf einen
Prozess basieren darauf, dass ber die Prozess-ID der zugehrige PCB gesucht
wird. Die PCBs der im System vorhandenen Prozesse mssen aufjeden Fall im
Speicher direkt zugreifbar gehalten werden, weil sie die Information ber die
belegten Ressourcen enthalten. Deshalb she eine mgliche Implementierung so
aus, dass an fester Stelle im Speicher ein Array vorgesehen wird, dessen
Elemente die PCBs sind. Die Prozess-ID knnte dann als Index zum Zugriff
benutzt werden.
Problem: Wird Linux als Betriebssystem fr einen PC genutzt, der fr
Broarbeiten verwendet wird, so zeigt das Programm ps -A4 vielleicht 50
Prozesse an, die hchste ausgegebene Prozess-ID liegt dabei hufig ber 4000.
Das eben skizzierte Vorgehen wrde bedeuten, dass ein nicht unbetrchtlicher
Teil des Speichers ungenutzt bliebe.
Die Linux-Implementierung findet man in kernel/pid.c und den Include- Dateien
include/linux/pid.h und include/linux/hash.h. Diese Implementierung zeigt ein
schnes Beispiel fr den Konflikt zwischen Zugriffsgeschwindigkeit und
Speicherbelegung: Statt die PCBs als Array zu verwalten, wird Hashing
verwendet. Wird ein neuer Prozess angelegt, so wird die Prozess-ID als HashSchlssel benutzt, an der berechneten Hash-Adresse wird im Prinzip der Pointer
auf den neuen PCB eingetragen.
Hashing bedeutet bekanntlich, dass ein groer, wenig genutzter Adressraum auf einen kleinen Raum abgebildet wird. Dabei kann es zu Konflikten
kommen, wenn eine Adresse auf einen bereits belegten Platz abgebildet werden
soll. Linux geht mit Hash-Konflikten folgendermaen um: an jeder durch den
Hash-Algorithmus berechneten Stelle befindet sich der Kopf einer verketteten

ps zeigt im System befindlichen Prozesse an. Die Option -A bewirkt, dass alle Prozesse
angezeigt werden.
4

3.2 Erzeugen von Prozessen

25

Liste, die Eintrge fr alle Prozesse mit gleicher Hash-Adresse enthalten. Die
Listeneintrge enthalten neben den notwendigen Listenpointern die jeweilige
Prozess-ID und einen Pointer zum jeweiligen PCB.
Wird nach einem bestimmten Prozess gesucht, so wird ber die Prozess- ID
die Hash-Adresse berechnet und die verkettete Liste nach der gewnschten
Prozess-ID linear durchsucht. Diese Liste enthlt nur einen sehr kleinen Teil
aller Prozesse und kann somit schnell durchsucht werden.
3.1.4

Sessions und Process Groups

Im PCB sind auf S. 21 weitere wichtige Eintrge zu finden: zur Session und zur
Prozessgruppe.
Mit einem login meldet sich der Benutzer an. Der ihm zugeordnete Prozess in der Regel der erste fr den Benutzer gestartete Kommandoprozessor (Shell) ist der Session-Leader. Die Login-Sitzung enthlt alle Prozesse, die von diesem
abgeleitet sind. (vgl. Abschn. 3.2).
Eine Prozessgruppe dient der Steuerung zusammengehrender Prozesse.
Gibt der Benutzer z.B. das Kommando cat datei | more5 ein, so erzeugt die Shell
aus den beiden Prozessen cat und more eine Prozessgruppe, die die Shell wie
einen einzigen Prozess behandeln kann. Die Eingabe von Ctrl-C, d.h. ein Abbruch
(vgl. Abschn. 6.4.1), wirkt sich somit automatisch aufbeide Prozesse aus.

3.2

Erzeugen von Prozessen

3.2.1

Der ursprngliche fork()-Aufruf

In einem Multi-User, Multitasking-System wie Unix muss das Betriebssystem


neue Prozesse erzeugen knnen.
Dies geschieht mittels des fork()-Aufrufs. Das Betriebssystem berprft, ob
der Prozess weitere Prozesse erzeugen darf. Da jeder Prozess Ressourcen in
Anspruch nimmt, knnte das Betriebssystem feststellen, dass bereits zu viele
Prozesse im System vorhanden sind und daraufhin den Aufruf mit einem Fehler
beenden. Es knnte auch sein, dass der Benutzer sein Limit an Prozessen
ausgeschpft hat. Ein solches Benutzer-bezogenes Limit wird vorgegeben, damit
ein Benutzer nicht alle Prozess-Ressourcen fr sich in Anspruch nehmen kann.
Nach einer erfolgreichen berprfung ermittelt das Betriebssystem eine
freie Prozess-ID, erstellt einen neuen PCB fr diese Prozess-ID und erzeugt eine
vollstndige Kopie des aufzurufenden Prozesses, d.h. der Adressraum wird
vollstndig kopiert. Damit bekommt der neu erzeugte Prozess Zugriff auf
dieselben Ressourcen wie der erzeugende Prozess, so z.B. Zugriff auf die

Bildschirmweise Ausgabe der Datei datei.

26

3 Prozesse und Threads

geffneten Dateien. Beide Prozesse - der erzeugende sowie der durch fork() neu
erzeugte - setzen die Arbeit nach dem fork()-Aufruf fort. Der einzige Unterschied6
liegt im Rckgabewert dieses System Calls. Damit kann ein Prozess nach dem
fork-Aufruf erkennen, ob er der Eltern- oder ein Kindprozess ist.
Der nachfolgende Programmausschnitt im C-Code, der den fork()-Aufruf
beinhaltet, zeigt noch weitere wichtige Aspekte: Jeder System Call in C ist eine
Funktion, die ber den Rckgabewert Erfolg bzw. Misserfolg des Aufrufs anzeigt.
In der Regel gilt:

Ist der Rckgabewert kleiner als 0, so konnte der System Call nicht
ausgefhrt werden; in diesem Falle sollte eine genauere Fehleranalyse
und -behandlung durchgefhrt werden.
Der Rckgabewert 0 zeigt an, dass der Aufruf korrekt verarbeitet wurde.

Im Falle der Funktion fork() haben positive Rckgabewerte und 0 eine besondere
Bedeutung:

Der Wert 0 wird an den erzeugten Kindprozess zurckgegeben.


An den Elternprozess wird ein positiver Wert zurckgegeben, der der
Prozess-ID des erzeugten Kindes entspricht. Der Elternprozess kann
damit das Kind adressieren, d.h. ansprechen, - beispielsweise um das
Kind zu beenden.
int pid; /* Variable, die die ProzessId enthaelt */
pid = fork(); if (pid > 0)
printf("Eltern\n")
; else if (pid ==
0)
printf("Kind\n");
else {
printf("Fehler bei fork-Aufruf\n");
/* weitere Fehlerbehandlung */

>

Die Abb. 3.1 auf Seite 27 beschreibt die Situation im Speicher direkt vor und
direkt nach dem fork()-Aufruf. Dabei fllt deutlich ins Auge, dass anschlieend
sowohl der ursprngliche Prozess als auch der neu erzeugte Kindprozess den
gleichen Programmcode ausfhren. Jedoch sorgt die if-Anweisung, in der der
Rckgabewert berprft wird, dafr, dass nun andere Teile des Codes bearbeitet
werden.
3.2.2

Ausfhren eines anderen Programms: exec()

Ein neuer Prozess soll blicherweise ein anderes Programm als das vorherige
ausfhren. Dazu muss in den Adressraum des Kindes ein neues Programm

6
Es gibt noch weitere Unterschiede: Die Eigenschaft des Session-Leaders (vgl. Abschn.
3.1.4) darf natrlich nicht kopiert werden.

3.2 Erzeugen von Prozessen

27

Ausgabe:

Prozesse
Eltern

Kind

Zeitpunk
t des
forkAufrufs

Abb. 3.1. Auswirkung des fork()-System Calls

geladen werden, was dann zur Ausfhrung gebracht wird. Dies kann in
demjenigen Codeabschnitt geschehen, der vom Kind durchlaufen wird. Der
folgende Programmausschnitt zeigt den Zweig, der vom Kindprozess durchlaufen
wird
else
if (pid == 0) { int
rc;
rc = execl("/bin/ls", "ls", "-1", (char *)0 ); printf("Fehler bei
execl-Aufruf: %d\n", rc); exit(l);

Die Familie der exec()-System Calls7 bernimmt die Aufgabe, ein neues
Programm in den Adressraum zu laden und auszufhren. Damit ist auch der
vorherige Programmcode verschwunden. Das hat zur Folge, dass bei einem
erfolgreichen Aufruf von execl() die nachfolgenden Anweisungen printf() und
exit() nicht mehr vorhanden sind, sondern stattdes- sen das Programm /bin/ls
geladen und ausgefhrt wird. Sollte allerdings der execl()-Aufruf fehlschlagen, so
werden die nachfolgenden Anweisungen printfO- und exit() abgearbeitet.
Die Aufrufe der exec()-Familie unterscheiden sich in der Art, wie die Argumente
bergeben werden. In diesem Fall werden mehrere Zeichenketten

7
Im Folgenden wird kurz von exec() gesprochen, obwohl es diese Funktion gar nicht
gibt. Gemeint ist dann immer das entsprechende Mitglied aus der Familie der exec. . ()Calls.

28

3 Prozesse und Threads

bergeben, (char *)0 markiert das Ende. Die Bedeutung der einzelnen
Argumente ist:
1.
2.
3.

Das auszufhrende Programm,


der Name des Programms und
die an das Programm zu bergebenden Argumente.

In diesem Falle soll also /bin/ls -1 ausgefhrt werden, d.h. die DirectoryEintrge
sollen mit allen Detail-Informationen ausgegeben werden.
Mit dem exit()-System Call wird ein Prozess beendet (vgl. Abschn. 3.3), das
Betriebssystem gibt daraufhin die von diesem Prozess angeforderten Ressourcen
frei, so z.B. den Speicher, geffnete Dateien usw.
3.2.3

Prozesse und Vererbung

Wird ein Betriebssystem konzipiert, so knnen hinsichtlich der Ausfhrung von


Prozessen eine Reihe von Entscheidungen getroffen werden:

Ausfhrung:
Elternprozess und Kind werden gleichzeitig durchgefhrt oder
der Elternprozess wartet darauf, dass das Kind beendet wird.
Ressourcen:
Eltern und Kind teilen sich alle Ressourcen,
das Kind teilt sich mit dem Elternprozess einen Teil der Ressourcen oder
Kind und Eltern haben keine Ressourcen gemeinsam.
Adressraum:
Das Kind ist ein Duplikat des Elternprozesses oder
das Kind fhrt durch automatisches Laden ein neues Programm aus.

Die Entscheidungen, die in Unix getroffen wurden, sind nach den


vorangegangenen berlegungen klar. Durch den fork()-System Call werden

Eltern- und Kindprozess gleichzeitig ausgefhrt,


das Kind teilt mit dem Elternprozess einen Teil der Ressourcen
(geffnete Dateien) und
das Kind ist ein Duplikat des Elternprozesses.

Ein Blick auf den PCB unter Linux (vgl. S. 21, ff.) lsst vermuten, dass auch
geffnete Dateien zu den vererbten Ressourcen zhlen.
Mit zwei einfachen Programmen test.c und testl.c kann dieses Verhalten
demonstriert werden. Auf jegliche Fehlerbehandlung ist dabei bewusst verzichtet
worden. Die Programme werden - wie auch schon das vorige Beispiel - mit gcc
bersetzt:
gcc -o test test.c
gcc -o testl
testl.c

und mit dem Aufruf

3.2 Erzeugen von Prozessen 29

7. Programm
test.c #include
<stdio.h>
#include <fcntl.h>
#include <errno.h>
main (int argc, char *argv[]) { int pid,
fhd, rc, status; fhd =
open("xxx",0_WR0NLY|0_CREAT,0666); rc
= write(fhd,"Eltern\n",7); pid =
fork(); if (pid == 0) {

rc =
execl("testl","testl","",0);
exit(0);

}
else if (pid > 0) {
rc = waitpid(pid, &status, 0);

>

rc write(fhd,"..done\n",7);
close(fhd); exit(0);
Listing 3.4. Programm test. c
7, Programm testl.c #include <stdio.h>
#include <fcntl.h>
main(int argc, char *argv[]) { int rc,
fhd = 3; rc = write(fhd,"Kind\n",5);
exit(0);

Listing 3.5. Programm testl.c: Vererbung geffneter Dateien

./test

ausgefhrt (vgl. Listings 3.4 und 3.5). Die simple Verwendung von 3 als
FileHandle ist dabei ein wenig problematisch, sollte aber unter Linux bei diesem
einfachen Beispiel problemlos funktionieren.
Nicht in jedem Falle ist gewnscht, geffnete Dateien auch ber einen exec()Aufrufhinweg geffnet zu lassen. Der System Call fcntl() kann dazu benutzt
werden, dieses Verhalten auszuschalten (vgl. Kap. 8).

30

3 Prozesse und Threads

Frage: Lassen sich aus dem PCB weitere Ressourcen erkennen, die
mglicherweise vererbt werden?
Dem PCB nicht direkt zu entnehmen ist die Vererbung von Umgebungen. Eine
Umgebung ist eine durch \0 terminierte Folge von Zeichenketten der Form
name=wert. Typische Beispiele fr diese Umgebungsvariablen sind
HOME=/home/achilles
PATH=/usr/bin:/usr/XllR6/bin:/bin:/opt/kde3/bin:
HOME bezeichnet den absoluten Pfad zum Home-Directory des jeweiligen

Benutzers, PATH gibt den Suchpfad an, in dem nach ausfhrbaren Dateien
gesucht wird.
7. Programm environ.c
main (int argc, char **argv, char **envp) {
char **p;
while (*envp != (char *)0)
printf("7,s\n", *envp++) ;
exit(0);

Das obige Programm kann die vererbte Umgebung anzeigen. Soll die Umgebung
nicht vererbt werden, soll vielmehr einem Programm eine neue Umgebung
mitgegeben werden, so sind spezielle Varianten der exec()-System Calls zu
whlen, die als drittes Argument die neue Umgebung bergeben: execle() oder
execve().
Problem: Wenn sofort nach einem fork()-Aufruf der Adressraum eines Prozesses
mit einem neuen Programm berlagert werden soll, so wre es nachteilig,
zunchst den ganzen Adressraum zu kopieren. Einen wesentlich performanteren Weg, der auch von Linux eingeschlagen wird, beschreibt Abschn. 3.4.

3.3

Beenden eines Prozesses

In den Programmbeispielen wurde der System Call exit() verwendet. Dies ist eine
von mehreren Mglichkeiten, um einen Prozess zu beenden. Beim Beenden eines
Prozesses muss das Betriebssystem ggf. den Elternprozess benachrichtigen und
den Returncode des beendeten Kindes bermitteln. Zustzlich muss er die
Ressourcen freigeben, die der beendete Prozess im Laufe seines Lebens belegt
hat. Dazu gehren unter anderem belegter Speicher und geffnete Dateien.
3.3.1

exit() und wait()

Es gibt mehrere Mglichkeiten, einen Prozess zu beenden:

3.3 Beenden eines Prozesses

31

Der Prozess fhrt seine letzte Anweisung aus,


der Prozess macht einen exit()-System Call oder
der Prozess wird von auen durch ein Signal beendet (vgl. Abschn.
6.4.1).

In manchen Situationen erwartet der Elternprozess eine Information darber,


wie das Kind beendet wurde. Wird beispielsweise von der Benutzer-Shell aus ein
Programm gestartet - d.h. die Shell, die ja selber ein Prozess ist, ruft fork() auf
und ldt anschlieend in dem erzeugten Kind das auszufhrende Programm -, so
rechnet der Benutzer anschlieend damit, dass er abfragen kann, ob das
Programm fehlerfrei arbeitete oder mit einem Fehler endete. Dies wird in ShellKommandos ausgenutzt. Somit muss der Prozess als letzte Anweisung exit()
aufgerufen und damit einen Integerwert zurckgegeben haben (vgl. Listing 3.5).
Durch den wait()-System Call kann der Elternprozess auf das Ende seines
Kindes warten und danach den Returncode erhalten:
int pid; /* Variable, die die Prozess-ID enthaelt */
int status; /* Returncode, enthlt ggf. Signalnummer */
pid = wait(&status);

Die Variable pid gibt die Prozess-ID desjenigen Kindes zurck, das beendet
wurde. Das ist ntig, da der Elternprozess zu diesem Zeitpunkt bereits mehrere
Kinder erzeugt haben kann und wait() auf das Ende irgendeines Kindes wartet.
Wurde das Kind normal beendet, dann findet der Elternprozess anschlieend in
der Variablen status den Wert, mit dem das Kind exit() aufgerufen hat.
Kinder knnen jedoch auch anders beendet werden, beispielsweise durch
einen Abbruch vom Terminal aus mittels Ctrl-C oder durch einen
Programmfehler, wenn z.B. versucht wird, durch Null zu dividieren. In all
solchen Fllen wird ein Signal erzeugt (vgl. Abschn. 6.4.1). Damit der
Elternprozess Informationen ber diese Art der Beendigung bekommt, wird das
Signal in den unteren 8 Bit der Variablen status gespeichert, der Rckgabewert sofern das Kind normal beendet wurde - jedoch darber. Um beide Aspekte
analysieren zu knnen, msste der obige Programmausschnitt lauten:
int pid,
/* Variable, die die Prozess-ID enthaelt */
status,
/* Returncode, enthlt ggf. Signalnummer */
status_sig; /* ggf. Signalnummer */
pid = wait(&status); status_sig =
status & 0xFF ; if (status_sig == 0)
status = status>>8; else status = 0;

Listing 3.6. Der System Call wait()

32

3 Prozesse und Threads

3.3.2

Synchronisation

Durch wait() wird der Elternprozess angehalten und wartet auf das Ende eines
Kindprozesses. Damit entstehen mehrere Fragen:

Wie kann auf ein bestimmtes Kind gewartet werden?


Wenn der Kindprozess bereits vor einem wait()-Aufruf beendet wird, wo
wird dann der Status des Kindprozesses aufbewahrt?
Was passiert, wenn der Elternprozess beendet wird, ohne mit einem
wait() auf das Ende eines noch aktiven Kindes zu warten?

Wie bereits in Listing 3.6 gezeigt, gibt der wait()-System Call die Prozess- ID des
Kindes zurck, das beendet wurde. Der Elternprozess, der auf das Ende eines
bestimmten Kindes wartet, muss dann die zurckgegebene Prozess- ID mit
derjenigen vergleichen, auf die er wartet. Stimmen die beiden nicht berein, so
muss der Elternprozess erneut wait() aufrufen.
int pid; /* Variable, die die Prozess-ID enthaelt */
int status; /* Returncode, enthlt ggf. Signalnummer
*/ int rc;
rc = waitpid(pid, &status, options);
Listing 3.T. Der System Call waitpid()

Dies lsst sich auch einfacher erledigen. Der in Listing 3.7 dargestellte
Programmausschnitt wartet auf das Ende des durch die Prozess-ID pid
spezifizierten Kindes. Durch options kann das Verhalten genauer gesteuert
werden. So bedeutet die Konstante WNOHANG, dass das Warten sofort beendet
wird, wenn zur Zeit des Aufrufs kein beendetes Kind vorhanden ist. Fehler
knnen auftreten, wenn z.B. eine ungltige Prozess-ID angegeben wird, pid
darfbeim waitpid()-Aufruf im Gegensatz zu Prozess-IDs auch nicht positive
Werte annehmen:

>0: Warten auf das Kind mit der durch pid gegebenen Prozess-ID,
0: Warten auf irgendein Kind mit gleicher Prozessgroup-ID wie der
Elternprozess,
-1: Dies entspricht dem wait()-Aufruf,
<-l: Warten auf irgendein Kind, dessen Prozessgroup-ID dem Betrag von
pid entspricht.

Wenn ein Kindprozess beendet wird, so kann es vorkommen, dass der


Elternprozess noch kein wait() oder waitpid() aufgerufen hat. Doch die bentigte
Information muss fr den Fall aufbewahrt werden, dass der Elternprozess spter
einen solchen System Call durchfhrt. Diese Stelle ist im PCB unter exit_code
und exit_signal auf S. 21 zu finden. Der PCB eines beendeten Prozesses kann
somit erst nach einem solchen System Call freigegeben werden.

3.4 Leichtgewichtige Prozesse 33

Prozesse, die beendet sind, aber deren PCB noch aufbewahrt werden muss,
werden als Zombie bezeichnet. In der Ausgabe des Befehls ps -A8 werden
Zombies mit dem Zusatz <defunct> gekennzeichnet.
Wird ein Elternprozess beendet, whrend ein von ihm erzeugter Kindprozess
noch aktiv oder bereits ein Zombie ist, ohne wait() aufzurufen, so bleibt ein
verwaistes Kind oder ein verwaister Zombie zurck. Ohne weitere Vorkehrungen
knnten durch verwaiste Zombies alle Prozesseintrge aufgebraucht werden. Aus
diesem Grunde werden verwaiste Prozesse und Zombies dem init-Prozess (vgl.
Kap. 10) zugeordnet. Dieser Prozess entsorgt dann die verwaisten Zombies, d.h.
die nicht lnger bentigten PCBs werden vom init-Prozess freigegeben.

3.4

Leichtgewichtige Prozesse

Um das auf S. 30 angesprochene Problem zu lsen, das durch unntiges Kopieren


des Adressraums entsteht, setzt Linux - wie auch andere Betriebssysteme sogenannte leichtgewichtige Prozesse (lightweightprocess) ein. Leichtgewichtige
Prozesse ermglichen es, wesentlich mehr Ressourcen zu teilen, als es mit dem
ursprnglichen fork() mglich ist. So kann z.B. der gesamte Adress- raum von
leichtgewichtigen Prozessen gemeinsam genutzt werden. Dies wird bentigt,
wenn Threads eingesetzt werden sollen (vgl. S. 35).
3.4.1

Der neue fork()-System Call

Die Implementierung von fork() wurde gendert, die Wirkung bleibt jedoch
erhalten. Zunchst einmal wird ein neuer PCB angelegt, danach werden die
Pagetables (vgl. Kap. 5) fr den Kindprozess erzeugt. Es werden jedoch keine
Pages des Prozesses kopiert, vielmehr greift das Kind auf die Daten des
Elternprozesses zu. Sobald jedoch einer der beiden Prozesse Daten verndert,
wird die angesprochene Page fr das Kind kopiert, die Pagetable des Kindes
angepasst und die Daten entsprechend verndert. Jeder Prozess hat jetzt an
dieser Stelle seine private Kopie der Page. Auf diese Weise mssen nur
diejenigen Pages kopiert werden, die auch wirklich bentigt werden. Fr den
Benutzer hingegen hat sich die Wirkung des fork()-Aufrufes nicht verndert.
3.4.2

Die vfork()-Variante

Im Hinblick darauf, dass sehr hufig auf einen f ork ( ) - Aufruf im Kindprozess
ein exec()-Aufruf folgt, wurde eine spezielle Variante des fork()-Aufrufs
entwickelt: vfork() (vgl. Listing 3.8).
Die Wirkungsweise von vfork() entspricht nahezu derjenigen von fork(),
jedoch greifen beide Prozesse auf dieselbe Pagetable zu. Der Elternprozess

Auflistung aller Prozesse im System mit ihren wichtigen Eigenschaften.

3 Prozesse und Threads

34

int pid; /* Variable, die die Prozess-ID enthaelt */


pid = vfork(); if (pid == 0) { int rc;
rc = execl("/bin/ls", "ls", "-1", (char *)0 );

}
Listing 3.8. Der Aufruf vfork()

wird angehalten, bis das Kind exit() oder exec() aufruft, denn ohne einen dieser
beiden Aufrufe ist die Wirkung nicht vorhersagbar.
vfork() ist so implementiert, dass der exec()-Aufruf fr eigene Pagetables des
Kindprozesses sorgt.
3.4.3 Implementierung mit clone()
Die Implementierung von fork(), vfork() usw. wird in Linux durch den System
Call clone() vorgenommen. Dieser System Call sollte nicht benutzt werden, wenn
man portable Programme schreiben will, dennoch lohnt sich ein Blick auf die
Fhigkeiten von clone().
clone() ermglicht es, genau festzulegen, welche Ressourcen von Eltern- und
Kindprozess gemeinsam angesprochen werden. Der Aufruf lautet:
rc = clone(routine, stack, flags, args);

Dabei gibt routine() die Adresse des auszufhrenden Codes an, stack ist der fr
den Kindprozess bereitzustellende Stackbereich, args sind Argumente, die an
den auszufhrenden Code bergeben werden knnen, falls es sich hierbei um
eine Prozedur handelt. Das gemeinsame Zugreifen auf Ressourcen wird ber
flags gesteuert, die durch logisches Oder verknpft werden knnen. Wichtig sind
hierbei folgende Rags9:

CL0NE_FS bewirkt, dass beide Prozesse auf dieselbe Filesystem-

Information inklusive Wurzel, Current Directory und umask zugreifen.


So wirkt sich ein Wechsel des Working Directories mittels chdir auf
beide Prozesse aus. Ist dieses Flag nicht gesetzt, so arbeitet das Kind
auf einer Kopie der Filesystem-Information (vgl. Kap. 8).
CLONE_FILES bedeutet, dass beide Prozesse auf dieselbe FileDeskriptorTabelle zugreifen. Das Schlieen oder Offnen einer Datei in
einem der beiden Prozesse wirkt sich damit auf beide aus. Bei nicht
gesetztem Flag erbt das Kind eine Kopie der File-Deskriptor-Tabelle.
CLONE_SIGHAND: Beide Prozesse greifen auf dieselbe Tabelle von
SignalHandlern zu (vgl. Abschn. 6.4.1). Vernderungen an den SignalHandlern betreffen somit beide Prozesse. Die Signale selbst sowie die
Signal-Masken

Flags sind spezielle Bits, die die Ausfhrung des Aufrufs beeinflussen.

3.4 Leichtgewichtige Prozesse

35

zum Blockieren von Signalen sind jedoch an den einzelnen Prozess gebunden.
CLONE_VFORK stoppt den Elternprozess, bis der Kindprozess einen der
Aufrufe exec() oder exit() ausfhrt. Ist dieses Flag nicht gesetzt, so
knnen beide Prozesse vom Scheduler aufgerufen werden.
CLONE_VM bewirkt, dass beide Prozesse denselben physischen
Adressraum benutzen. nderungen am Speicherinhalt sowie -gre
(durch Anfordern oder Freigeben von Speicher) wirken sich auf beide
Prozesse aus. Bei nicht gesetztem Flag arbeitet das Kind auf einer Kopie
des Adressraums.
CLONE_THREAD ordnet das Kind der Threadgruppe des Elternprozesses
zu.
Die Verbindung von fork(), vfork() und clone() ist Architektur-abhngig. Man
findet sie deshalb in den Dateien entry.S und process.c10.
Abbildung 3.9 zeigt einen Einsatz von clone(). Der neue Prozess bentigt
einen eigenen Stack, deshalb muss zunchst ein entsprechender Speicherbereich
angelegt werden. Das wird erreicht durch die Festlegung der Stackgre sowie
die Vereinbarung und Initialisierung von stack im Hauptprogramm.
Die mit printf() erzeugten Ausgaben sollen nur das Verhalten verdeutlichen
und insbesondere zeigen, dass eine Vernderung der globalen Variablen im
Thread zu einer Vernderung im Elternprozess fhrt; das Flag CLONE_VM bewirkt
also wirklich, dass Eltern und Kind auf denselben Speicherbereich zugreifen.
stack + STACK_SIZE/sizeof(void **)

im Aufruf von clone() bewirkt, dass der Pointer auf das obere Ende des
angelegten Stack-Bereichs zeigt. Dies ist wichtig, da der Stack in der x86Architektur nach unten wchst. In diesem Falle soll nur der Speicherbereich als
gemeinsame Ressource benutzt werden.
waitpid() wartet auf die Beendigung eines Kindes. 0 veranlasst, dass auf
einen Prozess mit derselben Prozessgroup-ID gewartet wird.
Neben dem Anlegen des Stacks muss darauf geachtet werden, dass dieser
Speicherbereich auch wieder freigegeben wird, wenn der Thread beendet ist:
free(stack).
3.4.4

Threads

Das Programm 3.9 zeigt bereits deutlich, wie Threads in Linux implementiert
werden. Im Gegensatz zu anderen Betriebssystemen, die im Kern eine
Untersttzung fr Threads vorsehen, ist in Linux ein Thread als normaler
Prozess implementiert, der mit dem Elternprozess alle entscheidenden
Ressourcen teilt. Die Flags, die dafr dem clone()-Aufruf mitgegeben werden
mssen, sind
CLONE_VM I CLONE_FS I CLONE_FILES | CLONE_SIGHAND

10

Definiert in arch/i386/kernel.

36

3 Prozesse und Threads


#include <sched.h>
#include <sys/wait.h>
#define STACK_SIZE 16384
int global = 0;
int mach_was(void *nr) {
global++;
printf("Kind 7,d\n", nr);
_exit(0);

// Thread-Funktion

main() {
// Hauptprogramm
int pid; int nr = 1;
void **stack;
stack = (void **) malloc(STACK_SIZE);
printf("Wert von global: 7,d\n",
global);
pid = clone(mach_was, stack + STACK_SIZE/sizeof( void
** ), CL0NE_VM, (void *)nr); if (pid > 0) { int rc,
status;
rc = waitpid(0, &status, __WCL0NE);
printf("Wert von global nach Thread-Aufruf: 7,d\n", global);

>

free(stack); exit(0);

Listing 3.9. Beispiel fr die Verwendung von clone()

Gemeinsame Threads greifen somit auf denselben Speicher zu, sehen dieselbe
Filesystem-Information, benutzen dieselben Dateien und verwenden dieselben
Signal-Handler.
Da der clone()-Aufruf Linux-spezifisch ist und nicht auf anderen Systemen
zur Verfgung steht, tritt das Problem auf, wie Threads so eingesetzt werden
knnen, dass sie sich auf andere Systeme portieren lassen. Hier helfen die
LinuxThreads, eine Implementierung der POSIX-konformen pthreadBibliothek, die die Aufrufe unter Linux auf den clone()-System Call abbilden.
Diese Implementierung befindet sich z.B. im Paket uClibc, das bei SuSE Linux
automatisch installiert wird. Listing 3.10 zeigt in einem einfachen Beispiel den
Einsatz von pthread-Aufrufen.

3.4 Leichtgewichtige Prozesse1

37

/*------------------------------------gcc -o test -lpthread test.c

--------------------------------------*/
#include <pthread.h> void
*thread_fc(void *arg)

>

printf("im Thread\n"); sleep(l);


pthread_exit(0);

main()

int rc;
pthread_t thrd; void *thread_rc;

rc = pthread_create(&thrd, NULL, thread_fc, NULL);


if(rc !=0) {
printf("Fehler bei Threaderzeugung\n"); exit(l);

>

printf("Warten auf den Thread\n");


rc = pthread_join(thrd, &thread_rc); exit(rc);

Listing 3.10. Beispiel fr den Einsatz der pthread-Bibliothek

Die wichtigsten Aufrufe der pthread-Bibliothek sind

pthread_create() zum Anlegen eines neuen Threads,


pthread_exit() zumBeendendesThreads,
pthread_cancel() zum Beenden des im Argument angegebenen Threads,
pthread_join() zum Anhalten des gegenwrtigen Threads, bis der im
Aufruf angegebene Thread beendet wird.

Das obige Programm zeigt die Erzeugung, das Beenden und das Warten auf
einen Thread mit den Aufrufen der pthread-Bibliothek. Der Quelltext ist unter
test. c gespeichert, der Aufruf zum bersetzen befindet sich im Kommentar des
Programmkopfes. Dem Compiler gcc muss die Option -lpthread mitgegeben
werden, damit er die Bibliothek fr die Implementierung von pthread einbindet.

3 Prozesse und Threads

38

3.5

Zusammenfassung

Nach einer Untersuchung des Prozessbegriffes zeigen die Listings 3.1-3.3, wie
komplex ein PCB (Process Control Block) in einem konkreten Betriebssystem
aussehen kann. Die Struktur zeigt deutlich, dass ein Betriebssystem nicht
einfach in getrennte Teile wie Scheduling, Speicherverwaltung,
Prozessmanagement usw. zerfllt, sondern dass wir an vielen Stellen
Querverbindungen haben.
Die Erzeugung eines neuen Prozesses sowie das Ausfhren eines neuen
Programms in einem existierenden Prozess wurde diskutiert und die
Verwendung der System Calls fork() und exec() an Beispielprogrammen
dargestellt. Bei erfolgreicher Ausfhrung von fork() unterscheiden sich Elternund Kindprozess in der Rckgabe des Aufrufs: im Elternprozess ist es die
Prozess-ID des erzeugten Kindes, im Kindprozess hingegen 0. Fehler werden
durch einen negativen Rckgabewert angezeigt.
Die Familie der exec()-System Calls dient zur berlagerung eines Prozesses
durch ein anderes Programm: der Inhalt des Adressraums wird berschrieben,
eine Rckkehr zum vorher ausgefhrten Programm ist nicht mglich. Bei der
berlagerung werden eine Reihe von Eigenschaften des alten Prozesses
vererbt: sofern nicht ausdrcklich etwas anderes bestimmt wird, erbt der
berlagerte Prozess die Umgebung und die File-Handles.
Zum Beenden eines Prozesses wird der System Call exit() verwendet. Wir
haben diskutiert, was das Betriebssystem erledigen muss, wenn ein Prozess
beendet wird. Dabei tauchte auch die Frage nach einer einfachen
Synchronisation zwischen Eltern- und Kindprozess auf: der System Call wait()
hlt einen Prozess an, bis eines seiner Kinder beendet wird. Zugleich liefert er
den Returncode des beendeten Kindprozesses. Im Gegensatz zu wait() kann mit
dem System Call waitpid() gezielt auf das Ende eines ganz bestimmten Kindes
gewartet werden. Kinder, deren Elternprozess ohne einen entsprechenden wait()Aufruf beendet wird, werden dem 1. Prozess des Systems, dem init-Prozess
zugeordnet.
Die Verbesserung durch Einfhren leichtgewichtiger Prozesse wurde
diskutiert. Whrend ursprnglich bei einem fork()-Aufruf alle Pages des
Elternprozesses kopiert wurden, stellen leichtgewichtige Prozesse Mittel bereit,
dass nur diejenigen Pages kopiert werden, die auch tatschlich verndert
werden. In diesem Zusammenhang wurde clone() vorgestellt, ein Linuxspezifischer System Call, um fork() bzw. vfork()11 und Threads zu
implementieren. Der clone()-Aufruf sollte jedoch nicht explizit eingesetzt werden,
wenn portable Programme mit Threads erstellt werden sollen. Um portable
Threads zu erzeugen, sollte die pthread-Bibliothek mit den Aufrufen
pthread_create(), pthread_exit(), pthread_join(), pthread_cancel(), usw. eingesetzt
werden. 11
vfork() ist eine Variante, die den Elternprozess anhlt, bis ein exec()- oder exit()-Aufruf
erfolgt.
11

4
Scheduling

4.1

Grundlagen

Scheduling hat die Aufgabe, die Ressourcen - insbesondere die CPU eines
Rechners - mglichst gut auszunutzen. Dabei mssen hufig eine Reihe von
zustzlichen Anforderungen beachtet werden:

Fairness gegenber Benutzern: alle sollen mglichst gleich


bercksichtigt und bedient werden,
Prioritten: manche Prozesse sind ggf. wichtiger als andere,
hoher Durchsatz: CPU-lastige Prozesse sollen mglichst effektiv
bearbeitet werden,
schnelle Reaktionszeit: im Timesharing-Bereich mchte der Benutzer
eine schnelle Reaktion des Systems erleben,
Realtime-hnliche Anforderungen: bei Multimedia-Anwendungen
mssen Prozesse innerhalb vorgegebener sehr kurzer Zeiten die CPU
zugeteilt bekommen.

Diese Anforderungen stehen zum Teil in Konflikt miteinander: eine schnelle


Reaktionszeit ist leicht auf Kosten eines hohen Durchsatzes zu verwirklichen.
Es gibt drei groe Bereiche, bei denen Scheduling fr Prozesse eine Rolle
spielt: beim Erzeugen eines Prozesses, beim Freiwerden der CPU und beim Ausund Einlagern eines Prozesses.
Beim Erzeugen eines neuen Prozesses kann darauf geachtet werden, ob alle
Ressourcen in ausreichender Menge zur Verfgung stehen. Dieser Ansatz wird in
Batch-orientierten Betriebssystemen durchgefhrt: jeder Job (Prozess) erhlt
Job-Control Karten1, auf denen die Systemanforderungen wie Speicherbedarf,
CPU-Zeit, Druckzeilen, bentigter Plattenplatz usw. vermerkt sind. Auf Grund
dieser Angaben und der derzeitigen Auslastung des 1

Heute sind es natrlich keine Karten im eigentlichen Sinne mehr, sondern spezielle
Job-Control Anweisungen.
1

40

4 Scheduling

Systems entscheidet der Scheduler, ob der Job in das System als neuer Prozess
aufgenommen werden kann oder ob er noch warten muss.
Wenn die CPU einen rechnenden Prozess abgibt, weil er z.B. beendet wurde,
auf einen I/O-Vorgang wartet oder seine Zeitscheibe2 abgelaufen ist, muss der
Scheduler entscheiden, welcher Prozess als nchster der CPU zugeteilt wird.
Zwischen diesen beiden Arten von Scheduling (im Englischen mit Longterm Scheduling und Shortterm Scheduling bezeichnet) besteht ein groer
Unterschied: whrend das Longterm Scheduling nur einmal zu Beginn im Leben
eines Prozesses aufgerufen wird, erfolgt das Shortterm Scheduling in kurzen
Zeitabstnden (im Bereich von ms) immer wieder.
Es gibt noch weitere Scheduling-Entscheidungen, die Prozesse betreffen
knnen. Werden Prozesse aus dem Speicher ausgelagert (Swapping, vgl. Kap. 5),
so muss der Scheduler entscheiden, wann ein guter Zeitpunkt dafr ist, den
Prozess wieder einzulagern, um ihn weiter zu verarbeiten. Dieses Scheduling
wird als Mediumterm Scheduling bezeichnet; wenn diese Scheduling-Art in
einem System implementiert ist, so wird sie viel hufiger aufgerufen als das
Longterm Scheduling, aber im Vergleich zum Shortterm Scheduling sehr selten.
Abbildung 4.1 zeigt den Lebenszyklus eines Prozesses und die verschiedenen
bergnge, die mglich sind. Das Betriebssystem verwaltet eine Reihe von
Warteschlangen, die den jeweiligen Prozesszustnden entsprechen. So gibt es
eine Warteschlange fr die angemeldeten Prozesse, die erst noch erzeugt werden
mssen. Die Warteschlange, die mit bereit gekennzeichnet ist, enthlt
diejenigen Prozesse, die rechnen knnen, denen also die CPU zugeteilt werden
kann. Die Warteschlange wartend3 enthlt diejenigen Prozesse, die auf das
Ende eines I/O-Vorgangs oder auf das Eintreffen eines bestimmten Signals
warten. Die mit ausgelagert gekennzeichneten Zustnde bedeuten, dass der
Prozess ausgelagert worden ist. Der Zustand rechnend ist gegeben, wenn der
Prozess die CPU besitzt.
Soweit die bergnge durch den Scheduler verursacht werden, ist dies in der
Abb. vermerkt. Der bergang zu wartend kommt dadurch zustande, dass der
Prozess auf das Ende eines von ihm angeforderten I/O-Vorgangs oder auf das
Eintreffen eines bestimmten Signals wartet. In beiden Fllen muss der Prozess
die CPU freigeben. Von rechnend zu bereit gelangt ein Prozess, wenn er
freiwillig die CPU abgibt oder wenn ihm die CPU entzogen wird. Dies kann z.B.
geschehen, wenn der Prozess sein Zeitlimit berschreitet oder wenn ein Prozess
mit hherer Prioritt in den Zustand bereit wechselt. Der bergang zu
beendet tritt ein, wenn der Prozess die letzte Anweisung

In vielen Systemen wird einem Prozess eine Zeitscheibe zugeteilt, sobald er die CPU
erhlt. Luft die Zeitscheibe ab, so muss er die CPU abgeben.
2

Bei greren Rechenanlagen wird es in der Regel mehrere solche Warteschlangen


geben, so z.B. fr jedes Gert, auf das mittels I/O zugegriffen wird, eine eigene
Warteschlage.
3

4.1 Grundlagen 41

Die Strichdicke soll die (mgliche) Hufigkeit der bergnge symbolisieren.

ausfhrt oder explizit den System Call exit() aufruft. Von wartend zu bereit
wechselt der Prozess, wenn der I/O-Controller durch einen Interrupt anzeigt,
dass der Vorgang abgeschlossen ist, oder wenn das erwartete Signal eintritt. Der
bergang von wartend nach ausgelagert wartend oder von bereit nach
ausgelagert bereit bedeutet, dass auf Grund besonderer Speicheranforderungen
ein Prozess ausgelagert werden muss, um mehr Platz im Speicher zu schaffen
(vgl. Kap. 5).
Je hufiger eine Routine des Betriebssystems aufgerufen wird, um so mehr
muss darauf geachtet werden, dass sie nicht nur effizient ist, sondern auch nur
sehr kurze Zeit luft. Dies gilt insbesondere fr den Shortterm-Scheduler, der in
Linux auf dem PC alle paar Millisekunden aufgerufen wird.
Im Folgenden sollen wichtige grundlegende Algorithmen fr den ShorttermScheduler betrachtet werden. Betriebssysteme, die darauf basieren, dass
Prozesse freiwillig die CPU abgeben, sind problematisch: ein Prozess, der in eine
enggefhrte Endlosschleife mndet, blockiert das gesamte System. Deshalb
gehen wir in den folgenden Betrachtungen von vornherein davon aus, dass die
CPU den Prozessen entzogen werden kann.
Wird ein Prozess vom Scheduler ausgewhlt und bekommt damit die CPU, so
wird eine Hardware-gesteuerte Uhr gestartet, die nach einer vorgegebenen Zeit,
z.B. nach 50 Millisekunden, einen Interrupt auslst, der dafr sorgt, dass das
Betriebssystem die Kontrolle zurck bekommt, dem Prozess die CPU entzieht
und den Scheduler aufruft. Gibt der Prozess beispielsweise durch einen I/OVorgang vorher die CPU freiwillig ab, dann wird ebenfalls der Scheduler
aufgerufen.

42

4 Scheduling

4.1.1

Priorittsgesteuert

Werden unterschiedliche Prioritten fr Prozesse vergeben, so wird der


Scheduler Prozesse hherer Prioritt vorziehen. Wenn er aufgerufen wird, sucht
er den rechenbereiten Prozess mit hchster Prioritt aus. Die rechenbereiten
Prozesse knnen beispielsweise in einer verketteten Liste verwaltet werden, die
nach absteigender Prioritt sortiert ist. Der Scheduler entfernt dann immer den
ersten Prozess aus der Liste. Ein Prozess, der rechenbereit wird, muss bzgl.
seiner Prioritt an der richtigen Stelle in die Liste einsortiert werden.
Das Verfahren kann ohne und mit Verdrngung (engl, preemption) realisiert
werden:
Ohne Verdrngung:
jeder Prozess darf rechnen, bis er freiwillig die CPU abgibt oder die CPU
durch Ablauf der Zeitscheibe entzogen wird.
Mit Verdrngung:
Wird ein Prozess rechenbereit, so vergleicht der Scheduler die Prioritt des
rechnenden Prozesses mit derjenigen des neuen Prozesses. Ist die Prioritt
des neu hinzugekommenen Prozesses grer, so wird dem rechnenden
Prozess die CPU entzogen und der neue Prozess ausgewhlt.
Bei priorittsgesteuerten Verfahren kann es passieren, dass Prozesse mit
niedriger Prioritt nicht zum Ende kommen, weil zu viele Prozesse mit hherer
Prioritt ins System gelangen. Dies bezeichnet man als Starvation
(Verhungern). Dem kann durch Altern vorgebeugt werden: ein Prozess
niedriger Prioritt, der bereits eine Weile im System ist, ohne gerechnet zu
werden, bekommt eine hhere Prioritt zugestanden.
4.1.2

Round Robin

Das Round Robin Verfahren ist insbesondere fr interaktive MultiuserUmgebungen gedacht, bei denen man jedem Benutzer einen gleichen fairen
Anteil an der CPU garantieren mchte. Die rechenbereiten Prozesse werden in
einer Schlange angeordnet, deren Ende auf den Anfang der Schlange verweist.
Der Scheduler whlt jedesmal denjenigen Prozess aus, der dem gerade
gerechneten in der Schlange folgt. Dabei ist die Lnge der zugeteilten Zeitscheibe
fr alle Prozesse gleich.
Bei einer geeigneten Wahl der Zeitscheibe kann man davon ausgehen, dass
mehrfach dieselben Prozesse in gleicher Reihenfolge nacheinander vom
Scheduler aufgerufen werden, bevor sich die Schlange durch Hinzukommen oder
Ausgliedern eines Prozesses ndert. Damit erhlt jeder der Prozesse in jeder
Runde genau den gleichen Anteil an der Prozessorleistung.
4.1.3

Priorittsgesteuert mit Feedback

Prozesse knnen sich in ihrem Leben sehr unterschiedlich verhalten. Eine


Programmier-Entwicklungsumgebung ist whrend der Zeit der Entwicklung

4.1 Grundlagen

43

I/O-orientiert, wennjedoch der Compiler ttig ist, ist der Prozess CPU-lastig.
Wenn noch weitere Prozesse auf dem System laufen, wre es wnschenswert, die
Reaktionszeit whrend der Entwicklung kurz zu halten, jedoch die CPULastigkeit whrend des Compilerlaufs zu untersttzen. Dies kann nur gelingen,
wenn der Scheduler das unterschiedliche Verhalten des Prozesses mitbekommt.
Manche Betriebssysteme richten mehrere Scheduler-Warteschlangen mit
unterschiedlich langen Zeitscheiben und Prioritten ein. Ein Prozess, der
mehrfach bei hoher Prioritt seine Zeitscheibe vllig ausgenutzt hat, ohne eine
I/O-Anforderung zu starten, ist anscheinend CPU-lastig und kann in eine
Scheduler-Warteschlange mit geringerer Prioritt eingelagert werden. Dafr
wird dann seine Zeitscheibe verlngert. Bei einer I/O-Unterbrechunge wird der
Prozess wieder mit krzerer Zeitscheibe in die Warteschlange hchster Prioritt
eingegliedert. Der Scheduler bevorzugt Warteschlangen hherer Prioritt.
Um das Verhalten eines Prozesses zu bewerten, kann das System z.B.
mitzhlen, wie viele I/O-Unterbrechungen in einer gewissen Zeitspanne erfolgen.
4.1.4

Lnge der Zeitscheibe

Wie bereits erwhnt, werden bei vielen Scheduling-Verfahren Zeitscheiben


eingesetzt. Einem rechenbereiten Prozess, der die CPU erhlt, wird eine
Zeitscheibe zugeteilt. Nach Ablauf der Zeitscheibe muss er die CPU abgeben. Die
richtige Wahl der Zeitscheibenlnge ist bei solchen Verfahren sehr bedeutend fr
ein gutes Scheduling.
Werden die Zeitscheiben zu kurz gewhlt, so treten viele Unterbrechungen
ein, obwohl die Prozesse weiterrechnen knnten. Nehmen wir an, der SchedulingVorgang selbst bentige 1 ms und die Zeitscheibe werde ebenfalls auf 1 ms
festgelegt, so ist der Prozessor die Hlfte seiner Zeit mit Scheduling- Aufgaben
beschftigt, kann also in dieser Zeit nicht Problem-bezogen arbeiten.
Werden die Zeitscheiben hingegen zu lang gewhlt, so leidet die
Reaktionszeit. Die Prozesse geben dann hufig durch einen I/O-Aufruf die CPU
von selbst frei.

4.1.5

Mehrprozessor-Systeme

Problem: Die bisherigen Strategien sind fr ein Einprozessor-System gedacht.


Wie beeinflussen mehrere Prozessoren die Scheduling-Strategie?
Eine globale Scheduling-Strategie knnte bewirken, dass Prozesse nach einer
Unterbrechung hufig auf einer anderen CPU weiterlaufen. Der Nachteil liegt
darin, dass die alte CPU mglicherweise in ihrem Cache noch Informationen zu
diesem Prozess aufbewahrt, die neue jedoch sicher nicht. Somit wre
anzustreben, dass Prozesse mglichst auf der CPU fortfahren, auf der sie zuletzt
aktiv waren.

44

4 Scheduling

Das fhrt zu lokalem Scheduling. Frjede CPU wird ein eigenes Scheduling
mit eigenen Warteschlangen etabliert. Doch auch dieses Vorgehen ist nicht
unproblematisch: ohne weitere Modifikation knnte es dazu kommen, dass eine
CPU mit Prozessen berhuft ist, whrend andere nahezu arbeitslos sind.
4.1.6

Realtime

Realtime oder Echtzeit-Anforderungen liegen vor, wenn das System auf das
Eintreten eines Ereignisses innerhalb einer vorgegebenen Zeitspanne reagieren
muss. Diese Zeitspanne hngt von den jeweiligen Anwendungen ab: bei
Maschinensteuerungen kann der Bereich in der Grenordnung von
Mikrosekunden liegen, bei Heizungssteuerungen im Bereich von Minuten.
Nicht nur die Zeitspanne, innerhalb derer reagiert werden muss, ist wichtig,
sondern auch die Frage, was passiert, wenn die Zeitspanne doch einmal
berschritten wird. Bei der Einspritzpumpe eines Motors htte ein
berschreiten ble Folgen: der Motor beginnt zu stottern, es kommt langfristig
zu Schden. Bei Videosequenzen wre ein kurzfristiges Ruckeln zu bemerken,
aber es ist nicht mit langfristigen Folgen zu rechnen. Deshalb wird zwischen
harten und weichen Echtzeitanforderungen unterschieden: bei harten
Echtzeitanforderungen mssen die Zeitspannen eingehalten werden, bei weichen
sollten sie so weit wie mglich eingehalten werden.
Die Realisierung kann Hardware-mig oder als Software-Lsung
implementiert werden. Wenn wir weiche Echtzeitanforderungen zur
Untersttzung von Video- und Audio-Sequenzen betrachten, die mit Hilfe von
Software realisiert werden sollen, dann knnte eine mgliche Lsung darin
bestehen, dass besonders hoch priorisierte Prozesse geringer priorisierte
Prozesse verdrngen und sofort die CPU bekommen, sobald sie rechnen knnen.
Da auf einem Desktop in der Regel hchstens ein oder zwei solcher Prozesse
aktiv sein werden, sollte damit den damit verbundenen Anforderungen gengt
werden knnen.

4.2

Linux Scheduling

Das Scheduling unter Linux basiert auf einem priorittsgesteuerten Verfahren


und untersttzt damit sowohl Timesharing (vgl. die folgenden Ausfhrungen und
Abschn. 4.4) als auch weiches Software-gesttztes Realtime-Scheduling (vgl.
Abschn. 4.3).
In der Datei kernel/sched.c befinden sich die wesentlichen Strukturen und
Aspekte des Scheduling. Linux verwendet dazu die in Listing 4.1 dargestellte
Struktur, die als runqueue bezeichnet wird.
Mit den Zeigern *active und *expired wird auf zwei Priorittsarrays (arrays
[2] ) verwiesen, die die rechenbereiten Prozesse enthalten. Durch diese Technik
kann schnell zwischen den beiden Arrays umgeschaltet werden.

46

4 Scheduling

4.2 Linux Scheduling 45

struct runqueue
Priorittsarrays und Scheduling-Algorithmus
{ spinlock_t
lock;
/* Sperre zum Runqueue-Schutz */
unsigned long
nr_running;/* Anzahl lauffhiger Prozesse */
unsigned long
nr_switches;/* Anzahl Kontextswitches */
misigned long
expired_timestamp;
/* Zeit des letzten Arrayswaps */
unsigned long
nr_uninterruptible
/* Anzahl schlafender, nicht
unterbrechbarer Tasks */
struct task-struct *curr; /* auf CPU laufender Prozess */
struct task_struct *idle; /* idle taks fr diese CPU */
Runqueue
*prev_mm; /* Pagetables des zuletzt
struct mm_struct
gelaufenen Prozesses */
Arrays
struct prio_array *active; /* Zeiger zu aktivem Prio-Array */
*active *expired
struct prio_array *expired; /* Zeiger zu altem Prio-Array */
struct prio_array arrays[2]; /* aktuelle Prio-Arrays */
int
prev_cpu_load[NR_CPUS];
/* Load auf jeder CPU */
struct task_struct migration_thread;
/* MigrationsThread fr diese CPU */
struct list_head
migration_queue;
/* MigrationsQueue dieses Prozessors */
atomic_t
nr_iowait;
/* Anzahl Prozesse auf 10 wartend */

4.2.1

prio_array
n Liste aller
I
queue
Prozesse
Listing 4.1. Die Struktur runqueue
J
mit
Prioritt 4
111
Das
I
mit
*active
I
adressierte Array enthlt diejenigen Prozesse, die noch eine
0
139
Zeitscheibe mit positiver Zeit
besitzen. Ein Prozess, der erstmalig rechenbereit
bit wird, bekommt als Zeitscheiben first_time_slice sowie time_slice zugeteilt

(vgl. S. 21) und wird in dieses Array gem seiner Prioritt eingefgt. Whrend
first_time_slice dazu dient, die ursprngliche Setzung zu merken, wird in
time_slice festgehalten, wie viel Zeit dem Prozess noch verbleibt. Ist diese Zeit
abgelaufen, so wird der Prozess unterbrochen und in dem mit *expired
adressierten Array an entsprechender Stelle4 eingefgt. Wird der Prozess aus
anderem Grunde unterbrochen - z.B. Interrupt-Behandlung und der Prozess wird
wieder bereit, Ankunft eines hher priorisierten rechenbereiten Prozesses usw. und ist seine Zeitscheibe noch positiv, so wird er wieder in das *active Array
eingefgt, dabei behlt er seine restliche Zeitscheibe5 und kann sie spter
aufbrauchen.

Bestimmt durch die Prioritt des Prozesses.

Der Begriff Zeitscheibe wird also etwas anders verwendet, als die vorausgehenden
allgemeinen Bemerkungen erwarten lassen.
5

4.2 Linux Scheduling 47

ersten Prozess, der hinter dem zugehrigen Listenkopf angehngt ist. Mit dem
Entfernen des Prozesses aus der Liste muss zugleich die Variable nr_active um
1 verringert werden. Zustzlich muss eventuell das Bit in der Bitmap
zurckgesetzt werden, wenn durch diese Auswahl der letzte Prozess der Liste
entfernt wurde.
Dieser Algorithmus ist sehr schnell und unabhngig von der Anzahl der
rechenbereiten Prozesse. Ein derartiges Laufzeitverhalten wird in der Informatik
mit 0(1) bezeichnet.
Auf jeder Priorittsstufe wird nach dem Round Robin Prinzip verfahren. Alle
Prozesse gleicher Priorittsstufe werden somit gleich behandelt. Niedriger
priorisierte Prozesse erhalten erst dann die CPU, wenn keine hher priorisierten rechenbereit sind.
Problem: Ein Prioritts-orientiertes Verfahren neigt dazu, dass Prozesse
verhungern, d.h. nicht mehr zum Zuge kommen, weil stndig hher priorisierte
Prozesse rechenbereit
Wird dem vorgebeugt?
Abb.sind.
4.2. Priorittsarrays
beim Scheduling
In kernel/sched.c
befindet
sich ebenfalls
die Struktur der Priorittsarrays, die im
4.2.2
Der
Wechsel
der Priorittsarrays
Listing 4.2 dargestellt ist. Neben der Anzahl der in der Struktur enthaltenen
Was
passiert,
wenn ein
seineund
Zeitscheibe
aufgebraucht
hat? Er
wird in
Prozesse
(nr_active)
ist Prozess
eine Bitmap
ein Array
von Listenkpfen
enthalten
das
mit
*expired
adressierte
Priorittsarray
eingegliedert.
Von
da
an
wird er
(vgl. Abb. 4.2). Die Lnge dieses Arrays - und letztlich die Gre der Bitmap
- ist
vom
Scheduler
nicht
mehr
bercksichtigt,
bis
auch
alle
anderen
Prozesse
ihre
6
durch die Konstante MAX_PRI0 festgelegt.
Zeitscheibe aufgebraucht haben; bei der Auswahl des nchsten rechenbereiten
Prozesses befindet sich also kein Prozess mehr in dem aktiven Priorittsarray.
struct prio_array {
Damit wird einem Verhungern vorgebeugt.
int nr_active;
Damit
der Scheduler bei Bedarf schnell zwischen den beiden Priorittsarrays
unsigned long
umschalten
kann, mssen beim Eingliedern
eines Benutzer-Prozesses in das
bitmap[BITMAP_SIZE];
struct
*expiredlist_head
Priorittsarray
die
Zeitscheibe
und
die Prioritt fr den Prozess neu
queue[MAX_PRI0];
>; werden. So kann nach dem Umschalten der Scheduler sofort einen
berechnet
rechenbereiten Prozess auswhlen. Der Programmausschnitt aus der Funktion
Listing 4.2. Struktur der Priorittsarrays prio_array
schedule()8 in Listing 4.3 zeigt den zugehrigen Code.
Jede Priorittsstufe besitzt einen Listenkopf, der auf eine Liste rechenbereiter
4.2.3 mit entsprechender
Prozesswechsel
Prozesse
Prioritt weist.
Die
Prioritt
wird
in
bestimmten
Momenten
ebenso
wie die Zeitscheibe
Nachdem der Scheduler einen rechenbereiten
Prozess
ausgewhlt
hat, muss des
ein
Prozesses
dynamisch
angepasst.
Wird
ein
Prozess
rechenbereit,
so
er ander
die
Prozesswechsel erfolgen - es sei denn, der Scheduler hat denjenigenwird
Prozess,
Liste
mit
entsprechender
Prioritt
hinten
angehngt
und
zugleich
wird
in
der
zuvor die CPU besa, wieder ausgewhlt.
Bitmap
zugehrige
Bit fr
den Funktion
entsprechenden
Listenkopf
gesetzt. Zustzlich
Das das
Umschalten
erfolgt
in der
schedule()
in kernel/sched.c
muss
die
Variable
nr_active
um
1
erhht
werden.
durch den Aufruf von context_switch(). Diese Funktion ist ebenfalls dort
9
Durch
diese
wird switchnmn()
die Auswahl des
nchsten
rechnenden
definiert
und
ruftOrganisation
im Wesentlichen
auf,
um die zu
Pagetables
fr
Prozesses sehr einfach und effizient: der Scheduler sucht in dem mit *active
adressierten Array ber die Bitmap das erste gesetzte Bit7 und nimmt den
schedule()
6
ist definiert
in kernel/sched.c.
Die Konstante
ist in include/linux/sched.h
definiert.
Definiert
Je kleiner
inder
include/asm-i386/mmu_context.h
Index, desto hher ist die Prioritt:
wegen0 der
hat Hardware-Abhngigkeit.
die hchste Prioritt, 139 die
kleinste.
8

97

48

4 Scheduling
if (unlikely(!array->nr_active)) {

/*

* Switch the active and expired


arrays. */
rq->active = rq->expired; rq>expired = array; array = rq>active; rq->expired_timestamp =
0;

Listing 4.3. Programmausschnitt aus der Punktion schedule()


Gezeigt wird das Umschalten der Priorittsarrays.

den neuen Prozess bereitzustellen, und dann switch_to()10, um die Register und
den Stack des alten Prozesses zu speichern und aus dem Kontext des neu
gewhlten Prozesses die Register und den Stack zu setzen.
Damit Prozesse hoher Prioritt schnell genug die CPU bekommen, muss der
Scheduler sehr hufig aufgerufen werden. Er wird nicht nur aufgerufen, wenn
die Zeitscheibe fr einen Prozess abgelaufen ist, sondern beispielsweise auch
dann, wenn ein Prozess auf einen Interrupt wartet. Damit in diesem Fall die
CPU freigegeben wird, wechselt ein solcher Prozess in den Zustand wartend
und wird an eine entsprechende Warteschlange angehngt, die fr diesen
Interrupt eingerichtet ist (siehe auch Kap. 7). Der Scheduler sucht nun nach dem
nchsten rechenbereiten Prozess.
Damit aber nicht genug: Der Aufruf des Schedulers erfolgt sogar nach jedem
Interrupt und am Ende jedes System Calls. Dabei prft der Scheduler nur, ob fr
den derzeitig laufenden Prozess das THREAD_NEED_RESCHED Flag gesetzt ist.
Wenn das nicht der Fall ist, endet der Scheduler und der Prozess nimmt seine
Arbeit wieder auf; andernfalls whlt der Scheduler einen Prozess aus, der nun
die CPU bekommt.
Das Flag wird gesetzt, wenn die Zeitscheibe fr den Prozess abluft oder
wenn ein Prozess mit hherer Prioritt, der in einer Warteschlange verweilte,
wieder in den Zustand bereit wechselt.

4.3

Realtime

Linux ermglicht Software-untersttztes Realtime-Scheduling. Es werden zwei


Arten untersttzt: SCHED_FIF0 und SCHED_RR. Die jeweilige Methode muss im
PCB unter policy eingetragen werden (vgl. S. 21). Auerdem muss eine RealtimePrioritt angegeben werden, die im Bereich von 1 bis 99 liegt, wobei 1 die
hchste, 99 die geringste Prioritt bedeutet.
Diese Funktion ist ebenfalls auf Grund der Hardware-Abhngigkeit definiert in
include/asm-i386/system.h.
10

4.3 Realtime

49

Linux garantiert zwar keine harten Zeitanforderungen beim


RealtimeScheduling. Aber die nachfolgend beschriebenen Verfahren sind so
gewhlt, dass Realtime-Prozesse mglichst sofort die CPU zugeteilt bekommen,
wenn sie rechenbereit werden. Dazu trgt nun die Tatsache bei, dass auch der
Kernel so gestaltet ist, dass seine Routinen weitgehend unterbrechbar sind und
somit dem Scheduling unterliegen.
4.3.1

Realtime mit FIFO

Die Vorgehensweise bei SCHED_FIFO ist tatschlich ein First-In-First-Out


Algorithmus. Ein solcher Prozess liegt in der Prioritt ber allen
TimesharingProzessen. Wird er rechenbereit, so verdrngt er alle Prozesse mit
geringerer Prioritt, also auch alle Timesharing-Prozesse, und behlt die CPU
ohne Bercksichtigung einer Zeitscheibe. Ein solcher Prozess gibt die CPU nur in
folgenden Fllen ab:

Er wartet auf einen Interrupt, z.B. durch eine I/O-Anforderung,


er gibt die CPU freiwillig durch den Aufruf sched_yield() ab oder
ein (Realtime-)Prozess mit hherer Prioritt wird rechenbereit.

Ein Prozess, der dem SCHED_FIFO Scheduling unterliegt, behlt stndig seine
Prioritt, eine Zeitscheibe ist nicht von Bedeutung. Ein solcher Prozess wird auch
nie in das *expired-Array verschoben.

4.3.2

Realtime mit Round Robin

Etwas weniger kritisch sind Prozesse, die dem SCHED_RR-Algorithmus


unterliegen: auch fr sie gilt, dass sie eine feste vorgegebene Prioritt zwischen 1
und 99 haben, dass sie, wenn sie rechenbereit sind, smtliche Prozesse
geringerer Prioritt verdrngen und dass die Prioritt fest vorgegeben ist. Aber
im Gegensatz zu SCHED_FIFO-Prozessen besitzen sie eine Zeitscheibe, so dass
sie - neben den fr FIFO genannten Ereignissen - sptestens nach Ablauf der
Zeitscheibe die CPU abgeben. In diesem Falle werden sie im *active-Array am
Ende der Warteschlange der vorgegebenen Priorittsstufe eingefgt. Hier
handelt es sich somit um Round Robin-Scheduling.
4.3.3

Realtime System Calls

Ein Prozess muss entsprechend sicher11 sein, damit er ins Realtime-Scheduling


aufgenommen werden kann. Deshalb knnen nur entsprechend privilegierte
Prozesse Realtime-Scheduling erlangen.
Mit dem Aufruf sched_setscheduler() kann bei Administrator-Rechten
die Variable policy auf SCHED_FIFO oder SCHED_RR eingestellt werden. Mit
sched_setparam() kann die Prioritt des Prozesses gesetzt werden. Zur
D.h. er muss sorgfltig getestet worden sein.

11

50

4 Scheduling

Abfrage des verwendeten Verfahrens oder der gesetzten Prioritt knnen


sched_getscheduler() bzw. sched_getparam() verwendet werden.
Das Programm im Listing 4.4 demonstriert, wie ein Programm das Schedulingverfahren Round Robin mit der absoluten Prioritt von 10 bekommt. Dabei
fllt auf, dass es nicht mglich ist, als normaler Benutzer diese nderung
vorzunehmen. Das Programm gibt die Fehlermeldung aus, dass dazu keine
Berechtigung vorliege. Wird das Programm jedoch mit Superuser-Privilegien
gestartet, so arbeitet es erfolgreich.
Nicht nur fr den Prozess selbst, sondern auch fr andere Prozesse oder fr
eine Prozessgruppe kann mit sched_setscheduler() das Schedulingverfahren
sowie die absolute Prioritt verndert werden.
Problem: Was passiert, wenn ein Prozess gem FIFO oder RR verwaltet wird
und in eine enggekoppelte Endlosschleife gert?
Gibt es keinen Prozess hherer Prioritt, so wird ein solcher Prozess nicht mehr
verdrngt.12 Deshalb sollte der Administrator insbesondere beim Testen solcher
Prozesse eine Shell mit hherer Prioritt laufen lassen, um den Prozess notfalls
beenden zu knnen.
#include <errno.h>
#include <sched.h> main (int
argc, char *argv[]) { int rc;
struct sched_param param;
param.sched_priority = 10; rc =
sched_setscheduler(0,SCHED_RR,&param); if
(rc < 0) {
switch (errno) {
case EINVAL: printf(" Falsche Policy \
oder falsche Parameter\n"); break; case
EPERM: printf(" keine Erlaubnis, \
Prioritt fr Prozess zu setzen\n");
break; case ESRCH: printf(" kein gltiger
Prozess \ unter angegebener ID zu finden\n");
break; default: printf(" Problem
unbekannt\n");

}
}

>

else printf("...done\n"); exit(0);

Listing 4.4. Beispiel fr RR Realtime Scheduling

RR-verwaltete Prozesse sind etwas weniger kritisch - warum?

12

4.4 Timesharing

51

Mit sched_yield() kann der Prozess die CPU freiwillig abgeben. Er wird dann
sofort als rechenbereiter Prozess wieder an das Ende der Warteschlange
eingeordnet, die zu der gegebenen Prioritt gehrt.

4.4

Timesharing

Beim Timesharing-Verfahren fliet das Verhalten der Prozesse im Sinne von


Feedback in die Bestimmung der Prioritten und Zeitscheiben ein, so dass
interaktive Prozesse eine schnelle Reaktionszeit besitzen, CPU-basierte Prozesse
andererseits einen mglichst guten Durchsatz haben.
Um Realtime-Prozesse zu bevorzugen, wird der Priorittsbereich der Realtime-Anwendungen, der von 1 bis 99 geht, um einen zustzlichen Bereich von 100
bis 139 fr die Timesharing-Prozesse erweitert. Da kleine Zahlen hohe Prioritt
bedeuten, ist die Prioritt damit so gering, dass Kernel- und Realtime-Prozesse
eine hhere Prioritt erhalten als jeder normale Benutzerprozess, der als
Timesharing-Prozesse luft.
4.4.1

Dynamische Prioritten und Zeitscheibenberechnung

Die statische Prioritt eines Timesharing-Prozesses wird durch die Funktion


nice() (vgl. Abschn. 4.4.2) gesetzt und ist auf 0 voreingestellt. Ein neu erzeugter
Prozess bekommt die statische Prioritt (static_prio) des Elternprozesses sowie
dessen dynamische Prioritt (prio) (vgl. S. 21).
Damit das Verhalten des Prozesses angemessen bercksichtigt wird,
berechnet der Scheduler in dem Moment, in dem ein Prozess in das *expiredArray eingestellt wird - also in der Regel nach Aufbrauchen der Zeitscheibe - die
dynamische Prioritt und die Lnge der Zeitscheibe fr diesen Prozess neu.
Die Berechnung der dynamischen Priorittsstufe wird durch die Funktion
effective_prio()13 durchgefhrt und kann die statische Prioritt von TimesharingProzessen vermindern oder erhhen14. Der Algorithmus greift dazu auf den Wert
sleep_avg im PCB zu. Dieser Wert wird benutzt, um in gewissen Grenzen
nachzuhalten, wie lange der Prozess in Warteschlangen verbracht hat gegenber
der Zeit, die er den Prozessor besessen hat. Der ermittelte Wert wird auf das
obige Intervall abgebildet und zur statischen Prioritt addiert. Dabei wird noch
darauf geachtet, dass die Grenzen 100 nicht unter- und 139 nicht berschritten
werden.

Diese Funktion gibt fr Prozesse, die dem Realtime-Scheduling unterliegen, sofort die
statische Prioritt zurck.
14
D.h. die Prioritt des Prozesses wird damit umgekehrt erhht bzw. vermindert, denn
je grer die in der statischen Prioritt gespeicherte Zahl ist, desto geringer ist in
Wirklichkeit die Prioritt des Prozesses. Das Ergebnis der Berechnung bewegt sich fr
Timesharing-Prozesse, deren Priorittsnummer oberhalb der Realtime-Prozesse
angesiedelt ist, zwischen 100 und 139.
13

52

4 Scheduling

Die Zeitscheibe wird auf Grund der auf dem nice-Wert beruhenden
Prioritt ermittelt. Die Funktion task_timeslice() bildet das Intervall 139 - 100
linear auf das Zeitintervall 10 ms - 200 ms ab: je hher die Prioritt, d.h. je
geringer die Priorittsstufe, desto lnger die Zeitscheibe.
4.4.2

Timesharing System Calls

Die Prioritt eines Timesharing-Prozesses kann durch den nice()-Aufruf


verndert werden, nice() akzeptiert Integerwerte von -20 bis 20. Dabei muss
zwischen den niceWerten und der eigentlichen Priorittsstufe unterschieden
werden: die niceWerte werden, wie bereits dargestellt, auf die Priorittstufen
100 bis 139 abgebildet.
Normale Benutzerprozesse drfen dabei nur positive Werte verwenden. Sie
bewirken damit, dass sich der Prozess freundlicher gegenber anderen verhlt,
indem er eine geringere Prioritt erhlt.
Um die Chancen des Prozesses gegenber anderen Prozessen zu verbessern,
muss ein negativer Wert benutzt werden. Dies setzt allerdings voraus, dass der
Prozess entsprechende Rechte besitzt.
Auerdem kann der Benutzer auch den sched_yield()-Aufruf benutzen, der
jedoch eine etwas andere Bedeutung hat als im Realtime-Scheduling: der Prozess
wird sofort in das *expired-Array eingegliedert und bekommt damit erst dann die
CPU wieder zugeteilt, wenn die Arrays vertauscht werden.
4.4.3

Neue Prozesse

Damit sich ein Benutzer nicht dadurch unzulssige Vorteile verschaffen kann,
indem er rechtzeitig einen Kindprozess erzeugt und damit eine volle neue
Zeitscheibe erhlt, geht der Scheduler folgendermaen vor: ein neuer Prozess
bekommt dieselbe statische und dynamische Prioritt wie der Elternprozess, die
verbliebene Zeitscheibe des Elternprozesses wird zwischen Eltern- und
Kindprozess halbiert. Damit wird das Erzeugen vieler Kindprozesse nicht
besonders attraktiv. Die Funktion copy_process()15 erledigt diese Aufgabe.
Wird ein Prozess noch whrend seiner ersten Zeitscheibe beendet, erhlt der
Elternprozess durch sched_exit() den verbliebenen Rest der Zeitscheibe wieder
gutgeschrieben.

4.5

Load Balancing

Im Falle eines Mehrprozessor-Systems richtet Linux fr jede CPU eine eigene


Runqueue ein. Dadurch wird das Scheduling CPU-bezogen. Ein Prozess, der auf
einer CPU luft, bleibt in der Regel bei dieser CPU. Das ist vorteilhaft, da
15

Vereinbart in kernel/fork.c.

4.6 Zusammenfassung 53

vielfach Informationen im Cache der CPU bleiben, auf die wieder zugegriffen
werden kann, wenn der Prozess die CPU erneut zugeteilt bekommt.
Wenn jedoch die Belastung der CPUs sehr unausgeglichen ist, benutzt Linux
Load Balancing, um die Arbeit gleichmiger auf die CPUs zu verteilen. Die dazu
verwendete Methode ist load_balance()16. Wenn beim Aufruf von schedule() keine
lauffhigen Prozesse mehr in der Runqueue sind, wird load_balance() aufgerufen.
Diese Methode sucht zunchst nach derjenigen Runqueue, die die meisten
lauffhigen Prozesse besitzt. Sodann versucht die Methode, vom *expired-Array
der gefundenen Runqueue Prozesse in die leere Runqueue zu bertragen. Die
Prozesse werden aus dem *expired-Array ausgewhlt, damit sichergestellt ist,
dass im CPU-Cache keine Information mehr fr die Prozesse gespeichert ist, da
sie eine Weile nicht mehr gerechnet haben. Da es wichtig ist, Prozesse hoher
Prioritt mglichst gut zu verteilen, werden die Prozesse hchster Prioritt, also
geringster Priorittsstufe gesucht.
Nicht jeder gefundene Prozess kann bertragen werden: zunchst einmal
muss er auf der anderen CPU berhaupt laufen drfen. Auf welchen CPUs ein
Prozess laufen darf, ist im PCB im Eintrag cpus_allowed festgelegt (vgl. S. 21).
Mit dem Aufruf sched_setaffinity() kann festgelegt werden, auf welchen CPUs der
Prozess rechnen darf, denn damit wird der Wert cpus_allowed im PCB gesetzt.
Auf diese Weise lassen sich Prozesse dezidiert CPUs zuordnen oder garantieren,
dass Prozesse auf bestimmten CPUs nicht laufen, die damit fr andere Aufgaben
zur Verfgung stehen.
Die Methode load_balance() wird zustzlich auch in regelmigen Abstnden
- je nach Auslastung zwischen 1 ms und 200 ms - aufgerufen, um die
gleichmige Auslastung der CPUs zu berprfen. Um ein Verschieben von
Prozessen auszulsen, muss in diesem Falle die Runqueue mit den meisten
lauffhigen Prozessen mindestens 25% mehr Prozesse als die gerade untersuchte
Runqueue besitzen.
Frage: Wie weicht der Quelltext von dieser Beschreibung ab?

4.6

Zusammenfassung

Obwohl das Scheduling auf drei verschiedenen Ebenen stattfinden kann, haben
wir nur das Shortterm Scheduling betrachtet. Longterm Scheduling in einem
Unix-hnlichen Betriebssystem reduziert sich auf die Frage, ob der fork()- Aufruf
erfolgreich ist oder nicht, fork() scheitert, wenn bereits insgesamt zu viele
Prozesse oder zu viele Prozesse des konkreten Benutzers im System vorhanden
sind. Auch das Mediumterm Scheduling ist in Linux nicht ausgeprgt.
Zu Beginn stand eine Betrachtung allgemeiner Verfahren, die bei Shortterm
Scheduling eingesetzt werden. Ein Vergleich mit der konkreten Situation in
Linux zeigt, dass in die Implementierung eine Reihe von Verfahren eingeflossen
sind. So haben wir ein priorittsgesteuertes Verfahren, bei dem jedoch
load_balanceO ist in kernel/sched.c definiert.

16

54

4 Scheduling

auch Aspekte des Round Robin zum Tragen kommen. Nicht zuletzt gibt es ein
Feedback, das normale, d.h. Timesharing-orientierte Prozesse nach dem Ablauf
der jeweiligen Zeitscheibe neu bewertet.
Linux untersttzt aber zugleich auch ein weiches Realtime-orientiertes
Scheduling, das Round Robin sowie FIFO kennt. Diese Prozesse werden allen
Timesharing-orientierten Prozessen vorgezogen.
Durch Ausnutzung von Priorittsarrays, die sowohl die Realtime- als auch
die Timesharing-orientierten Prozesse enthalten, ist der SchedulingAlgorithmus sehr schnell; er sucht unabhngig von der Anzahl der Prozesse, die
bereit sind, in konstanter Zeit den nchsten Prozess aus.
Bei einem Mehrprozessor-System wird fr jede CPU ein eigenstndiger
Scheduling-Prozess eingerichtet. Damit ist gewhrleistet, dass ein Prozess, der
auf einer CPU luft, dort in der Regel verbleibt. Damit jedoch die Prozessoren
einigermaen gleichmig ausgelastet sind, wird bei leerer Runqueue auf einem
der Prozessoren ein Load Balancing durchgefhrt, bei dem versucht wird, von
der am strksten belasteten CPU einen Teil derjenigen Prozesse zu bernehmen,
die in der expired-Queue zu nden sind. Das Load Balancing muss dabei einige
Nebenbedingungen beachten. Zustzlich wird das Load Balancing in
regelmigen Zeitabstnden aufgerufen.
Mit nice(), sched_yield() und sched_setscheduler()
sindFunktionen bekannt geworden, mit denen ein Anwendungsprogramm entsprechende Au- torisierung vorausgesetzt - das Scheduling des eigenen
Prozesses beeinflussen kann. Die Funktion sched_setaffinity() dient dazu,
diejenigen CPUs festzulegen, auf denen der Prozess laufen darf.

5______________________
Speicherverwaltung

5.1

Grundlagen

In einer Maschine, in der mehrere Prozesse gleichzeitig ablaufen und damit im


Speicher bereitstehen mssen, ist eine Verwaltung des Speichers dringend
erforderlich. Zum einen muss einem Prozess freier Speicher zugeteilt werden,
zum anderen muss das System darauf achten, dass ein Prozess nicht im
Speicherbereich eines anderen Prozesses - oder gar des Betriebssystems selbst lesen oder schreiben darf.

P3

P2

P1

physischer
Adressraum =
Speicher

Abb. 5.1. Adressabbildung mit Basis- und Grenzregister Die physische


Basisadresse des Prozesses ist im Basisregister (BR) gespeichert, die hchste logische
Adresse im Grenzregister (GR). Prozess P2 besitzt gerade die CPU.

Dabei muss unterschieden werden zwischen dem logischen und dem realen
Adressraum eines Prozesses. Nur bei ganz einfachen Architekturen stimmen

56

5 Speicherverwaltung

diese beiden Sichten berein: in DOS hatte jeder Prozess dieselbe Startadresse
und fand in den darunter liegenden Adressen den Einsprung in die
DOSRoutinen. Jedes Programm musste so in den Speicher geladen werden, dass
die Startadresse mit der entsprechenden physischen Adresse des Speichers
bereinstimmte. Ein derartiges System kann natrlich nicht mehrere Prozesse
gleichzeitig verwalten. Um dieses Ziel zu verwirklichen, mssen die Adressen
zumindest relokabel, d.h. verschiebbar, sein. Die Abbildung von logischer zu
realer Adresse erfolgt dann einfach durch Addition der logischen Adresse mit der
Verschiebung. Abbildung 5.1 zeigt diese Art der Adressenumsetzung zusammen
mit einem Speicherschutz. Der logische Adressraum eines Prozesses reicht von 0
bis zu einer vorgegebenen Obergrenze. Die CPU operiert nur mit den logischen
Adressen. Durch Addition der logischen Adresse mit dem Basisregister wird die
zugehrige physische Adresse erzeugt. Um den Speicherschutz zu verwirklichen,
wird im Grenzregister die hchste logische Adresse des Prozess-Adressraums
abgelegt. Wird versucht, mit einer greren logischen Adresse zuzugreifen, so
wird ein Interrupt erzeugt. Damit nicht auf physische Adressen unterhalb der
Basisadresse zugegriffen werden kann, muss noch ein berlauf bei der Addition
der Basisadresse verhindert werden.
Diese Art der Speicherverwaltung setzt voraus, dass fr jeden neuen Prozess
ein gengend groer freier Speicherbereich gefunden werden kann. Im Lauf der
Zeit wird der Speicher jedoch stark fragmentiert (zersplittert), wie die Abb. 5.2
andeutet. Ein mgliches Vorgehen wre, den Speicher zu defragmentieren, um
so wieder zusammenhngenden freien Speicherplatz zu schaffen. Dabei bleibt
das Problem, dass der gesamte Speicherplatz fr einen Prozess als realer
Speicher vorhanden sein muss, weshalb es sinnvoll ist, ber andere Arten der
Speicherverwaltung nachzudenken.

W.

....-,..,,

^>i^j><<-^>y

t-

Zeit
Abb. 5.2. Fragmentierung des Speichers
Dargestellt wird die wechselnde Belegung des Speichers in Momentaufnahmen. Die
unterschiedlich schraffierten Bereiche stellen die Adressbereiche fr einzelne Prozesse dar.
Durch das Beenden von Prozessen entstehen im Lauf der Zeit Lcken im Speicher. Freier
Speicher ist nicht schraffiert.

5.1 Grundlagen

57

Es gibt zwei Anstze, um das Problem anzugehen: Segmentierung und


Paging. Das Paging wird insbesondere dazu eingesetzt, virtuelle
Speicherverwaltung zu implementieren (siehe Abschn. 5.1.3). In manchen
Betriebssystemen werden Segmentierung und Paging in einer Mischform
eingesetzt.
5.1.1

Segmentierung

Aus Sicht des Programmierers ist der Adressraum eines Programms keineswegs
linear zusammenhngend. Statt dessen wird er als eine Reihe von linear
zusammenhngenden Teilen betrachtet: Programmcode, Datenbereich, Stack,
Prozedurl, Prozedur2 usw. Dabei werden die Prozeduren ebenso in einzelne Teile
zerlegt.
Dies fhrt zu einer feineren Einteilung des Speichers, freie Lcken lassen
sich so besser ausnutzen.
Ein Vorzug der Segmentierung liegt darin, dass die gemeinsame Nutzung
von Programmen besonders leicht untersttzt wird. Ein Programm muss nur
reentrant geschrieben und einmal als Segment geladen werden. Dann knnen
mehrere Benutzer mit jeweils eigenen Datenbereichen darauf zugreifen.
5.1.2

Paging

Die Idee des Paging besteht darin, sowohl den physischen Speicher als auch die
logischen Adressrume der Prozesse in gleich groe Abschnitte einzuteilen. Die
Abschnitte des Speichers heien Prames, die des logischen Adressraums werden
Pages1 genannt. Anstatt wie beim Segmentieren eine inhaltliche Unterteilung
des logischen Adressraumes vorzunehmen, erfolgt eine formale Unterteilung. Der
Adressraum eines Prozesses wird als linear zusammenhngender Raum
angesehen.
Die Gre der Abschnitte wird so gewhlt, dass auf einen performanten
Zugriff auf den Speicher geachtet wird. Die Abschnitte sind je nach System 512
B, 1 KB, . . . , 4 KB usw. gro; im Speicher werden sie hintereinander ohne
Lcken so angeordnet, dass der Anfang jedes Bereichs jeweils eine geeignete
Zweier-Potenz ist.
Die Abbildung der logischen Adressrume auf den Speicher erfolgt mit Hilfe
einer Pagetable. Die Abb. 5.3 zeigt die Umsetzung von logischer in physische
Adresse. Die logischen Adressen werden zweigeteilt in eine Page-Adresse und
einen Offset. Die Gre des Offsets ist so gewhlt, dass damit jede Adresse
Der Quelltext von Linux unterscheidet nicht zwischen Frames und Pages. Dennoch
differenziere ich im Folgenden und benutze den Begriff Frame, wenn es sich eindeutig um
einen Abschnitt des physischen Speichers handelt, und Page, wenn es sich um Abschnitte
des logischen Adressraums handelt. Manchmal ist die Unterscheidung nicht eindeutig:
wenn es sich zwar um einen Frame handelt, der wesentliche Aspekt aber die Zuordnung
zum Prozess beinhaltet. In solchen Fllen neige ich zum Begriff Page.
1

58

5 Speicherverwaltung

innerhalb einer Page angesprochen werden kann. Wenn ein Zugriff auf eine
Adresse erfolgt, verweist die Page-Adresse auf den entsprechenden Eintrag in
der Pagetable, der den Anfang des zugeordneten Frames im Speicher enthlt. Zu
diesem Anfang muss der Offset addiert werden, um die korrekte Adresse im
Speicher zu ermitteln.
logischer
Adressraum

physisc
scher
SpeichiLer

Pagetable
Abb. 5.3. Adressierung mittels Pagetable Physische Adressen des
Speichers sind mit durchgezogenen Linien dargestellt, logische Adressen und Adressrume
sind gestrichelt. Logischer Adressraum und physischer Speicher sind in gleich groe
Abschnitte - Pages bzw. Frames - eingeteilt. Die Adressberechnung benutzt den PageAnteil der logischen Adresse als Index in die Pagetable und addiert zu der dort gefundenen
Adresse den logischen Offset. Die gepunktete Linie zeigt vom Eintrag in der Pagetable auf
die Basisadresse des adressierten Frames.

5.1.3

Virtueller Speicher

Die bisher betrachteten Verfahren haben den Nachteil, dass der gesamte
Adressraum eines Prozesses im Speicher vorgehalten werden muss. Bei der
Entwicklung einer Anwendung wird Code fr die Behandlung von Fehlern
erzeugt, der bei einem normalen Programmablauf in der Regel nie aufgerufen
wird. Ebenso werden Initialisierungen beim Starten des Prozesses bentigt

5.1 Grundlagen 59

und anschlieend nicht mehr angefasst. Es wre also vorteilhaft, wenn es


gelingen knnte, nur denjenigen Programmcode und diejenigen Datenbereiche im
Speicher vorzuhalten, die der Prozess im Augenblick auch wirklich bentigt.
Die Struktur der Abschnitte fest vorgegebener Gre, die beim Paging
verwendet werden, wirkt sich hier gut aus. Durch zustzlichen Aufwand kann
das Betriebssystem feststellen, ob die Page, auf die die logische Adresse eines
Prozesses verweist, auch wirklich im Speicher vorhanden ist. Dazu mssen die
Eintrge der Pagetable um ein Valid-Bit ergnzt werden. Ist bei dem Zugriff
dieses Bit gesetzt, so befindet sich die Page an der angegebenen Adresse, ist es
nicht gesetzt, so muss die Page von der Platte geladen werden. Beim Zugriff muss
bei nicht gesetztem Valid-Bit ein Interrupt, der Pagefault-Interrupt, ausgelst
werden. Das Verfahren wird Paging on Demand genannt.
Als Folge des Pagefault-Interrupts muss das Betriebssystem die
nachstehenden Schritte ausfhren:

berprfen, ob die Anforderung erlaubt ist,


einen freien Prame im Speicher suchen2,
die bentigte Information auf der Platte suchen und in den gefundenen
Frame kopieren und
den zugehrigen Eintrag in der Pagetable korrigieren, d.h. die Adresse
der Page eintragen und das Valid-Bit setzen.

Um schnell einen freien Frame zu finden, kann z.B. eine Freelist verwaltet
werden. Als Freelist eignet sich ein Bitvektor, der fr jeden Frame ein Bit
enthlt. Ist das Bit gesetzt, so wird der Frame benutzt, andernfalls ist er frei und
kann belegt werden. Bei Verwendung einer Freelist muss diese natrlich
ebenfalls korrigiert werden, sobald ein Frame belegt oder freigegeben worden ist.
Probleme treten auf, wenn kein freier Frame gefunden werden kann. In
diesem Fall muss ein bereits belegter Frame3 verdrngt werden, um fr die
bentigten Daten Platz zu machen.
Problem: Wie kann eine geeignete Page zum Verdrngen gefunden werden?
Eine Page, die nach kurzer Zeit wieder bentigt wird, ist kein geeigneter
Kandidat zum Verdrngen. Andererseits gibt es keine Mglichkeit,
vorherzusagen, wann ein Prozess auf eine Page zugreifen wird. Wegen des
lokalen Verhaltens unserer Prozesse kann man aber versuchen, aus der
Vergangenheit abzuleiten, ob die Page gefragt ist: eine Page, auf die lange Zeit
nicht zugegriffen wurde, wird wohl auch in naher Zukunft nicht bentigt. Eine
einfache Implementierung dieser berlegung besteht in einem Schieberegister
und einem Access-Bit, die jeder Page in der Pagetable zugefgt werden. Bei
einem Zugriff auf die Page wird das Access-Bit gesetzt; in regelmigen
Abstnden

Falls kein freier Frame gefunden werden kann, muss erst durch Auslagerung des
Inhalts eines Frames auf den Backup-Speicher Platz geschaffen werden.
3
D.h. eine gltige Page eines Prozesses, deshalb wird in diesem Kontext Page und
Frame synonym verwendet.
2

60

5 Speicherverwaltung

durchluft eine Routine des Betriebssystems smtliche Page-Eintrge, verschiebt


den Inhalt des Schieberegisters um 1 Bit nach rechts und fgt links den Inhalt
des Access-Bits ein. Danach wird das Access-Bit zurckgesetzt. Wird nun nach
derjenigen Page gesucht, die am lngsten nicht benutzt wurde, so ist das
gleichlautend mit der Suche nach demjenigen Eintrag mit dem kleinsten
Schieberegister-Inhalt.
Ein weiteres Kriterium ist wichtig: wurde die Page seit dem Bereitstellen
verndern? Wurde sie nicht verndert, so kann sie einfach berschrieben
werden, da ihr Inhalt auf der Platte zu finden ist. Andernfalls muss sie zunchst
auf die Platte geschrieben werden, um den genderten Inhalt bis zum nchsten
Zugriff aufzubewahren. Auch dieses Vorgehen kann durch ein weiteres Bit fr
jeden Eintrag in der Pagetable realisiert werden: das Dirty-Bit. Bei dem
Bereitstellen der Page wird dieses Bit zurckgesetzt, sobald jedoch auf diese
Page schreibend zugegriffen wird, wird das Bit gesetzt.
Ein weiteres Kriterium kann beim Suchen nach einer geeigneten Page
herangezogen werden: soll Prozess-bergreifend oder nur in den Pages des
Prozesses selbst nach einem geeigneten Kandidaten gesucht werden.
Sind zu viele Prozesse im Speicher, so kann als Problem das sogenannte
Thrashing auftreten: tritt ein Pagefault-Interrupt auf, so werden nur noch
aktive Pages gefunden. Somit knnen nur Pages verdrngt werden, die kurz
danach wieder bentigt werden und somit als Folge einen weiteren PagefaultInterrupt auslsen. Dieses Problem lsst sich lsen, indem niedrig priorisierte
Prozesse ausgelagert werden, sobald die Anzahl der Pagefaults pro Zeiteinheit
ein vorgegebenes Ma berschreitet.

5.2

Ziele fr Linux

5.2.1

Vielzahl von Hardware-Plattformen

Die grundstzlichen berlegungen des letzten Abschnitts gehen davon aus, dass
die Hardware entsprechend angepasst wird, d.h. dass eine Pagetable durch
schnelle spezifische Hardware untersttzt wird. Ein wesentlicher Gesichtspunkt
bei der Entwicklung von Linux liegt aber darin, dass es auf einer Vielzahl von
Hardware-Plattformen4 ohne zustzlichen Aufwand lauffhig sein soll. Die
Verfahren knnen also nicht auf speziell entwickelte Hardware zugeschnitten
werden.
5.2.2

Inhomogener Speicher

Schon ein Blick auf die PC-Architektur zeigt, dass der Speicher nicht
notwendigerweise homogen ist. So muss beachtet werden, dass der eingebaute
Die x86-Architektur ist nur eine von vielen Hardware-Plattformen, die Linux
untersttzt. Das Spektrum reicht inzwischen von der Smartcard bis zu
Grorechnerarchitekturen .
4

5.2 Ziele fr Linux

61

Cache ggf. nur einen Teil des Speichers beschleunigt. Auf Cache-Inhalte kann
der Prozessor sehr schnell zugreifen; ist jedoch eine Speicheradresse nicht im
Cache enthalten, muss mit merklich grerem Zeitaufwand auf den Speicher
direkt zugegriffen werden, wobei Adresse und Inhalt automatisch in den Cache
eingetragen werden. Ggf. werden dadurch andere Cache-Eintrge verdrngt.
Auerdem gibt es immer noch Hardware, die DMA nur auf einem kleinen Teil
des Speichers zulsst: zum Teil kann DMA nur in den ersten MBs des Speichers
durchgefhrt werden. DMA dient dazu, bei I/O die Daten direkt zwischen
Controller und Speicher auszutauschen. Die CPU kann dadurch whrend des
Transfers fr andere Aufgaben eingesetzt werden.
5.2.3

NUMA-Architekturen

Eine andere Art von Inhomogenitt des Speichers findet man in MehrprozessorArchitekturen, bei denen jedem Prozessor ein eigener Speicherbus zugeordnet
ist. Der Zugriff von einem Prozessor auf den eigenen Speicherbereich ist schnell,
der Zugriff eines Prozessors auf den Speicherbereich eines anderen Prozessors ist
zwar mglich, aber wesentlich langsamer als im vorhergehenden Fall. Hier muss
versucht werden, zusammengehrende Pages jeweils einem Prozessor
zuzuordnen.
5.2.4

Page Cache

Um eine gute Performance bzgl. des Paging und des Platten-I/O zu erreichen,
werden vom Betriebssystem Caches im Speicher verwaltet. Da Zugriffe ber den
Speicher um mehrere Grenordnungen schneller als Plattenzugriffe sind, reicht
eine software-mige Implementierung. Diese Caches untersttzte Linux bereits
seit der Version 2.2. in der Version waren jedoch zwei unterschiedliche Caches
implementiert: ein Cache fr das Paging, der andere fr das I/O von Daten. Dies
fhrte zu einer doppelten Verwaltung sowie zum Teil zu berschneidungen und
damit Synchronisationsproblemen bei diesen Caches. Seit der Version 2.4 setzte
eine Vereinheitlichung der beiden Caches ein: Linux verwaltet fr beide
Aufgaben einen einheitlichen Page Cache, durch den das gesamte I/O zur Platte
geleitet wird.
Bei frheren Linux-Versionen wurde der Page Cache mittels Hash-Table
adressiert. Dabei traten folgende Probleme auf:

Durch die Verwendung eines globalen Lock-Mechanismus litt die


Performance,
der Speicherplatz fr den Hash musste alle Pages bercksichtigen, nicht
nur die tatschlich verwendeten und
ein fehlgeschlagenes Nachschlagen im Hash war aufwndiger als ntig.

Seit der vorliegenden aktuellen Version 2.6 wird versucht, diese Problematik mit
einer Radix-Tree-Verwaltung zu lindern.

62

5 Speicherverwaltung

5.2.5

pdflush: Zurckschreiben vernderter Pages

Das System muss darauf achten, vernderte Pages zurckzuschreiben. Dies ist
aus zwei Grnden ntig: Zum einen mssen nderungen an den Daten, dem
Filesystem usw. auf den Platten gesichert werden, um in regelmigen
Abstnden eine gewisse Synchronitt zwischen der Information im Speicher und
den Platten zu erreichen. Zum anderen knnen belegte und vernderte Pages
nur dann fr andere Aufgaben benutzt werden, wenn ihr Inhalt vorher gesichert
wurde (vgl. Abschn. 5.1.3, S. 60).
Um Engpsse zu vermeiden, wird das System versuchen, die
Synchronisation mglichst dann durchzufhren, wenn nur wenige Zugriffe auf
die Platten anstehen. Die zweite Aufgabe greift jedoch dann, wenn ein
Speicherengpass eintritt, wenn also die Anzahl der freien Pages unter ein
vorgegebenes Niveau sinkt.
Durch die Vereinheitlichung des Paging mit dem Platten-I/O reicht dazu ein
einheitlicher Prozess: pdflush. Um durch die langsamen Plattenoperationen
nicht gebremst zu werden, wird das System versuchen, mehrere solcher Prozesse
bereitzustellen. Diese Prozesse ruhen, wenn keine Synchronisation notwendig ist
oder wenn gengend freie Speicherseiten vorhanden sind. Ruht ein solcher
Prozess fr lngere Zeit, so wird das System diesen beenden. Dabei achtet das
System darauf, dass eine Mindestanzahl solcher Prozesse immer im System
verbleibt. Treten andererseits hohe Anforderungen auf, werden weitere solcher
Prozesse bis zu einer vorgegebenen Obergrenze erzeugt.
5.2.6

Slab: Verwaltung von Kernel-Objekten

Der Kernel muss immer wieder komplexe Objekte wie PCBs usw. im Speicher
anlegen und freigeben. Um die Performance zu erhhen, wurde der Slab
Allocator5 entwickelt. Der Slab dient als Cache fr komplexe Datenstrukturen:
Ein freigegebenes Objekt kann wieder im gleichen Sinne verwendet werden.
Damit wird zum einen der Overhead beim Vorbereiten eines solchen Objektes
verringert und zum anderen der Fragmentierung des Speichers vorgebeugt (vgl.
Abb. 5.2).
5.2.7

Frame-Allocation zum sptesten Zeitpunkt

Frames mssen fr einen Prozess nicht dann bereitgestellt werden, wenn der
Prozess Pages anfordert, sondern erst dann, wenn der Prozess versucht, darauf
zuzugreifen. Ggf. erfolgt ein solcher Zugriff gar nicht; damit wren dann
Speicher und Zeit fr das Bereitstellen eines Frames gespart.
Linux verschiebt aus diesem Grunde das Bereitstellen physischen
Speicherplatzes bis auf den letzten Moment. Die Anwenderprozesse knnen
somit nicht direkt Speicher anfordern, sondern nur logischen Adressraum.
Die Entwicklung stammt aus dem SunOS (heute Solaris) der Firma Sun.

5.3 Prozess-Adressraum

5.3

63

Prozess-Adressraum

struct mm_struct {
struct vm_area_struct * mmap; /* Liste Speicherbereiche */
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache;
/* zuletzt benutzter Bereich
*/ unsigned long free_area_cache;/* erster freier Bereich */
pgd_t * pgd;
/* Anzahl zugreifender Prozesse */
atomic_t mm_users;
/* Zugriff auf Struktur */
atomic_t mm_count;
/* Anzahl Speicherbereiche */
int map_count;
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock; /* Zugriffsschutz fuer Pages */
/* Liste aller mm_structs */
struct list_head mmlist;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
arg_end,unsigned
env_start,
longenv_end;
arg_start,
misigned long rss,
vm, locked_vm;
total_v
unsigned long def_flags;
cpumask_t cpu_vm_mask;
unsigned long
swap_address
unsigned long
saved_auxv[4
unsigned dumpable: 1 ;
#ifdef CONFIG_HUGETLB_PAGE
/* Architektur-abhaengige Daten */
int used_hugetlb;
#endif
mm_context_t context; int
core_waiters;
struct completion *core_startup_done,
core_done; rwlock_t ioctx_list_lock; struct
Listing 5.1. Memory Deskriptor

Der Prozess-Adressraum ist der logische Adressraum eines Prozesses. Es handelt


sich dabei um einen linear zusammenhngenden Raum, der diejenigen
Adressbereiche beschreibt, auf die der Prozess zugreifen darf. Je nach
Architektur handelt es sich um einen Raum, dessen Adressen mit 32- oder 64-Bit
beschrieben werden knnen. Da der Prozess nicht auf alle Bereiche mit gleichen
Rechten zugreifen darf - so darf auf den Teil, in dem gemeinsame Bibliotheken
bereitgestellt werden, nicht schreibend zugegriffen werden -, wird dieser Raum in
einzelne Speicherbereiche unterteilt. Zur Verwaltung wird eine

64

5 Speicherverwaltung

lineare Liste von Speicherbereichen zusammen mit weiteren Informationen im


Memory Deskriptor mm_struct bereitgestellt (vgl. Listing 5.1).
5.3.1

Memory Deskriptor

Es fllt auf, dass die Struktur mm_struct6 des Memory Deskriptors nicht im
Bereich der brigen Speicherdefinitionen, sondern im Bereich des Scheduling zu
finden ist. Diese Design-Entscheidung hngt damit zusammen, dass der ProzessAdressraum im PCB verankert ist (vgl. S. 21).
Auf den nchsten Blick verblfft, dass die ersten beiden Eintrge - mmap
und mm_rb - nahezu redundant sind: der erste bildet eine lineare Liste, der
zweite einen Red-Black-Tree der zugeordneten Speicherbereiche. In der Regel
wird man solche Dopplungen vermeiden, jedoch hat es hier einen Vorteil auf
Grund der unterschiedlichen Strukturen: eine lineare Liste ist optimal, wenn alle
Eintrge bearbeitet werden mssen, ein balancierter Baum ist vorteilhaft, wenn
nach einem gegebenen Element gesucht wird.
mm_users gibt die Anzahl derjenigen Prozesse an, die auf diesen ProzessAdressraum zugreifen. Da Threads in Linux (vgl. Abschn. 3.4.4) eigenstndige
Prozesse sind, die sich denselben Prozess-Adressraum teilen, kann man sagen,
dass mm_users die Anzahl der Threads anzeigt, mm_count ist eine generelle
Referenz der Zugriffe, wobei unterschiedliche Threads nur einmal gezhlt
werden. Wenn dieser Zhler auf 0 fllt, kann der Prozess-Adressraum
freigegeben werden.
Alle Memory Deskriptoren werden in einer linearen Liste verwaltet, die ber
mmlist erreicht wird.
Angelegt wird ein Memory Deskriptor, sobald ein neuer Prozess mittels fork()
erzeugt wird. Dabei wird aus einem entsprechenden Slab Cache (vgl. Abschn.
5.2.6) ein freies Objekt genommen. Ist der erzeugte Prozess ein Thread, so ist das
Flag CLONE_VM gesetzt. Dies bewirkt, dass der neue Prozess den Memory
Deskriptor des Elternprozesses zugewiesen bekommt und dort mm_users um 1
erhht wird.
Auch um das Freigeben eines Memory Deskriptors kmmert sich der Kernel
selbst.
5.3.2

Die Speicherbereiche

Der Prozess-Adressraum wird in einzelne zugeordnete Speicherbereiche


gegliedert, deren Struktur durch vm_area_struct7 definiert ist. Diese
Speicherbereiche sind nicht berlappend und spannen nur einen Teil des
gesamten Prozess- Adressraums auf. Gibt es einen Prozess im System mit der
Prozess-ID 4711, so kann ein erster Eindruck von dieser Organisation gewonnen
werden durch das Kommando cat /proc/4711/maps. Fr ein ganz einfaches CProgramm,
6
7

mm_struct ist definiert in include/linux/sched.h.


vm_area_struct ist in include/linux/mm.h definiert.

5.3 Prozess-Adressraum
aQachilles:~cat /proc/4711/maps
08048000-08049000 r-xp 00000000 03:0b
08049000-0804a000 rw-p 00000000 03:0b
40000000-40018000 r-xp 00000000 03:0e
40018000-40019000 rw-p 00017000 03:0e
40019000-4001a000 rw-p 00000000 00:00 0
4002b000-40157000 r-xp 00000000 03:0e
40157000-4015c000 rw-p 0012c000 03:0e
4015c000-4015e000 rw-p 00000000 00:00 0
bfffe000-c0000000 rwxp fffff000 00:00

12681
12681
127
127
124
124

65

/home/a/spei
cher
/home/a/speiche
r /lib/ld2.3.2.so
/lib/ld2.3.2.so

Listing 5.2. Ausgabe des Kommandos cat /proc/4711/maps

das eine Eingabe und eine Ausgabe macht, knnte das Ergebnis wie in Listing
5.2 aussehen.
Die ersten beiden Angaben bilden die Anfangs- und Ende-Adresse des
jeweiligen Speicherbereichs. Es folgt eine Angabe, wie auf den Speicherbereich
zugegriffen werden darf. Die Code-Anteile sind les- und ausfhrbar, Daten und
Stack sind les- und schreibbar, aber nicht ausfhrbar. Jeweils am Ende einer
Zeile befinden sich die Inode-Nummer und der Name der jeweiligen Datei bzw.
Bibliothek.
Die Struktur zur Verwaltung eines Speicherbereichs (VMA - Virtual Memory
Area) wird beschrieben durch die Struktur vm_area_struct8. Listing 5.3 zeigt
den Aufbau. Die Speicherbereiche beschreiben fr einen Prozess jeweils
zusammenhngende disjunkte Intervalle im Speicher mit gleichen
Eigenschaften.
struct vm_area_struct {
struct mm_struct * vm_mm; /* zugehriger Memory Descriptor */
unsigned long vm_start;
/* Anfangsadresse */
unsigned long vm_end;
/* Endadresse */
struct vm_area_struct *vm_next; /* Liste der VMAs */
pgprot_t vm_page_prot; /* Zugriffsrechte fr dieses VMA. */
unsigned long vm_flags; /* Flags */
struct rb_node vm_rb; /* zugehriger rb-Knoten */
struct list_head shared;
struct vm_operations_struct * vm_ops;
/* Funktionen auf diesem VMA */
unsigned long vm_pgoff;
/* Offset in Datei */
struct file * vm_file;
/* Mapped Datei */
void * vm_private_data;

Listing 5.3. VMA-Struktur


8

Definiert in include/linux/mm.h.

5 Speicherverwaltung

66

In dieser Struktur sind wichtige Angaben wiederzufinden, die durch das


Kommando cat /proc/4711/maps angezeigt werden. Die Art des Zugriffs auf den
Speicherbereich ergibt sich aus den Flags: Die wichtigsten Flags sind VM_READ,
VM_WRITE und VM_EXEC fr Lesen, Schreiben und Ausfhren des
Speicherbereichs. VM_SHARED beschreibt einen Speicherbereich, auf den
mehrere Prozesse gemeinsam zugreifen. Wie blich knnen diese Flags mit
logischem Oder verknpft werden.
Um mglichst wenig Speicher zu belegen, werden in Linux nicht nur
diejenigen Speicherbereiche mit gesetztem VM_SHARED-Flag, sondern auch
diejenigen, die nicht vernderbar sind, fr die also das VM_WRITE-Flag nicht
gesetzt ist, nur einmal im physischen Speicher gehalten. Somit knnen z.B.
mehrere Prozesse denselben Editor verwenden, ohne dass der zugehrige
Programmcode mehrmals im Speicher vorhanden sein muss.
5.3.3

System Calls

Hufig erfolgt eine Erweiterung des Prozess-Adressraums ohne explizite


Aufforderung im Programm, so bei einem der exec()-Aufrufe, wenn der
bestehende Prozess-Adressraum berlagert wird, um ein anderes Programm
auszufhren. Eine implizite Erweiterung erfolgt auch, wenn ein Prozess
automatisch weiteren Speicherbereich anlegt, indem z.B. ein Block betreten wird,
der lokale Variablen besitzt. Mit dem Verlassen des Blocks wird der
Speicherbereich wieder freigegeben. Das Beispiel in Listing 5.4 zeigt die
Struktur dieser berlegungen.9
main()
{ int
i;
for (i=l; i<100; i++)
{ char c;

Listing 5.4. Implizite Erweiterung des Speicherbereichs

Andererseits gibt es Situationen, in denen ein Prozess explizit mit dem Aufruf
malloc() weitere gltige Speicherbereiche anfordern muss. Dies ist immer dann
der Fall, wenn die Menge des Speichers oder die Zeit, zu der der Speicher
bentigt wird, vom Verlauf des Prozesses abhngt. Listing 5.5 zeigt, wie dies
mittels malloc() geschieht.
Die sofort nach dem Aufruf von malloc() erfolgende Prfung stellt fest, ob ein
gewnschter Speicherbereich gefunden wurde. Ist der Speicherbereich
vorhanden, sollte eine Initialisierung stattfinden, da der Inhalt unbestimmt ist.
Die Freigabe erfolgt mit Hilfe des free()-Aufrufs, der als Argument nur einen
Tatschlich wird hier jedoch der logische Prozess-Adressraum nicht erweitert.

5.3 Prozess-Adressraum

67

main()
{ int
i;
struct foobar
{ int
feld[1000];
void *next;

};

/* Anweisungen */ struct
foobar *ptr =
(struct foobar *) malloc(sizeof (struct
foobar)); if (ptr == 0) {
/* Fehlerbehandlung */

ptr->next = 0;
/* Initialisierungen der Struktur */
for (i = 0; i < 1000; i++) { /* Initialisierungen ... */
ptr->feld[i] = i;

/* weitere Anweisungen */
free(ptr); exit(0);
Listing 5.5. Verwendung von malloc()
#include <sys/mman.h>
#include <fcntl.h>
main() { char ch;
int fd = open("speicherl.c", 0_RD0NLY);
int *ptr = mmap(0, lseek(fd,0,2),
PR0T_READ, MAP_SHARED, fd,
0); if (ptr == 0) {

>

/* Fehlerbehandlung */

munmap(ptr,
lseek(fd,0,2)); exit(0);

Listing 5.6. Mappen einer Datei in den Speicher

durch malloc() initialisierten Pointer besitzen darf. Da malloc() nur einen


(logischen) Speicherbereich anfordert, ein Frame aber erst dann angefordert
wird, wenn konkret auf die Page zugegriffen werden soll, kann trotz korrekter
Rckmeldung von malloc() der physische Speicher spter berbelegt werden
(overcommit_memory), worauf Linux mit dem Abbruch von Prozessen reagiert.
Dies kann vom Systemadministrator durch
echo 2 > /proc/sys/vm/overcommit_memory

verhindert werden.

68

5 Speicherverwaltung

Listing 5.6 zeigt eine weitere Art, wie man einen neuen gltigen
Speicherbereich dem Adressraum hinzufgen kann. Es wird eine geffnete Datei
- in diesem Falle ein C-Quelltext - in den Speicher gemappt und kann dort direkt
adressiert werden. Das Beispiel geht davon aus, dass die Datei ausschlielich
zum Lesen verwendet wird und dass sie von anderen Prozessen ebenfalls
gemappt werden kann. Die Speicheradresse, die auf den Anfang des
Speicherbereichs zeigt, wird in ptr zurckgegeben. Nach der Benutzung wird der
Speicherbereich durch munmap() wieder freigegeben.
Dem Prozess-Adressraum kann auch dadurch ein gltiger Speicherbereich
hinzugefgt werden, indem ein gemeinsam von mehreren Prozessen genutzter
Speicher angelegt und dem Prozess zugnglich gemacht wird. In diesem Fall
spricht man von Shared Memory. Dies ist eine von drei Aufgaben10, die mit dem
sogenannten IPC (Inter Process Communication) gelst werden kann. IPC wurde
schon frh Unix hinzugefgt. Im Gegensatz zu anderen Objekten in Unix werden
IPC-Objekte ber einen numerischen Schlssel identifiziert. An dieser Stelle soll
nur die Verwendung der System Calls dargestellt werden. Eine weitergehende
Betrachtung von IPC findet im Abschn. 9.3 statt.
Listing 5.7 zeigt, wie ein Speicherbereich bereitgestellt und in den
Adressraum eingebunden wird. Die Funktion shmget() wird verwendet, um
einen Speicherraum bereitzustellen. Der Schlssel MYKEY, der in der
Headerdatei mymem.h definiert wird, identifiziert das IPC-Objekt. Als weitere
Parameter des shmget()-Aufrufs werden die Speichergre und Flags, die
auch die Zugriffsberechtigungen beinhalten, angegeben. Zurckgegeben wird
dann ein interner Identifier, hier memID benannt. Um auf den Speicher zugreifen
zu knnen, muss er in den Prozess-Adressraum mit shmat() eingebunden
werden. Hier wird shmat() so verwendet, dass das System selbst die beste
Adresse bestimmt, an der die Einbindung erfolgen soll. Wird der Speicherbereich
nicht mehr bentigt, sollte er mit shmdt() aus dem Prozess-Adressraum
entfernt werden. Das IPC-Objekt besteht aber immer noch, der angeforderte
Speicher existiert somit weiter, kann aber vom Prozess aus nicht mehr
angesprochen werden. Soll auch das IPC-Objekt gelscht werden, so muss dies
mit dem Aufruf shmctl() erfolgen, wobei der Parameter IPC_RMID benutzt
wird.

5.4

Pagetable

Die Umsetzung einer logischen Adresse des Prozess-Adressraums auf die


zugehrige physische Adresse im Speicher erfolgt ber Pagetables (vgl. Abschn.
5.1.2). Jeder Prozess besitzt eine eigene Pagetable. Die prinzipielle Art der
Adressumsetzung ist in Abb. 5.3 dargestellt. Fr jeden Prozess ist der lineare
Prozess-Adressraum nur dnn mit gltigen Adressbereichen besetzt. Es msste
somit fr jeden Prozess eine sehr groe Pagetable bereitgestellt werden, die im
Wesentlichen ungltige Eintrge enthlt.
10
Die anderen beiden Aufgaben sind Bereitstellung von Semaphoren und von Message
Queues.

5.4 Pagetable 69
/* Header-Datei mymem.h
*/ typedef struct { int
i;
} Daten;
key_t MYKEY = (key_t) 4711; /* IPC-Schlssel */

/* Programm speicher.c */
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include "mymem.h" main()
{
Daten *daten; int memID;
memID = shmget(MYKEY, 1000, IPC_CREAT|0600);
daten = (Daten *) shmat(memID, 0, 0); daten->i
= 10;

/* ----- */

shmdt(daten); /* zum Entfernen aus dem Adressraum */


/* falls das IPC-Objekt nicht mehr bentigt wird:
shmctl(memID, IPC_RMID, 0); andernfalls berlebt
es ggf. alle Prozesse, die darauf zugreifen.

*/

exit(0);

>

Listing 5.7. IPC Shared Memory


Achtung: das Programm enthlt keine Fehlerberprfungen! Hier ist nur der Prozess
dargestellt, der das IPC-Objekt erzeugt; andere Prozesse wrden im shmget()- Aufruf
nicht das Flag IPC_CREAT verwenden.

Wie Abb. 5.4 auf Seite 70 zeigt, implementiert Linux fr jeden Prozess die
Pagetable in drei Ebenen, um damit einen dnn besiedelten Adressraum mit nur
geringem Platzaufwand abzubilden. Bei manchen Hardware-Architekturen fallen
jedoch zwei der Ebenen zusammen, so in der x86-Architektur die zweite und
dritte Ebene. Die Adresse wird in mehrere Teile unterteilt. Diese Adressumsetzung scheint sehr aufwndig und langsam zu sein, Linux nutzt jedoch
durch Anpassung an die jeweilige Architektur die Hardware-Untersttzung aus
und erreicht dadurch schnelle Zugriffe.

70

5 Speicherverwaltung
logische Adresse

PGD
PMD
PTE
page global directory page middle directories
page table

Abb. 5.4. Linux Pagetables und Adressierung

Wesentliche Aspekte dieser Umsetzung sind in folgenden Dateien zu finden:


Hardware-orientiert in include/asm/page.h, include/asm/pgalloc.h sowie
include/asm/pgtable.h und allgemein in include/linux/mm.h.11
Die Struktur, die zur Beschreibung eines Frames im Speicher verwendet
wird, ist in Listing 5.8 dargestellt. Beim Start von Linux wird fr jeden Frame im
im Speicher eine solche Struktur angelegt. In flags wird der Status aufbewahrt.
Die mglichen Bitflags sind in der Datei include/linux/page-flags.h
definiert. Wichtig ist z.B., ob die Page verndert wurde (dirty) oder ob sie im
Speicher verbleiben muss (locked, vgl. Abschn. 5.4.1). In count werden die
Referenzen auf den Frame verwaltet. Die drei darauf folgenden Attribute dienen
dazu, die Frames via Page Cache zu verwalten (vgl. Abschn. 5.6) und damit
schneller zugreifbar zu machen. Der Eintrag virtual wird bentigt, um
Highmem-Speicher (vgl. S. 73), der sich nicht stndig im Kernel-Adressraum
befindet, zu adressieren.
Wird ein Frame bentigt, so wird der Kernel in der geeignetsten Zone
versuchen, diesen anzufordern (vgl. S. 73). Werden die Frames beispielsweise bei
einem DMA-Vorgang bentigt, so werden sie in der Z0NE_DMA angelegt.
free_area[MAX_ORDER] ist ein Array, das zusammenhngende freie Frames
ber Listen verwaltet. Gerade bei DMA sind zusammenhngende Frames ntig,
da in der PC-Architektur DMA nicht mit der Adressumsetzung
zusammenarbeitet.
5.4.1

Page locking

Ein Betriebssystem, das Paging on Demand untersttzt, wird auf Anforderung


Pages im Speicher anlegen und bei Bedarf freigeben. Gerade das Anlegen ist 11
asm ist ein Link auf das Directory fr die jeweilige Hardware-Version, fr die x86Architektur knnte man auch direkt include/asm-i386/. . . angeben.
11

5.4 Pagetable

71

struct page {
unsigned long flags;
atomic_t count;
struct list_head list;
struct address_space *mapping;
unsigned long index;
struct list_head lru;
union {
struct pte_chain *chain;
pte_addr_t direct;
>
pte;
unsigned long
private; void
*virtual;

};

Listing 5.8. Datenstruktur zur Verwaltung eines Frames

jedoch sehr zeitintensiv. Deshalb kann es bei zeitkritischen Prozessen sinnvoll


sein, Pages im Speicher gegen das Freigeben durch Page Locking zu schtzen. Sie
stehen dann dem Prozess ohne Pagefault stndig zur Verfgung. Ein weiterer
Grund fr das Page Locking kann darin liegen, dass besonders schtzenswerte
Daten nicht in den Swapping-Bereich geschrieben werden sollten, da sie dort
noch lange nach Beenden des Prozesses auf der Platte gefunden werden knnen.
Aus diesem Grund knnen einzelne oder auch alle Pages eines Prozesses im
Speicher gelockt werden. Voraussetzung dafr ist, dass der Prozess RootRechte besitzt.
Listing 5.9 zeigt, wie mit dem Aufruf mlockall() alle Pages des Prozesses
gegen Auslagern gesichert werden.
#include <sys/mman.h>
main() { int rc;
rc = mlockall(MCL_CURRENT |
MCL_FUTURE); if (rc == 0) {
printf("Pages
gesperrt\n"); rc =
munlockall(); if (rc == 0)
printf("Pages wieder entsperrt\n");

>

exit(0);

Listing 5.9. Sperren von Pages mit mlock()

5 Speicherverwaltung

72

MCL_CURRENT sorgt fr das Sperren aller derzeitig verwendeten Pages. Wenn


zuknftig bentigte Pages des Prozesses gesperrt werden sollen, so ist
MCL_FUTURE einzusetzen.
Natrlich knnen auch einzelne Pages gesperrt werden. Der dazu bentigte
Aufruf lautet mlock(), das Preigeben erfolgt mit munlock().
Gesperrt werden logische Pages (vgl. Listing 5.8). In den flags wird das
entsprechende Bit gesetzt.

5.4.2

PSE

In einigen speicherintensiven Anwendungen ist der Verwaltungsaufwand mittels


einer dreistufigen Architektur der Pagetable zu gro. Hier hat Linux mit der
Kernel-Version 2.6 die PSE (Page Size Extension oder auch Large Pages)
eingefhrt. Die 32-Bit Adresse wird dann in 22 Bit fr die Adressierung
innerhalb einer 4 MB groen Page und 10 Bit zur Adressierung der Pages
eingeteilt. Der verringerte Verwaltungsaufwand wird durch sinkende
Granularitt des Speichers erkauft. Dies ist bei speziellen Anwendungen wie
beispielsweise Datenbank-Servern aber kein Nachteil.

5.5

Paging

Wird auf eine Adresse zugegriffen, die in einem gltigen Speicherbereich liegt,
aber keinem Frame im Speicher zugeordnet ist, so wird ein Pagefault-Interrupt
ausgelst. Durch diesen Interrupt wird das Betriebssystem dazu veranlasst, eine
oder sogar mehrere zusammenhngende Frames bereitzustellen und - sofern
erforderlich - die bentigten Daten aus dem Swap-Bereich oder aus einem
anderen Plattenbereich einzulesen. Ein Pagefault auf Grund eines
Benutzerprozesses stellt immer nur einen einzelnen Frame bereit; mehrere
zusammenhngende Frames knnen bentigt werden, wenn der Kernel selbst
Speicher anfordert.
5.5.1

Pagefaults

Mehrere Situationen fhren zu einem Pagefault-Interrupt:

Wie in Kap. 3.4 angesprochen, kann die Copy-on-Write-Technik dazu


fhren, einen Pagefault auszulsen. Wird nach einem fork() ein
Schreibzugriff ausgefhrt, so muss die entsprechende Page kopiert und
dem Prozess zugeteilt werden. Ein freier Frame wird bereitgestellt und
die Daten werden dann von der ursprnglichen Page kopiert.
Paging on Demand: Wird ein Prozess ausgefhrt oder eine Datei im
Speicher gemappt, so werden nicht alle Pages auf einmal im Speicher
bereitgestellt, sondern nur diejenigen, die angefordert werden. Wird
dann auf eine Page zugegriffen, die noch nicht im Speicher vorhanden
ist, so wird

5.5 Paging

73

ein freier Frame zur Verfgung gestellt und der entsprechende Teil der
gemappten Datei eingelesen.
Anforderung einer Page, die bereits einem Prozess gehrt, aber derzeit
nicht im Speicher vorhanden ist. Die Daten mssen sich im SwapBereich befinden. In diesem Falle muss ebenfalls ein freier Frame
bereitgestellt und die Daten aus dem Swap-Bereich gelesen werden.
Anschlieend muss in jedem Fall die Pagetable korrigiert werden.
5.5.2

Zones und NUMA

Da in der PC-Architektur der Speicher unterschiedliche Eigenschaften hat, stellt


Linux die Mglichkeit bereit, die realen Speicherseiten in drei Zonen zu
unterteilen:

ZONE_DMA enthlt diejenigen Frames, die durch DMA (Direct Memory

Access) untersttzt werden -- ISA-Schnittstellen knnen nur die ersten


16 MB mit DMA ansprechen,
ZONE_NORMAL umfasst diejenigen Frames, die fest im KernelAdressraum enthalten sind und ggf. nicht mit DMA angesprochen
werden knnen; as ist der Bereich von 16 MB bis 896 MB, und
ZONE_HIGHMEM fasst diejenigen Frames zusammen, die nicht permanent
in den Kernel-Adressraum eingebunden sind - in der x86 Architektur
alle Pages jenseits von 896 MB.

Jede der drei Zonen wird durch eine Struktur beschrieben, die in Listing 5.10
auszugsweise dargestellt ist.12
Die Anzahl der freien Frames in der jeweiligen Zone wird in free_pages
verwaltet. Ein freier zusammenhngender Block von 2l Frames wird in die Liste
free_area[i] eingehngt. Damit kann schnell auf zusammenhngende freie
Frames zugegriffen werden.
Nicht nur Zonen bewirken, dass der Speicher inhomogen ist. Einige
Architekturen, wie z.B. Alpha-Mehrprozessor-Systeme, haben von einer CPU aus
unterschiedliche Zugriffszeiten fr verschiedene Speicherbereiche. Dies passiert,
wenn in einem Mehrprozessor-System jede CPU ihren eigenen Speicher und
Speicherbus hat. Damit soll die Last auf mehrere Speicherbusse verteilt und die
Skalierbarkeit des Systems erhht werden.
Wird die NUMA-Architektur (Non-Uniform Memory Access) beim
Kompilieren des Kernels bercksichtigt, so wird der physische Speicher in
mehrere Knoten partitioniert: der lokale Speicher jeder CPU bildet einen eigenen
Knoten. Fr eine CPU gilt dann, dass die Speicherzugriffszeit innerhalb des
zugehrigen Knotens gleich ist. Fr eine CPU und zwei verschiedene Knoten oder
einen Knoten und zwei verschiedene CPUs kann aber die Zugriffszeit
unterschiedlich sein.

12

Diese Struktur ist in include/linux/mmzone.h definiert.

74

5 Speicherverwaltung
struct zone {
spinlock_t lock; unsigned long free_pages;
unsigned long pages_min, pages_low, pages_high;

spinlock_t lru_lock; struct list_head


active_list;
struct list_head inactive_list;
atomic_t
ref ill_counter;
unsigned long nr_active;
unsigned long nr_inactive;
all_unreclaimable;
int
unsigned long pages_scanned;
int temp_priority; int prev_priority;
struct free_area free_area[MAX_ORDER];
wait_queue_head_t * wait_table; unsigned long
wait_table_size; misigned long
wait_table_bits;
struct per_cpu_pageset pageset[NR_CPUS];
struct pglist_data *zone_pgdat; struct page
*zone_mem_map; unsigned long zone_start_pfn;

Listing 5.10. Verkrzte Datenstmktur einer Zone


Verkrzung wird durch ... angedeutet.

NUMA-

UMA-Architektur

Abb. 5.5. NUMA- und UMA-Architektur sowie Zones NUMA- (= NonUniform Memory Access) und UMA-Architektur spielen nur bei MehrprozessorArchitektur eine Rolle.

5.6 Page Cache

75

Der Speicher innerhalb eines Knotens kann wiederum in Zonen unterteilt


sein. Die verwendete Struktur ist pg_data_t13.
Bei der Anforderung von Kernel-Datenstrukturen, die CPU-bezogen sind,
versucht das Betriebssystem, die Frames so zu whlen, dass der Zugriff
mglichst gnstig wird.
Generell mssen die Zonen bei der Anforderung bercksichtigt werden: bei
DMA-Beteiligung mssen die Frames aus der betreffenden Zone kommen.
Ansonsten wird zunchst die normale Zone bevorzugt und nur bei Platzmangel
auf die anderen Zonen zugegriffen.
Die Abb. 5.5 zeigt die Zonen sowie die Unterschiede des Speicherzugriffs bei
Mehrprozessor-Systemen. Bei NUMA ist jedem Prozessor ein Speicher mit
Speicherbus zugeordnet, ein gemeinsamer Bus verbindet die CPUs, bei UMA
greifen die CPUs ber einen gemeinsamen Speicherbus auf einen Speicher zu.
5.5.3

Anforderung zusammenhngender Frames

Damit diese Aufgabe schnell gelst werden kann, werden fr jede Zone mit Hilfe
von free_area[MAX_ORDER] Listen von freiem Speicher mit 2l
zusammenhngenden Frames verwaltet (vgl. Abschn. 5.5.2). Werden k
zusammenhngende Frames gesucht, so wird zunchst in der Liste
l
free_area[i] geschaut, fr die 2 gerade grer oder gleich k ist. Nur fr den
Fall, dass diese Liste leer ist, werden grere Werte fr i in Betracht gezogen.
Nicht bentigte Frames werden entsprechend den Listen free_area[0] ...
free_area[i-l] zugeordnet.
Das Gleiche passiert, wenn Frames vom System freigegeben werden. Hier
soll nur die Idee skizziert werden.14 Ziel des Vorgehens ist, mglichst groe freie
Bereiche zu bilden.
Die frei gewordenen Frames werden in Listen eingefgt. Wenn ein Eintrag in
eine Liste erfolgt, wird dabei geprft, ob der Buddy - der benachbarte Eintrag - frei
ist. In diesem Falle werden die beiden Eintrge zu einem greren verschmolzen
und in die nchst-hhere Liste eingefgt.
Der hier verwendete Algorithmus wird als Buddy System Algorithmus
bezeichnet.

5.6

Page Cache

Jede Seiten-orientierte Ein- und Ausgabe, d.h. jede Ein- und Ausgabe einer Datei
eines Filesystems, eines Block-orientierten Gertes oder einer gemapp- ten Datei
(siehe S. 67), wird durch den Page Cache geleitet. Der Page Cache kann als eine
sehr schnelle Erweiterung der Platten usw. angesehen werden.
pg_data_t ist definiert in include/linux/mmzone.h.
Die Implementierung ist in include/linux/mmzone.h und in der Funktion
__free_pages_bulk in mm/page_alloc. c enthalten.
13
14

76

5 Speicherverwaltung

Der Vorteil dieses Verfahrens liegt zum einen darin, dass das System
zunchst prft, ob eine angeforderte Page bereits im Page Cache vorhanden ist.
In diesem Falle kann sofort ohne physischen Plattenzugriff direkt auf die
gewnschte Page zugegriffen werden. Andererseits mssen Vernderungen an
einer Page nicht sofort auf das Filesystem oder auf das Gert ausgegeben
werden, sondern knnen zunchst im Page Cache gehalten werden; damit
befindet sich im Page Cache ein aktuellerer Zustand als auf der Platte - der Fall,
dass der Eintrag im Page Cache lter ist als auf der Platte, kann nicht
vorkommen.
Nach einer gewissen Zeit mssen natrlich auch die vernderten Eintrge
aus dem Page Cache zurckgeschrieben werden, damit Platte und Page Cache
wieder bereinstimmen. Das Zurckschreiben kann jedoch auf einen fr die
Gesamtperformance gnstigen Zeitpunkt verlagert werden.
Die Verwaltung wre nicht problematisch, wenn die Gre der Pages und die
Blocklnge auf den Speichermedien bereinstimmen wrde. Dies ist jedoch in der
Regel nicht der Fall: So hat eine Page in der x86-Architektur die Gre von 4 KB,
wohingegen eine Blocklnge auf einem Speichermedium hufig nur 512 Byte
belegt. Aus diesem Grunde benutzt Linux die Struktur address_space15 zur
Verwaltung der Pages im Page Cache.
struct address_space {
struct inode
*host;
/* owner: inode, block_device */
struct radix_tree_root page_tree;/* Radix Tree aller Pages */
/* Spinlock zum Schutz */
spinlock_t struct page_lock;
/* Liste unvernderter Pages */
list_head struct clean_pages;
list_head struct dirty_pages; /* Liste vernderter Pages */
list_head struct locked_pages; /* Liste gesperrter Pages */
io_pages;
/* Vorbereitet fr 10 */
list_head
nrpages;
/* Anzahl der Pages */
unsigned long
struct address_space_operations *a_ops; /* Methoden */ struct
list_head i_mmap;
/* Liste von private Mappings
*/
struct list_head i_mmap_shared; /* Liste von shared Mappings */
struct semaphore i_shared_sem; /* Schutz fr beide Listen */
unsigned
atomic_t long
dirtied_when;
truncate_count; /*
/* Race
Zeitpunkt
Condition
fr nderung
bei trunc.*/
der ersten page */
unsigned long
flags;
/* Error bits/gfp Maske */
struct backing_dev_info *backing_dev_info; /* Readahead, usw */
spinlock_t
private_lock;
/* Schutz des Adressraums*/
struct list_head private_list;
/* dito */
struct address_space *assoc_mapping; /* dito */

>;

Listing 5.11. Datenstruktur zur Verwaltung des Page Cache


15

address_space ist in include/linux/fs.h definiert.

5.6 Page Cache 77

Im Page Cache befindet sich zu der durch inode angegebenen Datei oder
dem Block-Device eine durch nrpages angegebene Anzahl von Pages. In der in
Listing 5.11 dargestellten Struktur folgen auf den Eintrag page_lock drei
doppelt-verkettete Listen, die auf die sauberen (d.h. unvernderten), die
vernderten sowie die gesperrten Pages dieses Bereiches verweisen. Eine Page
ist gesperrt, wenn sie von der Platte zum Page Cache oder umgekehrt
geschrieben wird.
In der Struktur address_space_operations werden die auf dem Adressraum zur Verfgung stehenden Operationen verwaltet.
struct address_space_operations {
int (*writepage)(struct page
*page,
struct writeback_control
*wbc); int (*readpage)(struct file *, struct
page *);
int (*set_page_dirty)(struct page *page);

Listing 5.12. Verkrzte Datenstruktur der Adressraum-Operationen

Soll aus einer Datei gelesen werden, so wird im Programm read() benutzt,
read() ruft ber mehrere Stufen die Funktion do_genericunapping_read()
auf, die wiederum auf die Funktion find_get_page()16 zugreift (vgl. Abschn.
8.4).
Diese Funktion sucht die gewnschte Page im Page Cache des
entsprechenden Adressraumes. Wird die Page im Cache gefunden, so greift sie
mittels page_cache_get() darauf zu und gibt einen Zeiger auf die Page zurck;
der read()-Aufruf im Programm kann dann sofort auf die Page zugreifen.
Andernfalls wird eine neue Page im Cache mit Hilfe von
page_cache_alloc_cold() angelegt und mittels add_to_page_cache_lru()
als least recently used vorne in den Page Cache eingefgt. Mittels mapping>a_ops->readpage() werden dann die Daten von der Platte usw. in die Page
gelesen und danach an den Benutzerprozess zurckgegeben.
Das Schreiben kann direkt oder indirekt ausgefhrt werden. Wird die
Methode set_page_dirty() verwendet, um die Page als gendert zu markieren,
so erfolgt das Rckschreiben indirekt, da nach einer gewissen Zeit pdflush (vgl.
Abschn. 5.7.2) dafr sorgt, dass diese Page auf das Gert hinausgeschrieben
wird.
Mit Hilfe der Funktion __generic_file_aio_write_nolock()17 erfolgt das
direkte Rckschreiben. In dieser Funktion ist der Aufruf __grab_cache_page()
enthalten, um die Page im Page Cache zu finden oder - falls nicht vorfind_get_pageO ist in mm/filemap.c definiert.
Ebenfalls in mm/filemap.c definiert.

16
17

5 Speicherverwaltung

78

handen - im Page Cache anzulegen, a_ops->prepare_write() dient dazu, die


Schreibanweisung weiter vorzubereiten. Die Daten werden danach mit
filemap_copy_from_user() vom Prozess-Adressraum in einen Kernel-Puffer
kopiert, a_ops->commit_write() schreibt den Puffer auf die Platte.

5.7

Swapping

Wichtige Fragen, die die Speicherverwaltung beeinflussen, sind:

Gibt es Pages, die freigegeben werden knnen, ohne ausgelagert zu


werden?
Wann ist der Speicherplatz so knapp, dass Pages ausgelagert werden
mssen?
Nach welchen Kriterien werden die auszulagernden Pages ausgesucht.
Wie wird der Swap-Bereich verwaltet?

5.7.1

Kernel Caches

Der Kernel bentigt viele verschiedene Caches, unter anderem fr Inodes,


Dentry-Eintrge usw. Groe Caches helfen der System-Performance. Sobald
jedoch Speicherknappheit eintritt, sind groe Caches nicht vorteilhaft.
Da aber eine Verkleinerung eines Caches nicht unproblematisch ist, weil
wichtige Eintrge darin aufbewahrt werden mssen, steht fr jede Cache-Art
eine spezifische shrinker-Funktion zur Verfgung, die beim Kernel angemeldet
sein muss.
Stellt der Kernel Speicherknappheit fest, so ruft er fr jeden Kernel Cache
die entsprechende shrinker-Funktion auf, um auf diese Weise Platz zu gewinnen,
ohne Pages zurckschreiben zu mssen.
5.7.2

pdflush

Der Daemon zur Verwaltung der pdflush-Kernel-Threads18 muss zwei Aufgaben


erfllen:
1.
2.

Wird freier Speicher knapp, so werden vernderte (dirty) Pages auf


Platte geschrieben und knnen somit freigegeben werden.
Pages, die bereits seit einiger Zeit verndert sind, werden
zurckgeschrieben, damit bei einem Systemausfall der Datenverlust
mglichst klein ist: nicht zurckgeschriebene Pages wren andernfalls
verloren.

Bei der Initialisierung werden MIN_PDFLUSH_THREADS (Voreinstellung: 2)


pdflush-Kernel-Threads angelegt. Es knnen weitere pdflush-Threads bis
maximal MAX_PDFLUSH_THREADS (Voreinstellung: 8) angelegt werden, wenn alle
Threads fr wenigstens eine Sekunde beschftigt sind. Sind andererseits
18

Diese Kernel-Threads sind in mm/pdflush.c implementiert.

5.7 Swapping

79

Threads unttig, so werden sie nach einer gewissen Zeit beendet, wobei die
Mindestanzahl an diesen Threads im System verbleibt. Der Grund fr das
Erzeugen mehrerer dieser Threads liegt in der Langsamkeit der Platten
begrndet. Ein einzelner Thread knnte durch eine intensiv benutzte Platte
stark gebremst werden. Stehen mehrere Platten zur Verfgung, kann durch
mehrere solcher Threads eine bessere Leistung erzielt werden.
Die Arbeit eines pdflush-Threads wird durch pdflush_operation() bestimmt,
wobei eine Callback-Funktion bergeben wird. Im ersten Falle ist dies die
Funktion background_writeout()19. Hier wird dafr gesorgt, dass solange
vernderte Pages zurckgeschrieben werden, bis der freie Speicher die Schwelle
dirty_background_ratio wieder berschritten hat. Diese Variable kann vom
Administrator im laufenden Betrieb im Directory /proc/sys/vm gendert werden.
Im zweiten Falle wird wb_kupdate()20 bergeben. In regelmigen Abstnden
wird ein pdflush-Thread geweckt und dazu veranlasst, vernderte Pages, die
lter als dirty_expire_centisecs sind, auf die Platte zu schreiben. Sowohl die
Zeitspanne dirty_expire_centisecs als auch die Spanne zwischen dem Aufwecken
der Threads dirty_writeback_centisecs kann vom Administrator im laufenden
Betrieb verndert werden.
5.7.3

Auswahlstrategie

Eine gute Strategie ist fr ein System entscheidend: wrden wichtige Pages
entfernt, so litte die Performance. Hier soll nur die Idee skizziert werden, die in
Linux verfolgt wird.
Linux verwaltet die Pages in Listen: active und inactive. Innerhalb dieser
Listen wird mittels des Access-Bits ein Alterungsprozess simuliert: in greren
Zeitabstnden werden die Access-Bits aller Pages ausgewertet, Pages mit
gesetztem Bit werden an den Anfang der active-List eingefgt. Sobald auf eine
Page zugegriffen und damit das Access-Bit gesetzt wird, wird sie bei der nchsten
berprfung vorne in die active Liste aufgenommen.
Pages am Ende der active-List werden an den Anfang der inactive-List
verdrngt. Nach einiger Zeit werden vernderte Pages auf die Platte geschrieben,
in die zugehrige Datei oder ~ bei dynamisch erzeugten Pages ohne zugrunde
liegende Datei - in den Swap-Bereich. Gesicherte Pages am Ende der inactiveList knnen bei Page-Anforderungen vergeben werden. Es handelt sich somit um
eine Variante des LRU-Algorithmus (least recently used), die darauf abzielt, die
am wenigsten benutzten Pages zu finden. Wichtige Aspekte sind in mm/vmscan.c
zu finden.
Problem: Wie wirken sich Zones, wie NUMA aus?
In mm/vmscan.c ist eine Variable mit dem Namen vm_swappiness auf den Wert
60 initialisiert. Dieser Wert wird zur Laufzeit im Directory /proc/sys

19
20

Definiert in mm/page-writeback.c.
Ebenfalls in mm/page-writeback.c definiert.

5 Speicherverwaltung

80

unter dem Namen swappiness sichtbar und kann vom Administrator gendert
werden. Die Werte knnen zwischen 0 und 100 gewhlt werden und beeinflussen,
wie intensiv der Kernel versucht, Pages auszulagern.
Bei der Darstellung wurde nicht betrachtet, dass es eine Reihe von
Sonderfllen geben kann, wie z.B. Pages, die gesperrt sind.
5.7.4

kswapd

Das eigentliche Auslagern von Seiten auf die Swap-Bereiche wird durch den
kswapd-Daemon21 bereitgestellt. Bei Mehrprozessor-Architekturen kann fr
jeden Prozessor ein eigener CPU-spezifischer Kernel-Thread angelegt werden,
um Probleme mit NUMA zu umgehen.
Der bzw. die Kernel-Threads werden zum einen periodisch aufgerufen, wenn
die Anzahl der freien Seiten unter eine vorgegebene Grenze fllt, zum anderen,
wenn eine Speicheranforderung an das Buddy System nicht erfolgreich ist.
Die wesentliche Arbeit wird in der Funktion balance_pgdat()22 ausgefhrt.
Fr denjeweiligen dem Prozessor zugeordneten Speicherbereich werden alle
Zonen durchlaufen und festgestellt, ob die Anzahl freier Seiten innerhalb der
Zone gro genug ist. Ist das nicht der Fall, wird die Shrinker-Funktion
shrink_slab()23 aufgerufen, um aus der inactive-Liste die letzten Seiten
freizugeben, sofern das erlaubt ist.
5.7.5

Verwaltung des Cache-Bereichs

Linux ist in der Lage, mehrere Swap-Bereiche zur Verfgung zu stellen. Sowohl
eigens formatierte Partitionen als auch zusammenhngende Dateien fester
Gre knnen dazu benutzt werden. In der Datei /etc/fstab werden die SwapBereiche zusammen mit Prioritten vorgegeben und whrend des Boo- tens
bereitgestellt. Im laufenden Betrieb kann der Administrator mit swapon neue
Swap-Bereiche hinzufgen bzw. mit swapoff Swap-Bereiche abschalten.
Wenn der Kernel Pages auslagert, so greift er erst auf die Swap-Bereiche mit
hoher Prioritt zu. Man wird also Swap-Bereiche, die sich auf schnellen Devices
mit geringer Belastung befinden, mit einer hheren Prioritt versehen. Wenn auf
diesen Bereichen kein entsprechender Platz mehr gefunden werden kann,
werden Swap-Bereiche mit geringerer Prioritt benutzt. SwapBereiche gleicher
Prioritt werden mittels eines RR-Algorithmus (Round Robin) gefllt.
Die Swap-Bereiche werden durch struct swap_info_struct24 beschrieben
(siehe Listing 5.13). In dieser Struktur befindet sich unter anderem ein

Definiert in mm/vmscan.c.
Ebenfalls in mm/vmscan.c zu finden.
23
Diese Funktion ist ebenfalls in mm/vmscan.c.
24
In include/linux/swap.h definiert.
21
22

5.7 Swapping

81

Verweis auf den benutzten Block-Device bzw. auf die verwendete Datei (bdev
bzw. swap_file), sowie die Anzahl der Page Slots (pages), die insgesamt
belegt werden knnen, swap_map ist ein Integer-Array mit einem Eintrag fr
jeden belegbaren Slot. Darin wird festgehalten, wie viele Prozesse auf die
ausgelagerte Page Zugriff haben.
struct swap_info_struct {
unsigned int flags;
spinlock_t sdev_lock; struct
file *swap_file; struct
block_device *bdev; struct
list_head extent_list; int
nr_extents;
struct swap_extent *curr_swap_extent;
unsigned old_block_size;
unsigned short * swap_map;
misigned int lowest_bit;
unsigned int highest_bit;
unsigned int cluster_next;
unsigned int cluster_nr;
int prio;
/* swap priority */
int pages;
unsigned long max;
unsigned long inuse_pages;
int next;
/* next entry on swap list */

Listing 5.13. Die Struktur swap_info_struct

Jeder Swap-Bereich enthlt mindestens einen weiteren Page Slot, der nicht
belegt werden kann: im ersten Page Slot steht zum einen eine besondere
Kennung, an Hand derer der Kernel feststellen kann, dass es sich um einen
SwapBereich handelt, zum anderen mssen dort Informationen ber defekte
Slots permanent gespeichert werden.
Wie knnen ausgelagerte Pages in Swap-Bereichen wiedergefunden werden?
Die bentigte Information muss in der Pagetable enthalten sein. Der
entsprechende Eintrag hngt vom jeweiligen System ab und enthlt zumindest
folgende Informationen:

Die Kennzeichnung, dass die zugehrige Page zum Prozess gehrt, aber
ausgelagert ist,
einen Verweis auf den Swap-Bereich, in dem die Page gespeichert ist
und
die Nummer des Page Slots im Swap-Bereich.

82

5 Speicherverwaltung

5.8

Slab Layer

Der Kernel muss stndig Datenstrukturen anfordern und wieder freigeben.


Dabei sind folgende Gesichtspunkte zu bercksichtigen:

Hufig benutzte Datenstrukturen sollten aus Effizienzgrnden in einem


Cache verwaltet werden,
um Fragmentierung des Speichers bei hufiger Anforderung und
Freigabe der Objekte zu vermeiden, sollten die Objekte so verwaltet
werden, dass freigegebene Pltze durch Objekte gleichen Typs - und
somit gleicher Gre - wieder belegt werden knnen,
um weitgehend SMP-Locks bei Mehrprozessor-Systemen zu vermeiden,
sollte jedem Prozessor ein Teil des Caches und somit der verwalteten
Objekte direkt zugeordnet werden.

Aus diesen berlegungen wurde der Slab Layer entwickelt, der in hnlicher
Form bereits in SunOS 5.4 eingesetzt wurde. Hier soll nur sehr kurz auf diese
Technik eingegangen werden.
Fr jeden Objekt-Typ wird ein eigener Cache eingerichtet, so fr
ProzessDeskriptoren, fr Inodes usw. Die Caches werden in Slabs unterteilt, die
jeweils aus einem oder mehreren physisch zusammenhngenden Frames
bestehen. Jeder Slab enthlt eine Reihe von Objekten, den im Cache
vorgehaltenen Datenobjekten. Ein Slab kann voll sein und somit keinen freien
Platz mehr enthalten, teilweise gefllt oder leer sein.
Erfolgt eine Speicheranforderung fr einen bestimmten Objekttyp, so wird in
dem zugehrigen Cache nachgeschaut und dort nach einem teilweise gefllten
Slab gesucht, in dem das Objekt dann abgelegt wird. Falls es keinen teilweise
gefllten Slab mehr gibt, wird ein freier Slab genommen. Steht auch der nicht
zur Verfgung, wird ein neuer freier Slab angelegt. Da in einem Cache die
Objekte von gleichem Typ sind, wird auf diese Weise eine Fragmentierung des
Speichers weitgehend vermieden.
Abbildung 5.6 zeigt den Aufbau fr einen Objekttyp.

Slabs
voll
teilweise
gefllt

leer
Abb. 5.6. Slab Layer

5.9 Zusammenfassung 83

5.9

Zusammenfassung

Speicherbehandlung ist ein sehr komplexes, zum Teil sehr Hardware-nahes


Thema. Nach der allgemeinen Einfhrung in die Problematik wurde die
LinuxImplementierung betrachtet, die jedoch erst auf einen zweiten Blick den
Bezug zur allgemeinen Darstellung erkennen lsst. Die gewhlte Darstellung
bleibt Hardware-unabhngig; Hardware-Aspekte (wie die Adressierung auf die
x86- Architektur performant bertragen wird) wurden bewusst nicht untersucht,
um das Kapitel nicht unntig zu komplizieren.
Die Verwaltung des Prozess-Adressraumes erfolgt ber den Memory
Deskriptor mm_struct, der im PCB verankert ist. In dieser Struktur befindet
sich eine redundante Darstellung der Adressbereiche: als lineare Liste und als
Red-Black-Tree. Durch die Redundanz der Darstellung wird ein schneller Zugriff
sowohl beim Durchlaufen der vollstndigen Liste als auch beim direkten Suchen
erreicht.
Die Abbildung des logischen Adressraums auf den physischen Speicher wird
ber Pagetables erreicht, ein dreischichtiger Zugriff, der aus Grnden der
Performance eng mit der zugrunde liegenden Hardware verknpft ist. Das
Paging untersttzt virtuelle Speichertechniken. Die Wirkung beim Zugriff auf
eine zum Prozess gehrige Page, die derzeit nicht im Speicher vorhanden ist, lst
einen Pagefault-Interrupt aus, der zur Bereitstellung einer neuen Page fhrt.
Dabei wird auch die Untersttzung von Zones und NUMA deutlich, da die Pages
aus den entsprechenden physischen Speicherbereichen genommen werden
mssen. Dies wird untersttzt durch die Verwaltung freier Pages in free_pages,
wobei der sogenannte Buddy-Algorithmus dafr sorgt, zusammenhngende freie
Pages im Speicher in Blcke von 2er-Potenzen zusammenzufassen.
Mit PSE wurde auch der Einsatz von groen Pages besprochen, der zu einem
zweistufigen Adressierungsverfahren und damit zu einem verringerten
Verwaltungsaufwand bei speicherintensiven Anwendungen wie
Datenbankservern fhrt.
Der Page Cache hilft dabei, den Zugriff auf externe Speichermedien zu
verringern. Die Struktur address_space untersttzt die Verwaltung; sie enthlt
insbesondere drei Listen von Pages: eine Liste fr die unvernderten Pages, eine
fr die genderten (dirty), die somit gesichert werden mssen, und eine fr die
gesperrten Pages. Das Vorgehen beim Lesen25 wurde skizziert und dadurch
deutlich gemacht, wie der Page Cache beim Lesen genutzt wird. Auch beim
Schreiben hilft der Cache dadurch, dass eine Page nicht sofort auf das externe
Speichermedium geschrieben werden muss, sondern im Speicher gehalten
werden kann, bis ein fr das System zum Schreiben gnstiger Zeitpunkt
gekommen ist.
Ein Problem bei virtueller Speicherverwaltung ist unter anderem,
Kandidaten fr das Auslagern von Pages zu finden, wenn nicht mehr gengend
An anderer Stelle wird noch einmal ausfhrlich auf diesen Vorgang eingegangen, vgl.
Abschn. 8.4.
25

84

5 Speicherverwaltung

freie Pages zur Verfgung stehen. Hier wurde zum einen der pdflush-Daemon
besprochen, dessen Aufgabe es ist, vernderte Pages in eine Datei oder den
Swap-Bereich, wenn keine Datei zugrunde liegt, zurckzuschreiben. Zustzlich
wurde der Algorithmus vorgestellt, mit dem Linux die Zugriffsaktivitt auf die
Pages nachhlt: die Einordnung der Pages in active und inactive-Listen unter
Verwendung des Access-Bits, das in regelmigen Abstnden berprft wird. Bei
gesetztem Access-Bit wird eine Page vorne in die active-Liste eingehngt,
whrend hinten die Pages aus der active-Liste in die inactive-Liste altern.
Gesicherte, nicht gesperrte Pages der inactive-Liste stehen wieder zur
Verfgung.
Kurz angesprochen wurde auch die Verwaltung der Swap-Bereiche. Bei
Bedarf kann der Administrator im laufenden System weitere Swap-Bereiche
hinzufgen oder auch Swap-Bereiche stilllegen. Auch die Intensitt des Synchronisierens mit pdflush und des Swappens mit kswapd lsst sich im laufenden
Betrieb verndern.
Als besondere Technik, der Speicherfragmentierung vorzubeugen und
zugleich Zeit fr die Initiierung von Objekten zu sparen, wurde der Slab
Allocator betrachtet, der dazu dient, fr verschiedene, hufig benutzte
KernelObjekte Speicherbereiche zur Verfgung zu stellen.
Die bereitgestellten System Calls sind sehr berschaubar: malloc() zur
Anforderung von Speicher, mmapO (und munmapO) zum Mappen einer Datei,
die erst in Kap. 9 vorgestellten shmget() und shmat() zum Bereitstellen von
gemeinsam genutzten Speicherbereichen sowie mlock() zum Sperren von Pages,
die aufkeinen Fall ausgelagert werden drfen, munlock() gibt die Pages wieder
frei.

6
Synchronist ion

6.1

Grundlagen

In den vorigen Kapiteln wurden mehrfach Probleme entdeckt, bei denen


verschiedene Prozesse aufeinander Rcksicht nehmen mssen. So darf es in
einem Mehrprozessor-System nicht vorkommen, dass von einem Prozessor Load
Balancing angestoen wird, whrend ein anderer Prozessor gleichzeitig auf die
zu bearbeitenden Prozess-Queues zugreift. Wenn Prozesse auf gemeinsame
Daten zugreifen, mssen sie so synchronisiert werden, dass keine Fehler in der
Bearbeitung der Daten entstehen und die Prozesse sich nicht gegenseitig
behindern. Dies betrifft nicht nur bestimmte Kernel-Prozesse, sondern auch
Benutzerprozesse, die auf gemeinsamen Daten operieren.
Synchronisationsprobleme sind in der Regel nur schwer zu erkennen: werden
die beteiligten Prozesse gestartet, so treten die Fehler nicht automatisch auf,
vielfach werden sogar Probelufe gelingen. Nur in manchen Fllen kommt es
pltzlich zu unerklrlichen Phnomenen: die fehlerhafte Synchronisation hat
bewirkt, dass zufllig Daten falsch bearbeitet wurden.
6.1.1

Race Conditions

Zwei Threads, die jeweils eine groe Schleife enthalten, sollen durch Inkrementieren einer gemeinsamen Variablen count nachhalten, wie hufig jede der
beiden Schleifen durchlaufen wurde.
Das Inkrementieren der Variablen count setzt sich aus mehreren Schritten
zusammen: 1
1. Lies den Wert der Variablen count in ein Prozessor-Register,
2. erhhe den Wert des Prozessor-Registers um 1 und
3. schreibe den Wert des Prozessor-Registers zurck in die Variable count
Die Abb. 6.1 zeigt einen korrekten und einen fehlerhaften Ablauf der
Anweisungen. Der Fehler kommt dadurch zustande, dass Thread 1 innerhalb der

6 Synchronisation

86

drei eben genannten Anweisungen unterbrochen wird und Thread 2 in dieser


Zeit ebenfalls auf den Wert der Variablen count zugreift. Thread 2 sieht damit
den alten und nicht den aktualisierten Wert von count. Solche Fehler, die durch
einen Wettlauf der Prozesse auftreten, nennt man Race Conditions. Nun wird
auch die Bemerkung verstndlich, dass solche Probleme schwer zu erkennen
sind: nur sehr selten wird der Scheduler Thread 1 gerade in dieser Situation
unterbrechen.
a)

b)

Abb. 6.1. Race Condition


a)
b)

zeigt einen korrekten Ablauf der Anweisungen


ein nicht korrekter Ablauf: Thread 1 wird whrend der nderung unterbrochen,
Thread 2 vollzieht in der Zwischenzeit die nderung; beide Threads greifen auf
denselben Zustand der Variablen count zu. Nachdem auch Thread 1 seine
nderung fertiggestellt hat, ist damit einmal das Hochzhlen von count
vergessen worden.

6.1 Grundlagen

87

Die Situation wrde nicht auftreten, wenn die Threads an derjenigen Stelle,
an der sie die nderung von count vornehmen, nicht vom anderen beteiligten
Thread unterbrochen werden knnten. In diesem Falle sind es zwei gleich
aussehende Code-Strecken von drei Anweisungen in den beiden Threads. Eine
derartige Code-Strecke, die ein Prozess erst vollstndig abarbeiten muss, bevor
ein anderer beteiligter Prozess zum Zuge kommt, wird als Critical Region
bezeichnet. In Prozessen, die auf dieselbe Ressource zugreifen und deshalb
synchronisiert werden mssen, knnen die Critical Regions im Gegensatz zu
diesem Beispiel recht unterschiedlich aussehen.
Problem: Zwei Prozesse greifen zyklisch auf ein Array zu. Der eine Prozess
schreibt Werte in das Array, der andere liest diese Werte. Es ist dabei darauf zu
achten, dass der lesende Prozess nur solche Eintrge liest, die der schreibende
fr ihn bereit gestellt hat und dass der schreibende Prozess keine Eintrge
berschreibt, die noch nicht gelesen wurden. Wie knnten die Critical Regions
dieser beiden Prozesse aussehen?
Ursachen fr das Auftreten von Race Conditions sind:

Interrupts, die asynchron zu einem beliebigem Zeitpunkt auftreten und


den laufenden Prozess unterbrechen knnen,
Preemption, d.h. dem rechnenden Prozess wird die CPU nach Ablauf
einer Zeitscheibe entzogen, und
Mehrprozessor-Systeme, bei denen zwei zu synchronisierende Prozesse
auf unterschiedlichen CPUs gleichzeitig laufen knnen.

6.1.2

Anstze zur Synchronisation

Mehrere Mglichkeiten bieten sich an, dafr zu sorgen, dass die Bearbeitung der
Critical Region eines Prozesses nicht durch das Eintreten eines konkurrierenden
Prozesses in seine Critical Region gestrt wird:

Zusammenfassen mehrerer Anweisungen zu einer einzigen


ununterbrechbaren, atomaren Operation.
Die meisten Prozessoren bieten eine Anweisung wie test_and_set an, die es
ermglicht, in einer nicht unterbrechbaren Operation ein Bit zu testen und
ggf. zu setzen. Um allgemeine Critical Regions so zu behandeln, mssten
Erweiterungen der CPU-Operationen vorgenommen werden. Deshalb wird
man diesen Weg in der Regel nicht beschreiten knnen. Ein weiteres Problem
besteht darin, dass dieses Vorgehen nur bei EinprozessorSystemen garantiert
zum Erfolg fhrt.
Zusammenfassen mehrerer Anweisungen zu einer komplexen nicht
unterbrechbaren Operationenfolge.
In der Regel kann man Interrupts unterdrcken; die in dieser Zeit
ankommenden Interrupts werden dabei blicherweise gesammelt und nach
Freigabe der Interrupts abgearbeitet. Dieses Verfahren steht auf nahezu
allen CPUs zur Verfgung, kann also Hardware-unabhngig eingesetzt

88

6 Synchronisation

werden. Das Problem liegt jedoch darin, dass sich dieses Verfahren nur fr
sehr kurze, berschaubare Critical Regions eignet. Auch hier gilt: nur bei
Einprozessor-Systemen funktioniert dieses Verfahren einwandfrei.
Verwendung von Sperren.
Direkt vor Eintritt in eine Critical Region beantragt der Prozess eine Sperre.
Ist die Sperre nicht belegt, so wird die Sperre durch den beantragenden
Prozess belegt, er betritt seine Critical Region und gibt die Sperre am Ende
der Critical Region wieder frei. Ist die Sperre jedoch belegt, so muss der
Prozess warten, bis die Sperre von einem anderen Prozess, der gerade seine
Critical Region bearbeitet, freigegeben wird. Wenn mehrere Bewerber auf die
Sperre warten, so muss fair unter ihnen derjenige ausgewhlt werden, der
als nchster die Sperre setzen darf, die anderen mssen weiter warten.
Sperren eignen sich fr umfangreiche bzw. schwer berschaubare Critical
Regions.
Sperren mssen explizit vom Programmierer eingesetzt werden. Dies bedeutet
insbesondere, dass sie bereits beim Design einer Anwendung einzuplanen sind,
damit nachher keine Probleme auftreten. Abbildung 6.2 veranschaulicht den
Einsatz einer Sperre, die die Critical Regions zweier Threads synchronisiert.

Einsatz einer Sperre zur Synchronisation zweier Critical Regions. Das Bild zeigt fr
Thread 1 die Situation, dass die Sperre gerade frei ist. Durch Anfordern der Sperre ist sie
nun belegt, so dass Thread 2 zunchst warten muss.

6.2 Deadlock

89

Das Verhalten der Sperren, wenn sie belegt sind, unterscheidet sie
wesentlich voneinander. Die Unterschiede betreffen unter anderem das Warten
auf die Freigabe sowie die Auswahl des nchsten Prozesses, der die Sperre
zugeteilt bekommt. Im Folgenden werden unterschiedliche Sperren vorgestellt.
Einige werden ausschlielich bei der Kernel-Programmierung verwendet, diese
sind im Abschn. 6.3 zu finden, andere stehen dem Programmierer von
Anwendungssystemen zur Verfgung (vgl. Abschn. 6.4).
6.1.3

Contention und Scalability von Sperren

Eine Sperre ist stark ausgelastet, wenn sie hufig von einem Thread belegt ist
und gleichzeitig andere Threads darauf warten, diese Sperre zugeteilt zu
bekommen. Man spricht von contention. Ist eine Sperre stark ausgelastet, kann
sie zu einem Engpass (Bottleneck) werden, d.h. die System-Performance
verschlechtern. Die Planung sollte dahin gehen, dass Sperren mglichst nicht
stark ausgelastet sind.
Scalability (Skalierbarkeit) macht eine Aussage ber die Gre der
Datenbereiche, die gesperrt werden. Je grer die Datenbereiche sind, desto
schlechter skalierbar ist die Sperre. Einher geht die Gefahr von starker
Auslastung. Je kleiner die Datenbereiche, desto besser die Skalierbarkeit, aber
zugleich wchst der Overhead, da ggf. eine Reihe von Locks angefordert werden
mssen.

6.2

Deadlock

Durch den Einsatz von Sperren kann garantiert werden, dass jeweils nur ein
Prozess seine Critical Region betritt. Leider ist mit Sperren auch ein Problem
verbunden, das im Folgenden betrachtet werden soll.
Betrachtet werden zwei Threads, die auf zwei gemeinsame Variablen a und b
Zugriff nehmen. Thread 1 soll die Variable a um 1 erhhen. Ist dann der Wert
beider Variablen gerade, so soll b um 2, sonst um 1 erhht werden. Thread 2 soll
die Variable b um 1 verringern, sind dann beide Variablen ungerade, so soll
Variable a um 1, andernfalls um 2 verringert werden. Offensichtlich muss jeder
der beiden Threads auf beide Variablen ndernd zugreifen knnen, ohne dass der
jeweils andere Thread strend dazwischen eingreifen kann. Sollen die Variablen
auch von anderen Threads aus manipuliert werden knnen, so liegt es nahe, fr
jede der Variablen eine Sperre (Lock) vorzusehen. Listing 6.1 zeigt, wie die beiden
Threads zugreifen knnen.
Was passiert, wenn in diesem Beispiel in einem Thread die Reihenfolge fr
die zu setzenden Sperren vertauscht werden - wenn fr Thread 2 zunchst die
Sperre fr Variable b angefordert, whrend in Thread 1 die Reihenfolge
beibehalten wird? In vielen Durchlufen wird auch jetzt die Verarbeitung korrekt
erfolgen. Sollte jedoch ein Mehrprozessor-System verwendet werden oder Thread
1 (bzw. Thread 2) zufllig nach Anforderung der ersten Sperre unterbrochen
werden, so kann es passieren, dass der andere Thread ebenfalls seine

90

6 Synchronisation

erste Sperre erfolgreich anfordern kann. In diesem Falle haben beide Threads
jeweils eine Variable gesperrt und warten darauf, die Sperre fr die andere
Variable zu bekommen. Die Freigabe der jeweils gesperrten Variablen kann erst
erfolgen, wenn die zweite Sperre gesetzt und die Verarbeitung durchgefhrt
werden kann. Doch das wird nie geschehen. Eine solche Situation nennt man
Deadlock oder Verklemmung.
Das Beispiel zeigt erneut, dass Synchronisation sorgfltig von vornherein
einzuplanen ist. Nachtrglich zu Synchronisationszwecken willkrlich
eingefhrte Sperren werden kaum den gewnschten Effekt erreichen.
Thread 1:
beantrage Sperre fr Variable a
beantrage Sperre fr Variable b a
^a+1
falls ((a gerade) und (b gerade)) b
> b H- 2 sonst b ^ b + 1 gib
Sperre fr Variable b frei gib
Sperre fr Variable a frei
Thread 2:
beantrage Sperre fr Variable a
beantrage Sperre fr Variable b

b^b-1

falls ((a ungerade) und (b ungerade)) a ^


a 1 sonst a ^ a 2 gib Sperre fr
Variable 6 frei gib Sperre fr Variable a
frei

Listing 6.1. Beispiel fr Sperren in Pseudocode

Dieses Beispiel muss allgemein dargestellt werden. Dazu wird jetzt allgemein
von Ressourcen gesprochen, auf die die Prozesse zugreifen. Die folgenden vier
Kriterien mssen erfllt sein, damit ein Deadlock vorliegt:
Mutual exclusion (gegenseitiger Ausschluss):
Ressourcen sind unteilbar, d.h. auf eine Ressource kann zur Zeit jeweils nur
ein Prozess zugreifen.
Hold and wait (Festhalten und Warten):
Ein Prozess, der eine Ressource hlt, wartet darauf, eine andere Ressource
zu bekommen. Er bentigt diese weitere Ressource, um seine Arbeit zu
beenden. Erst danach gibt er die bereits gehaltene Ressource frei.
No preemption (kein Entzug):
Hlt ein Prozess eine Ressource, so kann er sie nur freiwillig freigeben. Es
gibt keine Mglichkeit, ihm diese Ressource zu entziehen.

6.3 Kernel Synchronisation 91

Circular wait (Warten im Kreise):


Eine Menge von Prozessen lsst sich grafisch in einem Kreis anordnen mit
der Eigenschaft, dass jeder Prozess auf eine Ressource wartet, die sein
Nachfolger im Kreis hlt.
Eine wichtige Methode zur Verhinderung von Deadlocks hat bereits das
verwendete Beispiel angedeutet: knnen Ressourcen so angeordnet werden, dass
alle Prozesse diese mit steigender Ordnungsnummer anfordern drfen, so ist
circular wait ausgeschlossen. Im Beispiel wurden nur die Ressourcen a und b
verwendet und in eben dieser Reihenfolge erfolgte der Zugriff bei beiden Threads.

6.3

Kernel Synchronisation

Obwohl Synchronisation im Kernel sehr bedeutend ist, wird dieser Abschnitt


kurz gehalten, da nur Kernel-Entwickler auf diese Methoden zugreifen werden.
6.3.1

Atomare Operationen

Jede Architektur, die von Linux untersttzt wird, bietet besondere Befehle, um
atomar Integer (oder genauer Werte vom Typ atomic_t) bzw. Bytes zu
manipulieren. Diese Operationen sind in include/asm/atomic.h bzw.
include/asm/bitopts.h definiert. Wenn es nur darum geht, den Zugriff auf einen
Zhler bzw. ein Byte zu synchronisieren, dann sind diese Operationen die
einfachste Mglichkeit.
Zu diesen Operationen gehren unter anderem solche Funktionen wie
atomic_set(), atomic_inc(), atomic_inc_and_test(), um Werte vom Typ atomic_t zu
manipulieren bzw. aufBytes operierende Funktionen set_bit(), clear_bit(),
change_bit(), test_and_clear_bitO.
Frage: Welche Funktionen gehren noch in diese Kategorie und was bewirken die
einzelnen Funktionen?

6.3.2

Spinlocks

Ein Spinlock1 ist eine sehr einfache Sperre: die Sperre ist entweder nicht gesetzt,
d.h. frei, oder der Prozess wartet auf das Freiwerden in einer Endlosschleife. *

Definiert in include/asm/spinlock.h und


include/linux/spinlock.h.

6 Synchronisation

92

Problem: Was ist problematisch an Sperren, die sich so verhalten? Vergleiche


dazu busy wait auf S. 10.
Eigentlich sollte man solche Sperren doch gar nicht verwenden, da ein Prozessor
bei gesetzter Sperre explizit mit Warten beschftigt ist und keine sinnvolle Arbeit
leisten kann. Dennoch sind diese Sperren im Linux-Kernel uerst sinnvoll, denn
sie werden nur dann erzeugt, wenn der Kernel fr ein Mehrprozessor-System
bersetzt wird oder wenn diese Sperren explizit getestet werden sollen (vgl.
include/linux/spinlock.h).
Damit ist nun auch der Einsatz dieser Sperren umrissen: sie werden auf
Grund ihrer Einfachheit dann eingesetzt, wenn ein Objekt gesperrt werden muss,
auf das von mehreren CPUs - d.h. mehreren Prozessen, die auf unterschiedlichen
CPUs laufen - zugegriffen werden kann, und wenn der Bereich, in dem die Sperre
gesetzt ist, sehr kurz und berschaubar ist. Insbesondere drfen in diesem
Bereich keine Funktionen aufgerufen werden, die den Prozess, der das Spinlock
blockiert, in den Zustand wartend versetzen (vgl. Abb. 4.1).
Die Benutzung ist in Listing 6.2 dargestellt. Dabei werden Interrupts lokal
unterdrckt, nach Freigabe des Spinlocks werden Interrupts jedoch wieder
sichtbar.
spinlock_t mr_lock = SPIN_LOCK_UNLOCKED; unsigned long
flags;
spin_lock_irqsave(&mr_lock, flags);
/* critical section ... */
spin_unlock_irqrestore(&mr_lock,
flag);

Listing 6.2. Spinlock-Benutzung mit lokaler Interrupt-Unterdrckung

6.3.3

Semaphoren

Die vorangehenden berlegungen haben gezeigt, dass auch fr den Kernel


weitere Sperrarten bentigt werden, denn sowohl die Behandlung mehrerer
Ressourcen als auch die Mglichkeit, dass der sperrende Prozess in den Zustand
wartend gelangt, sind mit Spinlocks nicht zu erzielen.
Aus diesem Grunde wird in include/asm/semaphore.hdie Funktionalitt fr
Semaphoren bereitgestellt.2 Die Methoden, die auf initialisierte Semaphoren
angewendet werden knnen, sind up(sem), down(sem) bzw. - um
Unterbrechungen zuzulassen - down_interruptible(sem) und down_trylock(sem).
Eine Kernel-Semaphore besitzt neben den oben angegebenen Funktionen
eine Datenstruktur, die in Listing 6.3 dargestellt ist.
Eine grundlegende Diskussion von Semaphoren findet in Abschn. 6.4.2 statt.

6.3 Kernel Synchronisation

93

struct semaphore {
atomic_t count; int
sleepers;
wait_queue_head_t
wait;

Listing 6.3. Datenstruktur einer Kernel-Semaphore

Frage: Wie wird die Initialisierung vorgenommen?


Der Zhler count der Semaphore sem wird ber bereitgestellte Methoden
verndert: Beim Aufruf von down(sem) bzw. down_interruptible(sem) wird der
Wert von sem->count um eins verringert. Zustzlich hlt der Zhler sleepers fest,
wie viele Prozesse in dieser Warteschlange warten. Fllt dabei der Wert unter 0,
so wird der aufrufende Prozess in den Zustand wartend versetzt und in die
Warteschlange eingefgt, auf die sem->wait verweist.
down_trylock(sem) arbeitet nicht blockierend. Ist die Semaphore bereits
belegt, so endet der Aufruf mit negativem Returncode, andernfalls wird ebenfalls
der Wert von sem->count um eins verringert.
Die Methode up(sem) erhht sem->count um 1. Wird dabei der Wert 0
erreicht, so wird daraufhin der nchste Prozess aus der zugehrigen
Warteschlange aufgeweckt.
Ein abstraktes Beispiel der Benutzung ist in Listing 6.4 zu finden. Die
Benutzung von down_interruptible() bewirkt, dass bei Eintreffen eines Signals
whrend des Versuchs, die Semaphore zu belegen, der Aufruf beendet und der
Wert -EINTR zurckgegeben wird.
struct semaphore mr_sem;
sema_init(&mr_sem, 1); /* usage count is 1 */
if (!down_interruptible(&mr_sem)) {
/* critical region (Semaphore belegt) ...
*/ up(&mr_sem);

Listing 6.4. Kernel-Semaphore

6.3.4

Reader-/Writer-Locks

Sowohl fr Spinlocks als auch fr Kernel-Semaphoren gibt es RW-Locks


(Reader-/Writer-Locks). Diese sind dann ntzlich, wenn Zugriffe auf die Critical
Region sich in reine Lese- und Schreibzugriffe einteilen lassen. Die dahinter
hegende Idee ist, dass zwar mehrere Leser gleichzeitig auf die Critical Region

94

6 Synchronisation

6.4 Synchronisation in Benutzerprogrammen 95

zugreifen
soll
jedoch geschrieben
werden,
ist einzig
und allein
Spterdrfen;
wurden
Semaphoren
hinzugefgt.
Derso
Begriff
Semaphore,
derein
bereits
schreibender
Zugriff
erlaubt. wurde
Die Verwaltung
erfolgt, indemInformatiker
bei Spinlock und
im
Abschn. 6.3.3
auftauchte,
von dem hollndischen
Djikstra
Semaphore
gleichermaen
derim
verwendete
Zhler
wird, um die
Art des
1965
entwickelt.
Unix erhielt
Laufe der Zeit
zurbenutzt
Kommunikation
zwischen
Zugriffs zudrei
verwalten:
positive
Werte
geben
die Anzahl
der Process
zugreifenden
Prozessen
Objekte,
die unter
dem
Begriff
IPC (Inter
lesenden Prozessebekannt
an, -1 zeigt
jedoch,sind.
dassNeben
ein schreibender
Prozess sich in seiner
Communication)
geworden
dem gemeinsamen
Critical Region aufhlt.
Speicherbereich,
der im Abschn. 5.3 schon erwhnt wurde, gehren dazu auch die
Mit diesem
Ansatz
ist aber ein Problem
verbunden:
willAbschn.
ein Prozess
besondere
Art der
Implementierung
von Semaphoren
(vgl.
6.4.2) sowie
schreibenQueues,
und sinddie
lesende
Prozesse
so knnen
neu Auf
hinzukommende
Message
in Abschn.
9.3.2aktiv,
dargestellt
werden.
die
lesende Prozesse die
weiterhin
Es besteht
die Gefahr
von
Implementierung
derSperre
IPC-Objekte
sollbelegen.
erst im Abschn.
9.3 somit
eingegangen
werden,
Starvation
(Verhungern)
S. 47).
hier
steht im
Abschn. 6.4.2(vgl.
die Anwendung
von Semaphoren im Vordergrund.
6.4.1
6.3.5

Signale
Big Kernel Lock

Die Implementierung von Signalen in Unix dient dazu, drei Aspekte zu


Der Big Kernel Lock (BKL) wurde in der Linux-Version 2.0 eingefhrt, um
behandeln: Fehler, I/O-bedingte Ereignisse sowie explizite Aufrufe durch den
Mehrprozessor-Fhigkeit herzustellen. Die Benutzung ist in Listing 6.5 zu
System Call kill().
finden. Es handelt sich dabei um einen Spinlock.
Ignorierb
Handl
Bedeutung
Default
ar
er
Sofortiger
Programmabbruch
Abbruch
nein
nein
lock_kernel();
Programm
beenden
(Ctrl-\)
Abbruch
ja
ja
/* critical region ...
Interrupt
(Ctrl-c)
Abbruch
ja__________
ja_______
*/ unlock_kernel();
SIGHUP
Terminal disconnected
Abbruch aller
Listing 6.5. Big Kernel Lock (BKL) Prozesse in
dieser
SIGILL
illegale Instruktion
Abbruch
ja3
ja4
3
SIGBUS
illegale Pointer-Referenz
Abbruch
ja
ja4
Da
der
BKL
eine
sehr
grobe
Sperre
darstellt,
wurde
seit
der
Kernelversion
3
SIGFPE
fataler arithmetischer Fehler
Abbruch
ja
ja4
3
2.2
viel
Arbeit
darauf
verwendet,
diese
Art
der
Sperre
durch
SIGSEGV illegaler Speicherzugriff
Abbruch
ja
ja4 fein
granulierte
Spinlocks
und
Kernel
Semaphoren
3
zu
ersetzen.
In der
SIGPIPE
Broken Pipe
Abbruch
ja
ja4
4
VersionSyscall
2.6 wird das BKL
nur noch an wenigen
Stellenjaeingesetzt.
SIGSYS
fehlerhafter
Abbruch
ja3__________
_______
Name
SIGKILL
SIGTER
M
SIGINT

J^

SIGALR
Ablauf eines Timers
Abbruch
ja
ja
M
SIGCHL
Kindprozess beendet
ignorieren
ja
ja
D
6.4 anhalten
Synchronisation
SIGSTOP
Prozess
anhaltenin Benutzerprogrammen
nein
nein
SIGCON
Prozess fortsetzen
fortsetzen
nein
ja5
T
SIGIO
Nicht
nur
bei
der
Entwicklung
des
ignorieren
Kernels,
sondern
ja
auch
bei
Applikationen
ja
gibt
Terminal oder Socket fr IO
es immer
wieder
Situationen,
die
zu
Synchronisationsproblemen
fhren.
Man
bereit
SIGUSR1
frei benutzbares
ja
ja
betrachte
den Fall, Signal
dass weit entfernte Rechner Messwerte
aufnehmen,
die an
SIGUSR2
frei
benutzbares
Signal
2__________________
ja__________
ja_______
einer Leitwarte grafisch aufbereitet werden sollen. Die grafische Aufbereitung

kann erst dann sinnvoll aktualisiert werden, wenn alle neuen Messwerte ber
das Netz eingetroffen sind.
Schon sehr frh wurde in Unix die Mglichkeit geschaffen, Ereignisse zu
behandeln, die asynchron zur Verarbeitung des Prozesses eintreffen knnen:
dazu gehren Fehlersituationen wie Division durch 0 ebenso wie Beenden
eines Kindprozesses, Timer-Interrupts oder das Eintreffen eines Ereignisses,
das ein anderer Prozess zum Zwecke der Synchronisation signalisiert. Signale,
die im Abschn. 6.4.1 besprochen werden, haben somit eine weite Bedeutung.

6 Synchronisation

96

Nicht bei allen Fehlersituationen werden Signale eingesetzt. Wenn ein


Fehler in einem System Call auftritt, so behandelt dieser die Situation und
bergibt als Ergebnis einen negativen Rckgabewert, auf den der Prozess
reagieren kann. Diejenigen Fehler, die auerhalb von System Calls auftreten und
auf die der Prozess reagieren muss, werden hingegen auf diese Weise behandelt.
Abbildung 6.3 zeigt die wichtigsten Signale und ihre voreingestellten Wirkungen.
Bei der Reaktion auf verschiedene Signale gibt es deutliche Unterschiede:
einige knnen durch einen eigenen Hndler vom Prozess aufgefangen werden;
in diesem Handler wird festgelegt, wie der Prozess mit der Situation umgehen
soll. Andere wiederum knnen ignoriert werden und einige Signale lassen sich in
keiner Weise abfangen oder modifizieren, der Prozess reagiert in
vorgeschriebener Weise darauf.
Signale und ihre Behandlung sind in manchen Aspekten Hardwareabhngig. kernel/signal. c und include/linux/signal.h betreffen die Hardwareunabhngigen Aspekte, Hardware-bezogen sind include/asm/signal.h und
arch/i386/kernel/signal.c.
Signal-Handler
Nicht immer will man als Ergebnis eines Signals einen Abbruch - oder die sonst
voreingestellte Aktion - sehen. Um ein gewnschtes Verhalten auf ein
bestimmtes Signal zu erreichen, muss man einen Signal-Handler schreiben. Dies
ist eine Funktion, die in bestimmter Weise im Programm verankert wird. In
Listing 6.6 ist ein einfacher Signal-Handler zu sehen: das Signal SIGINT soll
abgefangen und stattdessen eine entsprechende Information ausgegeben werden.
Die Arbeit des eigentlichen Programms besteht darin, in einer Endlosschleife
jeweils eine Zeile auszugeben und fr ca. 5 Sekunden zu pausieren.
Die Struktur neu beschreibt die Aktion folgendermaen: die Komponente
neu.saJhandler enthlt einen Zeiger auf den auszufhrenden Code, in diesem
Falle termJhandler(). Die Komponente neu.sa_mask kann dazu benutzt werden,
bestimmte Signale zu blockieren und die Komponente neu.sa_flags enthlt
weitere steuernde Informationen.
Der Signal-Handler wird mit dem Aufruf sigaction() aktiviert, dem als
Argumente das Signal und zwei Zeiger auf Strukturen fr Signal-Aktionen
bergeben werden. Der erste Zeiger zeigt auf die zu aktivierende Aktion, der
zweite Zeiger zeigt nach dem Aufruf auf eine Struktur, die die vor dem Aufruf
bestehende Aktion enthlt.
Da der System Call signal() untersttzt wird, mag die Frage gestellt werden,
Abb. benutzt
6.3. Wichtige
Signale
in Linux
warum dieser nicht einfach
worden
ist, um
den Signal-Handler zu
etablieren. Zum einen gibt es mit diesem Aufruf gewisse Probleme: Sein
Verhalten ist nicht in allen Unix-Varianten identisch. Zum anderen ermglicht
3
Das Ergebnis
ist dann unvorhersehbar.
sigaction()
das Blockieren
von Signalen.6

Wird ein Handler eingesetzt, um aufzurumen, so sollte er am Ende die


DefaultAktion fr dieses Signal einstellen und das Signal noch einmal erzeugen. Damit
6
sollte
nurso,
nach
Prfung
der mglichendes
Effekte
vorgenommen
endetDies
dann
derjedoch
Prozess
alsgrndlicher
ob er direkt
die Default-Aktion
Signals
ausgefhrt
5
werden.
Der Prozess wird in jedem Fall fortgesetzt.
4

6.4 Synchronisation in Benutzerprogrammen

97

#include <signal.h>
void term_handler(int sig) {
printf("Signal SIGINT abgefangen");

>

main () {
struct sigaction neu, alt;
neu.sa_handler = term_handler;
sigemptyset(&neu.sa_mask);
neu.sa_flags = 0;
sigaction(SIGINT, &neu, &alt);
while (1) {
printf("und noch eine
Zeile\n"); sleep(5);

>

exit(0);

Listing 6.6. Beispiel fr einen Signal-Handler

Erzeugen eines Signals


Soll fr den eigenen Prozess ein Signal erzeugt werden, kann dies mit der
Anweisung raise() erfolgen, raise() selbst korreliert nicht mit einem System Call,
sondern wird von der glibc-Bibliothek auf kill() abgebildet. Die Verwendung von
raise() wird im Listing 6.7 gezeigt, in der der Prozess sich selbst das Signal
SIGINT zuschickt:

main () {
sigaction(SIGINT, &neu, &alt);
while (1) { raise(SIGINT);
printf("und noch eine
Zeile\n"); sleep(5);

>

exit(0);

Listing 6.7. Beispiel fr den Einsatz von raise()

Anderen Prozessen kann jedoch auf diese Weise kein Signal geschickt werden,
denn dazu msste die Prozess-ID in den Aufruf mit eingehen. Der System Call
kill() ist allgemeiner. Das Beispiel in Listing 6.8 zeigt, wie er eingesetzt werden
kann, um zwischen zwei verwandten Prozessen das Signal SIGUSR1 zu senden.

6 Synchronisation

98

/* Programm testUSRl.c */
#include <signal.h> main ()
{ int pid; pid = fork(); if
(pid == 0) {
execl("testUSR2","testUSR2","\0");

>

else { sleep(l);
printf(" Eltern-Prozess");
kill(pid, SIGUSR1); sleep(l);
kill(pid, SIGTERM); exit(0);

/* Programm testUSR2.c */
#include <signal.h>
void term_handler(int sig) {
printf("Signal SIGUSR1 erhalten\n");

>

main () {
struct sigaction neu, alt;
neu.sa_handler = term_handler;
sigemptyset(&neu.sa_mask);
neu.sa_flags = 0;
sigaction(SIGUSRl, &neu, &alt);
while (1) {
printf("... und noch eine
Zeile\n"); sleep(l);

>

exit(0);

Listing 6.8. Signal erzeugen mit kill()

Die Anzahl der Zeilen, die das Kindprogramm ausgibt, kann von Lauf zu Lauf variieren.

6.4 Synchronisation in Benutzerprogrammen

99

Das Beispiel lsst vermuten, dass kill() nur zwischen verwandten Prozessen
eingesetzt werden kann. Das ist jedoch nicht der Fall. Zuerst muss beachtet
werden, dass das erste Argument von kill() viel allgemeiner verwendet werden
kann, als das Beispiel vermuten lsst:
pid > 0:
Das Signal wird an den angegebenen Prozess versendet.
pid = 0:
Das Signal wird an die Prozessgruppe gesendet, der der sendende Prozess
angehrt.
pid = -1:
Das Signal wird an alle Prozesse mit Ausnahme einiger spezieller Prozesse
versendet.
pid < -1:
Das Signal wird an die Prozessgruppe versendet, die durch den Prozess |
pid| gegeben ist.
Hier mssen natrlich Berechtigungen berprft werden, denn ein normaler
Benutzer darf keinem beliebigen Prozess ein Signal zukommen lassen, sondern
nur solchen Prozessen, die er selbst gestartet hat. In anderen Fllen werden in
der Regel Administrator-Rechte (root) bentigt.

Alarme
Der Aufruf alarm() startet einen Timer, der nach Ablauf der Zeit das Signal
SIGALRM an den Prozess sendet. Das in Listing 6.9 gezeigte Programm
verwendet diesen Aufruf, um mit Hilfe der Funktion pause() zu warten, bis
durch den Ablauf des Timers das Signal gesendet wird, pause() wartet auf ein
Signal, das entweder einen Handler aufruft oder den Prozess terminiert. Der
hier installierte Handler setzt eine globale Variable, die abgefragt wird.
Bei der Programmierung ist ein schwerwiegender Fehler passiert! In der
Regel wird das Programm zwar richtig ausgefhrt, aber in Ausnahmefllen kann
es passieren, dass das Signal eintrifft, nachdem die Variable geprft und bevor
pause() aufgerufen wurde. Damit schlft dieser Prozess. Solche Timing Errors
sind schwer zu nden, sie knnen mit Debuggern nicht entdeckt werden. Ein
sorgfltiges Design ist die einzige Mglichkeit, dem vorzubeugen.
6.4.2

Semaphoren

Bereits in Abschn. 6.3.3 wurden Semaphoren benutzt, ohne jedoch die zugrunde
hegende Idee genauer zu betrachten. Folgende Eigenschaften soll ein solches
Konstrukt untersttzen:
Bewerben sich mehrere Prozesse um ihren kritischen Abschnitt, so kann
hchstens ein Prozess diesen betreten, die anderen werden ausgesperrt, sie
mssen also warten.

100

6 Synchronisation
#include <signal.h> int
weiter = 0; void
alarm_handler(int sig)
{ weiter = 1;

>

main () {
struct sigaction neu, alt;
neu.sa_handler = alarm_handler;
sigemptyset(&neu.sa_mask);
neu.sa_flags = 0;
sigaction(SIGALRM, &neu, &alt);
alarm(5);
printf("Alarm nach 5 Sekunden\n");
/* hier stehen weitere
Anweisungen ... */
if (!weiter)
pause();
printf("... nach Eintreffen des TimerInterrupts\n"); exit(0);

Listing 6.9. Alarm erzeugen und darauf warten Achtung! Das


Programm enthlt einen schwerwiegenden Timing Error! Vgl. Ausfhrungen im Text.

Gibt es Bewerber und ist keiner in seinem kritischen Abschnitt, so wird


nach endlicher Zeit der nchste Kandidat ausgewhlt.
Der Auswahlprozess ist fair; wie beim Anstehen an der Kasse soll sich
kein anderer vordrngeln.

Dazu wird eine sogenannte Semaphore eingesetzt, ein Integer-Objekt, auf das
nur mit den Operationen wait() und signal() zugegriffen werden kann. Das
Prinzip einer Semaphore ist im Listing 6.10 in einer Art Pseudocode dargestellt.
Eine Warteschlange sorgt dafr, dass keine unntze Prozessorzeit verbraucht
wird.
Frage: Warum mssen die Operationen wait() und signal() gegen
Unterbrechungen geschtzt sein?
Die IPC-Implementierung der Semaphoren erweitert diese Idee in zwei
Richtungen:

Eine Semaphore kann mehrere Semaphoren-Werte verwalten, d.h. mehr als


ein Integer-Objekt.
Der Wertebereich 0,1 fr die Semaphorenwerte wird auf 0,MAX
erweitert, wobei MAX durch das jeweilige System vorgegeben ist.

Die Zusammenfassung mehrerer Semaphoren-Werte bedeutet, dass bei einem


Antrag mehrere unterschiedliche Ressourcen auf einmal einbezogen werden

6.4 Synchronisation in Benutzerprogrammen 101


type semaphore =
record wert:
integer ;
liste: list of PCB; //
Warteschlange end;
// ftLr eine Semaphore sem knnen die Operationen
so // festgelegt werden: wait(sem):
if sem.wert <= 0 then
begin
// fge PCB des Prozesses an das
Ende // von sem.liste
// Prozess ist dann in der Warteschlange
und // nicht lnger rechenbereit
// hier nimmt er dann spter seine Arbeit wieder
auf end;
sem.wert := sem.wert - 1;

signal(sem):
sem.wert := sem.wert +
1; if sem.wert > 0 then
begin
// entferne ersten PCB aus
sem.liste; // wecke zugehrigen
Prozess P; end;

Listing 6.10. Pseudocode fr die Implementierung einer Semaphore

knnen. Als Beispiel gelte der Zugriff auf mehrere unterschiedliche


Speicherbereiche, die von Prozessen in wechselnden Kombinationen zusammen
bentigt werden. Die Erweiterung des Wertebereiches hingegen ermglicht es,
mehrere gleiche Ressourcen zusammen zu verwalten. Hier kann man an
Bandgerte denken, die zu einer greren Anlage gehren. Ein Prozess kann fr
eine bestimmte Aufgabe eine Reihe von Bandgerten gleichzeitig bentigen.
Dabei kommt es im Gegensatz zum Speicherbereich nicht darauf an, welches
physische Gert tatschlich benutzt wird.
IPC-Semaphoren werden - wie auch Shared Memory - ber einen
numerischen Schlssel identifiziert. Die Funktion semget() liefert bei
erfolgreichem Zugriff analog zu shmget() eine Programm-interne ID, ber die
auf die Semaphore im folgenden zugegriffen werden kann. Das Beispiel in
Listing 6.11 zeigt ein einfaches Programm, das eine Semaphore mit zwei
Semaphoren-Werten anlegt.
Das Beispiel in Listing 6.12 zeigt, wie auf eine bereits angelegte Semaphore
zugegriffen wird, und mit Hilfe von semctl() einzelne Semaphoren-Werte
gesetzt bzw. ausgelesen werden und die Semaphore aus dem System entfernt
wird, wenn sie von keinem Prozess mehr bentigt wird. Beim Setzen und

102

6 Synchronisation
/* Header File mysem.h */
key_t MYKEY = (key_t) 4711; /* IPC-Schlssel */

/* Programm sem01.c */
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include "mysem.h"
main() { int semID;
union semun {
semID = semget(MYKEY, 2, IPC_CREAT|0666);
/* user, group und Rest der Welt drfen auf
Semaphore lesend und ndernd zugreifen */
exit(0);

>

Listing 6.11. IPC-Semaphore und Header-File


#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include "mysem.h"
main() { int semID;
union semun {
int val;
/* value for SETVAL */
struct semid_ds *buf; /* buffer for
IPC_STAT, IPC_SET */
unsigned short *array; /* array for GETALL, SETALL */
/* Linux specific part: */
struct seminfo *__buf; /* buffer for IPC_INF0 */
> args;
semID = semget(MYKEY, 2, 0);
args.val = 2;
semctl(semID, 1, SETVAL, args);
printf("l. Wert = #/d\n", semctl(semID, 0, GETVAL,
args)); printf("2. Wert = /,d\n", semctl(semID, 1,
GETVAL, args)); printf("rc = '/,d\n", semctl(semID, 1,
IPC_RMID, args)); exit(0);

Listing 6.12. Programmbeispiel fr semget() und semctl()

6.4 Synchronisation in Benutzerprogrammen 103

Auslesen eines Semaphoren-Wertes muss man beachten, dass in der


Programmiersprache C der Array-Index bei 0 beginnt. Ferner bentigt die
Funktion semctl() eine Struktur semun, die selbst - wie im Beispiel gezeigt definiert werden muss. Wenn zunchst das in Listing 6.11 dargestellte Programm
und dann das aus Listing 6.12 aufgerufen wird, sollte die Ausgabe die Zeilen
1.
2.

Wert = 0
Wert *= 2

enthalten.

semctl() kann auch benutzt werden, um alle Semaphoren-Werte


einzulesen oder zu setzen, um den letzten Benutzer zu ermitteln, der eine
Semaphoren-Operation ausgefhrt hat usw. Beim Setzen der Werte handelt es
sich jedoch nicht um die blichen Semaphoren-Operationen. Damit stellt sich die
Frage, wie wait() und signal() aussehen knnen. Hier haben wir es ja nicht
mit einem einzelnen Integer-Wert zu tun, sondern mit mehreren, auf denen in
beliebigen Kombinationen diese Operationen gleichzeitig (atomar) ausfhrbar
sein sollen.
Um Semaphoren-Operationen ausfhren zu knnen, muss die Funktion
semop() benutzt werden. Als erstes Argument wird wieder die Semaphoren-ID
bergeben, die Semaphore muss somit vorher zugreifbar gemacht worden sein.
Das zweite Argument ist ein Pointer auf ein Array, dessen Elemente die
Operationen auf den einzelnen Semaphoren-Werten beschreiben, das dritte
Argument ist die Anzahl der Array-Elemente. Die Struktur der Array-Elemente
besteht aus drei Teilen:
1.

sem_num: Damit wird die Nummer des jeweils zu beeinflussenden


7

Sema- phoren-Wertes angegeben.


sem_op: Hier handelt es sich um die mit dem angegebenen
Semaphoren- Wert auszufhrende Operation, sem_op ist ebenfalls eine
Integerzahl, die auch negative Werte annehmen kann. Im folgenden
wird die Interpretation beschrieben, die vom Wert von sem_op abhngt:
> 0 Dies entspricht der blichen signal-Operation, der Wert von sem_op
wird zum Semaphoren-Wert addiert. Prozesse, die darauf warten, dass
dieser Semaphoren-Wert erhht wird, werden aufgeweckt.
= 0 Der Prozess wird angehalten, bis der angegebene Semaphoren-Wert auf
0 fllt.
< 0 Verallgemeinerte wait-Operation: ist der betrachtete Semaphoren- Wert
grer oder gleich dem Betrag von sem_op, so wird sem_op vom
Semaphoren-Wert abgezogen, andernfalls wird der Prozess blockiert bis
der Semaphoren-Wert gro genug ist.
3. sem_flg: Mit den Flags kann die Operation weiter beeinflusst werden.
Erlaubt sind zwei Flags, die wie blich mit | verknpft werden knnen:
2.

Hier ist wieder die C-Konvention zu beachten, dass Array-Elemente von 0 an gezhlt
werden, nr = 0 bezeichnet somit den 1. Semaphoren-Wert.
7

104

6 Synchronisation

IPC_NOWAIT: Ist der Wert von op kleiner oder gleich 0 und ist dieses

Flag gesetzt, so wird der Prozess nicht blockiert, wenn die Anforderung
nicht erfllt werden kann. Stattdessen erzeugt semop einen negativen
Rckgabewert, der entsprechend im Programm behandelt werden muss.
IPC_UND0: Der Kernel erhlt eine Information ber die nderung, die der
Prozess vornimmt. Damit kann der Kernel korrigierend eingreifen, wenn
der Prozess beispielsweise durch einen Fehler vorzeitig beendet wird und
den Semaphoren-Wert nicht mehr freigeben kann.
Die Ausfhrung der in dem Array zusammengefassten Operationen ist atomar:
alle Operationen knnen zusammen ausgefhrt werden, oder der Prozess wird
blockiert und es wird keine Operation durchgefhrt.
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include
"mysem.h" main()
{ struct {
unsigned short
sem_num; short
sem_op; short
sem_flg;
} sembuf;
static struct sembuf ops[2] = {

0, 2, 0,

/* "signal"-Operation auf 1. Semaphoren-Wert:

1,

>;

Erhhung um 2 */
-1, SEM_UND0
/* "wait"-Operation auf 2. Semaphoren-Wert:
hier Verringerung um 1 mit SEM_UNDO-Flag */

int
semID;
int rc;
semID = semget(MYKEY, 2,
0); rc = semop(semID, ops,
2); exit(0);

Listing 6.13. Programmbeispiel fr semop()

Im Programm in Listing 6.13 wird ein Beispiel fr die Verwendung von semop()
vorgestellt. Nachdem auf die bereits existierende Semaphore vom Prozess
zugegriffen worden ist, erhht er in einer atomaren Operation den ersten
Semaphoren-Wert um 2 und verringert den zweiten um 1. Wenn in diesem
Moment der zweite Semaphoren-Wert auf 0 stand, dann wartet der Prozess.

6.5 Zusammenfassung 105

Wird er nun durch Ctrl-C beendet - oder war der zweite Semaphoren-Wert grer
als 0 und der Prozess endete normal - so wirkt sich das IPC_UND0- Flag aus: der
zweite Semaphoren-Wert wird auf den Wert vor dem Aufruf von semop()
zurckgesetzt.
Bei unvorsichtigem Vorgehen kann auch die Benutzung von IPC-Semaphoren zu sogenannten Race Conditions fhren: beim Erstellen einer Semaphore
werden smtliche Semaphoren-Werte auf 0 initialisiert. Sollen nun SemaphorenWerte auf andere Integer-Werte initialisiert werden, so muss nach dem Erstellen
der Semaphore ein semctl()-Aufruf erfolgen. Da der semgetO- und der semctl()Aufruf nicht atomar zusammen ausgefhrt werden knnen, kann in der
Zwischenzeit bereits ein anderer Prozess auf die noch nicht initialisierte
Semaphore zugreifen.
Problem: Wie kann man Race Conditions an dieser Stelle vermeiden?

6.5

Zusammenfassung

Die Notwendigkeit, Prozesse sowohl im Kernel- als auch im User-Bereich


synchronisieren zu knnen, wurde erkannt. Probleme, die bei unvorsichtigem
Vorgehen auftreten knnen, sind Deadlock und Race Conditions. Whrend ein
Deadlock auf die Dauer nicht zu bersehen ist, weil die beteiligten Prozesse nicht
fertig werden, kann eine Race Condition unangenehmere Auswirkungen haben:
unbemerkt schleicht sich bei manchen Einstzen ein Fehler ein, obwohl die
Prozesse scheinbar ordnungsgem zu Ende kommen.
Fr den Kernel sind atomar geschtzte Operationen auf Integer und Bytes
durch atomare Prozessoranweisungen vorgesehen. Sobald die zu sperrenden
Bereiche grer werden, mssen andere Verfahren eingesetzt werden. Das BKL
(Big Kernel Lock) wurde frher vielfach eingesetzt, ist jetzt aber aus
Performance Grnden fast ganz aus dem Quelltext von Linux entfernt worden.
Statt dessen sind Spinlocks und Semaphoren vielfach eingesetzte Verfahren.8
Spinlocks haben trotz der Problematik des Busy Wait ihre groe
Berechtigung, wenn sowohl die zu sperrenden Bereiche sehr klein gewhlt sind
als auch die Wahrscheinlichkeit des gleichzeitigen Zugriffs von zwei Prozessoren
aus gering ist.
Um grere Bereiche zu sperren, sind Spinlocks auf Grund des Busy Waits
ungeeignet. In solchen Fllen wird man hufig Kernel-Semaphoren whlen.
Im Kernel-Bereich gibt es weiterhin nach Lesen und Schreiben differenzierte
Sperren, die sowohl auf Spinlocks als auch auf Kernel-Semaphoren basieren
knnen. Sie garantieren einen Vorzug derjenigen Prozesse, die nur lesen wollen.
Damit verbunden ist die Gefahr des Verhungerns fr solche Prozesse, die
schreibenden Zugriff erlangen wollen.
8
Tatschlich ist das BKL auch eine Art Spinlock, jedoch ist der zu sperrende Bereich
fr ein Spinlock viel zu gro.

106

6 Synchronisation

Im User-Bereich wurden Signale und IPC-Semaphoren betrachtet.


Die Reaktion auf das Eintreffen von Signalen mit Hilfe eines SignalHandlers wurde ebenso gezeigt wie das Versenden von Signalen mittels kill().
Auf die System Calls alarm(), um dem eigenen Prozess nach einer
vorgegebenen Zeit das Signal SIGALRM zukommen zu lassen, und pause(), um
den eigenen Prozess fr eine Zeit schlafen zu legen, wurde ebenfalls
eingegangen. In diesem Kontext wurde auch die Problematik von Timing Errors
besprochen.
Die Erweiterung der IPC-Semaphoren durch Einfhrung von mehreren
Semaphoren-Werten pro Semaphore und Erweiterung des Wertebereichs der
Semaphoren-Werte auf 0..MAX gegenber dem ursprnglichen Konzept von
Djikstra wurde besprochen. Die notwendigen System Calls, um mit IPCSemaphoren umgehen zu knnen, sind semget() zum Zugreifen auf eine
Semaphore, semctl() zum Setzen von Semaphoren-Werten und zur Steuerung
der Semaphore - einschlielich Lschens -, und semop(), um die SemaphorenOperationen auszufhren.

108

7 Interrupts

erfolgen muss (vgl. Empfang eines Datenpakets durch eine Netzwerkkarte


auf S. 231)?
7.1.1

Interrupts

0
1
2
3
4
5
8
9
1
0
1
1
1
2
1
4
1
5
N
M
L
0
E
R
M
I

Erkennung eines Interrupts

Das
Konsolen-Kommando cat /proc/interrupts hilft uns bei dieser Frage ein
CPU0
7190214
XT-PIC Ausgabe
timer ist im Listing 7.1 zu sehen.
wenig weiter. Eine typische
26135
XT-PIC i8042
XT-PIC cascade
0
0
XT-PIC ohci_hcd
104
XT-PIC serial
60
XT-PIC dc395x, ohci_hcd
3
XT-PIC rtc
XT-PIC acpi
0
19852
XT-PIC ethl, ES1938,
ehci_hcd
2960
XT-PIC eth0
7.1
Grundlagen
488200
XT-PIC i8042
56030 die Betrachtung
XT-PIC
ide0
Bereits
auf S.
87 zeigte, dass Interrupts und deren Bearbeitung
100325
XT-PIC
idel
eine wichtige Rolle in einem Betriebssystem
spielen. Interrupts sollen das
0
Eintreffen
eines Ereignisses mitteilen. Es gibt zwei unterschiedliche Arten von
0
Interrupts
in einem Betriebssystem: asynchrone und synchrone.
0
Ein typischer Vertreter fr einen asynchronen Interrupt ist das Eintreffen
0

eines Nachrichtenpakets auf einer Netzwerkkarte (vgl. S. 231). Da dieser


Vorgang nicht von der CPU, sondern von auen gesteuert wird, tritt das Ereignis
vllig unabhngig von dem jeweiligen Zustand der CPU ein. Sobald das Paket
angekommen ist, erzeugt die Hardware der Karte einen Interrupt, die CPU
erkennt dies und ruft im Kernel eine Routine zur Bearbeitung der jeweiligen
Aufgabe auf.
Als Vertreter fr eine Exception, d.h. einen synchronen Interrupt, wren die
Division durch 0 oder der Pagefault zu nennen: die CPU erkennt diesen Zustand,
wenn sie bei der Divisionsverarbeitung an einer ganz bestimmten Stelle
angekommen ist bzw. wenn sie auf eine Page des Prozesses oder der Datei
zugreifen will, die nicht im Speicher vorhanden ist. Die CPU lst somit selbst die
Exception aus. Auch wenn Exceptions fr eine Anwendung unerwartet kommen,
so sind sie doch mit der Verarbeitung der CPU synchronisiert. Da beide Arten
von Unterbrechungen von vielen CPU-Architekturen hnlich behandelt werden,
sind in Linux die grundlegenden Strukturen gleich.
Bei der Reaktion auf einen Interrupt treten ein Reihe von Problemen auf:

Wie wird der Interrupt erkannt?


Wie wird die richtige Bearbeitungsroutine gefunden?
Drfen whrend der Bearbeitung der Routine andere Interrupts
auftreten?
Darf whrend der Verarbeitung derselbe Interrupt auftreten?
Wie lsst sich der Konflikt lsen zwischen einer sehr kurzen Reaktion,
damit die unterbrochene Arbeit mglichst schnell wieder aufgenommen
werden kann, und einer ggf. groen Menge an Arbeit, die bei der

7.1 Grundlagen 109

finden? Es muss somit eine Methode geben, dem System mitzuteilen, wie es den
Nummern die gewnschten Punktionen zuordnet. Diese Zuordnung erfolgt durch
das Registrieren eines Interrupt-Handlers; dabei wird die InterruptNummer
zusammen mit der zugehrigen Handler-Routine eingetragen. Bei dieser
Registrierung muss berprft werden, ob der Interrupt berhaupt zur Verfgung
steht. Bei einem von mehreren Devices benutzten Interrupt muss auerdem noch
geprft werden, ob die Gerte sich diesen Interrupt berhaupt teilen knnen.
7.1.2

Geschwindigkeit versus Umfang

Auf Seite 231 werden als Beispiel die Aktionen beschrieben, die durchgefhrt
werden mssen, wenn ein Datenpaket an einer Netzwerkkarte ankommt.
Letztendlich muss das Paket durch die verschiedenen Netzwerk-Schichten
durchgereicht und auf jeder Ebene entsprechend verarbeitet werden. Dies deutet
auf einen groen Umfang des Interrupt-Handlers hin. Wie schon festgestellt,
wrde das dazu fhren, dass derjenige Prozess, der gerade unterbrochen wurde,
fr eine Weile nicht zum Zuge kommt.
Wenn wir andererseits berlegen, was von den Arbeiten wirklich schnell
erledigt werden muss, so reduziert sich die Arbeit auf folgende Aktionen:

Anlegen einer Datenstruktur,


Kopieren der Daten von der Karte in die Datenstruktur,
Anhngen der Datenstruktur an eine verkettete Liste,
Anzeigen, dass in der verketteten Liste Datenpakete zur Verarbeitung
bereitstehen und
Zurcksetzen der Karten-Hardware.

Das Datenpaket wird somit in der Liste geparkt, bis es zu einem geeigneteren
Listing 7.1. Ausgabe des Kommandos cat /proc/interrupts
Zeitpunkt weiterverarbeitet werden kann. Die wirklich zeitaufwndige
Verarbeitung erfolgt also spter, wenn der Interrupt-Handler zweigeteilt wird:
der
half)
ist auf
diejenigen
Arbeiten reduziert,
diezweite
zeitkritisch
sind
Die eine
ersteTeil
Zahl(Top
in den
Zeilen
gibt
die Interrupt-Nummer
an, die
beschreibt,
und
sofortInterrupts
erledigt werden
damit
die Der
Hardware
wieder frei
wird. Der
wie viele
bereitsmssen,
eingetroffen
sind.
dritte Eintrag
beschreibt
den
nicht-zeitkritische
andere
Teil
(Bottom
half)zustndig
wird hingegen
spterist
durchgefhrt.
Interrupt-Controller,
der fr
den
Interrupt
ist; XT-PIC
der fr die
Dieses
Verfahrenbliche
wird inController.
vielen Betriebssystemen
eingesetzt.
x86-Architektur
Der vierte Eintrag
bezeichnet das Device.
Offensichtlich sind verschiedenen Gerten unterschiedliche Zahlen
7.1.3
Der Interrupt-Handler
zugeordnet.
Andererseits
gibt es Flle - z.B. 5 und 10 -, bei denen mehrere
Gerte auf dieselbe Interrupt-Nummer abgebildet sind. Wird der Interrupt mit
Durch
einen Interrupt
Exception
wechselt die CPU
wie beiInterrupt
einem
der Nummer
1 erzeugt,oder
danneine
wei
das Betriebssystem,
dass- dieser
System
Call
in
den
Kernel
Mode
bzw.
behlt
den
Kernel
Mode
bei,
wenn
vom Keyboard kommt. Nun haben insbesondere die Interrupt-Nummern 0sie
(fr
bereits
darin
war.
In der
muss whrend
der Bearbeitung
darauf
geachtet
den Timer)
und
1 (fr
dasRegel
Keyboard)
eine besondere
Eigenschaft,
die nicht
alle
werden,
dass
Interrupts
Verarbeitung
nicht
erneut unterbrechen,
daIn
dies
zu
Interrupts
teilen:
sie sinddie
zumindest
fr die
x86-Architektur
festgelegt.
einem
Synchronisationsproblemen
fhren
kann
(vgl.
Kap.
6).
Dazu
stellt
Linux
Aufrufe
solchen Fall sollte es nicht zu schwer fallen, die richtige Interrupt-Routine zu
zur
Verfgung, die (fast) alle oder gezielt einige Interrupts1
finden.
Doch wie steht es beispielsweise mit dem Interrupt 11 im Beispiel in Listing
7.1? Dieser wird dynamisch zugeordnet. Wie lsst sich da die richtige Funktion
1

Einige sehr kritische Interrupts lassen sich prinzipiell nicht abschalten.

110

7 Interrupts

abschalten knnen. Dies ist jedoch auch mit Problemen verbunden: dauert das
Abschalten zu lange, so knnen Interrupts, die in dieser Zeit entstehen, verloren
gehen.
Hier bewhrt sich wieder die Aufteilung des Interrupt-Handlers in Top und
Bottom Half. Whrend der Bearbeitung durch die Top Half sind
Unterbrechungen sehr kritisch, hier muss weitgehend garantiert werden, dass
kein neuer Interrupt entsteht. Andererseits dauert die Verarbeitung der Top
Half bei guter Aufteilung nur sehr kurze Zeit. Die umfangreichere Bearbeitung
durch die Bottom Half ist wesentlich unkritischer, hier knnen die meisten
Interrupts zugelassen werden.

7.2

Implementierung

7.2.1

Datenstrukturen

Da die Behandlung von Interrupts eng mit der Architektur verknpft ist, finden
wir Aspekte der Implementierung in dem Architektur-abhngigen Teil des
Quellcodes in arch/i386/kernel/irq.c. Wichtige Datenstrukturen sind in der
Include-Datei include/linux/irq.h zu finden. Der darin enthaltene Code dient auf
abstrakter Ebene der Interrupt-Behandlung.
typedef struct irq_desc {
unsigned int status;
/*
hw_irq_controller *handler;
struct irqaction *action; /*
unsigned int depth;
/*
unsigned int irq_count;
/*
unsigned int irqs_unhandled;
spinlock_t lock;
> ____cacheline_aligned
irq_desc.

IRQ status */
IRQ action list */ nested irq
disables */
For detecting broken interrupts
*/

.t;

Listing 7.2. Interrupt-Beschreibung: Struktur von irq_desc

Die Vereinbarung von irq_desc zusammen mit


extern irq_desc_t irq_desc [NR_IRQS];

beschreiben die einzelnen Interrupts, die durch die Architektur untersttzt


werden, in einem Array. Dem Interrupt 0 wird somit der Array-Eintrag 0
zugeordnet, Interrupt 5 der Eintrag 5. NR_IRQS ist eine Architektur-spezifische
Konstante.
Wird in einer Punktion ein bereits deaktivierter Interrupt erneut
deaktiviert, knnte bei komplexen Aufrufablufen der berblick verloren
gehen. Aus diesem Grund wird depth in der Struktur irq_desc eingefhrt; in
diesem

7.2 Implementierung 111

Eintrag wird mitgezhlt, wie hufig derjeweilige Interrupt deaktiviert ist: jede
Deaktivierung erhht den Wert um 1, jede Aktivierung verringert ihn
entsprechend. Hardware-mig wird der Interrupt erst dann freigeschaltet, wenn
der Wert wieder auf 0 gefallen ist. status beschreibt den Zustand des IRQ
genauer: so kann dort vermerkt werden, ob der zugehrige Handler gerade aktiv
ist, ob ein Interrupt ansteht und der Handler noch nicht ausgefhrt wurde, oder
ob der IRQ abgeschaltet wurde, whrend noch nicht abgearbeitete Interrupts
warten. Die jeweiligen Flags sind ebenfalls in der Include-Datei definiert,
handler und action verweisen auf Datenstrukturen fr die Verwaltung der IRQController-Hardware und der Handlerfunktionen.
Die Struktur zur Verwaltung des Hardware-Controllers, auf die handler
verweist, wird ebenfalls in der Include-Datei beschrieben:
struct hw_interrupt_type {
const char * typename;
unsigned int (*startup)(unsigned int irq); void (*shutdown)(unsigned
int irq); void (*enable)(unsigned int irq); void (*disable)(unsigned int
irq); void (*ack)(unsigned int irq); void (*end)(unsigned int irq);
void (*set_affinity)(unsigned int irq, cpumask_t dest);

>;

Listing 7.3. Verwaltung Hardware-Controller: Struktur hw_interrupt_type Eigentlich


erwartet man hier den Namen hw_irq_controller; durch die Zeile typedef struct
hw_interrupt_type hw_irq_controller in der Header-Datei include/linux/irq.h kommt jedoch
die oben gewhlte Form zustande.

Neben dem Namen (typename) und der Mglichkeit, den Interrupt in


Mehrprozessor-Systemen gezielt mit der Funktion set_affinity() einer oder
mehreren CPUs zuzuordnen, werden Zeiger auf Funktionen bereitgestellt, die bei
der Initialisierung (startup()) und beim Aktivieren und Deaktivieren eines
Interrupts bentigt werden: enable() bzw. disable(); end(), falls die
Handlerfunktion selbst den Interrupt deaktiviert, ack() dient zur Besttigung
der Bearbeitung des Interrupts, falls dies von der Hardware gefordert wird.
Der Verweis action auf eine Liste von Handlerfunktionen wird bentigt,
wenn sich mehrere Gerte einen Interrupt teilen. Alle Listeneintrge behandeln
dieselbe Interrupt-Nummer. Die Struktur der Listeneintrge ist in
include/linux/interrupt.h definiert: handler zeigt auf die Handlerfunktion, next
dient zur Verkettung der Liste. Da die Interrupt-Nummer allein in diesem Fall
nicht den Handler identifiziert, werden zur Erkennung Informationen ber das
Gert bentigt: name und dev_id beschreiben das Gert eindeutig. In flag werden
einige Eigenschaften des IRQ beschrieben. Allen Plattformen gemeinsam sind
folgende Flags:

112

7 Interrupts
struct irqaction {
irqreturn_t (*handler)(int, void *, struct pt_regs *);
unsigned long flags;
unsigned long mask;
const char *name;
void *dev_id;
struct irqaction *next;

};

Listing 7.4. Interrupt-Aktion

SA_INTERRUPT Der Interrupt-Handler unterdrckt alle Interrupts auf der


lokalen CPU.
SA_SHIRQ Mehrere Handler knnen sich diese Interrupt-Leitung teilen.
SA_SAMPLE_RANDOM Der Interrupt trgt dazu bei, Zufallszahlen zu
erzeugen, die aus /dev/random ausgelesen werden.
7.2.2

Registrierung

Damit ein Handler durch einen Interrupt aufgerufen werden kann, muss der
Gertetreiber ihn anmelden bzw. beim System registrieren. Dazu dient die
Punktion request_irq, deren Kopf in Listing 7.5 zu sehen ist.
int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *. struct pt_regs
*), unsigned long irqflags, const char * devname,
void *dev_id)
Listing 7.5. Funktionskopf der Funktion request_irq

Da diese Funktion systemabhngig ist, wird sie fr die PC-Architektur ebenfalls


in arch/i386/kernel/irq.c definiert. Sie enthlt die Eintrge, die in die Struktur
irqaction eingefgt werden mssen. Darber hinaus ist jedoch noch als erstes
Argument die Interrupt-Nummer gegeben. Diese ist fr den System Timer und
die Tastatur vorgegeben, muss fr andere Gerte jedoch blicherweise
dynamisch ermittelt werden.
In dieser Funktion werden zunchst die Parameter berprft, dann die
Struktur mit den bergebenen Werten gefllt. Die weitere Behandlung erfolgt
dann in der Funktion startup_irq(). Dort werden die Flags ausgewertet. Bei
gesetztem SA_SAMPLE_RANDOM wird mit rand_initialize_irq(irq) der
Zufallszahlengenerator neu initialisiert. Ist SA_SHIRQ gesetzt, so wird
berprft, ob ein fr diese Interrupt-Nummer bereits registrierter InterruptHandler das Teilen des Interrupts zulsst. Falls der Interrupt verfgbar ist und
der Handler

7.2 Implementierung 113

als erster registriert wird, ruft die Funktion die startup()-Routine auf, um diesen
Interrupt zu initialisieren (vgl. S. 111). Danach wird noch die Struktur irqaction
an die richtige Stelle eingehngt.
Die Registrierung misslingt, wenn der gewnschte Interrupt schon belegt ist
und sich die Handler den Interrupt nicht teilen knnen, d.h. die bereits
registrierten Handler lassen dies nicht zu oder SA_SHIRQ wurde fr den zu
registrierenden Handler nicht gesetzt. In diesem Falle gibt die Funktion
request_irq() einen Fehler zurck und die angelegte Datenstruktur irqaction
wird wieder freigegeben.
Wird ein Interrupt-Handler nicht mehr bentigt, so wird die Funktion
free_irq() benutzt:
void free_irq(unsigned int, void *dev_id)

Handelt es sich um einen geteilten Interrupt, so wird der zugehrige Handler


durch die Angabe dev_id bestimmt. Nur dieser Eintrag wird entfernt. Wenn der
letzte Eintrag eines geteilten Interrupts bzw. der einzige Eintrag eines
ungeteilten Interrupts entfernt wurde, so wird der Interrupt deaktiviert.
7.2.3

Interrupt Requests

Hier sollen die Schritte aufgezeigt werden, die beim Auslsen eines Interrupts
durch ein Device durchlaufen werden. Das Gert sendet ein Signal an den
Interrupt-Controller. Ist dieser spezielle Interrupt nicht maskiert2, dann sendet
der Interrupt-Controller seinerseits ein Signal an den Prozessor. Wenn der
Prozessor Unterbrechungen zulsst3, dann wird durch einen Sprung an eine zur
jeweiligen Interrupt-Nummer festgelegte Adresse (Interrupt-Vektor) die
Bearbeitung aufgenommen: Die prinzipielle Initialisierung des
InterruptControllers sowie des Interrupt-Vektors findet sich fr die PCArchitektur in der Include Datei include/asm-i386/. . ./irq_vectors.h und
der Datei arch/i386/kernel/i8259.c, die die wesentlichen Funktionen fr die
Steuerung des in dieser Architektur verwendeten Interrupt-Controllers enthlt.
An der Sprungadresse befindet sich hauptschlich Code, der dafr sorgt,
dass die Registerinhalte gesichert werden und die Interrupt-Nummer
weitergegeben werden kann. Danach bekommt do_IRQ()4 die Kontrolle. Diese
Funktion findet in dem bergebenen Argument regs die Interrupt-Nummer. Die
wichtigen Aktionen, die dann in dieser Funktion ausgefhrt werden, sind eine
Benachrichtigung des Handlers
desc->handler->ack(irq);

und der spter folgende Aufruf


action_ret = handle_IRQ_event(irq, &regs, action);

Durch das Setzen einer geeigneten Bit-Maske (Maskieren) kann dem InterruptController
mitgeteilt werden, nicht auf den entsprechenden Interrupt zu reagieren.
3
Interrupts knnen auch im Prozessor deaktiviert werden.
4
Definiert in arch/i386/kernel/irq.c.

114

7 Interrupts

der die fr diesen Interrupt angemeldeten Handler aufruft. Die Funktion


handle_IRQ_event() macht das in der in Listing 7.6 dargestellten Schleife.
do {
status |= action->flags;
retval |= action->handler(irq, action->dev_id, regs);
action = action->next;
} while (action);
Listing 7.6. Schleife in handle_IRQ_event() zum Aufruf der Handler

Dadurch wird die Liste der Handlerfunktionen abgearbeitet (vgl. S. 110, f.). Die
Code-Zeilen in Listing 7.7 zeigen die Struktur eines Interrupt-Handlers:
static void
irq_return xx_interrupt(int interrupt, void *dev_id,
struct pt_regs *regs)

unsigned long flags; spin_lock_irqsave(&xx_lock, flags);


if (!xx_korrekter_handler(devpid)) {
spin_unlock_irqrestore(xx_lock, flags);
return IRQ_N0NE; /* dieser Handler ist nicht gefordert */

xx_hole_daten_von_controller();
spin_unlock_irqrestore(&xx_lock, flags);
/* ab hier brauchen wir den Schutz nicht mehr */

/* und der Rest */ spin_lock(&xxtask_lock);


xx_handle_rest(...); spin_unlock(&xxtask_lock); return
IRQ_HANDLED;

Listing 7.7. Struktur eines Interrupt-Handlers

Zum Verdeutlichen dieser Struktur sind xx_hole_daten_von_controller(),


xx_korrekter_handler() und xx_handle_rest() Platzhalter fr einzufllende CodeStrecken, die spezifisch fr die jeweilige Aufgabe sind. Whrend diese Funktion
selbst in der Regel schnell erledigt werden muss (Top Half), kann man sich
vorstellen, dass in xx_handle_rest() durch geeignete Methoden eine Arbeit
definiert wird, die erst zu einem spteren Zeitpunkt ausgefhrt wird (Bottom
Half), wenn die Funktion xx_interrupt schon lngst beendet ist.

7.3 Interrupt Control 115

Durch spin_lock_irqsave() bzw. spin_lock() wird garantiert, dass kein anderer


Prozess den jeweiligen kritischen Abschnitt betritt (vgl. Abschn. 6.3.2).
Bislang war jeweils ein Prozess mit dem Kernel gekoppelt, das currentMakro
verwies auf diesen Prozess. Alle System Calls und alle Kernel-Threads wurden
bisher in diesem Prozess-Kontext ausgefhrt. Da beim ProzessKontext ein
Prozess mit dem Kernel gekoppelt ist, knnen z.B. beliebig sleep()5 oder der
Scheduler aufgerufen werden. Wenn jedoch ein Interrupt- Handler ausgefhrt
wird, befindet sich der Kernel im Interrupt-Kontext, das Makro current zeigt
zwar noch auf den unterbrochenen Prozess, ist aber bedeutungslos. Das hat zur
Folge, dass bei der Interrupt-Behandlung nicht blockiert werden darf und somit
nicht ohne weiteres beliebige Funktionen aus dem Interrupt-Kontext heraus
aufgerufen werden drfen.

7.3

Interrupt Control

Fr die Behandlung von Interrupts ist es ggf. ntig, darauf zu achten, dass keine
weitere Unterbrechung durch denselben oder durch irgendeinen Interrupt
erfolgt. Zu diesem Zweck werden Routinen bereitgestellt, die auf den InterruptController bzw. auf die CPU zugreifen knnen, um die gewnschten
Vernderungen vorzunehmen.
local_irq_disable();
/* ab hier wird der lokale Prozessor nicht mehr unterbrochen */
anweisungen;
local_irq_enable(); /* ab hier knnen Unterbrechungen erfolgen */
Listing 7.8. Unterdrckung von Interrupts

Um Unterbrechung des lokalen Prozessors auszuschlieen, werden die beiden


Funktionen local_irq_disable() bzw. local_irq_enable()6 bereitgestellt. Der Einsatz
dieser beiden Funktionen ist in Listing 7.8 dargestellt, wobei anweisungen
diejenige Code-Strecke symbolisiert, die nicht unterbrochen werden soll. Dabei
wird die Unterbrechung nur auf demjenigen Prozessor unterdrckt, der diese
Anweisung bearbeitet; in einem Mehrprozessor-System knnen andere
Prozessoren immer noch unterbrochen werden, sogar von demselben Interrupt.
Aus diesem Grunde ist die Absicherung mit Spinlocks, die wir in der Struktur
auf S. 114 gesehen haben, notwendig.
Das Beispiel in Listing 7.9 zeigt eine sicherere Art, Interrupts zu unterdrcken
als mit local_irq_disable(). Das Problem mit den vorher beschriebenen
Der Prozess darf somit blockiert werden.
Als Gegenstck zu local_irq_disable() werden hiermit die Interrupts wieder
freigegeben.
5
6

116

7 Interrupts
unsigned long flags;
local_irq_save(flags);
/* ab hier wird der lokale Prozessor nicht mehr unterbrochen,
*/ /* der vorherige Zustand wird in flags aufbewahrt
*/
anweisungen;
local_irq_restore(flags);
/* ab hier knnen wieder Unterbrechungen erfolgen */
/* der alte Zustand wurde wieder hergestellt
*/

Aufrufen liegt darin, dass die Umschaltung direkt erfolgt. Wenn also bei
geschachtelten Aufrufen Interrupts mit local_irq_disable() unterdrckt wurden
und auf unterer Ebene mit local_irq_enable() Unterbrechungen wieder
zugelassen werden, so findet die Funktion auf hherer Ebene nach der Rckkehr
der Aufrufe eine andere Situation vor, als sie erwartet. Aus diesem Grunde
werden diese Aufrufe durch local_irq_save() und local_irq_restore() ersetzt.
Nicht immer ist es ntig, jegliche Unterbrechung an einer CPU zu
unterbinden. Es kann sinnvoller sein, stattdessen fr das gesamte System den
gerade behandelten Interrupt zu unterdrcken. Whrend die eben vorgestellten
Funktionen sich an den Prozessor wenden, greifen die folgenden Funktionen in
den Interrupt-Controller ein: disable_irq(), disable_irq_nosync(), enable_irq() und
synchronize_irq(). Alle vier Funktionen, die fr die PC- Architektur in
arch/i386/kernel/irq.c definiert sind, haben als Argument die Interrupt-Nummer.
Die Funktionen disable_irq() und disable_irq_nosync() inaktivieren die
gewnschte Interrupt-Nummer, so dass dieser Interrupt an keine CPU
weitergereicht wird. Eine besondere Eigenschaft von disable_irq() ist es, auf
synchronize_irq() zuzugreifen und damit in einem Mehrprozessor-System zu
garantieren, dass sie erst zurckkehrt, wenn jeder fr diesen Interrupt derzeit
aktive Handler beendet ist. Zu jedem Aufruf der beiden Funktionen disable_irq()
bzw. disable_irqjQosync() gehrt ein korrespondierender Aufruf von enable_irq().
Bei geschachtelten Aufrufstrukturen wird der Interrupt erst dann wieder
aktiviert, wenn enable_irq() so hufig aufgerufen worden ist, wie Aufrufe zum
Inaktivieren erfolgten.
Anzumerken ist, dass das Inaktivieren eines Interrupts fr alle Gerte gilt,
die sich diesen Interrupt teilen! Deshalb mssen diese Funktionen fr Handler,
die sich Interrupts teilen knnen, mit Vorsicht gebraucht werden.
Um herauszubekommen, ob sich der Kernel im Prozess-Kontext oder im
Interrupt-Kontext befindet, muss auf das Makro in_interrupt7, zugegriffen
werden.

Definiert in include/asm-i386/hardirq.c.

7.4 Bottom Half 117

7.4

Bottom Half

Die Notwendigkeit, den eigentlichen Interrupt-Handler so kurz wie mglich zu


halten, ist bereits diskutiert worden. Um das erreichen zu knnen, muss die
weniger dringende Arbeit zu einem spteren Zeitpunkt abgewickelt werden. In
Linux haben sich im Laufe der Zeit dafr einige Methoden herauskristallisiert,
die hier dargestellt werden sollen. Drei dieser Methoden haben sich bislang
durchgesetzt:

Softinterrupts oder Soft-IRQs (vgl. Abschn. 7.4.2),


sogenannte Tasklets (vgl. Abschn. 7.4.3), die auf Soft-IRQs aufsetzen,
und
Work Queues (vgl. Abschn. 7.4.5).

7.4.1

Veraltete Anstze

Der erste Ansatz in Linux wurde ebenso benannt: Bottom Half oder kurz BH.
Das Interface bestand aus einer statischen 32 Eintrge umfassenden Liste von
Prozeduren, die global synchronisiert waren, d.h. es konnte jeweils nur eine BHProzedur zur gleichen Zeit laufen, was auch fr MehrprozessorArchitekturen
galt. Der Top Half Handler setzte ein Bit in einer statischen 32-Bit langen
Integer, um zu signalisieren, dass eine bestimmte BH-Prozedur ausgefhrt
werden sollte. Der sich ergebende Engpass und die Unflexibilitt dieser Idee sind
deutlich zu sehen.
Ein Ansatz, der teilweise BH ablste, wurde Task Queue genannt: der Kernel
gab eine Reihe von Queues vor, die eine verkettete Liste von aufzurufenden
Funktionen enthielten. Treiber konnten ihre jeweiligen Bottom Halves in einer
geeigneten Queue registrieren. Die Queues wurden zu bestimmten Zeitpunkten
abgearbeitet. Dieses Verfahren war insbesondere fr die Anforderungen der
Netzwerk-Software nicht performant genug.
Aus diesem Grunde wurden beide Anstze ab Kernel-Version 2.5 eingestellt,
nachdem Soft-IRQs und Tasklets in Version 2.3 und Work Queues in Version 2.5
eingefhrt worden waren.
7.4.2

Soft-IRQ

Soft-IRQs werden beim Kompilieren des Kernels statisch angelegt. Die Struktur
softirq_action8 beschreibt einen Soft-IRQ (vgl. Listing 7.10). In kernel/softirq.c
wird ein Array mit 32 Eintrgen dieser Struktur angelegt:
static struct softirq_action softirq_vec[32]
__cacheline_aligned_in_smp;

Auf Grund dieses Ansatzes kann es hchstens 32 Soft-IRQs geben. Durchsucht


man die Quellen nach open_softirq, derjenigen Punktion also, die zum Anmelden
von Soft-IRQs dient, so findet man nur sechs derartige Aufrufe:
Zu finden in include/linux/interrupt.h.

118

7 Interrupts
struct softirq_action

void (*action)(struct softirq_action


*); void *data;

>;

Listing 7.10. Soft-IRQ-Struktur

Einen Aufruf mit der Nummer HI_SOFTIRQ fr Tasklets mit hchster


Prioritt,
2. einen fr den Timer mit der Nummer TIMER_SOFTIRQ,
3. bei der Netzwerk-Software einen Aufruf fr das Senden unter der
Nummer NET_TX_SOFTIRQ,
4. und fr das Empfangen von Paketen mit der Nummer
1.

5.
6.

NET_RX_SOFTIRQ,
fr das SCSI-Subsystem mit SCSI_SOFTIRQ sowie
mit der Nummer TASKLET_SOFTIRQ einen weiteren Aufruf fr Tasklets.

Die Aufrufe sind mit fallender Prioritt der Soft-IRQs angeordnet. Wir finden
unter den 32 mglichen Soft-IRQs derzeit nur vier fest vorgegebene Soft-IRQs
und zwei, die fr die Untersttzung von Tasklets vorgesehen sind. Die fest
vorgegebenen Soft-IRQs sind fr diejenigen Aktionen vorgesehen, die als
besonders zeitkritisch angesehen werden.
Als Beispiel fr den open_softirq()-Aufruf wird aus der NetzwerkSoftware
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);

zitiert9. Der Aufruf bewirkt, dass im Vektor softirq_vec an der durch


NET_TX_SOFTIRQ angegebenen Stelle die Datenstruktur besetzt wird (vgl. Listing
7.11 und Quellcode kernel/softirq.c).
void open_softirq(int nr, void (*action)(struct softirq_action*),
void *data)

softirq_vec[nr].data = data;
softirq_vec[nr].action =
action;

Listing 7.11. Punktion zum Anmelden eines Soft-IRQ


Damit ein Soft-IRQ ausgefhrt wird, muss ein Software-Interrupt mit der
entsprechenden Nummer erzeugt werden. Dies leistet raise_irq(), im Falle des
Versendens eines Datenpakets lautet der Aufruf
raise_softirq(NET_TX_SOFTIRQ);
9

Enthalten in der Datei net/core/dev.c.

7.4 Bottom Half 119

Dieser Aufruf wird z.B. in der Rckgabefunktion netif_rx_schedule()


vorgenommen (vgl. S. 234).
Die eigentliche Ausfhrung eines Soft-IRQs kann an unterschiedlichen
Stellen angestoen werden:

Nach der Ausfhrung eines Hardware-Interrupts,


durch den Kernel-Thread ksoftirqd oder
durch Code wie in der Netzwerk-Software, der explizit berprft, ob
Software-Interrupts erzeugt wurden und die entsprechenden Handler
aufruft.

Der Aufruf eines solchen Handlers erfolgt immer aus do_softirq()10 heraus. Diese
Funktion hat die in Listing 7.12 gezeigte Gestalt.

asmlinkage void do_softirq(void)


__u32 pending;
unsigned long
flags; if
(in_interruptO)
return;
local_irq_save(flags);
pending = local_softirq_pending();
if (pending)
__do_softirq();
local_irq_restore(flags);
Listing 7.12. Aufruf der Soft-IRQ-Handler

Wenn das System sich im Interrupt-Kontext befindet, wird der Aufruf sofort
beendet. Ansonsten erfolgt mit local_irq_save() und local_irq_restore() die
Sicherung der lokalen CPU gegen Unterbrechungen und damit gegen Race
Conditions bei der Ermittlung noch ausstehender Softinterrupts. Die Funktion
local_softirq_pending() ermittelt, welche Soft-IRQs auf Bearbeitung warten.
Diese Information wird als Bitvektor in pending zurckgegeben. Ist wenigstens
ein Bit gesetzt, so wird die Funktion __do_softirq() zur weiteren Verarbeitung
aufgerufen. Die wichtigsten Verarbeitungsschritte in dieser Funktion sind in
Listing 7.13 gezeigt. Zunchst werden noch einmal die ausstehenden Soft-IRQs in
der lokalen Variablen pending ermittelt, die Information ber ausstehende SoftIRQs zurckgesetzt und danach fr den lokalen Prozessor Unterbrechungen
zugelassen, h zeigt auf den ersten Eintrag des Arrays softirq_vec. Die Schleife
dient dazu, diejenigen Soft-IRQs zu ermitteln, die tatschlich ausgefhrt werden
sollen: es werden alle Soft-IRQ-Nummern von hchster bis niedrigster Prioritt
durchlaufen, indem pending in jedem Schleifendurchlauf um ein Bit nach rechts
verschoben und zugleich h auf den
In kernel/softirq.c definiert.

10

120

7 Interrupts
pending = local_softirq_pending();
/* Reset the pending bitmask before enabling irqs
*/ local_softirq_pending() = 0;
local_irq_enable(); h = softirq_vec; do {
if (pending & 1>
h->action(h);

>

h++;
pending = 1;
> while (pending); local_irq_disable();
Listing 7.13. Ausschnitt der Funktion __do_softirq

nchsten Eintrag des Arrays gesetzt wird. Ist das unterste Bit von pending
gesetzt, so muss der zugehrige Handler ausgefhrt werden. Dies bewerkstelligt
die Zeile
h->action(h);

in der dem Handler zugleich die gesamte Struktur des dort gespeicherten SoftIRQ-Eintrags bergeben wird.11 Danach werden die Unterbrechungen fr die
lokale CPU wieder unterdrckt, damit bei Rckkehr nach do_softirq() der alte
Zustand wieder hergestellt ist.
Wenn ein neuer Soft-IRQ erstellt werden soll, so muss man sich ber eine
Reihe von Einschrnkungen bewusst werden. Zunchst einmal ist die Nummer
des neuen Soft-IRQ zwischen HI_SOFTIRQ und TASKLET_SOFTIRQ zu whlen, in
Abhngigkeit davon, in welcher Reihenfolge die Bearbeitung erfolgen soll. Beim
Soft-IRQ-Handler ist auf folgende Vorgaben zu achten:

Der Handler darf keinen Code benutzen, der blockiert. Semaphoren sind
somit ausgeschlossen.
Da Unterbrechungen zugelassen sind, muss besondere Sorgfalt
angewandt werden, wenn auf Daten zugegriffen werden soll, die mit
einem Interrupt- Handler gemeinsam geteilt werden: so mssen dafr
zumindest Unterbrechungen ausgeschlossen und geeignetes Locking
(vgl. Abschn. 6.3) verwendet werden.
Eine weitere Schwierigkeit tritt dadurch auf, dass der gleiche Soft-IRQHandler zur selben Zeit auf verschiedenen CPUs laufen kann. Sollte der
Handler globale Daten verwenden, so muss ebenfalls geeignetes Locking
beachtet werden. *

Dieser Kniff ermglicht es, in spteren Linux-Versionen noch


Informationen ber h->data auszuwerten, ohne weitere Eingriffe an
anderen Stellen des Quelltextes vornehmen zu mssen.
n

7.4 Bottom Half 121

Frage: Wie wird gewhrleistet, dass jeweils nur ein Soft-IRQ-Handler pro
CPU bearbeitet wird?
Frage: Was passiert, wenn whrend der Bearbeitung erneut Soft-IRQs erzeugt
werden?
Zur Beantwortung der Fragen mssen Sie in der Funktion __do_softirq() die
ausgelassenen Code-Teile nher betrachten.
7.4.3

Tasklets

Tasklets beruhen auf speziellen Soft-IRQs: HI_SOFTIRQ und TASKLET_SOFTIRQ.


Tasklets der ersten Art werden vor denjenigen bearbeitet, die dem zweiten
Soft-IRQ zugeordnet sind. Die Verwaltung von Tasklets erfolgt in einer Liste,
der tasklet_struct12, dargestellt in Listing 7.14:
struct tasklet_struct
{

>;

struct tasklet_struct *next;


unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;

/*
/*
/*
/*
/*

zum nchsten Eintrag */


Tasklet-Zustand */
Referenz-Zhler */
Tasklet-Handler */
Argument fr Handler */

Listing 7.14. Die Struktur tasklet_struct

Beim Initialisieren der Soft-IRQ wird die Funktion softirq_init() aufgerufen, die
die Soft-IRQ-Handler fr die Bearbeitung der Tasklet-Listen anmeldet (siehe
Listing 7.15). Die Arbeit dieser Handler wird - wie bereits im vorivoid __init softirq_init(void)

{
}

open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);


open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);

Listing 7.15. Anmelden der Tasklet-Handler

gen Abschnitt diskutiert - durch den Soft-IRQ-Mechanismus angestoen. Hier


soll nur der Handler tasklet_action() betrachtet werden (vgl. Listing 7.16), ebenso
verhlt sich taskletJhi_action(), nur greift er auf tasklet_hi_vec anstelle von
tasklet_vec zu. hnlich wie bei der Funktion do_softirq()
12

In include/linux/interrupt.h definiert.

122

7 Interrupts
static void tasklet_action(struct softirq__action *a)
struct tasklet_struct
*list; local_irq_disable();
list = __get_cpu_var(tasklet_vec).list;
__get_cpu_var(tasklet_vec).list = NULL;
local_irq_enable()
; while (list) {
struct tasklet_struct *t =
list; list = list->next; if
(tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (ltest_and_clear_bit(TASKLET_STATE_SCHED, &t>state)) BUG();
TRIG_EVENT(tasklet_action_hook, (unsigned
long) (t->func)); t->func(t->data);
tasklet_unlock(t); continue ;

>

>

tasklet_unlock(t);

local_irq_disable();
t->next = __get_cpu_var(tasklet_vec).list;
__get_cpu_var(tasklet_vec).list = t;
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();

}
Listing 7.16. Aufruf der Tasklet-Handler

wird zunchst in der lokalen Variablen list die Liste der zu bearbeitenden
Tasklets13, die in der fr den jeweiligen lokalen Prozessor definierten Liste
tasklet_vec stehen, ermittelt und die Liste zurckgesetzt. Dazu werden fr
den lokalen Prozessor Unterbrechungen unterbunden, um Race Conditions zu
vermeiden, und anschlieend wieder zugelassen.
Die wesentliche Arbeit wird in der Schleife while (list) {. . .} geleistet.
Hier werden Eintrag fr Eintrag die zu bearbeitenden Tasklets betrachtet. ber
tasklet_trylock(t)14 wird sichergestellt, dass dieses Tasklet nicht bereits
bearbeitet wird. Wird es nicht bearbeitet, so setzt dieser Aufruf in &t->state
das Flag TASKLET_STATE_RUN. Es kann somit immer nur eine Art

13
Die Eintrge in der Liste besitzen die Struktur tasklet_struct, es
handelt sich also um Tasklets.
14

In include/kernel/interrupt.h.

7.4 Bottom Half 123

von Tasklet im System bearbeitet werden. Dabei ist es durchaus mglich, dass
unterschiedliche Tasklets auf verschiedenen Prozessoren laufen.
Mit dem Zugriff auf &t->count wird berprft, ob das Tasklet gesperrt
(disabled) ist. Sind alle Prfungen erfolgreich beendet, wird der Tasklet- Handler
fr dieses Tasklet mit t->func(t->data) aufgerufen. Anschlieend wird mit
tasklet_unlock()15 das Flag TASKLET_STATE_RUN zurckgesetzt.
Um Tasklets einsetzen zu knnen, mssen sie vereinbart werden. Das kann
statisch bei der Kompilation des Kernels oder dynamisch zur Laufzeit erfolgen.
Statische Vereinbarung geschieht durch
DECLARE_TASKLET( name, tasklet_handler, data );

bzw. DECLARE_TASKLET_DISABLE. Der Unterschied liegt darin, dass im zweiten


Fall das Tasklet als gesperrt vereinbart wird. Hufiger wird jedoch das Tasklet
ber einen Zeiger t auf eine dynamisch erzeugte Struktur vom Typ
tasklet_struct initialisiert mit dem Aufruf
tasklet_init( t, tasklet_handler, data );

Der Tasklet-Handler unterliegt entsprechenden Einschrnkungen wie SoftIRQs: er kann nicht schlafen, blockierende Funktionen sind verboten. In gewisser
Weise sind Tasklets aber leichter zu handhaben: im Gegensatz zu Soft-IRQs wird
jeweils nur ein Tasklet der gleichen Art im System bearbeitet. Damit entfallen
Schutzmechanismen beim Zugriff auf globale Daten. Der Prototyp des Handlers
lautet
void tasklet_handler(unsigned long data)

Um ein Tasklet zur Ausfhrung zu bringen, muss hnlich wie beim Soft-IRQ das
Tasklet zur Ausfhrung vorgemerkt werden. Dies erfolgt durch den Aufruf
tasklet_schedule( &name );

bzw.
tasklet_hi_schedule( &name );

je nachdem, ob das Tasklet in der Schlange mit niedriger oder hchster Prioritt
eingegliedert werden soll. Listing 7.17 zeigt die Funktion
17
__tasklet_schedule()16, in der die wesentliche Arbeit stattfindet.
Unterbrechungen werden whrend der Verarbeitung unterdrckt, um Race
Conditions zu vermeiden, sodann wird das bergebene Tasklet in die Liste
tasklet_vec eingefgt und der TASKLET_SOFTIRQ erzeugt. Damit wird
sptestens nach dem nchsten Interrupt auch dieses Tasklet behandelt.
Tasklets knnen gesperrt und wieder freigegeben werden mit den beiden
Aufrufen
tasklet_disable( &name )
; tasklet_enable( &name
);

Ebenfalls in include/kernel/interrupt.h
16
definiert.
In linux/kernel/softirq.c definiert.
17
__tasklet_hi_schedule() ist entsprechend
aufgebaut.
15

124

7 Interrupts
void fastcall __tasklet_schedule(struct tasklet_struct *t)

unsigned long flags;


local_irq_save(flags)
;
t->next = ___get_cpu_var(tasklet_vec).list;
__get_cpu_var(tasklet_vec).list = t;
raise_softirq_irqoff(TASKLET_SOFTIRQ
); local_irq_restore(flags);
Listing 7.17. Die Funktion __tasklet_schedule

Ein Beispiel fr die Verwendung von Tasklets finden wir bei der
KeyboardUntersttzung: in drivers/char/keyboard.c wird das keyboard_tasklet
mit dem zugehrigen Tasklet-Handler keyboard_bh statisch vereinbart, wobei es
zu Beginn gesperrt ist. Aufgabe des Tasklets ist es, sich um die Behandlung von
CAPSLOCK, NUMLOCK und SCROLLLOCK zu kmmern. Bei der Initialisierung
eines Keyboards in der Funktion kbd_init ( ) wird am Ende durch die CodeZeilen
tasklet_enable (&keyboard_tasklet) ;
tasklet_schedule(&keyboard_tasklet);

das Tasklet freigegeben und zur Ausfhrung vorgemerkt. In der Punktion


kbd_refresh_leds() finden wir ein gutes Beispiel fr das Sperren und spter
folgende Freigeben eines Tasklets: Damit keine Race Conditions auftreten
knnen, wird zu Beginn das Tasklet gesperrt und am Ende wieder freigegeben.
Der Keyboard-Handler kbd_event() sorgt - wie zu vermuten - dafr, dass das
Tasklet bei jedem bearbeiteten Event wieder zur Ausfhrung vorgemerkt wird.
7.4.4

Bearbeitung von Soft-IRQs und Tasklets bei hoher Last

Wenn Soft-IRQs in hoher Zahl auftreten oder sich gar selbst aktivieren, gibt es
ein Problem:

Wrden whrend der Bearbeitung von Soft-IRQs neu auftretende SoftIRQs sofort mit bearbeitet, so knnten Benutzer-Prozesse schon bei
relativ geringer Last verhungern, da das System nur noch mit
Interrupt- und Soft- IRQ-Verarbeitung beschftigt ist.
Wrden andererseits whrend der Bearbeitung neu auftretende SoftIRQs ignoriert und erst behandelt, wenn der Kernel wieder ganz normal
Soft- IRQs bearbeitet, so wrden zwar Benutzer-Prozesse gut
untersttzt, jedoch knnten hierbei die Soft-IRQs verhungern.

Da beide Anstze nicht akzeptabel sind, wird ein Weg dazwischen gesucht. Dazu
wird fr jede CPU ein Thread mit Namen ksoftirq/n18 erzeugt. Der

n bezeichnet die Nummer der jeweiligen CPU.

18

7.4 Bottom Half 125

wesentliche Kern dieses Threads, dehniert in kernel/softirq.c, fhrt den in


Listing 7.18 dargestellten Code aus: Es handelt sich hierbei um eine enggefhrte
Schleife, in der zunchst einmal bei jedem Schleifendurchlauf schedule()
aufgerufen wird, sofern keine Soft-IRQs zu bearbeiten sind. Die innere Schleife
prft zunchst, ob die CPU noch aktiv ist und ruft bei positivem Ergebnis die
bereits bekannte Funktion do_softirq() auf. Am Ende der inneren Schleife wird
nochmal mit cond_resched()19 berprft, ob der Aufruf von schedule() ntig ist.20
set_current_state(TASK_INTERRUPTIBLE)
; while (!kthread_should_stopO) { if
(!local_softirq_pendingO) schedule();
__set_current_state(TASK_RUNNING);
while (local_softirq_pendingO) {
/* Preempt disable stops cpu going offline.
If already offline, well be on wrong CPU:
dont process */ preempt_disable();
if (cpu_is_offline((long)__bind_cpu))
goto wait_to_die; do_softirq();
preempt_enable(); cond_resched();

>

set_current_state(TASK_INTERRUPTIBLE);

Listing 7.18. Code-Ausschnitt des Threads ksoftirq

7.4.5

Work Queues

Sowohl Soft-IRQs als auch Tasklets laufen im Interrupt-Kontext und drfen


somit nicht blockiert werden. Wenn also eine Bottom Half bentigt wird, die
blockieren darf und vom Scheduler bercksichtigt werden kann, mssen andere
Wege beschritten werden. Eine Mglichkeit lge in der Einrichtung von eigenen
Kernel-Threads. Doch der Aufwand dafr ist nicht unerheblich. Aus diesem
Grunde wurden als Bottom Handler Work Queues eingefhrt, die im Gegensatz
zu Soft-IRQs und Tasklets die Eigenschaft haben, im ProzessKontext zu laufen.

ist.

19

In include/linux/sched.h definiert.

20

Dies ist ntig, wenn inzwischen ein hher priorisierter Prozess rechenbereit geworden

126

7 Interrupts

Die Implementierung stellt ein Interface bereit, das die Work Queues an
einen dafr bereitgestellten Kernel-Thread zur Bearbeitung bergibt. Fr jede
CPU wird ein solcher Kernel-Thread unter dem Namen event/n bereit gestellt.21
Weitere Threads knnen bei Bedarf erzeugt werden.
Die Implementierung des Interfaces befindet sich im Wesentlichen in den
Dateien include/linux/workqueue.hund kernel/workqueue. c. Eine Funktion, die
in einer Work Queue fr sptere Bearbeitung aufgehoben werden soll, hat die in
Listing 7.19 dargestellte Struktur. Um solche Arbeiten in einer verketteten Liste
verwalten zu knnen, wird entry als Zeiger auf den Listenkopf bentigt, func ist
die aufzurufende Funktion, der data als Argument beim Aufruf bergeben wird,
timer wird benutzt, um die Ausfhrung der Arbeit hinauszgern zu knnen.
struct work_struct {
unsigned long pending;
struct list_head entry;
void (*func)(void *);
void *data; void
*wq_data; struct
timer_list timer;

>;

Listing 7.19. Struktur fr work_struct

Um den Aufbau dieser Datenstruktur zu erleichtern, ist INIT_WORK


bereitgestellt worden. Dieses Makro initialisiert work vom Typ work_struct. Dazu
werden pending auf 0 gesetzt, die Funktion func() und die Argumente data
eingetragen, der Listenkopf entry und der Timer timer initialisiert. Das Makro
ist in Listing 7.20 dargestellt.
#define INIT_WORK(_work, _func, _data)
do {
INIT_LIST_HEAD(&(_work)->entry);
(_work)->pending = 0;
PREPARE_WORK((_work), (_func),(_data));
init_timer(&(_work)->timer);
} while (0)

\
\
\
\
\
\

Listing 7.20. Makro INIT_WORK


Zur Bedeutung von \ siehe Listing 2.4.

Nachdem diese Struktur initialisiert ist, muss sie in eine Work Queue
eingegliedert werden, damit sie spter bearbeitet wird. Die Struktur einer Work
n bezeichnet dabei die Nummer der CPU.

21

7.4 Bottom Half 127

Queue ist in Listing 7.21 zu sehen. Die Work Queue hat in cpu_wq ein Array, das
so viele Eintrge besitzt, wie CPUs im System vorhanden sind. Der Eintrag list
in der Struktur ermglicht es, alle Work Queues in einer Liste zu verwalten.
struct workqueue_struct {
struct cpu_workqueue_struct cpu_wq[NR_CPUS]; const char *name;
struct list_head list; /* Empty if single thread */

>;

Listing 7.21. Struktur einer Work Queue

Eine neue Work Queue wird angelegt mit


keventd_wq = create_workqueue("events");

Der bergebene Name ist derjenige, unter dem die Threads in der Prozessliste
auftauchen.22
Fr jede CPU wird ein Worker Thread bereitgestellt, von dem genau
derjenige Array-Eintrag bearbeitet wird, der zu der CPU gehrt. Die CPUbezogene Struktur ist in Listing 7.22 wiedergegeben.
struct cpu_workqueue_struct
{ spinlock_t lock;
long remove_sequence; /* Least-recently added (next to run) */
long insert_sequence; /* Next to add */
struct list_head worklist;
wait_queue_head_t more_work;
wait_queue_head_t work_done;

struct workqueue_struct *wq;


task_t *thread;
int run_depth; /* Detect rmi_workqueue() recursion depth */
} ____cacheline_aligned;

Listing 7.22. Struktur des CPU-bezogenen Teils der Work Queue

Ist eine Arbeit work vom Typ work_struct initialisiert worden und soll diese in
die Work Queue wq23 eingefgt werden, so lautet der Aufruf
rc = queue_work(wq, work);

bzw.
rc = queue_delayed_work(wq, work, delay);
In diesem Falle handelt es sich um die standardmig vorgegebene Work Queue.
wq ist vom Typ workqueue_struct.

22

23

128

7 Interrupts

wenn die Arbeit erst nach der durch delay angegebenen Zeitspanne aufgenommen werden soll. Im Fall der Funktion queue_delayed_work() wird der Timer
in work->timer gesetzt. Der Prozessor, auf dem der Aufruf luft, bestimmt,
in welcher der CPU-bezogenen Strukturen der Work Queue die Arbeit eingefgt wird. Dadurch wird die Wahrscheinlichkeit gro, dass die Arbeit auch
auf dieser CPU erledigt wird, eine Garantie dafr gibt es jedoch nicht.
Die Arbeit, die ein Worker Thread leistet, ist in worker_thread() zu finden. Der Kern dieser Funktion besteht aus den in Listing 7.23 gezeigten Zeilen.
set_current_state(TASK_INTERRUPTIBLE
); while (!kthread_should_stopO) {
add_wait_queue(&cwq->more_work,
&wait); if (list_empty(&cwq>worklist))
schedule()
; else
__set_current_state(TASK_RUNNING);
remove_wait_queue(&cwq->more_work,
&wait); if (!list_empty(&cwq->worklist))

>

set_current_state(TASK_INTERRUPTIBLE);

Listing 7.23. Ausschnitt aus der Funktion worker_thread()

Dies ist im Prinzip eine Endlosschleife. Der Thread cwq kennzeichnet sich selbst
als schlafend (TASK_INTERRUPTIBLE) und fgt sich in die Warteschlange wait
ein. Wenn die Liste &cwq->worklist (vgl. Listing 7.22) leer ist, ruft er schedule()
auf und schlft damit wirklich. Enthlt jedoch die worklist Arbeiten, so
kennzeichnet der Thread sich als arbeitend (TASKJtfJNNING) und entfernt sich
aus der Warteschlange. Die Abarbeitung der Aufgaben bernimmt dann die
Funktion run_workqueue().
Die Funktion run_workqueue() fhrt die aufgeschobenen Arbeiten durch,
indem sie jeden Eintrag der Liste der aufgeschobenen Arbeiten durchgeht, die
Funktion sowie die Argumente bestimmt, diese aufruft und den Eintrag aus der
Liste entfernt (siehe Ausschnitt in Listing 7.24.
Soll ein Handler, der Arbeit in eine Work Queue gestellt hat, beendet
werden, so ist das erst dann sinnvoll, wenn auch die aufgeschobene Arbeit
beendet ist. Dies garantiert die Funktion flush_scheduled_work(), die wartet, bis
alle Arbeiten in der Work Queue abgeschlossen sind. Damit wird jedoch keine
Arbeit beendet, die erst nach einer gewissen Zeit beginnen soll und deren Arbeit
noch nicht begonnen wurde. Die Funktion cancel_delayed_work() sorgt dafr,
dass fr eine solche Arbeit der Timer ausgelst wird.

7.4 Bottom Half 129


while (!list_empty(&cwq->worklist)) {
struct work_struct *work = list_entry(cwq>worklist.next, struct work_struct, entry); void (*f)
(void *) = work->func; void *data = work->data;
list_del_init(cwq->worklist.next);
spin_unlock_irqrestore(&cwq->lock, flags); BUG_ON(work>wq_data != cwq); clear_bit(0, &work->pending);
f(data);
/* fhre Arbeit aus */
spin_lock_irqsave(&cwq->lock,
flags); cwq->remove_sequence++;
wake_up(&cwq->work_done);

Listing 7.24. Ausschnitt aus der Punktion run_workqueue()

7.4.6

Warteschlangen

Im letzten Abschnitt wurden zwei Begriffe benutzt, auf die wir an anderer Stelle
bereits gestoen sind: Warteschlangen (vgl. Kap. 4) und Timer (vgl. Abschn. 6.4).
An dieser Stelle soll kurz aufWarteschlangen eingegangen werden.
Damit Prozesse, die auf das Eintreten von Ereignissen warten, das System
nicht durch Pollen - d.h. stndiges Anfragen - belasten mssen, werden
Warteschlangen eingefhrt:24 die Prozesse bekommen den Zustand
TASK_INTERRUPTIBLE bzw. TASK_UNINTERRUPTIBLE und werden in die
entsprechende Warteschlange eingegliedert. Damit werden sie beim Scheduling
nicht bercksichtigt, bis das gewnschte Ereignis eintritt und der Kernel sie
wieder aufweckt.
Eine Warteschlange oder Wait Queue besteht aus einer doppelt verketteten
Liste von Elementen, deren Struktur in Listing 7.25 dargestellt ist, mit einem
Listenkopf, der in Listing 7.26 gezeigt ist.
struct __wait_queue {
unsigned int flags;
struct task_struct *
task; wait_queue_func_t
func; struct list_head
task_list;

};

Listing 7.25. Datenstruktur __wait_queue

Die Implementierung befindet sich in den Dateien include/linux/wait.h sowie in


kernel/sched.c.
24

130

7 Interrupts
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;

>;

Listing 7.26. Warteschlangen-Listenkopf

Der Spinlock wird bentigt, um Race Conditions beim Zugriff auf die
Warteschlange und deren Elemente zu verhindern.
Das Einfgen in eine Warteschlange haben wir bereits auf S. 128 beim CodeAusschnitt auf dem Worker Thread gesehen:
add_wait_queue(*wq, *new);

Diese Funktion fgt den Prozess new in die Warteschlange wq ein. Entsprechend
entfernt remove_wait_queue() einen Eintrag aus der Warteschlange. Doch damit
ist noch nicht erklrt, wie das Aufwecken erfolgt. Der Kernel stellt dazu
wake_up(wq) bereit, mit dem Prozesse aus der Warteschlange wq aufgeweckt
werden knnen.

7.5

Zusammenfassung

Dieses Kapitel betrachtet Zusammenhnge, die auf den ersten Blick nur fr
denjenigen interessant zu sein scheinen, der sich mit Hardware Controllern
beschftigen muss. Doch darber hinaus bilden Interrupts einen wichtigen
Aspekt bei der Erstellung von Betriebssystemen, da viele Aufgaben anders nicht
sinnvoll gelst werden knnen.
Wie ein Interrupt erkannt und wodurch die Bearbeitung im System
eingeleitet wird, kann in den Abschnitten 7.1.1 und 7.1.3 grundstzlich betrachtet
werden. Detaillierter wird es in den Abschnitten 7.2.1 und 7.2.3 dargestellt. Wie
Interrupts dem System bekannt gemacht werden, wird in Abschn. 7.2.2
behandelt.
Da bei der Bearbeitung von Interrupts ggf. darauf geachtet werden muss,
bestimmte Interrupts oder gar alle Interrupts kurzfristig zu unterbinden, stellt
Linux eine Reihe von Methoden dafr bereit. Mit local_irq_save(flags) und
local_irq_restore(flags) kann der Zustand bzgl. der Unterbrechungen fr die
lokale CPU in flags gemerkt und die Unterbrechung fr die lokale CPU
abgeschaltet bzw. der alte Zustand wieder hergestellt werden. Die Funktionen
disable_irq() und enable_irq() unterdrcken global einen bestimmten Interrupt
bzw. lassen ihn zu. Abschnitt 7.3 befasst sich mit diesen Punktionen.
Wesentlich ist die Erkenntnis, dass blicherweise nicht die gesamte Arbeit
eines Interrupts im direkt angestoenen Interrupt-Handler bearbeitet werden
sollte, da die Arbeit in der Regel das Unterdrcken zumindest des auslsenden
Interrupts erfordert. Das System kann dann auf diesen Interrupt nicht

7.5 Zusammenfassung 131

mehr reagieren und dadurch Interrupts verlieren. Aus diesem Grunde wird
man versuchen, eine Aufteilung zu erreichen zwischen Arbeiten, die sofort auf
Grund des Interrupts erledigt werden mssen, und Arbeiten, die auf einen
spteren Zeitpunkt verschoben werden knnen. Bei den spter zu erledigenden
Aufgaben knnen in der Regel Interrupts zugelassen sein.
Im Abschnitt 7.4 haben wir fnf verschiedene Methoden kennengelernt, um
die auf spter verschiebbaren Aufgaben zu bearbeiten. Zwei dieser Anstze, die
in Linux eingefhrt wurden, sind inzwischen durch die weitere Entwicklung
berholt, da sie sich als zu wenig performant erwiesen: einerseits der BH- Ansatz
(Bottom Half) und andererseits die Task Queue (vgl. Abschn. 7.4.1). Beim BHAnsatz ist sofort ersichtlich, warum die Performanz nicht ausreicht: auch bei
einem Mehrprozessor-System wird durch entsprechende Implementierung
hchstens ein BH im gesamten System bearbeitet. Die Task Queue versprach
eine grere Flexibilitt, denn Treiber konnten ihre verschiebbaren Arbeiten in
Queues registrieren, die zu einem spteren Zeitpunkt bearbeitet wurden. Dieser
Ansatz eignet sich jedoch schlecht, um Bottom Halves zu bedienen, die eine
schnelle Reaktionszeit erfordern - z.B. die Behandlung von Netzwerk-Paketen.
Derzeit finden wir im Linux-Kernel drei Anstze, die auf unterschiedliche
Weise versuchen, mit dem Problem umzugehen, Performanz, Flexibilitt und
Einfachheit des Interfaces zu gewhrleisten: Soft-IRQs, Tasklets und Work
Queues.
Soft-IRQs werden nur sehr sparsam eingesetzt. Sie werden sehr schnell
aufgerufen, in der Regel im Anschluss an einen Interrupt bearbeitet und deshalb
bevorzugt in Situationen eingesetzt, wo schnelle Reaktion erforderlich ist. Die
Flexibilitt ist jedoch sehr eingeschrnkt, da Soft-IRQs statisch sind. Die Anzahl
von verschiedenen Soft-IRQs ist auf 32 beschrnkt. Pro CPU kannjeweils nur ein
Soft-IRQ bearbeitet werden. Es ist aber mglich, auf unterschiedlichen CPUs den
gleichen Soft-IRQ parallel zu bearbeitet. Mit raise_softirq(nr) wird signalisiert,
dass der Soft-IRQ mit der Nummer nr zur Bearbeitung ansteht. Werden sehr
schnell neue Soft-IRQs erzeugt, so dass whrend der Abarbeitung nicht alle SoftIRQs bearbeitet werden knnen, wird zustzlich ein eigener Kernel-Thread zur
Abarbeitung ausstehender Soft-IRQs erzeugt.
Tasklets benutzen den Soft-IRQ-Mechanismus. Sie sind leichter zu
handhaben und knnen dynamisch erzeugt werden. Im Gegensatz zu Soft-IRQs,
bei denen gleiche SoftIRQs auf unterschiedlichen CPUs gleichzeitig bearbeitet
werden knnen, gilt fr Tasklets, dass von jeder Art jeweils nur ein Tasklet im
gesamten System bearbeitet werden kann. Es ist jedoch durchaus mglich, dass
zwei unterschiedliche Tasklets auf unterschiedlichen CPUs gleichzeitig
bearbeitet werden knnen. Tasklets werden zwei Soft-IRQs zugeordnet: der SoftIRQ-Nummer mit hchster oder derjenigen mit niedrigster Prioritt. Wird der
entsprechende Soft-IRQ bearbeitet, so sorgt der jeweilige Handler dafr, dass
freigegebene, zur Verarbeitung vorgemerkte Tasklets nacheinander bearbeitet
werden. Auf Grund dieser Einschrnkung kann man jedoch nicht die Performanz
wie bei Soft-IRQs erwarten. Tasklets werden mit Hilfe

132

7 Interrupts

von tasklet_init(t,handler,data) als Tasklets bekannt gegeben und mit


tasklet_schedule(name) zur Verarbeitung vorgemerkt.
Soft-IRQs sowie Tasklets arbeiten im Interrupt-Kontext, drfen somit keine
Funktionsaufrufe verwenden, die blockieren. Wenn dies bei der Behandlung der
Bottom Half gewnscht wird, muss die Aufgabe an eine Work Queue delegiert
werden, d.h. sie wird in eine Queue eingetragen, die von einem Kernel- Thread
im Prozess-Kontext bearbeitet wird. Ist die Initialisierung einer Arbeit mit
INIT_WORK erfolgt, kann dieses Work Queue Element mit Hilfe von
queue_work() in eine Work Queue eingetragen werden. Der worker_thread, der
fr die jeweilige Work Queue die Abarbeitung der Eintrge vornimmt, gliedert
sich in eine Warteschlange ein und ruft schedule() auf, wenn die Eintrge
abgearbeitet sind. Aus dieser Warteschlange wird er erst dann geweckt, wenn
neue Auftrge vorliegen.

8
Dateisysteme und Plattenverwaltung

8.1

Grundlagen

Fr den Benutzer gehrt das Filesystem zu den am deutlichsten wahrnehmbaren


Teilen eines Betriebssystems. Die Aufgabe des Filesystems ist es, dem Benutzer
einen einheitlichen Zugriff auf gespeicherte Daten zu ermglichen, der
unabhngig von den speziellen physischen Eigenschaften des Speichermediums Magnetband, Platte usw. - ist. Bei dem Zugriff wird der logische Name, unter
dem der Benutzer die Datei sieht, umgewandelt in den physischen Speicherort
der Datei, und es werden die bentigten Zugriffsmethoden herangezogen. Das
Filesystem muss eine Reihe von Informationen ber die gespeicherten Daten
enthalten, damit bei einem Multiuserbetrieb nur derjenige auf die Daten
zugreifen kann, der auch die ntigen Rechte dazu besitzt. Deshalb gehren zu
den Metainformationen neben dem logischen Dateinamen und der Abbildung
auf den Speicherort in der Regel solche Informationen wie: Eigentmer (das ist
derjenige Benutzer, der die Datei angelegt hat), das Erstellungsdatum, das
Datum der letzten nderung, Zugriffsrechte.
8.1.1

Untersttzung von Dateitypen

Der Benutzer entscheidet, welcher Art die Daten sind, die in einer Datei
zusammengefasst werden. Dabei kann es sich um Quelltexte, um ObjectCode,
d.h. kompilierte Programme, numerische Daten usw. handeln, aber auch Audiooder Video-Sequenzen sowie beliebig komplex aus Texten, Bildern usw.
zusammengesetzte Dokumente sind mglich. Dateien einer Art - z.B. Grafiken
eines bestimmten Typs - werden hufig durch die sogenannte Extension
kenntlich gemacht. Damit bezeichnet man die nach dem letzten Punkt im
Dateinamen folgenden Buchstaben, also jpg oder jpeg fr einen ganz bestimmten
Grafik-Typ.
Welche Vorteile knnte es haben, wenn ein Betriebssystem Kenntnis von den
verschiedenen Dateitypen hat? Fehlbedienung kann in einem solchen Falle

134

8 Dateisysteme und Plattenverwaltung

vom Betriebssystem unterbunden werden: das ffnen einer Grafik mit dem
Editor oder das Ausdrucken einer Object-Datei, d.h. Maschinen-ausfhrbaren
Codes, liee sich so verhindern. Tatschlich wurde ein solcher Ansatz versucht
und im Betriebssystem TOPS-20 implementiert. Dass sich dieser Ansatz nicht
durchgesetzt hat, liegt in den Nachteilen, die entstehen, wenn das
Betriebssystem Dateitypen untersttzt. Diese sind:

Fr jeden definierten Dateityp muss das Betriebssystem eigene


Prozeduren zur Verwaltung vorsehen,
das Betriebssystem wchst damit stark,
werden weitere Typen bentigt, so muss das Betriebssystem ergnzt und
verndert werden - soll ein Textverarbeitungsprogramm wie "^X
eingefhrt werden, so muss das Betriebssystem um Prozeduren zur
Behandlung von .tex-, .sty-, .dvi-Dateien und viele mehr erweitert
werden -, und
die Flexibilitt wird dadurch eingeschrnkt.

Aus diesem Grunde untersttzen einige Betriebssysteme wie Unix keinerlei


Dateitypen. Es ist somit Aufgabe der Anwendungsprogramme, die Folge der
Bytes angemessen zu interpretieren. Damit kann der Kern des Betriebssystems
klein gehalten werden, allerdings gibt es weniger automatisch greifende
Schutzmechanismen.
Um dennoch dem Benutzer eine gute Untersttzung hinsichtlich der
unterschiedlichen Dateitypen zu geben, wurden im Lauf der Zeit grafische
Oberflchen mit entsprechenden Methoden versehen. So kann ein Benutzer einer
grafischen Oberflche wie KDE mitteilen, welche Programme er fr bestimmte
Dateitypen in der Regel verwenden mchte.1 Der Dateityp bestimmt sich aus der
Extension bzw. aus dem magischen Byte, einer Byte-Kombination, die fr einen
bestimmten Dateityp eindeutig ist.
8.1.2

Von der Lochkarte zur Platte

In den Anfngen der Datenverarbeitung wurden Dateien sequentiell auf


Datentrgern wie Lochkarten oder Lochstreifen gespeichert, spter dann auf
Magnetbndern. Da mehrere Dateien auf einem Magnetband gespeichert werden
knnen, kam damit die Notwendigkeit auf, eine Verwaltung der Dateien zu
entwickeln, und so war das Directory in seiner einfachsten Form entstanden,
das den Dateinamen und den Speicherplatz1 2 auf dem Band enthielt. Der Zugriff
auf eine Datei beinhaltete also folgende Arbeitsschritte:

Dies schliet nicht aus, dass er auch andere Programme einsetzt, jedoch bietet ihm
die Oberflche zunchst nur bestimmte Programme an.
1

Genauer: da die Information auf Bndern in Blcken gespeichert wird, enthielt es


diejenige Blocknummer, bei der die Datei begann.
2

8.1 Grundlagen 135

Suche des richtigen Bandes,


Lesen des Directories und Bestimmung des ersten Blocks der Datei auf
dem Band,
Positionieren des Bandes auf diesen Block und
das sequentielle Lesen aller Blcke bis zum Beginn der folgenden Datei.

Probleme treten bei dieser Art der Speicherung auf, wenn Dateien verndert
werden sollen. Dann muss nicht nur die zu verndernde Datei neu geschrieben
werden, sondern es mssen in der Regel auch alle dahinter liegenden Dateien
neu auf das Band kopiert werden. Mit Aufkommen von Platten lie sich dieses
Problem elegant lsen: physisch sind Platten in Blcke fester Gre3 organisiert,
auf die direkt - und nicht wie bei Bndern sequentiell - zugegriffen werden kann.
Das Betriebssystem muss auf Grund dieser Struktur eine Blockung der
Datenstze vornehmen: eine Reihe von aufeinanderfolgenden logischen
Datenstzen einer Datei werden in einem Block gemeinsam gespeichert. Eine
Plattendatei kann somit als eine Folge von Blcken betrachtet werden, wobei sich
alle I/O-Operationen auf die Blcke beziehen. Als Folge der Blockung tritt bei den
Filesystemen die auch bei der Speicherverwaltung beobachtete interne
Fragmentierung (vgl. Abschn. 5.1) auf.
8.1.3

Directory und Dateioperationen

Wie fr die Bnder muss auch auf der Platte an einer bestimmten vorgegebenen
Position eine Information ber die vorhandenen Dateien gespeichert sein. Die fr
jede Datei im Directory zu speichernden Informationen hngen vom jeweiligen
Betriebssystemen ab, in der Regel handelt es sich zumindest um folgende
Informationen:
Dateiname: Dies ist der logische Name, unter dem der Benutzer auf die Datei
zugreift.
Dateityp: Fr Betriebssysteme, die einzelne Typen unterscheiden, ist diese
Information wichtig. Werden Dateitypen - wie in Unix - nicht untersttzt, so
kann die Information ber den Zweck einer Datei dennoch fr die verarbeitenden
Dienst- und Anwendungsprogramme wichtig sein. Der Typ wird bei manchen
Systemen durch die Extension angezeigt.4 Speicherort: Um physisch auf die Datei
zugreifen zu knnen, mssen das Gert und die Lage der Datei - bei Platten
somit die Blocknummern derjenigen Blcke, in denen die Datei abgelegt ist gespeichert werden. Gre: Die Gre der Datei - und bei manchen Systemen
auch die maximal fr die Datei reservierte Gre - wird aufbewahrt.
Schutz: Informationen ber die Zugriffsrechte verschiedener Benutzer auf die
Datei. So kann in Unix spezifiziert werden, ob lesender, schreibender
und/oder ausfhrender Zugriff erlaubt sind.

Beim Band haben die Blcke hufig eine variable Lnge.


Unix-hnliche Betriebssysteme untersttzten den Dateityp nicht, er muss somit in
solchen Systemen nicht gespeichert werden.
3
4

136

8 Dateisysteme und Plattenverwaltung

Datum und Uhrzeit: Diese Information ist fr das Erstellungsdatum wichtig.


Manche Systeme bewahren stattdessen den Zeitpunkt der letzten nderung
oder auch beides auf. Eine in diesem Kontext wichtige Information ist auch,
wer Eigentmer der Datei ist, und wer die Datei das letzte Mal gendert
hat.
Die im folgenden beschriebenen Operationen, die ber System Calls
bereitgestellt werden, mssen vom Filesystem angeboten werden. Sie greifen alle
auf das Directory zu. Die Struktur des Directories hat deshalb in einem
komplexen System Einfluss auf die Performance des Filesystems. Die folgende
Auflistung zeigt, in welcher Art die wesentlichen Dateioperationen und das
Directory zusammenhngen :
Anlegen einer Datei (create): Zunchst muss freier Platz auf dem Gert fr die
Datei gefunden werden. Dann muss ein Eintrag fr die neue Datei im Directory
vorgenommen werden. Dieser Eintrag enthlt die oben erwhnten Daten und
beschreibt damit wichtige Merkmale der neuen Datei. Schreiben (write): Der
System Call enthlt den logischen Dateinamen und die zu schreibende
Information5, hierzu gehren der Speicherbereich und die Anzahl der zu
bertragenden Bytes. Das System durchsucht das Directory nach dem
angegebenen Namen. Mit Hilfe des gefundenen Eintrags werden das Ende der
Datei und die Lage des nchsten Blocks ermittelt und die Information
geschrieben. Danach muss im Directory die Lngeninformation auf den neuen
Stand gebracht werden.
Lesen (read): Der System Call enthlt den logischen Dateinamen und den
Speicherplatz, wohin der nchste Block gelesen werden soll. Das System
durchsucht das Directory wie beim Schreiben nach dem angegebenen Namen. An
Hand eines Zeigers auf den gelesenen Block und der DirectoryInformation wird
die Lage des nchsten zu lesenden Blocks ermittelt. Der Zeiger zum gelesenen
Block muss anschlieend auf den neuen Stand gebracht werden. Dieser Zeiger
wird oft als Current File Position bezeichnet. Die Current File Position kann
sowohl direkt im Directory als auch in File Control Blocks verwaltet werden, die
vom Betriebssystem beim ffnen einer Datei erzeugt werden. Eine
Entscheidung, wo diese Information aufbewahrt wird, beeinflusst die
Eigenschaften des Systems nachhaltig. Zurcksetzen einer Datei (reset): Hier
braucht nur das Directory durchsucht und die Current File Position auf den
Anfang der Datei zurckgesetzt zu werden.
Lschen einer Datei (delete): Nach Durchsuchen des Directories und dem
AufRnden des zugehrigen Eintrags wird der von der Datei belegte Platz
freigegeben und der Eintrag im Directory gelscht bzw. als ungltig
markiert. Die in den einzelnen Blcken der gelschten Datei gespeicherten
Daten werden hingegen nicht gelscht.

Hier wird zunchst nur sequentielles Anfgen betrachtet.

8.1 Grundlagen 137

8.1.4

Implementierungen fr Directories

Fr die Datenstruktur des Directories sind verschiedene Mglichkeiten in


unterschiedlichen Betriebssystemen eingesetzt worden. Die einfachste Art
bezglich der Programmierung ist diejenige einer linearen Liste. Auf Grund der
hufigen Zugriffe ist diese Methode sehr zeitaufwndig, da das Durchsuchen
einer solchen Liste verhltnismig lange dauert.
Ein sortiertes Array oder ein sortierter binrer Baum reduzieren den
Suchaufwand durch den Einsatz binrer Suchverfahren. Der Algorithmus ist
jedoch weitaus aufwndiger. Auerdem muss bei jedem Einfgen die Sortierung
erhalten bleiben, was in der Regel kostspielige Operationen voraussetzt.
Hash-Tabellen sind ebenfalls benutzt worden. Hiermit kann die Suchzeit
erheblich reduziert werden: Suchen ist nur noch erforderlich, wenn Kollisionen
auftreten. Nachteil dieser Methode ist die feste Gre der Hash-Tabellen.
Als Beispiel einer Implementierung sei in Grundzgen die Struktur der
Directories im VM/CMS6 betrachtet. Es handelt sich um eine sortierte Liste, die
bei Bereitstellung der Platte in den Speicher kopiert und dort mit
HashingVerfahren durchsucht wird. Dadurch werden Zugriffe zum ffnen und
Lesen von Dateien sehr schnell. Wird die Struktur des Filesystems durch
Schreiben, Lschen usw. verndert, so wird die Information aus dem internen
Speicher zurck auf die Platte geschrieben. Das Zurckschreiben erfolgt so, dass
auf der Platte immer zwei Versionen des Directories stehen: die derzeit gltige
sowie die vorhergehende. Die Blcke der Directory-Strukturen sind auf der Platte
mit Hilfe von Pointern verkettet. Sie beginnen mit dem physischen Block 3 bzw. 4.
In Block 2 gibt es ein Bit, das anzeigt, welche der beiden Versionen die derzeit
gltige ist, ob also das gltige Directory mit Block 3 bzw. Block 4 beginnt. Ein
Verndern der Dateienstruktur luft folgendermaen: zunchst wird die
vernderte Datei neu geschrieben, sodann wird die Directory-Information
geschrieben und danach das entsprechende Bit im Block 2 gesetzt. Bei Ausfall
des Systems kann deswegen damit gerechnet werden, dass das Filesystem intakt
ist - wenn auch ggf. die letzte nderung verloren ist. Ein Designziel war hierbei
offenbar ein mglichst hoher Schutz bei Systemausflle.
8.1.5

Zugriffsmethoden

Lesen und Schreiben sind die hufigsten Operationen, die im Filesystem


eingesetzt werden. Die Methoden, die das Betriebssystem bereitstellt, um diese
Operationen durchzufhren, knnen so einfach gehalten sein, wie es vom
Bandbetrieb her bekannt ist - sequentielles Lesen und Schreiben - oder so
vielfltig und komplex, wie es in manchen Betriebssystemen angeboten wird
VM/CMS war ein Dialog-Betriebssystem fr IBM-Grorechner. CMS selbst
untersttzte nur Single-User, Single-Task, jedoch diente das VM/SP dazu, die reale
Hardware virtuell zu multiplexen, so dass auf einer Maschine viele CMS-Systeme
zeitgleich eingesetzt werden konnten. Heute ist dieses System kaum noch von Bedeutung.
6

138

8 Dateisysteme und Plattenverwaltung

- so hlt IBM neben sequentiellem Zugriff auch ISAM (Index Sequential Access
Method) und VSAM (Virtual Storage Access Method) bereit.
Im Prinzip lassen sich alle Zugriffsmethoden auf zwei Anstze zurckfhren:
den sequentiellen und den direkten Zugriff.
Der sequentielle Zugriff bedeutet,

dass bei jedem Lesen der jeweils nchste Abschnitt der Datei gelesen
und die Current File Position entsprechend erhht wird,
dass beim Schreiben die neue Information an das Ende der Datei
angefgt wird und die Current File Position auf das neue Ende gesetzt
wird und
dass beim Zurcksetzen der Datei die Current File Position auf den
Anfang der Datei gesetzt wird.

Beim direkten Zugriff wird die Datei als logische Folge von Blcken betrachtet,
die logisch bei 0 beginnend aufsteigend durchnummeriert sind. Obwohl die Datei
in den Blcken in aufsteigender Folge gespeichert ist, knnen die Blcke deshalb
vllig unregelmig auf dem Speichermedium verteilt sein (vgl. Ab- schn. 8.1.6).
Nun wird die Tatsache ausgenutzt, dass beim Speichermedium Platte ein
Block direkt adressiert und gelesen bzw. geschrieben werden kann. In der Lesebzw. Schreiboperation wird mitgeteilt, welcher logische Block der Datei
betrachtet werden soll. Das Betriebssystem muss noch eine Umsetzung von der
logischen Blocknummer auf die physische Nummer des realen Speicherblockes
vornehmen. Danach knnen erst der Plattenarm positioniert und die gewnschte
Operation vorgenommen werden.
Mit Hilfe dieser beiden Methoden lassen sich komplexere Zugriffsmethoden
aufbauen. So lsst sich der gezielte Zugriff ber einen Schlssel auf eine groe
Datei, die geordnet und sequentiell gespeichert ist, dadurch beschleunigen, dass
eine separate Index-Datei angelegt wird, die zu jedem Block der ursprnglichen
Datei den letzten dort gespeicherten Schlssel und die zugehrige logische
Blocknummer enthlt. Zum schnelleren Zugriff kann diese Index-Datei selbst
mehrstufig gegliedert werden - dies ist im Prinzip die ISAM-Zugriffsmethode.
8.1.6

Bereitstellung von Speicherplatz

Beim Anlegen einer Datei bzw. beim Schreiben in eine Datei muss in der Regel
Speicherplatz zur Verfgung gestellt werden. Hier taucht das Problem auf, wie
das Betriebssystem freien Speicherplatz erkennen und vergeben kann - und wie
belegter Speicherplatz beim Lschen einer Datei wieder freigegeben wird.
Die Struktur der Platte mit ihrem freien Zugriff auf die Blcke legt drei
Arten der Platzreservierung fr Dateien nahe:

Fr eine Datei wird zusammenhngender Platz (contiguous) belegt, die


logischen Blcke sind auch physisch auf der Platte hintereinander
angeordnet.

8.1 Grundlagen 139

Die Blcke auf der Platte, die zu einer Datei gehren, werden durch
Zeiger miteinander verkettet (linked). Die letzten Bytes eines physischen
Blockes enthalten die physische Blockadresse des logisch nchsten
Blockes.
Zu jeder Datei gehrt ein Index (indexed), der geordnet die den logischen
Blcken zugeordneten physischen Blockadressen enthlt. Dieser Index
ist entweder ein einzelner Block oder besteht selbst aus mehreren
verketteten Blcken.

Auch die Verwaltung der freien Blcke muss bedacht werden, damit effizient ein
freier Block ermittelt werden kann. Hier werden folgende Verfahren benutzt:

Die Darstellung der freien Blcke erfolgt in einem Bit-Vektor. Basierend


darauf, dass die Blcke auf einer Platte durchnummeriert werden
knnen, kann dann eine 1 an n-ter Stelle in dem Bit-Vektor
signalisieren, dass der zugehrige n-te Block der Platte belegt, eine 0,
dass der Block frei ist.
Die Verwaltung in Form einer Linked List ist wenig effizient, weil bei der
Verwaltung in der Regel diese Liste von Block zu Block durchsucht
werden msste. Sind viele Zuweisungen so gestaltet, dass
zusammenhngende Blcke bereitgestellt bzw. befreit werden, so kann
die Verwaltung in einer Datei geschehen, die eine Folge von Paaren
enthlt: erster freier Block - Anzahl der darauf folgenden freien Blcke.

Zusammenhngende Speicherplatzzuweisung
Sowohl sequentieller als auch direkter Zugriff knnen bei zusammenhngender
Speicherplatzzuweisung leicht verwirklicht werden. Probleme treten nur auf bei
der Suche nach Platz fr eine neue Datei. Strategien wie First-Fit, Best-Fit bzw.
Worst-Fit sind mglich: der erste gengend groe freie Speicherplatz, der
kleinste bzw. der grte. Die Verfahren erzeugen eine externe Fragmentierung
des Speichers, die immer wieder durch Defragmentierung behoben werden muss,
um Platz zum Speichern grerer Dateien zu schaffen.
Vorteile dieser Methode liegen darin, dass zum einen sequentielle Zugriffe
gute Bedingungen vorfinden, zum anderen der Platzbedarf fr
Verwaltungsinformationen minimal ist im Vergleich zu den anderen Verfahren.
Verkettete Speicherplatzzuweisung
Erfolgt die Speicherplatzzuweisung als verkettete Liste von Blcken, so tritt
externe Fragmentierung nicht mehr auf. Dateien knnen nach ihrer Erzeugung
wachsen und stoen dabei nur an die Grenzen der Gesamtspeicherkapazitt des
Speichermediums. Der Nachteil besteht darin, dass sich diese Speicherzuweisung
nur fr sequentiellen Zugriff eignet. Direkter Zugriff kann hiermit nicht effizient
implementiert werden, da immer die gesamte Liste bis zur gesuchten Position
durchlaufen werden muss.

140

8 Dateisysteme und Plattenverwaltung


S

8.1 Grundlagen 141

user 1 user
2 in dem Verlust an Speicherplatz durch die
Weitere Probleme
liegen
Zeigerverwaltung und noch mehr in der Datensicherheit. Da die Zeiger im
Speicher vllig verstreut liegen, kann kaum eine Sicherung gegen Verlust oder
Zerstrung von Zeigern eingebaut werden. Der Bedarf an Speicherplatz fr
Verwaltungsinformationen wird im Wesentlichen durch den Quotienten
Zeigerlnge zu Blocklnge bestimmt.

Indizierte Speicherplatzzuweisung
Alle Zeiger werden nun in einem oder mehreren Blcken zusammengefasst;
damit kann ohne grere Probleme zustzliche Sicherung bei der Verwaltung der
Zeiger verwirklicht werden. Sowohl sequentieller als auch direkter Zugriff lassen
sich leicht verwirklichen. Externe Fragmentierung tritt nicht auf. Ein Problem
bildet jedoch der gegenber der verketteten Speicherzuweisung noch erhhte
Speicherbedarf fr die Verwaltung des Indexes.
Werden keine geeigneten Maahmen ergriffen, so betrgt bei der
Speicherung einer kleinen Datei, die in einen Block passt, das Verhltnis von
Nutz- zu Verwaltungsinformation im Wesentlichen 1:1, da ja auch der Index
einen Block belegt (der Directory-Eintrag wird hier noch nicht einmal mit
bercksichtigt). Um diesem Problem zu entgehen, wird bei manchen
Filesystemen, die auf diesem Ansatz basieren, im Directory-Eintrag ein Feld von
Zeigern fr die ersten n Blcke der Datei mitgefhrt. Erst wenn der Platzbedarf
der Datei grer wird, mssen zustzliche Indexblcke angelegt werden.
8.1.7

Directory

Aus Sicht des Benutzers ist die Struktur des Directories wichtig. Einfachste
Form ist das einschichtige Directory: alle Dateien liegen auf gleicher Stufe. Diese
Form gab es im PC-Bereich bei CP/M oder dem Nachfolger MS/DOS Version 1.
Beim Multiusersystem ist das zweischichtige Directory eine naheliegende
Erweiterung: fr jeden Benutzer wird ein eigenes einschichtiges Directory zur
Verfgung gestellt. Damit werden Namenskonflikte zwischen mehreren
Benutzern umgangen. Im CMS knnte man von einer dreischichtigen
DirectoryStruktur sprechen: fr jeden Benutzer werden eigene Directories
angelegt und zwar eins pro virtueller Platte. Die Struktur ist in Abb. 8.1
dargestellt.
Diese nicht ganz przise Darstellung der Directory-Struktur soll zugleich
andeuten, dass die Directories der S- und Y-Platten eine besondere Bedeutung
haben: es handelt sich um die Systemplatten, auf die alle Benutzer zugleich
lesenden Zugriff haben.
Ein zwei- bzw. dreischichtiges Directory kann als Spezialfall einer
Baumstruktur angesehen werden. Diese Art von Directory-Struktur wird in
einer Reihe von Betriebssystemen benutzt, so unter anderem in Unix (vgl. Abb.
8.2). Der Dateibaum hat ein Wurzelverzeichnis (root directory), d.h. ein oberstes

142

8 Dateisysteme und Plattenverwaltung

Jeder Benutzer arbeitet in einem sogenannten Current Directory7,


demjenigen Directory, von dem aus alle Suchvorgnge starten, sofern kein
kompletter Pfadname beginnend mit der Wurzel / angegeben wurde.8 Wird die
Datei nicht im Current Directory gefunden und hat der Benutzer keinen
weiteren Suchpfad angegeben, so wird die Suche erfolglos beendet. In diesem
Fall muss der Benutzer entweder das Current Directory wechseln oder einen
v
'-v Systemplatten Nv
N
Suchpfad vereinbaren. Dies geschieht in\-'.
MS/DOS
durch die Angabe von Path
'-. 'v fr alle Benutzei
fr .bat-, .com- und .exe-Dateien, durch Append fr andere Dateien.
Eine allgemeine Baumstruktur ermglicht dem Benutzer, seine eigenen
Ordnungskriterien zu verwirklichen und somit eine groe Menge von Dateien
bersichtlich zu speichern. In Unix ist jedem Benutzer ein eigenes HOMEDirectory zugeordnet. Der Benutzer kann die unter seinem HOME-Directory
liegende Baumstruktur in der Regel frei gestalten, whrend er andere Teile des
Dateibaums meistens nur lesend oder auch gar nicht ansehen darf.
8.1.8

Datenschutz und Datensicherheit


individuelle
Plattenpartitione
n fr die
Rechnersystemen
- gleich ob Ein- oder Multiusersysteme
Benutzer

Bei allen
- muss dafr
Sorge
getragen
werden, dass
nicht verloren gehen. Dies kann auf
Abb. 8.1.
Directory-Struktur
imInformationen
CMS
Unachtsamkeit der Benutzer oder auf Fehlern der Speichermedien bzw. des
Betriebssystems beruhen. Fehler der Speichermedien knnen durch redundante
Directory. Jedes
Directory
enthlt
eine Reihe
von Dateien
und/oder
Auslegung
der Medien
(RAID)
aufgefangen
werden.
Fr Datenverluste
auf
Subdirectories.
Dabei
wird
jede
Datei
durch
den
Pfad
vom
Root
Directory durch
Grund von Unachtsamkeit sollte in regelmigen Abstnden
eine
alle Subdirectories
bis hinunter
Datei die
eindeutig
beschrieben.
Sicherungskopie
derhindurch
Datentrger
angelegtzur
werden,
es ermglicht,
den letzten
Sicherungsstand wieder herzustellen.
In einem Multiusersystem tritt zustzlich das Problem auf, vertrauliche
Informationen vor dem Zugriff anderer Benutzer zu schtzen. Der Schutz kann
verschiedene Zugriffsrechte umfassen: Rechte zum Lesen oder Schreiben einer
Datei (ggf. aufgespalten in Verndern und Anfgen), Lschen aus einer Datei
sowie Ausfhren einer Datei knnen ggf. unterschiedlich vergeben werden.
Weitere Rechte knnen bezglich der Directories vorgesehen werden: Erzeugung
und Lschung von Dateien in einem Directory sowie Auflisten der Dateien eines
Directories mssen ggf. extra geschtzt werden.
Das Betriebssystem muss fr entsprechenden Schutz sorgen. Drei
Mglichkeiten sollen kurz angedeutet werden:
Manche Systemen bieten Passwortschutz an. Dateien bzw. Directories werden
vom Eigentmer mit einem Passwort versehen; andere Benutzer knnen erst
zugreifen, wenn sie dem Betriebssystem das gltige Passwort mitgeteilt
haben. So auch im VM/CMS, wo der Zugriff auf jeweils eine ganze Platte
durch Passwortschutz geregelt werden kann.

Auch Working Directory oder kurz pwd fr Print Working Directory genannt.
Abb. 8.2. Typische Unix Directory-Struktur
Bei einem kompletten Pfadnamen beginnt die Suche natrlich bei der Wurzel.

7
8

8.1 Grundlagen 143

Andere Systeme machen den Zugriff vom Benutzer abhngig. Jede


Datei, jedes Directory wird mit einer Zugriffsliste verknpft, in die der
Eigentmer eintrgt, welcher Benutzer welche Rechte auf das
betreffende Objekt hat. Dies entspricht den sogenannten Access-Listen.
In einfacherer Form wird der zweite Ansatz in Unix benutzt: fr jedes
Objekt werden 3 mal 3 Bit reserviert, jeweils drei Bit kontrollieren
Lese-, Schreib- bzw. Ausfhrungsrecht (rwx) und zwar fr den
Eigentmer, eine der Gruppen, der der Eigentmer angehrt, und alle
anderen Benutzer.

8.1.9

Unterschiedliche Filesysteme

Gerade ein System wie Linux verfolgt das Ziel, eine mglichst integrative
Plattform bereitzustellen. Das bedeutet aber, dass nicht nur ein
FilesystemFormat, sondern mehrere unterschiedliche untersttzt werden sollen.
Whrend Linux selbst lange Zeit ext bzw. spter ext2 einsetzte, soll auch auf
Platten zugegriffen werden knnen, die unter DOS und Windows oder auch OS/2
lesbar sind. Damit mssen solche Filesysteme wie FAT, VFAT, NTFS und HPFS
und viele andere mehr ebenfalls eingebunden werden. Aus der Sicht des
Benutzers muss die Einbindung anderer Filesysteme auf eine Weise geschehen,
die weiterhin eine einheitliche Sicht auf die Baumstruktur erhlt.
Die Konsequenz ist ein virtuelles Filesystem, das erst beim Zugriff auf eine
Datei entscheidet, welches konkrete Filesystem benutzt werden muss und erst
dann die fr dieses Filesystem entsprechenden Funktionen zum Offnen, Lesen,
Schreiben usw. anspricht.
8.1.10

Festplattenzugriffe

Bei greren Anlagen treten weitere Probleme beim physischen Zugriff auf die
Platten auf: in der Regel werden pro Platte eine ganze Reihe von I/OOperationen anstehen. Eine Platte ist in Oberflchen, Spuren und Blcke
eingeteilt, die Positionierung des Plattenarms bewirkt, dass bei einer
Umdrehung einer Platte auf allen Oberflchen eine Spur gelesen werden kann.
Soll auf einen bestimmten Block zugegriffen werden, so mssen die Oberflche,
die Spur und die Nummer des Blockes innerhalb der Spur bestimmt werden.
Damit sind die Position des Plattenarms sowie der Kopf, mit dem die Operation
erfolgen soll, festgelegt. Nach der Positionierung muss noch gewartet werden, bis
durch die Drehung der richtige Block unter dem Kopf erscheint (vgl. Abb. 8.3).
Da die Positionierung des Plattenarms gemessen an der Umschaltung zwischen
den Kpfen und der Umdrehungsgeschwindigkeit vergleichsweise langsam ist,
wird man in der Regel versuchen, die Zugriffe so zusammenzufassen, dass die
vom Arm zurckgelegte Strecke mglichst klein bleibt.
Eine typische Methode ist der sogenannte Scan-Algorithmus. Die
ausstehenden I/O-Anforderungen fr eine Platte werden so angeordnet, dass sie
mit

144

8 Dateisysteme und Plattenverwaltung

steigender Spurzahl bearbeitet werden. Erst wenn die Anforderung mit hchster
Spurzahl erreicht ist, wird der Arm so positioniert, dass er eine inzwischen neu
eingetroffene I/O-Anforderung mit kleinster Spurzahl bearbeiten kann. Die
weiteren Anforderungen werden wieder in aufsteigende Spurreihenfolge
gebracht. Ein Problem dieses Verfahrens ist Starvation, denn es handelt sich
ganz offensichtlich um ein priorittsgesteuertes Scheduling. Kommen schnell
Anforderungen im mittleren Plattenbereich hinzu, so kann es sein, dass der Arm
erst sehr spt oder gar nicht Anforderungen im oberen Bereich - und
anschlieend Anforderungen im unteren Bereich - bearbeiten kann. Ein
derartiges Verfahren muss also mit einer Alterung ausgestattet werden, damit
Starvation nicht eintreten kann.

^__j^- Armbewegung
Plattendrehung
Abb. 8.3. Aufbau einer Platte
Gepunktet ist die aktuelle Spur auf der Plattenoberflche unter dem 1. Kopf. In der Regel
befinden sich auch auf der Unterseite der Platten Kpfe, diese sind der bersichtlichkeit
halber nicht dargestellt.

8.2

Virtual File System (VFS)

Die eben angestellten berlegungen fhren dazu, dass zunchst der Fokus auf
dem Virtual File System liegen muss. Ein wichtiger Aspekt wird dabei sein, wie
bestimmt werden kann, welches Filesystem konkret benutzt werden muss, wenn
auf eine Datei zugegriffen werden soll. Abbildung 8.4 stellt den Zugriff auf
Dateien mit Hilfe des VFS dar.
8.2.1

VFS-Datenstrukturen fr Dateien

Die internen Datenstrukturen, die VFS benutzt, hneln denjenigen, die das
konkrete Filesystem ext2 verwendet (vgl. Abschn. 8.6). Der zentrale Begriff

8.2 Virtual File System (VFS) 145

Abb. 8.4. Der VFS-Layer


ist die Inode, die in den Listings 8.1 und 8.2 dargestellt ist. Die Inode enthlt
wesentliche Informationen ber die Datei bzw. die Gertedatei, die sie
reprsentiert.
Der erste Blick auf die Inode-Struktur9 offenbart einige interessante Aspekte:

Die Struktur enthlt nicht den Dateinamen,


es gibt eine Reihe von Listenkpfen, die mit der Speicherung wohl kaum
etwas zu tun haben knnen,
es gibt jeweils einen Eintrag, der

- die auf der inode erlaubten Operationen sowie


- die auf der Datei erlaubten Operationen
beschreibt,
es gibt einen Verweis auf den Superblock und
es gibt Verweise auf Gerte allgemein, Pipes, Block- und CharacterGerte.

Warum kein Dateiname aufgefhrt ist, wird im Abschn. 8.2.3 verstndlich.


Die verschiedenen Listenkpfe ermglichen einen schnellen Zugriff auf die
Inodes nach unterschiedlichen Kriterien. Da die Inodes fr das VFS im Speicher
erzeugt werden, mssen wir nicht erwarten, dass diese Eintrge mit der
konkreten Speicherung zu tun haben10; stattdessen geht es um die
SystemPerformance. Jede Inode wird in zwei unterschiedlichen Strukturen
eingetragen: in einer Hash-Tabelle, die zum schnellen Aufsuchen an Hand von
Definiert in der Datei include/linux/fs.h.
Es gibt Filesysteme, die im Gegensatz zu ext2 auf einem vllig anderen Ansatz
beruhen, so z.B. FAT oder Reiserfs. In diesen Fllen ist die gesamte inode-Struktur im VFS
ein durch Berechnung aus dem Filesystem erzeugtes Konstrukt.
9

10

146

8 Dateisysteme und Plattenverwaltung


struct inode {
struct hlist_node
i_hash; struct list_head
struct
i_list;list_head i_dentry;
unsigned long
i_ino;
atomic_t
i_count;
umode_t
i_mode;
unsigned int
i_nlink;
uid_t
i_uid;
gid_t
i-gid;
dev_t
i_rdev;
loff_t
i_size;
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
unsigned int
i_blkbits
unsigned long
i_blksize
misigned long
unsigned long
i_blocks
unsigned short
;
spinlock_t
i_bytes;
struct semaphore i_lock; /* i_blocks, i_t
struct rw_semaphorei_sem;
struct inode_operations i_alloc_sem;
struct file_operations
*i_op;
*i_fop;
/* former ->i_op->default_file_ops
struct super_block
*/ *i_sb;
struct file_lock
*i_flock;
struct address_space
*i_mapping; i_data;
struct address_space
*i_dquot[MAXQUOTAS];
struct dquot
/* These three should probably be a union */
struct list_head
i_devices;
struct pipe_inode_info *i_pipe;
struct block_device
*i_bdev;
struct cdev
*i_cdev;
int
i_cindex;
void
*i_filterdata;
/* Directory notify events and notification: */
unsigned long
i_dnotify_mask;
struct dnotify_struct *i_dnotify;

Listing 8.1. Die inode-Struktur: Teil 1


Diese Struktur dient dazu, wichtige Informationen ber die durch sie reprsentierte
Dateien bzw. Gerte zu verwalten.

8.2 Virtual File System (VFS)


unsigned long
unsigned long
unsigned int
unsigned char
atomic_t void
___u32
union {
void

i_state;
dirtied_when;
i_flags;
i_sock;
i_writecount;
*i_security;
i_generation;

147

/* jiffies of first dirtying */

*generic_ip;

> u;

>;

Listing 8.2. Die inode-Struktur: Teil 2


Nummer und Superblock (vgl. Abschn. 8.2.2) geeignet ist, sowie in eine von drei
Listen:

inode_unused11 ist der Kopf zur Liste aller Inodes, die nicht lnger
verwendet werden,
inode_in_use ist der Kopf zur Liste aller Inodes, die benutzt, aber nicht
verndert wurden,* 12
fr jeden Superblock eine Liste der Inodes dieses Filesystems, die
benutzt werden und schmutzig (dirty) sind, d.h. die verndert wurden.

i_list wird benutzt, um die Inode je nach Situation in eine dieser drei Listen
einzufgen.
Der Zeiger *i_pipe wird nur dann verwendet, wenn die Inode eine Pipe
realisiert. Die Zeiger *i_bdev bzw. *i_cdev werden verwendet, wenn die Inode
Gerte-Spezialdateien reprsentiert.
Viele weitere Eintrge sind leicht verstndlich: die Rechtestruktur wird
durch i_mode sowie die Kennungen i_uid und i_gid beschrieben. Dabei
beschreiben die beiden letzten Eintrge die Benutzer- und Gruppenkennung des
Eigentmers, whrend der erste die Rechte in der Unix-spezifischen Form
rwxrwxrwx beschreibt: drei Bit-Gruppen fr den Eigentmer, die Gruppe und
den Rest der Welt, die das Lesen (Read), Schreiben (Write) bzw. Ausfhren
(eXecute) erlauben. In Bezug auf Directories bedeutet das Recht zum Ausfhren,
dass der Benutzer das Directory mit chdir betreten und zu seinem Current
Directory machen darf.
i_size beschreibt die Lnge der Datei in Bytes, i_blocks gibt die Anzahl der
Blcke an. Dieser Wert hngt mit der Blocklnge des konkreten Filesystems
zusammen, i_atime, iuntime und i_ctime beschreiben den Zeitpunkt des letzten
Zugriffs (access), der letzten nderung (modification) und der Anlage der Inodenderung (creation).

inode_unused sowie inode_in_use sind in fs/inode.c definiert.

Fr die beiden Eintrge in der Struktur gilt i_count > 0, ijilink > 0.

12

148

8 Dateisysteme und Plattenverwaltung

Jede VFS-Inode hat in i_ino eine eindeutige Nummer bezogen auf die
Eintrge fr ein konkretes Filesystem. In i_count wird gezhlt, wie viele Prozesse
auf diese Inode zugreifen.
Zur Erluterung von i_nlink muss kurz auf den Begriff Link (oder
Verknpfung) eingegangen werden. Ein Link stellt eine Verbindung zwischen
Filesystem-Objekten her. Unix unterscheidet dabei zwischen symbolischen und
harten Links. Ein symbolischer Link spiegelt das Objekt scheinbar an einer
bestimmten Stelle des Dateibaums. Es wird eine eigene Inode angelegt, die
anstelle von Daten den Namen derjenigen Datei enthlt, auf die verwiesen
wird.13 Dadurch sind der Link und das Ziel des Links nicht fest verbunden: wird
das Ziel gelscht, dann verbleibt der Link im System, zeigt aber ins Leere. Ein
harter Link hingegen greift auf eine bereits vorhandene Inode innerhalb
desselben Filesystems zu. Ist nun unter dem Namen A eine Datei angelegt und
wird unter dem Namen B ein harter Link auf die Datei A erzeugt, so treten beim
Lschen von A Probleme auf: whrend normalerweise die Inode samt dem
zugehrigen Datenbereich gelscht wird, wre das in dieser Situation fatal: B
benutzt immer noch die alte Inode! Aus diesem Grunde wird in i_nlink die
Anzahl der harten Links gezhlt. Die Inode und der zugehrige Datenbereich
drfen erst dann gelscht werden, wenn keine harten Links mehr existieren.
Bezogen auf unser Beispiel heit das: nach dem Lschen von A bleiben die Inode
und der Datenbereich weiter gltig, da B noch darauf zugreift. Erst nach dem
Lschen von B werden die Inode und der Datenbereich gelscht.14
Einerseits sollen unterschiedliche Filesysteme untersttzt werden,
andererseits sollen die implementierungsspezifischen Details vor dem Benutzer
verborgen bleiben, der Benutzer soll ein einheitliches Filesystem mit einer
einheitlichen Schnittstelle wahrnehmen. Um dies zu verwirklichen, werden i_op
(vgl. Listing 8.3) und i_fop (vgl. Listing 8.4) benutzt: i_op beschreibt die
Funktionen, die auf die Inode angewendet werden knnen, i_fop15 enthlt die
Datei-relevanten Funktionen.
Whrend die Bedeutung der Funktionen wie create(), mkdir(), rmdir() usw.
offensichtlich ist16, bedrfen andere Funktionen noch einer Erklrung:
lookup() sucht die Inode eines Filesystemobjekts an Hand seines Namens. Dabei
wird das Argument mit der Struktur dentry benutzt, um die Verbindung
zwischen Inode und Dateinamen herzustellen (vgl. Abschn. 8.2.3). link() erstellt
einen harten Link und erhht imlink um 1. unlink() wird benutzt, um eine Datei
zu lschen. Dabei wird, wie oben erwhnt, zunchst der Link-Count imlink um 1
verringert, die Inode und der Datenbereich werden erst dann gelscht, wenn
dieser Zhler auf 0 steht.

Dies kann Filesystem-bergreifend geschehen.

13

Vorausgesetzt, es gibt keine weiteren harten Links auf diese Datei.


Beide Definitionen befinden sich in include/linux/fs.h.
16
Die Shell-Kommandos lauten gleich - mit Ausnahme von rename(), das als ShellKommando mv (move) heit - mv im selben Verzeichnis ist ein rename.
14
15

8.2 Virtual File System (VFS) 149


struct inode_operations {
int (*create) (struct inode *,struct dentry *,int, struct
nameidata *);
struct dentry * (*lookup) (struct inode *, struct dentry *,
struct nameidata *);
int (*link) (struct dentry *,struct inode *,struct dentry
*); int (*link_raw) (struct nameidata *,struct nameidata
*); int (*unlink) (struct inode *,struct dentry *); int
(*unlink_raw) (struct nameidata *);
int (*symlink) (struct inode *,struct dentry *,const char *);
int (*symlink_raw) (struct nameidata *,const char *);
int (*mkdir) (struct inode *,struct dentry *,int);
int (*mkdir_raw) (struct nameidata *,int);
int (*rmdir) (struct inode *,struct dentry *);
int (*rmdir_raw) (struct nameidata *);
int (*mknod) (struct inode *,struct dentry *,int,dev_t);
int (*mknod_raw) (struct nameidata *,int,dev_t); int
(*rename) (struct inode *, struct dentry *, struct inode *,
struct dentry *); int (*rename_raw) (struct nameidata *,
struct nameidata *);
int (*readlink) (struct dentry *, char _ user *,int);
int (*follow_link) (struct dentry *, struct nameidata *);
void (*truncate) (struct inode *);
int (*permission) (struct inode *, int, struct nameidata
*); int (*setattr) (struct dentry *, struct iattr *); int
(*setattr_raw) (struct inode *, struct iattr *); int
(*getattr) (struct vfsmount *mnt, struct dentry *, struct
kstat *);
int (*getattr_it) (struct vfsmount *, struct dentry *,
struct lookup_intent *, struct kstat *);
int (*setxattr) (struct dentry *, const char *,const void
*, s i z e _ t , i n t ) ;
ssize_t (*getxattr) (struct dentry *, const char *, void *,
size_t);
ssize_t (*listxattr) (struct dentry *, char *, size_t); int
(*removexattr) (struct dentry *, const char *);

Listing 8.3. Die Struktur inode_operations

symlink() legt einen symbolischen Link an. follow_link() sucht das Ziel eines
symbolischen Links, setxattr() bzw. getxattr() werden zum Anlegen, Lesen bzw.
Lschen von erweiterten Attributen eingesetzt, so wie z.B. Access-Listen (ACLs).
Auch die vom VFS bereitgestellten Operationen (Listing 8.4) auf Dateien
bedrfen einiger Erluterungen. Die Operationen mssen trotz der
Verwirklichung einer einheitlichen Schnittstelle dem Benutzer gegenber
hinreichend

150

8 Dateisysteme und Plattenverwaltung

>;

struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char _ user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, char _user *,
size_t, loff_t);
ssize_t (*write) (struct file *, const char _user *,
size_t, loff_t *);
ssize_t (*aio_write) (struct kiocb *, const char _user *,
size_t, loff_t);
int (*readdir) (struct file *, void *, filldir_t); unsigned int
(*poll) (struct file *, struct poll_table_struct *); int (*ioctl)
(struct inode *, struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *); int (*open)
(struct inode *, struct file *); int (*flush) (struct file *);
int (*release) (struct inode *, struct file *); int (*fsync)
(struct file *, struct dentry *, int datasync); int (*aio_fsync)
(struct kiocb *, int datasync); int (*fasync) (int, struct file
*, int); int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*readv) (struct file *, const struct iovec *, unsigned
long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec
*, unsigned long, loff_t *);
ssize_t (*sendfile) (struct file *, loff_t *, size_t,
read_actor_t, void __user *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t,
loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long,
unsigned long, unsigned long, unsigned long); long (*fcntl)(int
fd, unsigned int cmd, unsigned long arg, struct file *filp);

Listing 8.4. Die Struktur file_operations

differenziert sein, um die diversen Filesysteme angemessen zu untersttzen. So


erklrt sich ein Eintrag wie ioctl, der bentigt wird, um mit Hardwaregerten zu
kommunizieren. Dieser Eintrag wird nur bei Gertespezialdateien bentigt, um
Steuerkommandos zu bertragen. Fr alle anderen Dateiobjekte, also normale
auf einer Platte gespeicherte Dateien, steht an dieser Stelle ein Null-Zeiger.
Der Eintrag owner ermglicht es dem Kernel, ein konkretes Filesystem nicht
fest einzubinden, sondern als Modul nachzuladen. In diesem Falle zeigt dieser
Eintrag auf diejenige Datenstruktur, die das Modul reprsentiert.

8.2 Virtual File System (VFS) 151

readdir() dient ausschlielich dazu, den Inhalt von Directories einzulesen.


Ein Directory kann als Datei mit speziellen Eigenschaften betrachtet werden, fr
die eigene Funktionen bereitgestellt werden mssen.
Die Funktionen read(), write() und llseek()17 sind offensichtlich,
open() dient zum ffnen einer Datei und verbindet ein File-Objekt (vgl. Abschn.
8.2.4) mit einer Inode, release() bildet das Gegenstck zu open(): gibt es
keinen Benutzer eines File-Objekts mehr, so wird damit die Freigabe nicht mehr
bentigter Speicher- und Cache-Bereiche ermglicht, mmap() haben wir bereits
im Code-Beispiel auf S. 67 kennengelernt, um eine Datei in den Adressraum
eines Prozesses zu mappen.
fsync() wird benutzt, um die Synchronisation zwischen Speicher und
Datentrger durchzufhren. Dies ist wichtig, weil aus Performance-Grnden
nderungen zunchst nur im Speicher durchgefhrt, nicht aber sofort auf den
Datentrger geschrieben werden. Wrde dann ein Absturz des Systems erfolgen,
wre anschlieend mit Inkonsistenzen des Datentrgers zu rechnen, fsync(),
das durch den gleichnamigen System Call aufgerufen wird, stellt fr diese Datei
Konsistenz auf dem Datentrger her.
readv() und writev() werden fr die gleichnamigen System Calls
bereitgestellt, damit Lesen bzw. Schreiben nicht-zusammenhngenden Speicher
benutzen kann. Damit lassen sich mehrere aufeinanderfolgende read()- bzw.
write()-Operationen zu einer Operation zusammenfassen, was sich auf die
Performance gnstig auswirkt. Diese Technik ist unter dem Namen ScatterGather bekannt.
Um Sperren bei konkurrierenden Zugriffen auf die Datei zu untersttzen,
wird lock() bereitgestellt.
Die Funktion checkunedia_change() steht nur fr Gertedateien zur
Verfgung. Sie berprft, ob seit dem letzten Zugriff das Medium18 gewechselt
wurde. Ist das der Fall, dann kann nicht mehr ohne weiteres auf das Medium
zugegriffen werden, weil die im System vorgehaltene Information nicht mehr mit
den auf dem Datentrger gespeicherten Daten bereinstimmt.

8.2.2

VFS-Datenstrukturen fr Partitionen

Wie kommt man berhaupt zu den Dateiinformationen? Dazu muss doch


zunchst auf die Partition einer Platte zugegriffen werden. Dieser Vorgang wird
als Mounten19 bezeichnet. VFS stellt zur Verwaltung der gemoun- teten
Filesysteme die Struktur vfsmount20 bereit, die ihrerseits eine Struktur
super_block21 enthlt, welche wichtige Informationen ber die Partition
enthlt. Beide Strukturen sind in den Listings 8.5 und 8.6 dargestellt.

Positionieren in einer Datei.

17

Dies knnen in der Regel CD, DVD oder Floppy sein.


Das Kommando, das diesen Vorgang auslst, heit mount.
20
Definiert in include/linux/mount.h.
21
Diese Struktur ist in include/linux/fs.h definiert.
18
19

152

8 Dateisysteme und Plattenverwaltung 8.2 Virtual File System (VFS) 153

stru
ct
int
int
int
atom
ic.
void

struct
struct super_block {
struct list_head
s_list; /* Keep this first */ dev_t
vfsmount
{
s_dev;
struct list_head mnt_hash; /* search index; _not_
*/
unsigned
long
s_blocksize; /* fs we are mounted on */ struct
kdev_t
struct vfsmount
*mnt_parent;
unsigned
long
s_old_blocksize;
dentry *mnt_mountpoint;
/* dentry of mountpoint */
struct dentry
unsigned
char *mnt_root;
s_blocksize_bits;
/* root of the mounted tree */
struct super_block
*mnt_sb;
misigned
char s_dirt;
unsigned /* pointer to superblock */
struct
list_head
mnt_mounts;
long
long
s_maxbytes;
/* struct
/* listMax
of file
children,
anchored here */
size */
struct list_head *s_type;
mnt_child; /* and going through their *
file_system_type
mnt_child
struct super_operations *s_op;
struct */
atomic_t mnt_count;
dquot_operations
*dq_op; struct
int mnt_flags;
quotactl_ops
*s_qcop;
char *mnt_devname;
/* Name of device e.g.
struct
export_operations
* /dev/dsk/hdal */
*s_export_op; unsigned long
struct list_head mnt_list;
s_flags;
semaphor
s_loc
}; unsigned
long
s_magic;
es_count; k;
struct dentry
*s_root;
rw_semaphore
s_umount
s_syncing;
Listing 8.5. Struktur vfsmount
s_need_sync_f
s;
.t s_active;
*s_security;

stru
ct
stru
ct
stru
ct
stru
ct

list_hea
/
Filesystemens_dir
(mnt_parent)
und gibt den Mountpunkt an (mntjnountpoint), zum
d
ty;
*
list_hea
s_io;
/
anderen enthlt es den* Verweis auf die Superblock-Struktur (mnt_sb) sowie den
d
hlist_he
s_ano
/
Gertenamenn;der eingebundenen
Partitionen (mnt_devname).
ad
*
list_hea
s_fil
Alle Superblcke
im Speicher sind ber s_list verkettet, auerdem haben sie
d
es;

Zum einen beschreibt vfsmount die Eltern-Kind-Beziehung zwischen den

einen Verweis auf den Superblock des Root-Verzeichnisses (s_root). Der


Superblock gibt die Blocklnge des verwendeten Filesystems (s_blocksize) an und
enthlt einen Verweis auf die Informationen ber das verwendete Filesystem
(s_type). Weiter finden wir den auf S. 147 erwhnten Listenkopf fr die
vernderten Inodes (s_dirty), die darauf warten, dass sie und ihre Dateiinhalte
auf die Platte zurckgeschrieben werden, s_files wird im Ab- schn. 8.2.4
behandelt. Die Aufgabe von s_instances ist es, eine Mglichkeit zu schaffen, alle
Superblcke gleicher Filesysteme in einer linearen Liste zusammenzufassen.
Die mit einem Superblock verbundenen Operationen sind in der Struktur
super_operations beschrieben, s_op zeigt auf diese Funktionen.
read_inode() liest Inode-Daten ein, dirty_inode() markiert die Inode als
verndert; delete_inode() lscht die Inode aus dem Speicher und von der Platte.
Dabei werden jedoch auf der Platte nur die Zeiger auf die Datenblcke gelscht,
die Datenblcke selbst werden erst spter berschrieben, knnen somit noch
gelesen werden.
put_inode() verringert den Zhler i_count der Inode. Die Inode und die
zugehrigen Daten knnen erst aus dem Speicher entfernt werden, wenn i_count
auf 0 gefallen ist, wenn also kein Prozess mehr darauf zugreift.
write_super() schreibt den Superblock auf die Partition zurck, statfs() liefert
Statusinformationen ber das Filesystem, sync_fs() synchronisiert die Daten im
Speicher mit der zu Grunde liegenden Plattenpartition. Die Funk-

154

8 Dateisysteme und Plattenverwaltung


struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *);
void (*read_inode) (struct inode *);
void (*dirty_inode) (struct inode *);
void (*write_inode) (struct inode *, int);
void (*put_inode) (struct inode *);
void (*drop_inode) (struct inode *);
void (*delete_inode) (struct inode *);
void (*put_super) (struct super_block *);
void (*write_super) (struct super_block *);
int (*sync_fs)(struct super_block *sb, int wait);
void (*write_super_lockfs) (struct super_block *);
void (*unlockfs) (struct super_block *);
int (*statfs) (struct super_block *, struct kstatfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*clear_inode) (struct inode *);
void (*umount_begin) (struct super_block *);
int (*show_options)(struct seq_file *, struct vfsmount *);

>;

Listing 8.7. Die Struktur super_operations


dirty inodes */ parked
for writeback
*/
tion show_options() dient dem proc-Filesystem,
Informationen
anzuzeigen, proc
anonymous
dentries
for
ist ein virtuelles Filesystem, das vom Kernel
erzeugt
und im Speicher
gehalten
(nfs)bestimmte
exporting
*/ des Systems zu
wird, um dem Benutzer Informationen ber
Teile
geben oder auch dem Systemadministrator zu ermglichen, KernelParameter im
struct block_device *s_bdev;
laufenden Betrieb zu ndern.
struct list_head s_instances
struct
quota_info Diskquota specific options */
8.2.3
nach einer Datei
s_dquot; /*Suche
int s_frozen;
wait_queue_head_t s_wait_mifrozen; char 22
Um auf
die Datei /home/albrecht/buch/datei/vfs.tex
zuzugreifen, mssen
s_id[32];
/* Informational name
folgende
Schritte
unternommen
werden:
*/
kobject
kobj;bei
/*derjenigen
anchor for
sysfs
struct
Die Suche
beginnt
Inode,
die */
dasvoid
Wurzelverzeichnis /
*s_fs_info;
/* Filesystem
private
info
beschreibt. Der Datenbereich
dieser Inode
besteht
aus*/
den Datei- und
*
The next field is for VFS *only*. No
/* Directory-Namen,
die in diesem Directory enthalten sind, zusammen
filesystems have
mit der Inode-Nummer, die auf die entsprechende Datei verweist.
*
any business even looking at it. You had been
Im ersten Schritt wird in diesem Datenbereich nach dem
struct semaphore
s_vfs_rename_sem; /* Kludge */
warned. */

>;

Unterverzeichnis home gesucht und die zugehrige Inode lokalisiert.


Da es sich wiederum um ein Directory handelt, wiederholt sich der
vorige Schritt bis zu der Inode, die das Directory datei im Directory
Listing 8.6. Die Struktur super_block
/home /albrecht /buch reprsentiert.
In dieser Inode wird die Inode zu vfs.tex gesucht, in deren Datenbereich
sich die gewnschten Daten befinden.

Das ist auf meinem Rechner genau dieses Kapitel.

22

8.2 Virtual File System (VFS) 155

Der Suchprozess kann abbrechen, wenn auf irgendeiner Stufe kein passender
Eintrag gefunden wird. Es erscheint dann die Fehlermeldung File not found.
Dieser Suchvorgang ist sehr aufwndig und zu langsam, um bei jedem
Zugriff ber den Namen einer Datei durchgefhrt zu werden, auch wenn alle
Inodes bereits im Speicher vorhanden sind. Deshalb wird, um diesen Vorgang
schneller zu machen, nach dem ersten vollstndigen Nachschlagen ein
besonderer Cache-Eintrag erzeugt, der das Nachschlagen (lookupO) erheblich
beschleunigt.
Die Cache-Eintrge basieren auf der Struktur dentry23, Listing 8.8 zeigt
den Aufbau.
struct dentry {
atomic_t d_count;
spinlock_t d_lock;
/* per dentry lock */
misigned long d_vfs_flags; /* moved here to be on same
*
cacheline */
struct inode * d_inode;
/* Where the name belongs to
struct list_head d_lru;
*
NULL is negative */
struct list_head d_child;
/* LRU list */
struct list_head d_subdirs;
/* child of parent list */
struct list_head d_alias;
/* our children */
/*
alias
list */ */
unsigned long d_time;
/* inode
used by
d_revalidate
struct dentry_operations *d_op;
struct super_block * d_sb; /* The root of the dentry tree */
unsigned int d_flags; int d_mounted;
void * d_fsdata;
/* fs-specific data */
struct rcu_head d_rcu;
struct dcookie_struct * d_cookie; /* cookie, if any */ unsigned
long d_move_count; /* to indicated moved dentry while
*
lockless lookup */
struct qstr * d_qstr;
/* quick str ptr used in lockless
*
lookup and concurrent d_move */ struct dentry * d_parent; /*
parent directory */
struct qstr d_name;
struct hlist_node d_hash; /* lookup hash list */ struct
hlist_head * d_bucket; /* lookup hash bucket */ unsigned char
d_iname[DNAME_INLINE_LEN_MIN]; /* small names */

Listing 8.8. Die Struktur dentry

23

dentry ist in include/linux/dcache.h vereinbart.

156

8 Dateisysteme und Plattenverwaltung

d_name enthlt den Namen der Datei bzw. des Unterverzeichnisses.24 Der
verwendete Typ qstr enthlt nicht nur die Zeichenkette, sondern auch die Lnge
der Zeichenkette und einen Hashwert. d_inode ist ein Zeiger auf die gewnschte
Inode, d_subdirs enthlt die Eintrge fr Unterverzeichnisse eines Directories,
zu dem der dentry-Eintrag gehrt. Somit wird die Baumstruktur des Filesystems
auch durch die dentry-Eintrge nachgebildet; es befinden sich jedoch immer nur
die am hufigsten gebrauchten Eintrge im Cache, weil sonst der Speicherbedarf
zu gro wre.
d_paxent zeigt auf das bergeordnete Verzeichnis25, djnounted zeigt an, ob es
sich bei diesem dentry-Eintrag um einen Mountpoint handelt; in diesem Falle ist
der Wert 1, sonst 0. d_alias verknpft dentry-Eintrge gleicher Dateien, d.h.
unterschiedlicher Dateinamen, die durch Link auf die gleiche Datei zeigen, s_sb
verweist auf den Superblock des Filesystems, zu dem dieser Eintrag gehrt.
d_op verweist auf die Struktur dentry_operations26, die diejenigen
Funktionen enthlt, die auf dentry-Eintrge angewendet werden knnen. Listing
8.9 zeigt diese Struktur; die Funktionen mssen vom jeweiligen Filesystem
implementiert werden.

struct dentry_operations {
int (*d_revalidate)(struct dentry *, struct nameidata *); int
(*d_hash) (struct dentry *, struct qstr *);
int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);
int (*d_delete)(struct dentry *);
void (*d_release)(struct dentry *);

};

void (*d_iput)(struct dentry *, struct inode *);

Listing 8.9. Die Struktur dentry_operations

Hier sollen nur drei dieser Funktionen erwhnt werden:


dJiash() berechnet die Hashwerte, die beim Einfgen in die dentry-Hashtabelle verwendet werden.
d_compare ( ) dient zum Vergleich der Dateinamen von zwei dentryEintrgen. Dies ist nicht immer ein Vergleich zweier Zeichenketten, wie es
im VFS selbst der Fall ist: so muss beim FAT-Filesystem (und hnlichen
FileOrganisationen) darauf geachtet werden, dass keine Unterscheidung
zwischen Gro- und Kleinschreibung stattfindet.
d_revalidate() ist fr Filesysteme wichtig, die ber das Netzwerk
bereitgestellt werden, z.B. NFS. Diese Funktion dient zur berprfung, ob
die von
Dabei geht es nur um den Namen, nicht um den gesamten Pfad!

24

Beim Wurzeldirectory / zeigt es auf sich selbst.


In include/linux/dcache.h definiert.

25
26

8.2 Virtual File System (VFS) 157

den dentry-Eintrgen beschriebene Struktur noch mit der Situation des


Filesystems bereinstimmt, denn es wre mglich, dass in der Zwischenzeit
durch nderungen von einem anderen Rechner aus das Filesystem
verndert wurde.
Jeder dentry-Eintrag wird in einer Hashtabelle abgelegt, die ber die globale
Variable dentry_hashtable27 erreicht wird. Wird eine Anfrage an das Filesystem
gestellt, so gelangt sie ber VFS an die zugrunde liegende Implementierung und
es wird ein neuer dentry-Eintrag in der Hashtabelle erzeugt - es sei denn, es
existiert zu dieser Anfrage bereits ein Eintrag.
Soll nun eine Inode an Hand des Namens ermittelt werden, so wird dazu die
Funktion path_lookup()28 eingesetzt. Diese Funktion bekommt als Argumente
den gewnschten Namen, Flags, die die Suche steuern, und einen Zeiger auf eine
Struktur nameidata29 bermittelt. Die Struktur ist in Listing
8.10
dargestellt.

struct nameidata {
struct dentry *dentry;
struct vfsmount *mnt;
struct qstr
last ;
unsigned int
flags;
int
last_type;
struct lookup_intent intent;

>;

Listing 8.10. Die Struktur nameidata

path_lookup() bergibt die Flags an die Struktur nameidata und berprft, ob es


sich um einen absoluten oder einen relativen Pfadnamen handelt. Ein absoluter
Pfadname beginnt mit dem Zeichen /, die Suche muss dann im Wurzelverzeichnis
beginnen; andernfalls handelt es sich um einen relativen Pfadnamen, und die
Suche beginnt im Current Directory30. Entsprechend werden die Eintrge mnt
und dentry in der Struktur belegt, dentry zeigt dabei auf den jeweiligen dentryEintrag, mnt auf den Typ des Directories. Danach bernimmt die weitere Suche
die Funktion link_path_walk(), die bei der Rckgabe der Funktion path_lookup()
aufgerufen wird.
link_path_walk()31 bekommt als Argumente den Namen und einen Zeiger auf
die bereits vorbelegte Struktur nd vom Typ nameidata. In einer Schleife werden
als erstes der nchste Namensbestandteil ermittelt, an Hand des letzten
Directories die Zugriffsberechtigung berprft und der Hashwert des
dentry_hashtable ist in fs/dcache.c vereinbart.
Die Funktion ist in fs/namei.c definiert.
29
Die Struktur nameidata wird in include/linux/namei.h definiert.
30
In fs/namei.c mit pwd (Print Working Directory) bezeichnet.
31
link_path_walk() ist in fs/namei.c definiert.
27
28

158

8 Dateisysteme und Plattenverwaltung

Teilnamens berechnet. Dieser wird zusammen mit dem Namen in der


Datenstruktur this vom Typ qstr eingetragen. Sodann werden die Flle
behandelt, bei denen der nchste Namensbestandteil . oder .. ist, ein Verweis auf
das eigene oder das Eltern-Directory. Das eigentliche Nachschlagen des nchsten
Abschnitts erfolgt in der Funktion do_lookup()32, sofern der dentry-Eintrag nicht
existiert. Sonst wird ggf. noch ein Revalidate aufgerufen. Vor dem erneuten
Schleifendurchlauf wird geprft, ob vielleicht ein Filesystem eingehngt ist
(Funktion follow_mountO), und danach, ob es sich um einen symbolischen Link
handelt (Funktion do_follow_link()).
8.2.4

Filedeskriptoren: Geffnete Dateien

Beim Offnen einer Datei wird in einem Programm ein Filedeskriptor erzeugt, mit
dessen Hilfe im weiteren Verlauf auf die geffnete Datei zugegriffen werden
kann. Der Filedeskriptor ist eine ganze Zahl zwischen 0 und einem vorgegebenen
Maximum. Bei einem Zugriff muss der Kernel in der Lage sein, aus dieser
ganzen Zahl auf die Datei zu schlieen. Diese Verbindung muss ber die
Information erfolgen, die zu demjeweiligen Prozess gespeichert ist; es kann also
nicht wundern, dass im PCB entsprechende Eintrge zu finden sind (vgl. Listing
3.3). Dort sind die Zeilen33 enthalten, die in Listing 8.11 zu sehen sind.
struct fs_struct *fs;
/* Filesystem */
struct files_struct *files; /* geffnete Dateien */
struct namespace *namespace; /* Namespace */
Listing 8.11. Ausschnitt aus dem PCB

fs enthlt Informationen ber wichtige Verzeichnisse, wie das Wurzelverzeichnis


und das Current Directroy (Working Directory), files stellt die Verbindung
zwischen Filedeskriptoren und geffneten Dateien her.
Die Struktur fs_struct34 ist in Listing 8.12 dargestellt. Die aufeinander
bezogenen Eintrge root - rootmnt, ..., zeigen auf den dentry- und den
Filesystem-Eintrag fr das Wurzelverzeichnis, das Current Directory und die
Personality (altroot)35.
umask reprsentiert die Standardmaske, die beim Erzeugen einer neuen
Datei verwendet wird, um die Rechte festzulegen. Das Kommando umask sowie
der gleichlautende System Call dienen dazu, diesen Wert auszulesen bzw. zu
setzen.
32

Ebenfalls in fs/namei.c definiert.

Auf namespace soll nicht eingegangen werden.


Die Struktur fs_struct ist in include/linux/fs_struct.h vereinbart.
35
Diese wird bentigt, wenn fr Binrprogramme eine Emulationsumgebung geschaffen
werden soll, die eine andere Umgebung als Linux vorspiegelt.
33
34

8.2 Virtual File System (VFS) 159


struct fs_struct
{ atomic_t
count;
rwlock_t lock;
int umask;
struct dentry * root, * pwd, * altroot;
struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;

>;

Listing 8.12. Die Struktur fs_struct

Zur Verwaltung der geffneten Dateien dient die Struktur files_struct36. Im


Listing 8.13 fllt neben den Eintrgen zur Verwaltung count und file_lock
das Array fd_array vom Type file auf. NR_OPEN_DEFAULT deutet daraufhin,
dass in diesem Array die geffneten Dateien verwaltet werden, deren Anzahl
nach oben beschrnkt ist. Der Filedeskriptor ist somit ein Index, der auf einen
Eintrag dieses Arrays zeigt. Deshalb muss als nchstes die Struktur file
untersucht werden.
Doch zuvor sollten noch zwei weitere Eintrge betrachtet werden: open_fds
und close_on_exec. Beides sind Bitfelder, wobei das n-te Bit mit dem entsprechenden n-ten Filedeskriptor korrespondiert, open_fds zeigt durch gesetzte
Bits an, welche Filedeskriptoren tatschlich benutzt werden, close_on_exec
beschreibt, welche Dateien bei einem exec()-System Call geschlossen werden
sollen (vgl. Abschn. 3.2.3).
struct files_struct {
atomic_t count;
spinlock_t file_lock;

};

/* Protects all the below


members. * Nests inside tsk>alloc_lock */

int max_fds;
int max_fdset;
int next_fd;
struct file ** fd;
/* current fd array */
fd_set *close_on_exec
fd_set *open_fds;
fd_set close_on_exec_init;
fd_set open_fds_init;
struct file * fd_array[NR_OPEN_DEFAULT];

Listing 8.13. Die Struktur files_struct

Listing 8.14 zeigt die wichtigsten Eintrge der Struktur file37: f_list dient
dazu, die geffneten Dateien einer Partition in einer Liste zu verwalten; der
Die Struktur ist in include/linux/file.h definiert.
Die Struktur file ist in include/linux/fs.h definiert.

36
37

160

8 Dateisysteme und Plattenverwaltung

zugehrige Listenkopf s_list ist im Superblock verankert (vgl. Listing 8.6).


f_dentry zeigt auf den zugehrigen dentry-Eintrag, f_mode enthlt den Modus,
der beim Offnen der Datei bergeben wurde, im Wesentlichen also lesenden,
schreibenden oder lesenden und schreibenden Zugriff. Zur Verwaltung der
Zugriffsrechte dienen die Eintrge f_uid und f-gid, die die Benutzerund
Gruppenkennung des Benutzers enthalten, f_pos enthlt die Position des
Lese-/Schreibzeiger; festgehalten wird in diesem Eintrag der Offset vom Anfang
der Datei in Bytes.
8.2.5

Die Verbindung zum Speicher

Sowohl in file (Listing 8.14) als auch in inode (Listing 8.1) taucht eine Struktur
auf, die wir bereits bei der Speicherverwaltung kennengelernt haben: die
Struktur address_space (vgl. Listing 5.11). Diese Verbindung ermglicht es, die
Speicherbereiche zu bestimmen, in denen die Datei gemappt ist.
Umgekehrt hat die Struktur address_space den Eintrag host, der sie mit der
zugrunde liegenden inode-Struktur verbindet.
struct file {
struct list_head f_list;
struct dentry
*f_dentry;
struct vfsmount *f_vfsmnt;
struct file_operations *f_op;
atomic_t
f_count;
unsigned int
f_flags;
mode_t
f_mode;
int
f_error;
loff_t
f_pos;
struct fown_struct f_owner;
unsigned int
f_uid, f_gid;
struct file_ra_state f_ra;
unsigned long
f_version;
void

/* ... */

*f_security;

struct address_space
Listing 8.14. Auszug aus der Struktur file

f_op zeigt auf die mglichen Dateioperationen, vgl. Listing 8.4.

8.2 Virtual File System (VFS) 161

8.2.6 Filesysteme
Eine weitere wichtige Information wird unter dem Zeiger file_systems38
zusammengefasst: zwar enthlt jeder Superblock im Eintrag s_type die
komplette Information ber das verwendete Filesystem, zustzlich werden aber
alle registrierten Filesysteme in einer verketteten Liste, von file_systems
ausgehend, verwaltet. Ein Filesystem muss registriert werden, damit es
verwendet werden kann; dies kann dadurch geschehen, dass der zugehrige Code
fr das Filesystem fest in den Kernel eingebunden wird, aber auch dadurch, dass
im laufenden Betrieb der entsprechende Modul geladen wird. Beim Initialisieren
des Moduls wird das Filesystem dann registriert. Wenn durch die Verwendung
von Modulen Filesysteme registriert werden mssen, so ist auch der umgekehrte
Vorgang mglich, wenn ein entsprechender Modul entladen wird: das Filesystem
wird ebenfalls entladen und steht nicht mehr zur Verfgung.
Zur Verwaltung der Filesysteme wird file_system_type39 verwendet, es
handelt sich um eine verkettete Liste (vgl. Listing 8.15).
struct file_system_type
{ const char *name;
int fs_flags;
struct super_block *(*get_sb) (struct file_system_type *,
int, const char *, void *); void (*kill_sb) (struct
super_block *); struct module *owner; struct
file_system_type * next; struct list_head fs_supers;

};

Listing 8.15. Die Struktur file_system_type

Diese Struktur enthlt nicht nur in next einen Verweis auf das nchste
Filesystem, es enthlt auch in fs_supers den Listenkopf fr die verkettete Liste
aller Superblocks, die dieses Filesystem benutzen.
Wie wir gesehen haben, stellt das VFS eine Vielzahl von untereinander
vernetzten Objekten bereit, um auf abstrakter Ebene mit Directories und
Dateien als auch mit dem Speicher, in den die Dateien abgebildet werden,
umgehen zu knnen. Eine bersicht ber die Zusammenhnge der VFS-Objekte
gibt die Abb. 8.5.

Dies ist in fs/filesystems.c


39
definiert.
Definiert in
include/linux/fs.h.
38

162

8 Dateisysteme und Plattenverwaltung

vfsmount

Abb. 8.5. bersicht ber die VFS-Objekte

8.3

System Calls

Fr den Umgang mit Dateien stehen eine Vielzahl von System Calls bereit. Hier
sollen an Hand von Beispielen die wichtigsten Aufrufe der
Programmierschnittstelle fr Dateien betrachtet werden. Wie schon an anderen
Stellen in diesem Buch gehandhabt wird zugunsten der bersichtlichkeit auf
eine Behandlung von ggf. auftretenden Fehlern verzichtet.
Sequentielles und direktes Lesen einschlielich Positionieren, ffnen und
Schlieen von Dateien zeigt das Programm in Listing 8.16: Es ffnet mittels
open() die Datei lesen.c, die sich im aktuellen Directory befinden muss. Das
ffnen erfolgt ausschlielich zum Lesen. Wenn der Rckgabewert fh positiv ist,
so kann fh als File-Handle, d.h. als Index in dem Array fd_array, benutzt werden
(vgl. S. 159).
Das sequentielle Lesen wird dann in der Schleife mit read() ausgefhrt: es
wird also mit Hilfe des File-Handles auf die geffnete Datei zugegriffen und die
nchsten Bytes werden in den Puffer buff bertragen. Die Anzahl

8.3 System Calls 163


#include <fcntl.h>
#include
<unistd.h> #define
LAENGE 20
void lesen_direct(int fh, int pos, int von_wo)
{ char buff[LAENGE];
int anz, wo;
wo = lseek(fh, pos,
von_wo); if (pos == 0) {
printf("\nPosition ist: %d\n", wo);
else {
anz = read(fh, buff, LAENGE);
write(l, "\n", 1) ; anz) ;
write(l, buff, , 2) ;
write(l,
"\n\n"

main (int argc, char *argv[]) {


int anz, fh; char
buff[LAENGE]; fh =
open("lesen.c",0_RD0NLY); if
(fh > 0) {
while ( (anz = read(fh, buff, LAENGE)) >
0 ) write(l, buff, anz); lesen_direct(fh,
0, SEEK_END); lesen_direct(fh, 150,
SEEK_SET); close(fh);

>

exit(0);

Listing 8.16. Beispielprogramm fr Dateizugriffe

der bertragenen Bytes ist durch LAENGE gegeben. Der Inhalt des Puffers wird
mit write() in das File mit dem File-Handle 1 geschrieben. Dabei machen wir
uns zunutze, dass jeder Prozess automatisch ber drei geffnete Files verfgt: die
Standardeingabe (stdin) unter dem File-Handle 0, die Standardausgabe
(stdout) unter 1 und Error (stderr) unter dem File-Handle 2. blicherweise
ist der File-Handle 1 mit dem Terminal verbunden.40
Nachdem in der Schleife der Programmtext gelesen und angezeigt worden ist,
wird mit zwei verschiedenen Argumenten die Funktion lesen_direct
aufgerufen. Diese Funktion positioniert mit Hilfe von lseek() den
Lese-/Schreib- zeiger der durch das File-Handle fh geffneten Datei. Die
Positionierung hngt
Im obigen Programm wurde bewusst das File-Handle 1 anstelle der vordefinierten
Konstanten stdout verwendet
40

164

8 Dateisysteme und Plattenverwaltung

zugleich vom zweiten und dritten Argument von lseek() ab. Das dritte Argument
beschreibt, von welcher Stelle aus die Positionierung erfolgen soll41, whrend das
zweite angibt, um wieviel Byte42 die Position verschoben werden soll, lseek() gibt
nach erfolgreicher Durchfhrung die Position des Lese- /Schreibzeigers gemessen
vom Anfang der Datei zurck. Der erste Aufruf von lesen_direct() gibt somit die
Lnge der Datei aus, der zweite Aufruf zeigt einen Ausschnitt der Datei.
Die geffnete Datei wird am Ende mit close() geschlossen.
Frage: Ist die hier gewhlte Puffergre von 20 blicherweise ein guter Wert?

Listing 8.17 zeigt das Anlegen einer Datei sowie die Umlenkung der StandardAusgabe ohne allzu groe nderung des vorhergehenden Quelltextes. Wir wollen
jedoch keine Ausgabe auf dem Bildschirm, sondern in der geffneten Datei
erhalten:
#include <fcntl.h> #include <unistd.h> #define LAENGE
20

main (int argc, char *argv[]) { int anz, fh, fh2; char
buff[LAENGE]; fh = open("lesen.c",0_RD0NLY); fh2 =
creat("daten", 0666); close(l); dup(fh2); if (fh >
0) {
while ( (anz = read(fh, buff, LAENGE)) >
0) write(l, buff, anz); close(fh);
close(l);

>

exit(0);

Listing 8.17. Dateizugriffe, Umlenkung der Standard-Ausgabe mittels dup()

Zum Anlegen und zugleich ffnen wird hier der Aufruf creat() verwendet.43
Danach folgen zwei Programmzeilen, die so zunchst nicht erwartet werden: das
Schlieen der geffneten Datei mit dem Filedeskriptor 1 sowie der dupO- Aufruf.
Der dup()-Aufruf dupliziert den angegebenen Filedeskriptor - hier
41
SEEK_SET = Anfang der Datei, SEEK_END = Ende der Datei, SEEK_CUR = aktuelle
Position des Lese/Schreibzeigers
42
Negativ: Positionierung in der Datei nach links; positiv: nach rechts.
43
Mit entsprechenden Argumenten kann auch open() selbst dazu benutzt werden.

8.3 System Calls 165

fh2, also die neu erzeugte Datei, in die geschrieben werden soll - auf den
kleinsten zur Verfgung stehenden Filedeskriptor. Da wir gerade die Datei zum
Filedeskriptor 1 geschlossen haben, steht dieser zur Verfgung. Die
nachfolgenden write()-Aufrufe, die auf den Filedeskriptor 1 zugreifen, schreiben
somit in die gerade geffnete Datei daten.
Im Programm in Listing 8.18 wird zunchst mit mkdir()44 ein neues
Unterdirectory angelegt, sodann mit chdir() in das neu erzeugte Directory
gewechselt. Damit wird das neue Directory zum Current Directory. Mit dem
nachfolgenden link()-Aufruf wird im Current Directory ein harter Link auf die
mit dem vorigen Programm erzeugte Datei daten im Elternverzeichnis gesetzt
und als nchstes diese Datei mit unlink() gelscht. Wie man sich leicht
berzeugen kann, stimmt der Inhalt des Links x genau mit dem Inhalt der
gelschten Datei daten berein.
#include <fcntl.h>
#include <unistd.h>
main (int argc, char *argv[]) { int rc;
mkdir("X",0777);
chdir("X");
link("../daten","x")
;
unlink("../daten");
exit(0);

Listing 8.18. Ein weiteres Beispiel fr Dateizugriffe, insbesondere link()


Listing 8.19 zeigt die Verwendung von umask(), um beim Anlegen von Dateien
Rechte zu beschrnken. Durch den Aufruf von umask() wird in der Struktur
fs_struct (vgl. S. 159) der gleichnamige Eintrag gesetzt und der vorher
eingestellte Wert zurckgegeben. Der Eintrag umask wird folgendermaen
benutzt: wird eine Datei mit creat() angelegt, so werden die Rechte ermittelt
durch
0666 & ~umask

Der Aufruf von stat(), der Informationen ber die angegebene Datei ermittelt45,
zeigt, dass die Rechte tatschlich auf 0644 gesetzt sind, so dass nur der
Eigentmer der Datei lesend und schreibend zugreifen kann, whrend alle
anderen nur lesen drfen. Durch chmod werden die Rechte anschlieend explizit
auf 0666 gesetzt. Durch den Aufruf der Funktion chown() lsst sich auch der
Eigentmer einer Datei bzw. die Gruppe ndern.
Ein Directory wird mit dem Aufruf mkdir angelegt, ein leeres Directory mit rmdir()
gelscht.
45
fstat() gibt dieselbe Information ber eine geffnete Datei zurck.
44

166

8 Dateisysteme und Plattenverwaltung


#include <fcntl.h>
#include <unistd.h>
#include
<sys/types.h>
#include <sys/stat.h>
main (int argc, char *argv[])
{ struct stat status; int
rc;
mode_t oldmask;
oldmask = umask(0022);
creat("test",0666);
stat("test", &status);
printf ("Status '/,o\n", status. st_mode) ;
chmod("test",0666);
exit(0);

Listing 8.19. Beispiel fr die Verwendung von umask()

Die Punktion fcntl() liest und verndert Informationen ber geffnete Dateien.
Ein kleines Beispiel zeigt der Programmausschnitt in Listing 8.20. Auch Sperren
lassen sich mit fcntl() setzen.
#include <fcntl.h>
#include <unistd.h>
int fd, rc;
if ( (fd = open("test",0_RD0NLY)) >
0) rc = fcntl(fd, F_SETFD); rc =
execl("./link", "");

Listing 8.20. Beispiel zur Verwendung von fcntl()

Im Beispiel nicht gezeigt werden System Calls wie mount(), umount() und sync
( ), die nur mit der Berechtigung des System Administrators ausgefhrt werden
knnen, mount() bindet eine Partition46 mit ihrem Filesystem in den
Verzeichnisbaum an einer vorgegebenen Stelle ein, umount() entfernt die
Partition wieder aus dem Verzeichnisbaum.
sync() synchronisiert den im Speicher gecachten Zustand des Filesystems mit
den Platten, indem die Inodes und die brigen Datenblcke auf die Platte
geschrieben werden.

46

Allgemeiner: ein Gert.

8.4 Beispiel: read() 167

8.4

Beispiel: read()

Hier wollen wir am Beispiel eines read()-Aufrufes einmal genauer verfolgen, was
bei diesem Aufruf passiert. Warum eignet sich gerade read() besonders zum
Hinschauen? Schon kurzes Nachdenken zeigt uns, dass hier eine Reihe von
unterschiedlichen Aspekten miteinander verknpft werden mssen:

Es muss eine Brcke geschlagen werden zwischen der geffneten Datei,


die ein Prozess benutzt, und einer im System bekannten Datei,
der zu lesende logische Block der Datei muss ermittelt werden,
die logische Blocknummer muss in die physische Blocknummer
umgewandelt werden47, unter der der Abschnitt auf dem
Speichermedium abgelegt ist,
auf alle Flle sollte aus Performance-Grnden nachgeschaut werden, ob
der Block bereits im Speicher vorhanden ist, denn dann msste er nicht
noch einmal gelesen werden,
falls der Block im Speicher ist, muss er nicht mehr aktuell sein und
muss dann doch erneut gelesen werden (denken wir an einen ber nfs
gelesenen Block, der inzwischen auf dem Server verndert wurde),
ggf. muss Speicherplatz bereitgestellt werden, um den Inhalt des Blockes
aufzunehmen,
der Auftrag muss in eine Work Queue gestellt werden, die von einem
Gerte-Treiber bearbeitet wird. Dabei mssen die konkreten Methoden
des verwendeten Filesystems benutzt werden,
die Auftrge sind ggf. noch so anzuordnen, dass auch physische Zugriffe
des Plattenarms optimiert werden (vgl. Abschn. 8.5).

Die Anwenderfunktion read() wird durch die glibc-Bibliothek in den System Call
sys_read() (vgl. Abschn. 2.1) umgesetzt.
Der Programmausschnitt in Listing 8.21 auf S. 168 zeigt die
Implementierung des System Calls sys_read()48. Zu Beginn fllt sofort in der
Funktionsdefinition das Schlsselwort asmlinkage auf. Hiermit wird dem
Compiler signalisiert, dass die Argumente auf eine Weise bergeben werden, die
typisch fr Assembler-Programme ist. Die nchste Aufflligkeit ist die nach dem
Funktionsblock folgende Zeile EXPORT . . .. Hiermit wird der Einsprungspunkt
sys_read auch auerhalb des kompilierten Codes bekannt gegeben.
Sodann fallen die zwei Zeilen FSH00K. . . und TRIG_EVENT. . . ins Auge.
Definiert sind sie in den Header-Dateien include/linux/fshooks.h bzw.
include/linux/trigeventJiooks.h und greifen auf fs/fshooks.c zu. An dieser Stelle
wollen wir nicht ins Detail gehen, sondern nur folgende berlegung festhalten:
da die Untersttzung fr ein Filesystem nicht im Kernel fest einkompiliert sein
muss, sondern bei Bedarf als Modul dazugeladen werden kann, mssen Hooks
(software-mige Haken) bereitgestellt werden, um

47

Darauf wird erst in Abschn. 8.6 (ext2-Filesystem) eingegangen.

48

sys_readO ist in fs/read_write.c zu finden.

168

8 Dateisysteme und Plattenverwaltung


asmlinkage ssize_t sys_read(unsigned int fd, char _user * buf,
size_t count)

struct file *file; ssize_t ret; int


fput_needed;
FSHOOK_BEGIN(read, ret, .fd = fd, .buffer = buf,
.length = count, .plen = &ret)
ret = -EBADF;
file = fget_light(fd, &fput_needed); if (file)
{
TRIG_EVENT(read_hook, fd, count);
ret = vfs_read(file, buf, count, &file->f_pos);
fput_light(file, fput_needed);

>
>

FSH00K_END(read, ret >= 0 ? 0 : ret) return


ret;

EXPORT_SYMBOL_GPL(sys_read);

Listing 8.21. Implementierung des System Calls sys_read()

die Kommunikation zwischen Kernel und Modul zu regeln. Der Vorteil, nicht
jedes Filesystem fest im Kernel zu kompilieren, liegt darin, einerseits einen
schlankeren Kernel zu haben, andererseits aber dennoch flexibel in der Lage zu
sein, auf Datentrger mit anderen Filesystemen zugreifen zu knnen. Dies kann
insbesondere fr Wechselplatten von Interesse sein.
Die Funktion stellt zunchst einen Zeiger auf eine Struktur vom Typ file
bereit. Prozesse kennen Filedeskriptoren, jedoch keine Files. Deshalb muss mit
dem im Argument bergebenen Filedeskriptor fd eine Verbindung zum
verwendeten File hergestellt werden. Hierfr ist die Funktion fget_light()
zustndig. Vor dem Aufruf von fget_light() wird der Rckgabewert ret mit der
Fehlerkonstanten EBADF - also Bad File Number - initialisiert. Nach dem
Aufruf folgt eine berprfung, ob tatschlich ein aktives File gefunden wurde; in
diesem Falle wird die eigentliche Aufgabe des Lesens an die Funktion vfs_read()
weitergereicht, die anstelle des Filedeskriptors ein File als Argument bergeben
bekommt. Die Argumente buf und count wurden bereits dem Aufruf sys_read()
bergeben und werden einfach weitergereicht. Es kommt jedoch ein neues
Argument hinzu, nmlich &file->f_pos. Dies ist aber nach den Ausfhrungen in
Abschn. 8.2.4 gerade die aktuelle Position des Lese-/Schreibzeigers fr die
betrachtete Datei.
Listing 8.22 zeigt den Aufbau der Funktion fget_light()49. Das Schlsselwort
fastcall50 im Funktionskopf wird durch den Prcompiler umgesetzt in
fget_light() ist in fs/file_table.c enthalten.

49

fastcall ist implementiert in include/asm386/linkage.h.


50

8.4 Beispiel: read() 169

__attribute__((regparm(3)) und bedeutet, dass der Compiler spezielle Register


zur Argumentbergabe benutzt, current ist ebenfalls ein Makro, das auf den
PCB des aktuellen Prozesses verweist. Deshalb zeigt files auf die entsprechende
Struktur des aktuellen Prozesses (vgl. Listing 8.13). Greift nur ein Prozess auf
diese Struktur zu, so wird der File-Eintrag mit fcheck() ermittelt - nur diesen
Fall wollen wir hier betrachten.
Frage: Knnen Sie Umstnde angeben, unter denen die Alternative else . . . in
der Funktion fget_light durchlaufen wird?

struct file fastcall *fget_light(unsigned int fd, int


*fput_needed) {
struct file *file;
struct files_struct *files = current->files;
*fput_needed = 0;
if (likely((atomic_read(&files->count) == 1)))
{ file = fcheck(fd);
} else {
spin_lock(&files->file_lock);
file = fcheck(fd); if (file) {
get_file(file);
*fput_needed = 1;

>

spin_unlock(&files->file_lock);

return file;

Listing 8.22. Die Funktion fget_light()

fcheck()51 besteht aus zwei Bestandteilen: dem Makro fcheck() selbst und der als
inline gekennzeichneten Funktion fcheck_files() (vgl. Listing 8.23). Das Makro
fcheck() bewirkt, dass der Prcompiler anstelle von fcheck() den Funktionsaufruf
fcheck_files() eintrgt. Auf Grund von inline wird beim Kompilieren der
Anweisungsteil der Funktion eingetragen und bersetzt. Die return-Anweisung
bewirkt, dass der ursprngliche Quelltext fcheck(fd) durch das Ergebnis der
Funktion ersetzt wird. Im Anweisungsteil wird zunchst ein Zeiger auf eine
Struktur vom Typ file vereinbart und diesem NULL zugewiesen. Sodann wird
berprft, ob der Filedeskriptor im zulssigen Bereich liegt. Ist dies der Fall, so
wird einfach der Array-Eintrag files->fd[fd] zugewiesen (vgl. dazu die Struktur
files_struct in Listing 8.13).

In include/linux/file.h definiert.

51

170

8 Dateisysteme und Plattenverwaltung


static inline struct file * fcheck_files(struct files_struct *files,
unsigned int fd)

}
/*

struct file * file = NULL; if (fd <


files->max_fds) file = files>fd[fd]; return file;

* Check whether the specified fd has an open file.

*/

#define fcheck(fd) fcheck_files(current->files, fd)


Listing 8.23. Der Aufruf fcheck()

Als nchstes muss jetzt die Funktion vfs_read()52 (Listing 8.24) betrachtet
werden. Als Argumente werden ein Zeiger auf die direkt zuvor mittels
fget_light() ermittelte Struktur file, das Array buf, count (die Lnge des Arrays
buf) und ein Zeiger auf pos bergeben, buf und count werden dabei unverndert
von der Funktion sys_read() weitergereicht. Listing 8.14 zeigt, dass pos ein Teil
der Struktur file ist und somit nach der Bestimmung von file zur Verfgung
steht. Bislang ist jedoch noch nicht der wesentliche Inode-Eintrag fr die
gewnschte Datei gefunden. Die Abb. zeigt, dass gleich die erste Zeile im
Anweisungsteil der Funktion vfs_read() ber den dentry- Eintrag auf die
gesuchte Inode zugreift.53
Bevor auf die fr das File eingetragene Funktion zum Lesen zugegriffen
werden darf, mssen erst einige Tests absolviert werden. Die Frage ist, ob der
eingetragene Modus das Lesen erlaubt und ob berhaupt Operationen fr das
Lesen dieses Files eingetragen sind. In Fehlerfllen endet die Funktion sofort
mit return, wobei eine entsprechende Konstante zurckgegeben wird, die
Aufschluss ber den Fehler liefert.
Die nchsten beiden berprfungen betreffen Sperren und besondere
Sicherheitsberprfungen, die hier jedoch nur kurz erwhnt werden sollen. Es
handelt sich um:

locks_verify_area()54: Der Aufruf berprft und setzt die bentigten


Sperren.
security_file_permission()55: Diese Funktion gehrt zum SELinuxProjekt, das es ermglichen soll, Linux sicherer zu machen und globale
Rechte wie z.B. root-Rechte zu vermeiden.

Definiert in fs/read_write.c.
Hier kann man - wie an vielen anderen Stellen auch - die Ntzlichkeit der
Strukturen erkennen: ein Durchlaufen der Pointerkette ber ggf. mehrere Strukturen
hinweg bringt das gewnschte Ergebnis.
54
Die Funktion ist in include/linux/fs.h definiert.
55
Dies ist in include/linux/security.h enthalten.
52
53

8.4 Beispiel: read() 171


ssize_t vfs_read(struct file *file, char _ user *buf,
size_t count, loff_t *pos)

struct inode *inode = file->f_dentry->d_inode;


ssize_t ret;
if (!(file->f_mode & FMODE_READ)) return -EBADF;
if (!file->f_op || (!file->f_op->read&& !file->f_op->aio_read))
return -EINVAL;
ret = locks_verify_area(FLOCK_VERIFY_READ, inode, file,
*pos, count);
if (! ret) {
ret = security_file_permission (file,
MAY_READ); if (!ret) {
if (file->f_op->read)
ret = file->f_op->read(file, buf, count,
pos); else
ret = do_sync_read(file, buf, count, pos);
if (ret > 0)
dnotify_parent(file->f_dentry, DN_ACCESS);

>

>

return ret;

EXPORT_SYMBOL(vfs_read);

Listing 8.24. Die Funktion vfs_read()

Sind nun alle Schritte erfolgreich verlaufen, so wird die Funktion read()
aufgerufen, die unter file->f_op56 fr diese Datei vorgesehen ist. Falls diese
Funktion jedoch nicht bereitgestellt wurde, so wird stattdessen die weitere
Bearbeitung durch do_sync_read() ausgefhrt. Die Argumente der Funktion
vfs_read() werden dabei einfach weitergereicht. Wir wollen die Darstellung im
Weiteren nur auf den ersten Fall beschrnken und gehen davon aus, dass die
Datei sich in einer Partition mit dem Filesystem ext2 befindet.
In fs/ext2/file.c wird die Struktur ext2_file_operations initialisiert
und damit werden die File-Operationen definiert, die im Filesystem ext2
durchgefhrt werden knnen. Dabei wird generic_file_read() der Funktion
read() zugeordnet. Beim Erzeugen einer Inode fr dieses Filesystem wird diese
Struktur dem Eintrag i_fop zugewiesen (vgl. Listing 8.1); wird eine Datei
geffnet, so wird dieser Eintrag der zugrunde liegenden

Auch dies ist wieder ein gutes Beispiel fr die Verwendung von Strukturen und
Pointern.
56

172

8 Dateisysteme und Plattenverwaltung

Inode den Datei-Operationen zugewiesen. Letztendlich wird also die Funktion


generic_file_read() aufgerufen.57
Problem: Plattenzugriffe sind - verglichen mit anderen Vorgngen im Rechner sehr langsam. Lsst sich aus der Tatsache, dass Blcke einer Datei hufig nahe
benachbart auf einer Platte gespeichert werden, ein gutes Verfahren fr I/OVorgnge ableiten, wenn sequentiell zugegriffen werden soll?
Offensichtlich ist es in solchen Situationen sinnvoll, Blcke vorab zu lesen und im
Speicher vorzuhalten. Statt vieler einzelner Leseoperationen, die auf der Platte
ausgefhrt werden mssen, kann das Problem dann umgewandelt werden in
wenige zusammenhngende Leseoperationen auf der Platte sowie
Speicherzugriffe, die die gelesenen Blcke vorfinden. Voraussetzung dafr ist
aber, dass beim Speichern einer Datei benachbarte Blcke (weitgehend) auch
physisch zusammenhngend gespeichert werden.
Problem: Was ndert sich, wenn stattdessen im Wesentlichen direkt auf eine
Datei einer Platte zugegriffen wird (Beispiel: Zugriffe auf Daten eines
Datenbankservers)?
In diesem Fall wre ein Vorab-Lesen von nachfolgenden Blcken eine
Verschwendung von Speicher, da beim folgenden Lesezugriff vermutlich von einer
ganz anderen Position der Datei aus gelesen werden soll.
Die erste berlegung zeigt, dass Platten-I/O offensichtlich mit der
Speicherverwaltung zusammenhngt. Die zweite macht deutlich, dass das
Betriebssystem in der Lage sein muss, auf besondere Flle Rcksicht zu nehmen.
Auf Grund des Zusammenhangs von Platten-I/O und Speicherverwaltung ist
es nicht verwunderlich, die Funktion generic_file_read() in der Datei mm/filemap.c zu
finden: jeder physische Lesevorgang auf dem externen Speichermedium bewirkt
das Fllen von Puffern im Speicher, und bevor aktuell auf der Platte
nachgeschaut wird, sollten doch erst die Pufferbereiche daraufhin kontrolliert
werden, ob die Information nicht bereits dort zu finden ist. Listing 8.25 zeigt die
Funktion generic_file_read().
Der Aufruf init_sync_kiocb()58 ist ein Makro, das die Struktur kiocb
initialisiert, wait_on_sync_kiocb()59 wartet, bis der Lesevorgang, der an die
Funktion __generic_file_aio_read() weitergereicht wurde, vollstndig erledigt ist.
Solange der physische Lesevorgang nicht abgeschlossen ist, d.h. solange noch
iocb->ki_users gesetzt ist, kann der aktuelle Prozess nicht weiter arbeiten. Damit
andere rechenbereite Prozesse whrend dieser Zeit unabhngig von dem
Lesevorgang des aktuellen Prozesses weiterarbeiten knnen, wird der Zustand
des wartenden Prozesses60 auf TASK_UNINTERRUPTIBLE gesetzt und

Diese Betrachtung gilt fr eine ganze Reihe von Filesystemen, jedoch nicht fr alle:
ext3 und nfs beispielsweise greifen stattdessen auf do_sync_read() zu. Samba hingegen ist
ein Filesystem, das eine eigene Funktion bereitstellt.
58
Definiert in include/linux/aio.h.
59
Diese Funktion ist in fs/aio.c definiert.
60
Dies ist zugleich der hier aktuelle Prozess.
57

8.4 Beispiel: read() 173


ssize_t
generic_file_read(struct file *filp, char _ user *buf, size_t
count,
loff_t *ppos)

struct iovec local_iov = { .iov_base = buf, .iov_len = count };


struct kiocb kiocb; ssize_t ret;
init_sync_kiocb(&kiocb, filp);
ret = __generic_file_aio_read(&kiocb, &local_iov, 1, ppos);
if (-EIOCBQUEUED == ret)
ret =
wait_on_sync_kiocb(&kiocb);
return ret;

Listing 8.25. Die Funktion generic_file_read()


while (iocb->ki_users) {
set_current_state(TASK_UNINTERRUPTIBLE
); if (!iocb->ki_users) break;
schedule();

__set_current_state(TASK_RUNNING);
return iocb->ki_user_data;

Listing 8.26. Code-Ausschnitt der Funktion wait_on_sync_kiocb()

der Scheduler aufgerufen. wait_on_sync_kiocb() erledigt das in dem


CodeAusschnitt, der in Listing 8.26 dargestellt ist.
Betrachten wir nun, was __generic_file_aio_read()61 macht. Die Listings 8.27
und 8.28 zeigen die Funktion. Der erste Teil beinhaltet eine Fehlerprfung und
dabei zugleich eine Berechnung von count: bei jedem Schleifendurchlauf wird
diese Variable entsprechend erhht, access_ok ist ein in include/asmi386/uaccess.h definiertes Makro, das auf __range_ok, ein weiteres Makro,
zugreift. Dieses zweite Makro, in derselben Header-Datei definiert, setzt zur
berprfung x86-Assembler-Anweisungen ein. Fr das Einlesen langer
Dateibereiche werden mehrere Speicherbereiche bereitgestellt.
8.4.1

Lesen ohne Readahead

Ist ein direkter Zugriff ohne Bercksichtigung des Caches gefordert62 (siehe
Code unter dem Kommentar coalesce the iovecs . .. in Listing 8.27),

Diese Funktion ist in mm/filemap.c vereinbart.

61

Dies wird beim ffnen einer Datei, d.h. beim Aufruf von open(), durch das Flag
O_DIRECT bewirkt.
62

174

8 Dateisysteme und Plattenverwaltung

so wird zunchst berprft, ob pos, diejenige Position, von der ab das Lesen
erfolgen soll, innerhalb der Datei liegt: der Aufruf i_size_read()63 gibt die Lnge
der Datei zurck. Liegt pos innerhalb des Dateibereichs, so wird die Funktion
generic_file_direct_I0()64 aufgerufen. ber das Mapping wird auf address_space>a_ops->direct_IO() zugegriffen. Fr das ext2- Filesystem bedeutet dies: es wird
ext2_direct_I0()65 aufgerufen, welches wiederum auf die inline-Funktion
blockdev_direct_I0()66 zugreift. Dies ist ein Wrapper fr __blockdev_direct_I0()67.
Diese Funktion greift auf die Funktion direct_io_worker()68 zu, deren Aufgabe es
ist, eine dio-Struktur (DirectIO) zu initialisieren. Schrittweise wird die PageStruktur derjenigen angepasst, die fr Block-I/O besser geeignet ist: ein I/OVektor, der eine Reihe von Datenblcken der Platte aufnehmen kann und nicht
zusammenhngend sein muss. Dies geschieht durch weitere Aufrufe von
dio_bio_submit() und von dort submit_bio().
Die letzte Funktion ist in drivers/block/ll_rw_blk.c definiert und greift auf
generic_make_request() zu. Diese Funktion erwartet, dass erstens in der bioStruktur der Eintrag bi_io_vec die Puffer im Speicher beschreibt, zweitens bi_dev
und bi_sector die Device-Adresse angeben und dass drittens bi_end_io69 korrekt
gesetzt ist.
Die Funktion fhrt nach einer Fehlerprfung im Wesentlichen folgende
Schritte durch:

Mit bdev_get_queue() wird die Request Queue des Block-Devices


ermittelt, auf das sich die I/O-Anforderung bezieht,
mit der Funktion blk_partition_remap() werden bei Partitionen die
relativen Sektornummern in absolute Sektornummern vom Beginn des
Devices umgewandelt und
danach wird mit q->make_request_fn() die Anforderung an den
Gertetreiber weitergereicht. Dies ist in vielen Fllen die
Standardfunktion __make_request(), die auch in
drivers/block/ll_rw_blk.c definiert ist.

__make_request() fgt die Anforderung in die Request-Queue ein und versucht,


Anforderungen umzuordnen und benachbarte zusammenzufassen, um die
Performance zu erhhen. Dazu wird die Abarbeitung der Request-Queue
zunchst blockiert und erst dann zur Bearbeitung freigegeben, wenn hinreichend
viele Anforderungen eingeordnet sind. Eine genauere Darstellung zu diesem
Thema ist in Abschn. 8.5 enthalten.

63

Definiert in include/linux/fs.h.

Diese Funktion ist in mm/filemap.c definiert.


Vereinbart in fs/ext2/inode.c.
66
Die inline-Funktin ist definiert in include/linux/fs.h.
67
Definiert in linux/fs/direct-io.c.
68
Ebenfalls dort definiert.
69
Hier wird der Empfnger fr das Signal nach Beendigung des I/O-Vorgangs
angegeben.
64
65

8.4 Beispiel: read() 175


ssize_t
__generic_file_aio_read(struct kiocb *iocb, const struct iovec
*iov,
unsigned long nr_segs, loff_t *ppos)

struct file *filp = iocb->ki_filp;


ssize_t retval;
misigned long seg;
size_t count;
count = 0;
for (seg = 0; seg < nr_segs; seg++)
{ const struct iovec *iv =
&iov[seg];
/* If any segment has a negative length, or the
cumulative * length ever wraps negative then return
-EINVAL.

*/

count += iv->iov_len;
if (unlikely((ssize_t)(count|iv->iov_len) < 0)) return
-EINVAL;
if (access_ok(VERIFY_WRITE, iv->iov_base, iv->iov_len))
continue; if (seg == 0) return -EFAULT; nr_segs = seg;
count -= iv->iov_len; /* This segment is no good */
break;

/* coalesce the iovecs and go direct-to-BI0 for 0_DIRECT


*/ if (filp->f_flags & 0_DIRECT) { loff_t pos = *ppos,
size; struct address_space *mapping; struct inode *inode;
mapping = filp->f_mapping; inode = mapping->host; retval
= 0; if (!count)
goto out; /* skip atime */ size =
i_size_read(inode); if (pos < size) {
retval = generic_file_direct_IO(READ,
iocb, iov, pos, nr_segs);
if (retval >= 0 && !is_sync_kiocb(iocb))
retval = -EIOCBQUEUED; if (retval > 0)
*ppos = pos + retval;

file_accessed(filp) ; goto out;

Listing 8.27. Die Punktion __generic_file_aio_read() - Teil 1

176

8 Dateisysteme und Plattenverwaltung


/* Readahead
*/ retval = 0;
if (count) {
for (seg = 0; seg < nr_segs; seg++) {
read_descriptor_t desc; desc.written = 0; desc.buf =
iov[seg].iov_base; desc.count = iov[seg].iov_len; if
(desc.count == 0) continue; desc.error = 0;
do_generic_file_read(filp,ppos,&desc,f
ile_read_actor); retval += desc.written; if (!
retval) {
retval =
desc.error; break;

>

out :
return retval;

Listing 8.28. Die Punktion __generic_file_aio_read() - Teil 2

8.4.2

Lesen mit Readahead

Im Folgenden soll nun derjenige Teil der Funktion __generic_file_aio_read


betrachtet werden, der durchlaufen wird, wenn Readahead zugelassen ist (vgl.
Listing 8.28). Hier wird in einer Schleife jeweils eine Variable vom Typ
read_descriptor initialisiert und die weitere Verarbeitung an die Funktion
do_generic_file_read() weitergereicht.
do_generic_file_read()70 ist ein Makro, das mit den zustzlichen Argumenten
filp->fjnapping und &filp->f_ra auf do_generic_mapping_read() (vgl. S. 77)
zugreift. Der Grund fr die zustzlichen Argumente liegt darin, dass dadurch
die Zugriffe auf das Mapping im Speicher, auf den Readahead- Status sowie auf
die zugrunde liegende Inode direkt erfolgen knnen.
In der Schleife, deren Struktur in den Listings 8.29 und 8.30 gezeigt ist,
greift do_generic_mapping_read()71 auf page_cache_readahead()72 zu.
Beim Readahead werden zwei Fenster verwaltet: das eine enthlt die
aktuellen Inhalte, die gerade vom Prozess bentigt werden, das andere wird im
vorausschauenden Lesen mit Daten gefllt. Die wichtige Aufgabe der Funktion

Die Funktion do_generic_file_read() ist definiert in


include/linux/fs.h.
In mm/filemap.c implementiert.
72
Die Funktion ist in mm/readahead.c zu finden.
70
71

8.4 Beispiel: read() 177

page_cache_readahead() liegt nun darin, abzuschtzen, wie viele Pages im

Voraus gelesen werden sollen:

Jede Anforderung einer der aktuellen Pages fhrt dazu, dass - bis zu
einer vorgegebenen Obergrenze - die Anzahl der vorausschauend zu
lesenden Pages um 2 erhht wird. Wird auf jede aktuelle Page
zugegriffen, so verdoppelt sich die Anzahl der vorauszulesenden Pages.
Andererseits bewirkt die Anforderung einer Page, die auerhalb der
aktuellen Pages liegt, dass die Anzahl der vorauszulesenden Pages
deutlich vermindert wird.

Dahinter steht die Idee, dass jeder Treffer dafr spricht, dass die Datei im
Wesentlichen sequentiell gelesen wird, whrend jeder Zugriff auerhalb dieses
Bereichs ein Indiz dafr ist, dass das Lesen mehr den Charakter des Direct- I/O
besitzt. Wird das aktuelle Fenster verlassen, so wird in der Funktion
page_cache_readahead() durch den Aufruf von
do_page_cache_readahead(), der wiederum __do_page_cache_readahead()
aufruft, das Vorablesen ausgelst: dadurch werden die bentigten Pages im
Speicher bereitgestellt, damit das vorausschauende Fenster angelegt und mit
read_pages() die Pages gefllt.
Die Funktion read_pages() greift ber die bergebene Struktur mapping
vom Typ address_space auf mapping->a_ops->readpage zu. Dieser Pointer
zeigt in der Regel auf die Funktion mpage_readpage()73. In dieser Funktion
wird letztlich wieder auf submit_bio() zugegriffen, womit der physische I/OVorgang im weiteren so abluft, wie es fr den direkten Zugriff bereits auf S. 174
beschrieben wurde.
Die Funktion do_genericjnapping_read() hat mit dem Aufruf der
Funktion page_cache_readahead() ihre Arbeit erst begonnen, denn jetzt kann
davon ausgegangen werden, dass sich die Page, auf die zugegriffen werden soll,
bereits im Cache befindet. Die Endlosschleife, innerhalb derer die Arbeit
stattfindet, wird durch break-Anweisungen verlassen, wenn das Lesen erfolgreich
war oder wenn ein grundstzlicher Fehler aufgetreten ist.
Als erstes wird cond_resched() aufgerufen. Damit wird berprft, ob in
der Zwischenzeit ein Prozess hherer Prioritt eingetroffen ist und somit
schedule() aufgerufen werden muss (vgl. Abschn. 4.2.3).
Die diversen goto-Anweisungen muten auf den ersten Blick merkwrdig und
nicht mehr zeitgem an, sind aber bei genauerem Hinsehen nachzuvollziehen.
Es knnen eine Reihe von Problemen auftreten, die bearbeitet werden mssen,
bevor die Funktion zurckkehrt oder endgltig aufgibt. Die Bearbeitung eines
Problems kann dabei dazu fhren, dass andere Teile des Codes erneut
durchlaufen werden mssen. Funktionsaufrufe anstelle von gotos htten diese
Situation langsamer, aber nicht bersichtlicher gemacht.
Da durch das Readahead davon ausgegangen werden kann, dass die
gewnschte Page im Cache ist, wird als nchste Aktion nach dem Label

73

Enthalten in fs/mpage.c.

178

8 Dateisysteme und Plattenverwaltung

find_page die Funktion find_get_page() aufgerufen. Diese Funktion schlgt im


Radix Tree (vgl. S. 76) nach und gibt die gefundene Page zurck. In der Regel
wird eine Page gefunden, die auf dem neuesten Stand ist, und damit wird das
Label page_ok erreicht.
for (;;) {
/* ... */ cond_resched();

/* . . . */

page_cache_readahead(mapping, ra, filp,


index); find_page:
page = find_get_page(mapping, index); if
(unlikely(page == NULL)) {
handle_ra_miss(mapping, ra,
index); goto no_cached_page;

if (!PageUptodate(page)) goto
page_not_up_to_date; page_ok:

/* . . . */

mark_page_accessed(page);
ret = actor(desc, page, offset, nr);
/* ... */ break;
page_not_up_to_date:
if (PageUptodate(page)) goto page_ok;
if (lock_page_wq(page, current->io_wait)) {
/* ... */ goto
sync_error;

>

/* Did it get unhashed before we got the lock?


*/ if (!page_mapping(page)) { unlock_page(page);
page_cache_release(page); continue;

/* Did somebody else fill it already? */ if


(PageUptodate(page)) { unlock_page(page); goto
page_ok;

Listing 8.29. Struktur des Vorablesens in do_generic_mapping_read - Teil 1

8.4 Beispiel: read()

179

readpage:
error = mapping->a_ops->readpage(filp,
page); if (!error) {
if
(PageUptodate(page))
goto page_ok;
if (wait_on_page_locked_wq(page, current->io_wait))
{ /* ... */ goto sync_error;

>

if (PageUptodate(page)) goto page_ok; error = -EI0;

sync_error:
/* melde Fehler ... */ break;
no_cached_page:
/* . . . */
cached_page = page_cache_alloc_cold(mapping);
/* . . . */
add_to_page_cache_lru(cached_page, mapping,
index, GFP_KERNEL); page = cached_page;
cached_page = NULL; goto readpage;

Listing 8.30. Struktur des Vorablesens in do_generic_mappingjread - Teil 2

Nach diesem Label wird die Page mittels mark_page_accessed()74 im Page


Cache nach vorne gebracht. Damit wird verhindert, dass sie in naher Zukunft
durch Swappen ausgelagert wird. Wenn eine Page bentigt wird, ist die
Wahrscheinlichkeit hoch, dass auch in naher Zukunft auf sie zugegriffen wird.
Anschlieend werden mit Hilfe des actor()-Aufrufs, der blicherweise auf
file_read_actor() zeigt, die Daten in den Userspace-Adressraum bertragen.
Es kann jedoch durchaus sein, dass find_get_page() die Page im Cache
findet, diese aber nicht mehr aktuell ist. Deshalb muss mit PageUptodate()
geprft werden, ob die Page aktualisiert werden muss. In diesem Falle wird zum
Label pagemot_up_to_date gesprungen. Nachdem die Ausfhrung des
Quelltextes dieses Label passiert hat, wird zunchst berprft, ob die Page
inzwischen ggf. aktualisiert wurde. Ist das nicht der Fall, so wird das Label
readpage erreicht. Hinter diesem Label wird ein Lesevorgang mit mapping>a_ops->readpage() auslst und bei fehlerfreiem Ausgang zum Label page_ok
zurck gesprungen.

Definiert in mm/swap.c.

74

180

8 Dateisysteme und Plattenverwaltung

Beim Aufruf von find_get_page() kann sich aber auch herausstellen, dass die
Page gar nicht im Cache zu finden ist; dies ist dann gleichbedeutend mit der
Rckgabe von NULL. Grnde hierfr knnen Fragmentierung bei der
Speicherung oder Lcher in der Datei sein. In diesem Falle wird die Funktion
handle_ra_miss() aufgerufen und danach zum Label no_cached_page verzweigt.
Die Aufgabe von handle_ra_miss()75 liegt darin, wegen des Fehlgriffs die Anzahl
der vorab zu lesenden Pages zu verringern. Nach dem Passieren des Labels
no_cached_page wird mit page_cache_alloc_cold() eine neue Page im Cache
erzeugt und mit der Funktion add_to_page_cache_lru() markiert.76 Anschlieend
wird zum Label readpage verzweigt, um einen Lesevorgang auszulsen.

8.5

Elevator

Auf Seite 174 stieen wir bereits auf den IO-Scheduler; hier sollen die
verwendeten Verfahren betrachtet werden. Dabei wird die Darstellung nicht so
detailreich wie im vorangegangenen Abschnitt sein. Wer die Implementierung im
Detail nachlesen will, muss im Wesentlichen in include/linux/elevator.h,
drivers/block/elevator.c sowie den vier IO-Schedulern cfq-iosched.c, noopiosched.c, deadline-iosched.c und as-iosched.c nachschlagen, die ebenfalls in
drivers/block enthalten sind. Linux ermglicht die Auswahl zwischen diesen
unterschiedlichen IO-Schedulern beim Boot-Vorgang. Durch die Kernel-Option
elevator=xxx

wird der entsprechende Scheduler77 ausgewhlt.


Warum sind IO-Scheduler ntig? Liegen einzelne Lese- und
Schreibanforderungen an eine Platte78 vor, so mssen diese nacheinander
abgearbeitet werden. Liegen jedoch mehrere Anforderungen gleichzeitig vor, so
mssen sie in eine Request-Queue eingetragen werden, aus der der Gertetreiber
nach Beendigung einer Anforderung die nchste Anforderung vom Kopf der
Queue auswhlt. Natrlich kann dabei ein ganz einfacher Algorithmus gewhlt
werden: FIFO, nach der Reihenfolge des Eingangs werden die Anforderungen
abgearbeitet. Dies ist zwar ein absolut faires Verfahren, ist es aber sinnvoll?
Folgende berlegung zeigt das Problem: wenn alternierend zwei Prozesse Blcke
von Dateien lesen wollen, die an beiden Extremen der Platte liegen, so muss
zwischen jedem Lesevorgang der Plattenarm vom einen Ende der Platte zum
anderen positioniert werden; gerade das Positionieren ist aber eine der
langsamsten Aktionen, die vom Betriebssystem veranlasst werden.

75

Diese Funktion ist definiert in mm/readahead.c.

Beide Funktionen sind in include/pagemap.h definiert.


xxx steht fr as, cfq, deadline bzw. noop.
78
allgemeiner: Block-Device.
76
77

8.5 Elevator 181

Die Aufgabe eines IO-Schedulers ist es daher, die Reihenfolge der Zugriffe
auf ein Block-Device mglichst gnstig fr die gesamte Performance anzuordnen.
Weiterhin muss zumindest fr Fairness79 gesorgt werden. Damit die
Anforderungen sinnvoll angeordnet werden knnen, mssen zunchst einmal
mehrere Anforderungen vorhanden sein. Deshalb wird eine Queue zunchst
geplugged; sie wird also nicht sofort zur Bearbeitung freigegeben, sondern es
werden erst einige Anforderungen darin gesammelt, die der jeweilige 10Scheduler in geeigneter Reihenfolge anordnet. Erst wenn gengend viele
Anforderungen in der Request-Queue vorhanden sind, wird sie zur Bearbeitung
durch den Gertetreiber freigegeben.
Einen wichtigen Schritt haben wir bereits auf S. 174 gesehen: in jedem Fall
wird versucht, Lesezugriffe80 auf benachbarte Blcke zusammenzufassen.
Allen IO-Schedulern liegt als weiteres Prinzip zugrunde, Anforderungen auf
Blcke, die nicht zusammengefasst werden knnen, sinnvoll in die Schlange
einzuordnen, so dass die Armbewegung mglichst gering bleibt. Der in noopiosched.c definierte Scheduler hngt eine Anforderung, die nicht mit anderen
zusammengefasst werden kann, an das Ende der Request-Queue. Die IOScheduler deadline-iosched.c und as-iosched.c (engl. Anticipatory Scheduler:
vorausschauender Scheduler) versuchen in einem solchen Fall, eine Anforderung
so in die Queue zu stellen, dass sie zwischen zwei Anforderungen mit jeweils
einer greren und einer kleineren Blocknummer als die Anforderung selbst
platziert wird. Die dahinter stehende Idee ist, damit die Armbewegung in eine
Richtung zu lenken und die Bewegungen mglichst klein zu halten81, was zu
kurzen Positionierungszeiten fhrt. Der Arm fhrt also wie ein Aufzug (englisch:
Elevator - daher der Name dieser Verfahren) in eine Richtung und kehrt bei der
maximal erreichten Position die Bewegungsrichtung um. Der IO-Scheduler cfqiosched.c lsst die Armbewegung immer nur in Richtung aufsteigender
Blocknummern laufen und kehrt dann mit einem Schritt wieder zur kleinsten
Blocknummer zurck.
Problem: Welches prinzipielle Scheduling-Verfahren wird bei diesen Schedulern
eingesetzt und welche Schwierigkeiten sind deshalb zu erwarten?
Auf Seite 42 wird als Problem priorittsgesteuerten Schedulings - und hier
handelt es sich offensichtlich um ein priorittsgesteuertes Verfahren - das
Verhungern (Starvation) aufgefhrt und als Gegenmanahme Altern
angefhrt. Unsere nchste Frage muss also lauten: wie wird das Altern
eingefhrt? Ein wichtiges Prinzip ist dabei die Einfhrung von Barrieren: wartet
eine Anforderung in der Queue bereits lange auf die Bearbeitung, so wird die
neue Anforderung erst hinter der alten eingefgt; in diesem Falle wird auch
nicht versucht, die neue Anforderung mit frher gelegenen Anforderungen
zusamDer Begriff Fairness ist nicht festgelegt und wird somit unterschiedlich interpretiert.

79

oder allgemeiner: Zugriffe gleicher Art, d.h. Lese- oder Schreibzugriffe.


Idee: nimm als nchstes diejenige Anforderung, die die kleinste Armbewegung
bentigt.
80
81

182

8 Dateisysteme und Plattenverwaltung

menzufassen. Auf diese Weise wird das Starvation-Problem gelst. Damit lsst
sich festhalten, dass der cfq-IO-Scheduler Starvation vermeidet, in gewissem
Sinne fair ist, da kein Block bevorzugt wird, und - wie Untersuchungen zeigen eine gute Performance hat. Eine Eigenschaft des cfq-Schedulers wurde nicht
erwhnt: Realtime-Anforderungen werden am Kopf der Queue eingefgt und
somit sofort behandelt. Der noop-Scheduler ist sicher auch in diesem Sinne fair,
aber die Performance ist in der Regel wesentlich schlechter.
Die beiden Scheduler deadline und as mssen noch genauer betrachtet
werden. Die Idee dieser Scheduler beruht darauf, dass in der Regel Lesezugriffe
vor Schreibanforderungen zu bevorzugen sind: wird eine Leseanforderung
gestellt, so muss der Prozess warten, bis die Daten vorhanden sind, beim
Schreiben hingegen kann der Prozess weiterarbeiten, auch wenn der Block auf
Grund des Cachings erst viel spter auf der Platte erscheint. Deshalb pflegen
diese beiden Scheduler neben der Request-Queue zwei weitere Queues: jeweils
eine Queue fr die Lese- und eine Queue fr die Schreibanforderungen. Die
Anforderungen erscheinen also in optimierter82 Form in der Request-Queue sowie
in der Lese- bzw. Schreib-Queue, hier jedoch in der zeitlichen Reihenfolge. Jede
Anforderung wird zudem noch mit einer Ablaufzeit versehen, nach der sie
sptestens durchgefhrt werden muss, z.B. bei Leseanforderungen innerhalb von
500 ms, bei Schreibanforderungen 5 s.
Der deadline-Scheduler nimmt in der Regel jeweils den ersten Eintrag aus
der Request-Queue und minimiert so die Armbewegung. Luft jedoch die Zeit fr
die jeweils erste Anforderung in der Lese- oder Schreib-Queue ab (FIFOOrganisation), so wird die Reihenfolge der Request-Queue verlassen und diese
Anforderung bearbeitet. Damit wird jede Anforderung innerhalb ihrer Ablaufzeit
bercksichtigt, Starvation ist somit ausgeschlossen. Auf Grund unterschiedlicher
Ablaufzeiten in der Lese- und Schreib-Queue werden auerdem die
Leseanforderungen bevorzugt.
Der deadline-Scheduler lsst noch ein Problem offen, denn wenn das System
unter hoher Schreiblast steht, passiert folgendes: jede Leseanforderung fhrt
dazu, dass innerhalb kurzer Zeit die regulre Reihenfolge der Anforderungen
zugunsten der Leseanforderung unterbrochen wird. Unverzglich danach wird
die nchste Schreibanforderung behandelt. Befinden sich die Lese- und
Schreibanforderungen weit voneinander entfernt, wird bei jeder Leseanforderung
eine zweimalige Positionierung des Arms erforderlich. Der as- Scheduler
versucht, auf Grundlage des deadline-Schedulers, dieses Problem anzugehen:
zum deadline-Scheduler wird hier eine Heuristik hinzugefgt, die das Verhalten
der Prozesse vorherzusagen versucht. Wird eine Leseanforderung ausgefhrt, so
kehrt der Arm beim as-Scheduler nicht sofort zurck, vielmehr wartet er fr eine
kurze Zeit.83 Die dahinter stehende Idee ist die, dass auf einen Lesevorgang
hufig ein weiterer auf einen nahe benachbarten
D.h. benachbarte Anforderungen werden zusammengefasst und die Anforderungen im
obigen Sinne optimal sortiert.
82

83

Typisch ist eine Wartezeit von 6 ms.

Blockgruppe 0

184

Blockgruppe 1

8 Dateisysteme und Plattenverwaltung

Blockgruppe n

8.6 Extended Filesystem 2

183

Block folgt. Geschieht dies whrend der Wartezeit, wird der nchste Lesevorgang
ohne zeitaufwndige Armbewegung durchgefhrt. Der Scheduler sammelt
Boot
Informationen
ber das Verhalten der Prozesse und versucht damit, die Strategie
block
geeignet zu beeinflussen. Jede dadurch eingesparte Armbewegung wirkt sich
positiv auf die Performance aus. Trotzdem hat der as-Scheduler immer noch die
gute Eigenschaft des deadline-Schedulers, dass Anforderungen, deren Ablaufzeit
erreicht ist, schnell bearbeitet werden.
Noch im laufenden Betrieb lassen sich die Scheduler durch elvtune (Elevator
Tuning) beeinflussen. So kann der Administrator damit fr ein BlockDevice
unter anderem die Ablaufzeit fr die Leseanforderungen oder fr die
Schreibanforderungen setzen und folglich die Disk-Performance und die
Interaktivitt verndern.
^ '_________________________________________ ________________________________________"
^
DatenInodeInode
Datenblcke
Gruppen
bitmap
bitmap
tabelle
deskrip
Supe
Extended Filesystem 2
r
toren 8.6
bloc
k

Wie wir bereits auf S. 161 gesehen haben, enthlt das VFS Strukturen, um
unterschiedliche
zu untersttzen.
Dazu ngehren Filesysteme wie
k
1 Filesysteme
1
m

proc und sys, die vom Kernel erzeugt und im Speicher gehalten werden,
um Informationen ber das System und seine Prozesse anzuzeigen und
zum Teil verndern zu knnen,
ext, ext2, die zum klassischen Repertoire von Linux gehren und dazu
dienen, Dateien auf Platte zu speichern,
Journaling-Systeme wie ext3 und reiserfs, die neben den Daten auch die
Aktionen speichern, die mit den Dateien durchgefhrt werden. Mit
diesen Informationen kann bei einem Systemabsturz die Reparatur des
Filesystems merklich schneller erfolgen.
fat, HPFS, NTFS, VFAT usw., die originr von anderen Betriebssystemen
kommen.

ext2 ist dasjenige System, welches der Struktur von VFS am hnlichsten ist. Bei
anderen Filesystemen muss in der Regel mit mehr Aufwand gerechnet werden,
um die Strukturen, die auf der Platte verwaltet werden, in die Strukturen von
VFS umzuwandeln. Dennoch gibt es auch bei ext2 einige Betrachtungen, die so
nicht im VFS zu finden sind.
Der Aufbau einer Platte oder Partition ist in Abb. 8.6 zu sehen: zu Anfang
steht der Bootblock84, der dazu dient, Betriebssysteme, die auf dem Rechner
installiert sind, zu starten. Danach folgen beim ext2-Filesystem eine Reihe von
Blockgruppen, deren Feinstruktur ebenfalls zu sehen ist.
In den Blockgruppen befinden sich neben den eigentlichen Datenblcken, die die
Inhalte der Dateien und Directories aufnehmen, auch Informationen ber

Er steht am Anfang der Platte oder einer Partition, die ein Betriebssystem enthlt.

84

8.6 Extended Filesystem 2

185

zu ermglichen, andererseits zu gewhrleisten, dass externe Fragmentierung88


des Speichermediums, wie es bei zusammenhngender Organisation auftritt,
kein Problem darstellt.
Problem: Speicherplatzverwaltung mit einem Index, der ja selbst Dateiblcke
belegt, macht wenig Sinn, wenn man viele kleine Dateien (so z.B. ShellKommandos) auf der Platte verwaltet.
Diese berlegung fhrte dazu, dass in der Inode selbst Platz fr ein paar
Blockzeiger bereitgestellt wird: ein Zeiger auf einen Dateiblock nimmt 4 Byte in
Anspruch, in der Inode werden 12 solcher Zeiger verwaltet, die auf die ersten 12
Dateiblcke zeigen. Warum nicht mehr? Wrden alle Zeiger in der Inode
verwaltet, msste diese sehr gro werden, um Dateien mit tausenden von
Datenblcken mit der Gre von 4KB zu verwalten. Also hat man sich das
Anzahl der Blcke
Konzept der Indirektion einfallen
lassen: der 13. Zeiger zeigt auf einen
Abb.
8.6.
Platte bzw.
Partition Man muss nicht
Indexblock, dessen EintrgeAufbau
auf dieeiner
Datenblcke
verweisen.
lange nachrechnen, um festzustellen, dass auch dieses Verfahren nur relativ
kleine Dateien zulsst. Wird mehr Platz bentigt, tritt der 14. Zeiger in der Inode
das Filesystem: Im Superblock sind Blocklnge, Anzahl freier und
in Kraft, der eine doppelte Indirektion ermglicht: er verweist auf einen
belegter Blcke, aktueller Zustand des Systems, Zeitpunkt des letzten
Indexblock, dessen Eintrge auf Indexblcke verweisen. Bei wirklich groen
Mountens usw. enthalten;
Dateien kann auch der 15. Zeiger in der Inode zur dreifachen
Indirektion
die Blockgruppen: Die Gruppendeskriptoren85 enthalten z.B. Anzahl
herangezogen werden. Bei gleicher Inode-Gre knnen also sehr unterschiedlich
freier Blcke und freier Inodes in jeder Gruppe,
groe Dateien ohne Platzverschwendung durch nicht bentigte Indexblcke
freie Datenblcke und Inodes: Als Bitmaps werden die in der jewiligen
efRzient gespeichert werden. Abbildung 8.7 zeigt das Verfahren.
Blockgruppe
Datenblcke und
Inodes
aufgefhrt,
sowie
Zurck
zur Frage freien
der Fragmentierung:
die
Abb. zeigt,
dass der

ber
die
in
der
Blockgruppe
gespeicherten
Dateien:
Diese
Information
Speicherraum einer Datei logisch zusammenhngend ist, wohingegen
durch die
ist
in
der
Inode-Tabelle,
die
alle
Inodes
der
jeweiligen
Blockgruppe
Verwendung von Pointern die Blcke, in denen die Datei gespeichert ist, auf der
enthlt,sein
enthalten.
Platte verstreut
knnen. Dieses Verfahren ermglicht es, die Platte gut
Offensichtlich besitzt
werdenaber
einige
so der
Superblock
undder
dieBlcke das
auszunutzen,
denInformationen
Nachteil, dass- bei
starker
Streuung
86
Gruppendeskriptoren
- sehr
redundant
behandelt.
Doch die Vorteile
liegen in
auf
sequentielle
Lesen89 einer
Datei
sehr lange
dauert gegenber
der Situation,
Hand:
einerseits
ist zusammenhngend
ein Systemabsturz, gespeichert
der diese Information
beschdigt,
der die
Blcke
physisch
sind. Ein wichtiges
nicht mehr
soext2
kritisch,
die Informationen
aus den
Kopien
wiederhergestellt
Anliegen
von
ist esda
daher,
benachbarte Blcke
einer
Datei
physisch
werden knnen,
und zum anderen
wird dieHier
Lnge
der Armbewegungen
reduziert,
mglichst
nahe beieinander
zu speichern.
kommen
nun die Blockgruppen
da Verwaltungsinformation
und Dateiinhalte
nahebenachbart,
benachbart und
gespeichert
ins
Spiel: alle Blcke einer Blockgruppe
sind nahe
findet sich in
87
werden.
einer
Blockgruppe wirklich kein geeigneter Block mehr, so wird versucht,
Im Abschnitt 8.1.6
habeninwir
unsanderen
berlegt,
auf welche grundstzlichen
Arten
zusammenhngende
Blcke
einer
Blockgruppe
zu finden. Dann wren
Speicherplatz
bereitgestellt
werden
kann.
Wie
werden
diese
berlegungen
auf
zumindest jeweils eine Reihe von Blcken zusammenhngend gespeichert.
ext2 bertragen? Grundstzlich ist sicherlich ein Index zur Verwaltung der
Plattenblcke anzustreben, um einerseits einen schnellen direkten Zugriff

Genauer:
in jeder
Blockgruppe
befinden
sich
fr jede sein
im - dies
Dennoch kann
eine
Datei auf viele
Blcke
derGruppendeskriptoren
Platte verstreut gespeichert
Filesystem
vorhandene
Blockgruppe.
muss auf andere Weise mglichst weitgehend verhindert werden.
86
89
Tatschlich
wird
bei Readahead
neueren ext2-Versionen
derdient
Superblock
in jede
Vgl. Abschn.
8.4:
(Vorauslesen)
dazu, nicht
die mehr
I/O-Performance
zu
Blockgruppe
gespeichert.
steigern. Der positive Effekt stellt sich aber nur ein, wenn die Blcke zusammenhngend
87
Durch Cachen
gespeichert
sind. des Superblocks wird auch dieses Argument relativiert.
85
88

186

8 Dateisysteme und Plattenverwaltung

Abb. 8.7. Indirektion beim Zugriff auf Datenblcke Die Linienart deutet
die Stufe der Indirektion an. Im brigen enthlt diese Skizze gewisse Ungenauigkeiten:
die Gre der Objekte relativ zueinander passt nicht und die Indexblcke sind natrlich
zugleich Datenblcke - mssten also ganz rechts eingeordnet sein.

Im Folgenden sollen folgende Aspekte betrachtet werden:

Welche Information enthlt der Superblock und wozu wird diese


bentigt?
Was ist in der Inode gespeichert und wie erfolgt die Zuweisung von
Datenblcken; wie hngt dies mit den Blockgruppen und den
Gruppendeskriptoren zusammen?
Wie wird Speicherplatz bereitgestellt und Indirektion untersttzt?

8.6.1

Der Superblock

Verstndlicherweise enthlt der gespeicherte Superblock andere Informationen


als die, die unter VFS im Superblock vorgehalten werden. Zum Einlesen dient
die Funktion ext2_get_sb()90. Diese Funktion gibt eine Struktur vom Typ
super_block zurck, also das, was VFS intern bentigt. Die Struktur des ext2Superblocks ist als ext2_super_block91 definiert.
"Definiert in fs/ext2/super.c.
91
Die Struktur ist in include/linux/ext2_fs.h enthalten.

8.6 Extended Filesystem 2

187

Beim Vergleich der Strukturen ext2_super_block und super_block (VFS)


fallen mehrere Aspekte sofort auf:

Die ungewhnlichen Datentypen __u32, __s32 usw. in ext2_super_block


hngen damit zusammen, dass die Daten unabhngig vom Prozessortyp
und dem dadurch vorgegebenen C-Datentyp immer mit gleicher Bitlnge
gespeichert werden mssen. Ansonsten wren austauschbare
Datentrger nicht denkbar.
Die in super_block vorgesehenen Listen, Semaphoren, dirty-Bits usw.
sind natrlich fr den Superblock auf der Platte vllig unerheblich.
Ebenso sind in ext2_super_block Eintrge enthalten, die fr die
Speicherinterne Darstellung super_block uninteressant sind, wie die
Anzahl der durchgefhrten Mount-Operationen, die Maximalanzahl von
Mount-Operationen bis zum nchsten erzwungenen Filesystem-Check,
die Anzahl der reservierten Blcke, die Magic Signature, der Status des
Systems usw. Listing 8.31 zeigt wichtige Teile der Datenstruktur.

Neben den offensichtlichen Eintrgen gibt es einige, die erlutert werden


mssen. So enthlt s_magic eine sogenannte magische Zahl, die fr den ext2Superblock typisch ist. Beim Mounten wird berprft, ob hier der korrekte Wert
steht, und ein Fehler angezeigt, wenn das nicht der Fall ist.
Ist die Platte bzw. Partition voll, sind also alle Blcke belegt, so knnen auch
entscheidend wichtige Prozesse nichts mehr auf die Platte schreiben. Bei einem
Datenbankserver knnte das Datenbankmanagement-System dann keine
weiteren Log-Eintrge schreiben, die Datenbank wrde jede weitere Aktion
verweigern. Um diese Gefahr abzuwehren, wird eine in s_r_blocks_count
gegebene Anzahl an Blcken reserviert, die nur von einem besonders
gekennzeichneten Benutzer (s_def_resuid) - in der Regel root, d.h. mit
Administratorrechten - verwendet werden drfen. Damit knnen wichtige
Systemprogramme, die in der Regel mit root-Rechten arbeiten, diese Reserve
noch ausnutzen; das System ist (noch) funktionsfhig, auch wenn der einzelne
Benutzer keine Blcke mehr auf die Platte schreiben kann. Der Administrator
muss diese Sicherheitsreserve ausnutzen, um wieder gengend Speicherplatz
bereitzustellen.
s_state wird benutzt, um festzustellen, ob die Partition nach dem letzten
Zugriff sauber ausgehngt worden ist, ob also umount durchgefhrt wurde.
Zwei Werte knnen in dem Eintrag stehen: am Ende eines ordnungsgem
verlaufenen umount wird dort ein bestimmter Wert gespeichert. Beim Mounten
wird zunchst der Wert dieses Feldes geprft; steht nicht der erwartete Wert
dort, so wird fsck.ext2 aufgerufen, um das Filesystem zu reparieren. Sofort nach
dem Mounten wird dieser Wert verndert, damit das System im Fehlerfalle einen
anderen als den erwarteten Wert vorfindet. Weitere Eintrge dienen ebenfalls
dem Schutz des gespeicherten Filesystems. Im Eintrag sunnt_count werden die
Mount-Vorgnge nach dem letzten Aufruf von fsck.ext2 gezhlt. Erreicht beim
Mounten dieser Wert den in sunax_mnt_count gespeicherten Wert, so wird
ebenfalls zunchst fsck.ext2 aufgerufen, um das Filesystem

188

8 Dateisysteme und Plattenverwaltung


struct ext2_super_block {
__u32 s_inodes_count;
__u32 s_blocks_count;
__u32 s_r_blocks_count;
__u32
s_free_blocks_count;
__u32
s_free_inodes_count;
__u32 s_first_data_block;
__u32 s_log_block_size;
__s32 s_log_frag_size;
__u32 s_blocks_per_group;
__u32 s_frags_per_group;
__u32
s_inodes_per__group;
__u32 s_mtime;
__u32 s_wtime;
__ul6 s_mnt_count;
__sl6 s_max_mnt_count;
__ul6 s_magic;
__u32 s_checkinterval;
__u32 s_creator_os;
__u32 s_rev_level;
__ul6 s_def_resuid;
__ul6 s_def_resgid;
/*
* These fields are for EXT2J
*/

/*
/*
/*
/*
/*
/*
/*
/*

Inodes count */
Blocks count */
Reserved blocks count */
Free blocks count */
Free inodes count */
First Data Block */
Block size */
Fragment size */
/* # Blocks per group */
/* # Fragments per group */
/* # Inodes per group */
/* Mount time */
/* Write time */
/* Mount count */
/* Maximal mount count */
/* Magic signature */
/* File system state */
/* Behaviour when detecting
errors*/ /* minor revision level */
/* time of last check */
/* max. time between checks */
/* OS */
/* Revision level */
/* Default uid for reserved
blocks*/ /* Default gid for
reserved blocks*/
YNAMIC_REV superblocks only.

/*
* Performance hints. Directory preallocation should only
* happen if the EXT2_C0MPAT_PREALL0C flag is on.
*/
___u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/
___u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */
___ul6 s_paddingl;

/*

* Journaling support valid if EXT3_FEATURE_C0MPAT_HAS_J0URNAL


set */
/* hier folgen Vereinbarungen fr das Journaling Filesystem ext3,
* das auf ext2
aufsetzt */

>;
Listing 8.31. Ausschnitte aus der Struktur ext2-Superblock

8.6 Extended Filesystem 2

189

zu berprfen und so Fehler zu korrigieren; am Ende dieses Prfvorgangs wird


der Wert in s_mnt_count auf Null gesetzt. Genau so werden die Eintrge
s_lastcheck und s_checkinterval benutzt: fsck.ext2 setzt die aktuelle Zeit in den
Eintrag s_lastcheck. Liegt beim Mounten zwischen dem gespeicherten Wert und
der beim Mounten aktuellen Zeit eine grere Zeitspanne als s_checkinterval92,
so wird ebenfalls automatisch fsck.ext2 aufgerufen.
s_log_block_size ist der binre Logarithmus der Blocklnge, gemessen in KB
(1024 Byte). Erlaubt sind zur Zeit die Werte 0, 1 und 2, sodass die aktuellen
Blocklngen lKB, 2KB oder 4KB betragen knnen. Diese Gre wird beim
Anlegen des Filesystems vereinbart. Hierzu verwendet der Systemadministrator
das Kommando mke2fs. Die Blocklnge sollte in Abhngigkeit der Nutzung des
Filesystems erfolgen: bei vorwiegender Speicherung groer Dateien ist die obere
Blocklnge zu whlen, um den Verwaltungsaufwand klein zu halten, bei
vorwiegend kleinen Dateien jedoch die untere, da in diesem Falle sonst zu viel
Platz verschenkt wird.
Wie viele Blcke in einer Gruppe zusammengefasst werden und wie viele
Inodes in einer Gruppe gespeichert werden knnen, wird in den Eintrgen
s_blocks_per_group und s_inodes_per_group festgehalten, die ebenfalls beim
Aufruf von mke2fs festgeschrieben werden.
8.6.2

Die Inode

Einen Ausschnitt aus der unter ext2 gespeicherten Struktur einer Inode zeigt
Listing 8.32.
Auch hier werden wieder die Typen __ul6,... eingesetzt. Auffllig ist die
berlagerung von Speicherplatz mit unterschiedlichen Strukturen durch die
zweifache Verwendung von union. Der Grund liegt darin, dass das Filesystem
ext2 auf mehreren Betriebssystemen - Linux, HURD und Masix - eingesetzt
wird. An denjenigen Stellen, an denen eine berlagerung mit Hilfe von miion
stattfindet, bentigen die anderen beiden Betriebssysteme andere Daten, als ext2
unter Linux sie erwartet.
Beim Vergleich mit der VFS-Struktur inode (vgl. S. 146, f.) knnen wir viele
Eintrge sofort zuordnen, wobei wiederum diejenigen Eintrge von inode nicht
vorhanden sind, die zum Speicher-internen Verketten bentigt werden.
iunode speichert die Zugriffsrechte fr den Eigentmer, die Gruppe und den
Rest der Welt in der blichen Unix-Form. Die schon aus inode bekannten
Zeitstempel finden sich in i_atime, i_ctime und i__mtime wieder. Der zustzliche
Zeitstempel i_dtime gibt an, wann die Datei gelscht wurde; im Gegensatz zu
VFS bleibt der Platz, an dem die ext2-Inode auf der Platte gespeichert ist,
erhalten. Die Benutzer- und Gruppenkennzahlen werden jeweils zerlegt: in die
oberen und die unteren 16 Bit. Der Grund ist in der Geschichte
Fr das ext2- und ext3-Filesystem kann der Administrator im laufenden Betrieb die
Werte s_max_mnt_count und s_checkinterval durch einen Aufruf von tune2fs verndern.
92

190

8 Dateisysteme und Plattenverwaltung


struct ext2_inode
__ul6 i_mode;
/* File mode */
__ul6 i_uid;
/* Low 16 bits of Owner Uid
__u32 i_size;
*/
__u32 i_atime;
/* Size in bytes */
__u32 i_ctime;
/* Access time */
__u32 i_mtime;
/* Creation time */
__u32 i_dtime;
/* Modification time */
___ul6 i_gid;
Low 16 bits
/*/*Deletion
Time of
*/ Group Id */
___ul6 i_links_count; /* Links count */
___u32 i_blocks; /* Blocks count */
___u32 i_flags; /* File flags */
union {
struct {
___u32 l_i_reservedl;
} linuxl;

>

/* und weitere BS abhngige Teile fr hurd und masix */


osdl;
/* 0S dependent 1 */
___u32 i_block[EXT2_N_BL0CKS];/* Pointers to blocks */
___u32 i_generation; /* File version (forNFS) */
___u32 i_file_acl;
/* File ACL */
___u32 i_dir_acl;
/* Directory ACL */
___u32 i_faddr;
/* Fragment address
*/
union {
struct
{
___u8 l_i_frag; /*
Fragment number */
___u8 l_i_fsize; /*
Fragment size */
___ul6 i_padl;
___ul6 l_i_uid_high; /* these 2 fields */
___ul6 l_i_gid_high; /* were reserved2[0]*/
___u32 l_i_reserved2;
> linux2;
/* und weitere BS abhngige Teile */

>

>;

osd2;

Listing 8.32. Ausschnitte aus der Struktur ext2-Inode

dieses Filesystems zu suchen, denn als es entworfen wurde, wurden Benutzerund


Gruppenkennzahlen noch als 16 Bit-Zahlen dargestellt. Mit dem Wechsel auf
grere Zahlen musste dies in spteren Versionen von ext2 nachvollzogen
werden. Insofern war die einfachste Methode, die oberen 16 Bit an einer freien
Stelle hinten anzuhngen: l_i_uid_high und l_i_gid_high entstanden auf diese
Weise.

8.6 Extended Filesystem 2

191

i_size gibt die Lnge der Datei in Bytes und i_blocks die Lnge in Blcken93
an. Warum sind hier zwei Werte ntig, die doch anscheinend auseinander
abgeleitet werden knnen? Die Indirektion bei ext2 bietet sich geradezu dazu an,
Dateien mit Lchern (englisch: file hole) zu speichern, denn werden groe
Dateien gespeichert, in denen die Information nur dnn verteilt ist, so enthlt
eine solche Datei groe Lcher. Diese mssen jedoch nicht physisch auf der Platte
gespeichert werden, es reicht, wenn der Pointer NULL ist, also auf einen leeren
Block verweist. Deshalb kann die Anzahl der gespeicherten Blcke nicht aus der
Lnge einer Datei, gemessen in Bytes, abgeleitet werden.
Das Array i_block dient dem Zugriff auf die Daten- bzw. Indexblcke und
entspricht der Darstellung in Abb. 8.7: die ersten 12 Eintrge zeigen auf die
ersten 12 Datenblcke, die Eintrge 13 bis 15 auf Indexblcke erster, zweiter und
dritter Indirektion.
i_links_count enthlt einen Zhler, der die Anzahl von Links - genauer von
harten Links - auf die Datei zhlt. Eine Datei darf frhestens dann gelscht und
die zugehrigen Blcke drfen freigegeben werden, wenn dieser Zhler auf 0 fllt.
Die gespeicherte Inode wird durch die Funktion ext2_read_inode()94
eingelesen, die auf ext2_get_inode() zum Holen des Blocks zugreift und dann die
Werte in der VFS-inode setzt. Dabei ermittelt ext2_get_inode() den korrekten
Blockgruppen-Deskriptor mit Hilfe von ext2_get_group_desc()95. Damit ist der
Zugriff auf die richtige Inode-Bitmap und den Inode-Tableblock gegeben und es
kann auf die Inode zugegriffen werden (vgl. Listing 8.33).

struct ext2_group_desc
___u32 bg_block_bitmap;
/* Blocks bitmap block */
___u32 bg_inode_bitmap;
/* Inodes bitmap block */
___u32 bg_inode_table;
/* Inodes table block */
___ul6 bg_free_blocks_count;
/* Free blocks count */
___ul6 bg_free_inodes_count;
/* Free inodes count */
___ul6 bg_used_dirs_count; /* Directories count */
___ul6 bg_pad;
___u32 bg_reserved[3];

Listing 8.33. Gruppendeskriptor, definiert in include/linux/ext2_fs.h

Abbildung 8.6 zeigt die Zerlegung einer Partition in Blockgruppen, in denen


jeweils auch ein Teil fr die Speicherung von Inodes reserviert ist. Weiter
Unabhngig von der aktuellen Blocklnge auf der Partition wird hier die Blocklnge
in Einheiten von 512 Byte gemessen.
93

94

Definiert in fs/ext2/inode.c.
Diese Punktion ist in fs/ext2/balloc.c enthalten.

95

192

8 Dateisysteme und Plattenverwaltung

wurde auf S. 189 erwhnt, dass die Anzahl der Inodes pro Blockgruppe beim
Erstellen des Filesystems festgelegt wird. Nun tut sich somit die interessante
Frage auf, ob beim Erzeugen einer neuen Datei - und damit beim Anlegen einer
neuen Inode auf der Platte - der Platz zum Anlegen einer neuen Inode beliebig
gewhlt werden kann oder ob auch hier berlegungen hinsichtlich der
Performance angestellt werden sollten.
Der Quellcode zeigt, dass auch das Anlegen einer neuen Inode, d.h. die
Auswahl eines vernnftigen Platzes, anspruchsvoll ist. Einerseits sollen
Directories gut ber die Partition verstreut werden, andererseits sollen
Nachkommen eines Directories mglichst zusammen mit den Dateien in einer
Blockgruppe gespeichert werden, um Armbewegungen beim Suchen und
Zugreifen auf Dateien minimal zu halten.
Die Suche nach einem geeigneten Platz fr eine Inode beginnt mit der
Funktion ext2jaew_inode()96. Nach dem Anlegen einer neuen VFS-Inode wird
geprft, ob es sich um eine normale Datei oder ein Directory handelt - das erfolgt
durch den Aufruf von S_ISDIR(mode). Handelt es sich um ein Directory, so
wird noch nachgeschaut, ob es im alten Stil - nach den Methoden bis
einschlielich Kernel-Version 2.4 - oder mit Orlov-Allocation behandelt werden
soll.
Die Orlov-Allocation wird benutzt, wenn find_group_orlov()97 aufgerufen
wird. Die Prinzipien dieser Strategie lassen sich so beschreiben:

Directories auf oberster Ebene bzgl. dieser Partition werden ber die
Blockgruppen verteilt:
- Von denjenigen Blockgruppen mit berdurchschnittlich vielen freien
Inodes und Blcken wird diejenige mit kleinster Directory-Anzahl
ausgewhlt,
- andernfalls wird eine beliebige Blockgruppe gewhlt.
Untergeordnete Directories werden, um Suchvorgnge zu beschleunigen,
unter Bevorzugung derjenigen Blockgruppe gespeichert, in der sich auch
das Eltern-Directory befindet. Dabei gelten folgende Anforderungen:
- In der Blockgruppe drfen nicht bereits zu viele Directories gespeichert
sein,
- es mssen gengend Inodes in der Blockgruppe frei sein und
- zustzlich mssen gengend Blcke in der Blockgruppe frei sein. Erfllt
die bevorzugte Blockgruppe diese Eigenschaften nicht, werden die
brigen Blockgruppen, ausgehend von der bevorzugten, zyklisch nach
einer Blockgruppe durchsucht, die mehr freie Inodes als der
Durchschnitt der Blockgruppen besitzt.
Erfolgt das Anlegen einer Directory-Inode nach der alten Methode, so wird
find_group_dir()98 aufgerufen. Der Algorithmus ist wesentlich einfacher als

96

In fs/ext2/ioalloc.c definiert.

Ebenfalls in fs/ext2/ialloc.c
definiert.
Ebenfalls in fs/ext2/ialloc.c.
97
98

8.6 Extended Filesystem 2

193

beim Orlov-Algorithmus: in einer Vorwrtssuche durch die Blockgruppen wird


nach einer Blockgruppe mit freien Blcken und einem mglichst guten Verhltnis
zwischen belegten Directory- und brigen belegten Inodes geschaut. Wenn keine
Blockgruppe mit derartigen Eigenschaften gefunden wird, dann wird nach
Blockgruppen mit berdurchschnittlich viel freiem Speicher gesucht und unter
diesen diejenige ausgewhlt, die die kleinste Anzahl von Directory- Inodes hat.
Das Ziel beider Verfahren ist es, Directory-Inodes mglichst gut zu verteilen
und zugleich darauf zu achten, dass gengend weiterer Speicherplatz in der
ausgewhlten Blockgruppe vorhanden ist. Der Orlov-Algorithmus versucht
zudem, Eltern-Kind-Beziehungen zwischen Directory-Inodes mglichst
benachbart zu halten.
Handelt es sich nicht um eine Directory-Inode, sondern ist eine Inode fr
eine Datei oder einen Link anzulegen, so wird find_group_other()" aufgerufen.
Dabei wird zunchst versucht, eine freie Inode in derjenigen Blockgruppe zu
finden, in der auch die Inode des Directories angesiedelt ist. Wenn dies
fehlschlgt, wird ausgehend von dieser Blockgruppe in den Schritten 2, 2 + 21,
2 + 21 + 22,..., also 1, 3, T,..., vorwrts gesucht. Fr den seltenen Fall, dass auch
dieses Vorgehen nicht zum Ziel fhrt, werden alle Blockgruppen nach einer
freien Inode durchsucht.
Neben dem Anlegen einer Inode muss auch auf der Partition die
DirectoryDatei auf den neuen Stand gebracht werden. Eine Directory-Datei ist
eine spezielle Datei, die nur vom Betriebssystem selbst verndert wird. Der
Aufbau ist in Listing 8.34 dargestellt. Jede Datei des Directories - und natrlich
auch Subdirectories - wird mit der vorgegebenen Struktur in die DirectoryDatei
eingetragen. Dabei steht der Name in dem Array name, die Lnge des Namens
ist durch name_len bekannt. Wird in dem Directory ein Dateiname
nachgeschlagen und gefunden, so ist durch inode die Inode-Nummer der Datei
bekannt und es kann somit auf die Inode der gesuchten Datei zugegriffen
werden.
Das Lschen von Dateien bedeutet, dass ein Eintrag aus dem Directory und
ggf. auch eine Inode - und die zugehrigen Datenblcke - gelscht werden
mssen. Fr ein Directory, das gelscht werden soll, passiert dabei folgendes:
durch rmdir() wird letztendlich ext2_rmdir()* 100 aufgerufen. Diese Funktion prft
zunchst mit ext2_empty_dir(), ob das Directory leer ist. Sodann wird
ext2_unlink()101 aufgerufen; hier landen wir auch, wenn normale Dateien
mittels unlink() entfernt werden sollen. Diese Funktion wiederum ruft
ext2_find_entry() und ext2_delete_entry()102 auf. Die erste Funktion sucht in den
Directory-Eintrgen nach dem Namen, die zweite berschreibt

"Ebenfalls in fs/ext2/ialloc.c.
In fs/ext2/namei.c definiert.
Ebenfalls in fs/ext2/namei.c.
102
Beide Funktionen sind in fs/ext2/dir.c zu finden.
100
101

194

8 Dateisysteme und Plattenverwaltung

struct
{
ext2_dir_entry_2
Inode number */
__u32 inode;
/* Directory entry length */
__ul6 rec_len;
/* Name length */
__u8 name_len;
/*
__u8 file_type;
char
name[EXT2_NAME.
};* Ext2 directory file types. Only the low 3 bits are used.
* other bits are reserved for
now. */
enum {

The

EXT2_FT_UNKN0WN,
EXT2_FT_REG_FILE
, EXT2_FT_DIR,
EXT2_FT_CHRDEV,
EXT2_FT_BLKDEV,
EXT2_FT_FIF0,
EXT2_FT_S0CK,
EXT2_FT_SYMLINK,
EXT2_FT_MAX

Listing 8.34. Struktur eines Directory-Eintrags in include/linux/ext2_fs.h

den Eintrag rec_len mit NULL. Danach wird in der Funktion ext2_unlink()
noch der Eintrag count der Inode um 1 verringert.
Bislang wurde allerdings etwas bersehen: Datenblcke - auch diejenigen
eines Directories - drfen erst dann freigegeben oder berschrieben werden,
wenn der Benutzungszhler der Inode auf 0 gesenkt worden ist und somit kein
anderer harter Link mehr auf die Datei verweist. Dabei werden die
bg_block_bitmap sowie die bg_inode_bitmap in den betreffenden
Blockgruppen-Deskriptoren korrigiert und der Superblock auf den aktuellen
Stand gebracht.
Letztendlich wird eine Datei nicht entfernt und lsst sich wiederherstellen,
solange die zugehrigen Datenblcke noch nicht berschrieben worden sind. Dies
ist der Grund dafr, dass Funktionen wie wipe() zur Verfgung gestellt
werden, die bei sicherheitsrelevanten Installationen fr das berschreiben und
somit das unwiederbringliche Lschen der zugehrigen Datenblcke sorgen.
8.6.3

Indirektion und Finden der Blcke

In Abschnitt 8.4 wurden fr das Lesen zwei Wege aufgezeigt: direkt oder mit
Cache-Einsatz. Was jedoch nicht betrachtet wurde, war die Frage, wie die Blcke
auf dem Speichermedium gefunden werden. Der direkte Zugriff verwendet beim
ext2-Filesystem die Funktion ext2_direct_I0() (vgl. S. 174),

8.6 Extended Filesystem 2

195

beim Zugriff unter Verwendung des Caches wird in read_pages() die Funktionmpage_readpage() aufgerufen (vgl. S. 177).
In ext2_direct_I0() wird beim Aufruf von blockdev_direct_IO()103 die
Punktion ext2_get_blocks() weitergegeben, die auf ext2_get_block()104
zugreift. Wird unter Verwendung des Caches zugegriffen, so bekommt die
Funktion mpage_readpage() beim Aufruf als Argument ebenfalls die Funktion
ext2_get_block() bergeben. Beim Schreiben wird ext2_get_block() ebenfalls
aufgerufen. Die unterschiedliche Nutzung wird durch das Argument create
untersttzt: beim Lesen enthlt es 0, beim Schreiben 1.
In Listing 8.35 soll eine stark verkrzte Form von ext2_get_block()
betrachtet werden. Zunchst muss die Abbildung zwischen der logischen
Blocknummer und dem physischen Speicherplatz, d.h. der Blocknummer in der
Partition, hergestellt werden. Dazu wird mit ext2_block_to_path() die
Indirektion untersttzt: aus der (logischen) Blocknummer iblock lsst sich
ableiten, welche Index-Blcke und welche Zeiger verwendet werden mssen, um
die Adresse des gewnschten Blocks zu finden. Mit Hilfe der Blocklnge, die
bereits im Superblock gespeichert ist, werden die Zeiger relativ zu den IndexPages direkt ermittelt und in offsets gespeichert, depth gibt an, wie viele
Stufen die Indirektion enthlt. Die Funktion ext2_get_branch() durchluft die
bentigten Indexblcke bis zur Datenseite. Wird diese gefunden, so wird NULL
zurckgegeben. In dem Quelltext, der auf den Aufruf von ext2_get_branch()
folgt, enthlt die Struktur bh_result einen Eintrag auf die gewnschte Page.
8.6.4

Bereitstellung von Speicherplatz

Wichtig ist beim Bereitstellen von Speicherplatz, die Datenblcke so


auszuwhlen, dass sie weitgehend nahe benachbart gespeichert werden. Wie
beim Lesen wird dabei auf die Funktion ext2_get_block() zugegriffen. Dabei
hat beim Schreiben das bergebene Argument create den Wert 1.
Ebenso wie beim Lesen muss zunchst einmal diejenige Stelle gefunden
werden, an der der Block eingefgt werden soll. Es wird ebenfalls der in Listing
8.35 dargestellte Teil der Funktion durchlaufen. In diesem Fall gibt aber die
Funktion ext2_get_branch() nicht NULL zurck, vielmehr enthlt die Struktur
partial einen Zeiger auf die Stelle, an der ein Zeiger fr einen neuen Block
eingefgt werden muss. Wesentlich ist nun, wie Blcke zum Einfgen in der
Partition bereitgestellt werden. Die Suche nach geeigneten Blcken wird in
ext2_find_goal() durchgefhrt.
Wird sequentiell geschrieben, so sollte der neue Block in der Partition nach
Mglichkeit auf den vorher geschriebenen folgen, denn dadurch wird beim Lesen
das Readahead untersttzt (vgl. S. 177).
Gelingt es der Funktion ext2_find_goal() nicht, den nchsten Block zu
belegen oder wird nicht sequentiell geschrieben, so wird versucht, einen nahe

103
104

Definiert in fs/direct-io.c.
Beide sind in der Datei fs/ext2/inode.c enthalten.

196

8 Dateisysteme und Plattenverwaltung


static int ext2_get_block(struct inode *inode, sector_t iblock,
struct buffer_head *bh_result, int create)

{
int err = -EI0; int offsets[4];
Indirect chain[4];
Indirect *partial; unsigned long goal; int left; int
boundary = 0;
int depth = ext2_block_to_path(inode, iblock, offsets,
&boundary);
if (depth == 0) goto out; reread:
partial = ext2_get_branch(inode, depth, offsets, chain,
&err); /* Simplest case - block found, no allocation needed */
if (!partial) { got_it:
map_bh(bh_result, inode->i_sb,
le32_to_cpu(chain[depthl].key)); if (boundary)
set_buffer_boundary(bh_result);
/* Clean up and exit */
partial = chain+depth-1; /* the whole chain
*/ goto cleanup;

Listing 8.35. Ausschnitt aus ext2_get_block() zum Auffinden von Blcken


goal = 0;
if (ext2_find_goal(inode, iblock, chain, partial, &goal) <
0) goto changed;
left = (chain + depth) - partial;
err = ext2_alloc_branch(inode, left, goal,
offsets+(partial-chain), partial);
if (err)
goto cleanup;
if (ext2_splice_branch(inode, iblock, chain, partial, left) < 0)
goto changed;
set_buffer_new(bh_result);
goto got_it;

Listing 8.36. Ausschnitte aus ext2_get_block() zum Einfgen von Blcken

8.7 Zusammenfassung 197

benachbarten Block mit Hilfe von ext2_find_near() zu finden. Die Kriterien dafr
knnen so beschrieben werden:

Wenn es einen direkten Vorgngerblock gibt, so ist der nachfolgende


Block unser Wunschziel,
wenn es einen Zeiger in einem Indexblock gibt, der auf den
anzulegenden Block zeigt, so sollte der Block nahe dieses Indexblockes
angesiedelt werden,
wenn der Zeiger auf den Block in den ersten 12 Zeigern der Inode
untergebracht ist, dann wird der Block in einem der ersten freien
Datenblcke der Blockgruppe gespeichert.

Damit auch weiterhin bei sequentiellem Schreiben aufeinanderfolgende Blcke


zur Verfgung stehen, wird mit ext2_alloc_branch() versucht, vorab von der
gefundenen Stelle ausgehend eine zusammenhngende Reihe von Blcken
bereitzustellen und mit ext2_splice_branch() in die Inode einzuhngen.
Die vorangehenden Betrachtungen sind sehr knapp gehalten: so wurde unter
anderem nicht betrachtet, was passiert, wenn die aufgerufenen Funktionen
fehlschlagen oder was erfolgen muss, wenn der nchste zu schreibende Block
keine Verwendung fr die alloziierten Blcke hat, weil er nicht sequentiell auf
den vorausgegangenen folgt usw. Eine Reihe von Details des Filesystems ext2
sind bei diesen Betrachtungen auer Acht gelassen worden, andere konkrete
Filesysteme werden berhaupt nicht betrachtet.

8.7

Zusammenfassung

Ziel dieses Kapitels war es, Filesysteme und deren Einbindung unter Linux zu
betrachten. Um zu verstehen, wie der Weg vom Betriebssystem zu einem
konkreten Filesystem auf einer Platte aufgebaut ist, musste zunchst das
Virtuelle File System (VFS) betrachtet werden, das eine Abstraktion konkreter
Filesysteme darstellt.
Die Datenstrukturen, die das VFS im Speicher aufbaut, wurden im Ab- schn.
8.2 ausfhrlich dargestellt; dabei wurde deutlich, an welchen Stellen das VFS
Informationen vorhlt, um ber die konkret verwendeten Filesysteme Bescheid
zu wissen.
Die wichtigsten System Calls im Zusammenhang mit Filesystemen - creat(),
open(), read(), write(), close() sowie mkdir(), rmdir() und link(), unlink() usw. wurden an Hand von Beispielen in Abschn. 8.3 vorgestellt.
Im Abschnitt 8.4 wurde im Detail nachvollzogen, wie eine Leseanforderung
durch die Funktionen des VFS hindurch unter Einbeziehung der
Speicherverwaltung bis zum Gertetreiber durchgereicht wird. Dabei wurde
insbesondere das vorausschauende Lesen als Methode der PerformanceSteigerung erkannt.
Dass auch die Reihenfolge, in der die auf dem Datentrger physisch
gespeicherten Blcke gelesen werden, sich auf die Performance auswirkt, zeigte

198

8 Dateisysteme und Plattenverwaltung

der Abschn. 8.5, in dem Strategien fr den Zugriff auf die Platten vorgestellt
wurden. Durch Umsortieren und Zusammenfassen von Schreib- und
Lesezugriffen kann die Bewegung des Plattenarms optimiert werden. Dabei ist
es wichtig, Starvation zu vermeiden.
Der letzte, sehr knapp gehaltene Abschn. 8.6 zeigt dann ausschnittsweise die
konkrete Implementierung des Filesystems ext2. Ausgewhlt wurde gerade
dieses System auf Grund der hnlichkeit zwischen den VFS- und ext2Strukturen. Das Ende dieses Abschnitts zeigt, wie in diesem Filesystem auf
unterster Ebene die Abbildung zwischen logischen und physischen Dateiblcken
erfolgt; dieses Wissen ist notwendig, um von der Blocknummer, so wie sie VFS
sieht, auf den physischen Speicherplatz schlieen zu knnen. Auerdem wurde
deutlich, dass beim Schreiben neuer Blcke darauf geachtet wird, diese gut zu
platzieren. Dabei werden mglichst zusammenhngende Blcke fr die
physischen Dateien bereitgestellt, um beim spteren sequentiellen Lesen das
vorausschauende Lesen ausnutzen zu knnen.

9
Kommunikation zwischen Prozessen

9.1 Grundlagen
Ein einfaches Beispiel zeigt, dass ein Betriebssystem nicht nur Mittel
bereitstellen muss, damit Prozesse synchronisiert werden knnen, sondern auch
dafr sorgen muss, dass Prozesse Daten untereinander austauschen knnen:
Programme wie ls zum Auflisten des Directory-Inhalts und more1 zum
seitenweisen Anzeigen gibt es fr nahezu alle Betriebssysteme. Gelingt es, die
Ausgabe des ersten in die Eingabe des zweiten umzulenken, so kann man, wenn
das Directory viele Eintrge enthlt, trotzdem in Ruhe lesen und dann den
Bildschirm weiterblttern.
Natrlich kann man dieses Problem sehr einfach lsen: man schreibt die
Ausgabe von ls in eine Datei, wendet darauf more an und gibt die so
entstandene Datei am Bildschirm aus. Dabei muss aber die langsame Platte
bemht werden. Fr dieses Beispiel mag das vielleicht noch nicht sehr strend
sein, aber es lassen sich weitere Beispiele finden, bei denen man sich diesen
Zwischenschritt aus Performance-Grnden ersparen mchte.
Aus diesem Grunde haben alle Unix-Varianten sogenannte Pipes entwickelt.
Pipes knnen als Dateien ohne Plattenbeteiligung betrachtet werden, d.h. sie
werden im Virtual File System (siehe Abschn. 8.2) wie normale Dateien
angelegt, ohne dass eine Datei auf der Platte erzeugt wird. Die gesamte Arbeit
findet im Hauptspeicher statt, dort gibt es auch einen Puffer, in dem die
Nachrichten zwischengespeichert werden, bis sie von einem anderen Prozess
gelesen werden.
Pipes werden von einem Prozess mit dem pipe()-Aufruf angelegt und mittels
Vererbung beim Erzeugen eines Kindprozesses weitergegeben. Wenn es nun
gelingt, die zugehrigen File-Handles bei einer berlagerung durch exec() zu
erhalten, kann direkt auf die Pipe zugegriffen werden. Mit Hilfe von dup() kann
eine Umlenkung auf die Standard-Ein- bzw. -Ausgabe vorgenommen werden.
Bei anderen Betriebssystemen mgen die Namen anders heien.

200

9 Kommunikation zwischen Prozessen

Der Nachteil des Verfahrens liegt ganz offensichtlich darin, dass nur
Prozesse, die mittels fork() eng verwandt sind, auf diese Weise miteinander
kommunizieren knnen. Um dem abzuhelfen, wurden Named Pipes eingefhrt.
Diese werden mit Hilfe des Kommandos mknod angelegt. Nach dem Anlegen
knnen Prozesse ber den Namen auf die Named Pipe zugreifen, sofern die
ntigen Rechte vorhanden sind. Auf diese Weise lassen sich auch
Kommunikationen zwischen Prozessen aufbauen, die nicht durch eine Folge von
fork()-Aufrufen miteinander zusammenhngen. Abschnitt 9.2 beschftigt sich
mit beiden Arten von Pipes.
Beide Arten von Pipes haben einen weiteren Nachteil: die Nachrichten
werden FIFO (First In First Out) gelesen - deshalb werden Named Pipes auch
FIFO genannt. Beteiligen sich mehrere Prozesse an einer Pipe, dann gibt es
keine Mglichkeit, gezielt Nachrichten zu versenden. Auerdem mssen sich die
Prozesse beim Zugriff auf die Pipe gut synchronisieren. Aus diesem Grunde ist
ein weiterer Mechanismus entwickelt worden, der - wie Shared Memory (vgl.
Abschn. 5.3) und IPC-Semaphoren (vgl. Abschn. 6.4.2) - unter den Oberbegriff
IPC (Inter Process Communication) fllt, nmlich die Message Queue. Im
Abschnitt 9.3.1 wird auf die gesamten Grundlagen der IPC-Objekte eingegangen,
in den Abschnitten 9.3.2-9.3.5 werden fr die drei verschiedenen Objekte die
Implementierungen betrachtet und in 9.3.3 insbesondere auch der Einsatz von
Message Queues gezeigt.
Sowohl Pipes wie auch Message Queues haben gemeinsam, dass sie nur
Prozesse kommunizieren lassen, die auf demselben Rechner laufen. Im Zeitalter
vernetzter Systeme wird eine weitere Mglichkeit bentigt, die Prozesse auf
unterschiedlichen Rechner befhigt, miteinander Daten auszutauschen. Diese
Art der Kommunikation wird durch Sockets ermglicht, auf die in Abschn.
9.4 eingegangen wird.

9.2

Pipes

9.2.1

Der prinzipielle Aufbau

Der pipe()-Aufruf erzeugt ein Inode-Objekt sowie zwei File-Objekte. Damit kann
der Prozess anschlieend mit read() und write()-Aufrufen darauf zugreifen. Das
erzeugte Inode-Objekt (vgl. Abschn. 8.2.1) verweist mit dem Feld i_pipe auf eine
zugleich angelegte Struktur pipe_inode_info2 (vgl. Listing 9.1).
Wie dieser Struktur zu entnehmen ist, wird zustzlich noch ein eigener PipePuffer angelegt, auf den base verweist. Dieser Puffer besteht aus einer Page, die
die geschriebenen und noch nicht gelesenen Zeichen enthlt. Die Verwaltung
dieses Puffer erfolgt zirkulr: ist beim Schreiben das Ende der Page erreicht und
ist nicht die gesamte Lnge ausgeschpft, so wird das nchste

Definiert in include/linux/pipe_fs_i.h.

9.2 Pipes

201

struct pipe_inode_info {
wait_queue_head_t wait;/* Warteschlange blockierter
Prozesse, die auf die Pipe
zugreifen wollen */
/* Adresse des Kernel Buffers */
char *base;
/* Anzahl geschriebener, aber noch nicht
unsigned int len;
gelesener Bytes */
/*
Leseposition
im Buffer */
unsigned int start;
/*
Anzahl
lesender
Prozesse */
unsigned int readers;
/*
Anzahl
schreibender
Prozesse */
unsigned int writers;
unsigned int waiting_writers;
unsigned int r_counter;
unsigned int w_counter;
struct fasync_struct *fasync_readers;
struct fasync_struct *fasync_writers;
>;
Listing 9.1. Die Struktur pipe_inode_info

Zeichen an den Anfang der Page geschrieben, start zeigt auf das nchste zu
lesendeZeichen, (start + len) modulo(Pagesize) zeigtaufdasnchstezu schreibende
Zeichen in der Page. Die maximale Kapazitt einer Pipe ergibt sich somit aus der
Gre einer Page. Weitere Zeichen kann die Pipe erst dann aufnehmen, wenn aus
ihr gelesen wurde.
Da mehrere Prozesse unabhngig voneinander lesend und schreibend auf die
Pipe zugreifen knnen, muss fr eine Synchronisation gesorgt werden, damit
kein Datenverlust durch Race Conditions auftritt. Das ist die Aufgabe der
Semaphore i_sem, die in der inode-Struktur definiert ist. Jeder Schreib- bzw.
Lesevorgang bzgl. der Pipe beantragt zunchst diese Semaphore. Kann der
jeweilige Auftrag nicht erfllt werden, weil in die gefllte Pipe geschrieben oder
aus der leeren Pipe gelesen werden soll, so wird der anfordernde Prozess in der
Regel blockiert, d.h. an die Warteschlange mit dem Kopf wait angehngt und die
Zhler waiting_writers bzw. r_counter (Anzahl der Prozesse, die auf neue Zeichen
in der Pipe warten) bzw. w_counter (Anzahl der Prozesse, die darauf warten,
Zeichen in die Pipe schreiben zu knnen) erhht, bevor die Semaphore wieder
freigegeben wird. Muss der Prozess nicht warten, so werden die Zeichen
vollstndig in die Pipe geschrieben bzw. aus der Pipe gelesen und die Semaphore
anschlieend wieder freigegeben. Beim Aufwecken eines Prozesses aus der
Warteschlange werden ebenfalls die Zhler entsprechend korrigiert, bevor die
Prfung stattfindet, ob die gewnschte Aktion ausgefhrt werden kann.
9.2.2

Beispiel zur Benutzung einer Pipe

Die Listings 9.2 und 9.3 zeigen den Datenaustausch mit Hilfe einer Pipe. Die
Pipe wird im Elternprozess angelegt, der anschlieend zwei Kindprozesse

9 Kommunikation zwischen Prozessen


/* der Elternprozess, der die pipe erzeugt und
zwei Kinder mittels fork() anlegt, die ber die pipe
kommunizieren */
#include <unistd.h> main (int argc, char
*argv[]) { int fhandle[2]; int rc;
pipe(fhandle)
; rc =
fork(); if
(rc == 0) {
/* Schlieen der Standard-Eingabe,
Umlenken von fhandle[0] auf die StandardEingabe, Schlieen der nicht mehr bentigten
fhandle[0] */ close(0); dup(fhandle [0]);
close(fhandle [0]);
/* der nun gestartete Prozess liest bei Zugriff auf
die Standard-Eingabe aus der Pipe */ rc =
execl("kindl","Kind l","\0");

else {
rc = fork();
if (rc == 0)
{
/* Schlieen der Standard-Ausgabe,
Umlenken von fhandle[l] auf die StandardAusgabe, Schlieen der nicht mehr bentigten
fhandle[l] */ close(l); dup(fhandle [1]);
close(fhandle [1]);
/* der nun gestartete Prozess schreibt bei Zugriff
auf die Standard-Ausgabe in die Pipe, zwischen den
beiden Kindprozessen ist dadurch eine EinwegKommunikation mittels Pipe erzeugt worden */
execl("kind2","Kind 2","\0");

>

>

sleep(l);
exit(0);

Listing 9.2. Verwendung des pipe()-Aufrufs 1. Teil

9.2 Pipes 203


/* kindl: Programm, das vom ersten Kind ausgefhrt
wird, liest aus der pipe */ main (int argc, char
*argv[]) { char buffer[100]; read(0, buffer, 28);
write(l, buffer, 28); exit(0);

/* kind2: Programm, das vom zweiten Kind ausgefhrt


wird, schreibt in die pipe */ main (int argc, char
*argv[]) {
write(l, "Dies erscheint in der Queue\n",
28); exit(0);

Listing 9.3. Verwendung des pipe()-Aufrufs 2. Teil

startet. Der eine Kind-Prozess schliet seine Standard-Eingabe, dupliziert das


erste Pipe-Handle auf die Standard-Eingabe und schliet es. Danach wird der
Adressraum mit einem neuen Programm berlagert, das aus der
StandardEingabe liest und auf die Standard-Ausgabe - das Terminal - schreibt.
Der andere Kind-Prozess geht entsprechend mit der Standard-Ausgabe und dem
zweiten Pipe-Handle um. Das berlagerte Programm schreibt nur einen Satz in
die Standard-Ausgabe. Dieses Beispiel macht zugleich noch einmal deutlich, dass
Prozesse, die ber Pipes miteinander kommunizieren, durch fork() (ggf. ber eine
kurze Hierarchie von fork()-Aufrufen) auseinander hervorgegangen sein mssen.
Das Lesen erfolgt durch die read()-Funktion in der Form
rc = read(fhandle[0], block, anzahl);

Dies entspricht der Darstellung in Abschn. 8.3. rc gibt die Anzahl der tatschlich
gelesenen Zeichen an. Ist die Pipe beim read()-Aufruf leer, so wird der Aufruf
blockiert, bis wieder Zeichen in die Pipe geschrieben worden sind. Entsprechend
erfolgt das Schreiben in die Pipe durch
rc = write(fhandle[l], block, anzahl);

Auch dieser Aufruf wird bei Bedarf blockiert.


9.2.3

Nicht-blockierender ZugrifF

Die bisherigen berlegungen gehen davon aus, dass Schreib- oder Lesezugriffe,
die nicht vollstndig erfllt werden knnen, solange blockiert werden, bis die
Pipe sie komplett bedienen kann. Es ist aber auch mglich, die Schreib- und
Lese-Operationen nicht blockierend auszufhren. In diesem Falle kehrt der
Aufruf mit -EAGAIN zurck, wenn beim Lesen nicht gengend Zeichen in der
Pipe sind bzw. beim Schreiben nicht gengend Platz.

204

9 Kommunikation zwischen Prozessen


#include <unistd.h>
#include <fcntl.h> main (int
argc, char *argv[]) { int
fhandle [2]; int rc;
pipe(fhandle); rc =
fork(); if (rc ==
0) { close(l);
dup(fhandle[1]);
close(fhandle[1]);
sleep(2);
write(l, "Satzl\n", 7); sleep(3);
write(l, "Satz2\n", 7); sleep(l);
write(l, "Satz3\n", 7);
exit(0);

else {
/* Schlieen der Standard-Eingabe,
Umlenken von fhandle[0] auf die StandardEingabe, Schlieen der nicht mehr bentigten
fhandle[0] */ int i = 0; char block[100]; close(0);
dup(fhandle[0]); close(fhandle[0]); fcntl(0,
F_SETFL, 0_N0NBL0CK);
/* Eingestellt ist nicht-blockierendes Lesen.
Durch Auskommentieren der vorhergehenden Zeile
wird das Lesen wie bislang blockierend */ for (i =
0; i < 21;) { rc= read(0, block, 21); write(l,
":", 1); if (rc > 0) {
write(l, block,
rc); i+=rc;

Listing 9.4. Blockierendes und nicht-blockierendes Lesen aus einer Pipe


Kompilieren und Ausfhren mit der Zeile fcntl(. . . ) und ohne diese Zeile zeigt die
Unterschiede sehr deutlich.

9.2 Pipes 205

Dazu muss mit dem fcntl()-Aufruf das jeweilige Pipe-Handle auf nichtblockierend eingestellt werden. Die Ausfhrung des Beispiels in Listing 9.4 zeigt,
wie sich blockierendes bzw. nicht-blockierendes Lesen auswirkt.
Bei nicht-blockierendem Lesen gibt der read()-Aufruf einen negativen Wert,
d.h. einen Fehler zurck, wenn die Pipe leer ist. Als Folge werden viele
Doppelpunkte auf dem Bildschirm angezeigt.
9.2.4

Schlieen einer Pipe

Was passiert, wenn eine Pipe durch einen der beteiligten Prozesse geschlossen
wird? Unter pipe_read_release() bzw. pipe_write_release() ist der zugehrige
Code zu finden: beide Funktionen rufen pipe_release()3 auf. Dort wird berprft,
ob noch lesende oder schreibende Prozesse vorhanden sind. Diese werden mit
dem Signal SIGI0 geweckt, damit sie auf die nderung der Pipe reagieren
knnen. Ist kein Prozess mehr vorhanden, der auf die Pipe zugreifen will, so
werden die entsprechenden Datenstrukturen freigegeben.
9.2.5

Named Pipes (FIFO)

Named Pipes unterscheiden sich von den bisher betrachteten dadurch, dass sie
mit einem Namen verbunden sind. Dadurch knnen auch nicht verwandte
Prozesse sich dieser Strukturen als Kommunikationsmittel bedienen. Jeder
Benutzer kann ber das auf Shell-Ebene verfgbare Kommando mknod oder
ber den System Call mknod() eine Named Pipe anlegen. Dennoch werden beim
Zugriff auf diese Struktur keine Daten auf die Platte geschrieben, vielmehr legt
das VFS bei Zugriff auf diese Datei Datenstrukturen an, die denen der Pipe
entsprechen.4 Der Code in Listing 9.5 zeigt den Umgang mit Named Pipes. In
diesem Beispiel wurde der System Call mknod() benutzt. Will man eine Named
Pipe direkt von der Kommandozeile aus anlegen, so lautet der Befehl
mknod pfad/dateiname p

In dieser Form kann der Befehl mknod, der eigentlich fr den Superuser zum
Anlegen von Special Devices gedacht ist, von jedem Benutzer aufgerufen werden.
Um die Datenstrukturen zu erzeugen, muss die Named Pipe zunchst mit
open() geffnet werden. Sobald schreibende als auch lesende Prozesse vorhanden
sind, kann die Kommunikation mit read() und write() wie bisher erfolgen.
Das Offnen einer Named Pipe erfolgt mit fifo_open()5. Diese Funktion basiert
darauf, dass eine Named Pipe benutzt wird. Auerdem sorgt sie dafr,

Alle drei Funktionen sind in fs/pipe.c enthalten.


Die Informationen ber eine Named Pipe werden aber in Form einer Inode auf der
Platte angelegt und gespeichert.
5
fifo_open() ist definiert in f s / fi f o . c .
3
4

206

9 Kommunikation zwischen Prozessen


#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h> main
(int argc, char *argv[]) { int
rc;
mknod("myfifo", 0600,
S_IFIF0); rc = fork(); if (rc
== 0) { int fd;
fd = open("myfifo",
0_WR0NLY); write(fd, "Satz
l\n", 7); close(fd);
exit(0);

else { int fd;


char block[20];
fd = open("myfifo",
0_RD0NLY); rc= read(fd,
block, 7); write(l, ":\ 1);
if (rc > 0)
write(l, block, rc); else
printf("Fehler: %d\n", rc);
close(fd); exit(0);

Listing 9.5. Beispiel fr die Verwendung einer Named Pipe (FIFO)

dass beim ffnen einer Named Pipe zum Lesen bzw. zum Schreiben solange
blockiert wird, bis zumindest ein Prozess mit gegenteiligem Modus, d.h. zum
Schreiben oder Lesen, die Pipe ffnet. Der Aufruf von fifo_open() erfolgt
automatisch durch den open()-Aufruf, da in der Struktur file_operations (vgl.
Listing 8.1 und 8.4) der Aufruf open() durch fifo_open() ersetzt wird.

9.3

IPC - Inter Process Communication

Die zwei IPC-Objekte Semaphoren (vgl. Abschn. 6.4.2) und Shared Memory (vgl.
Abschn. 5.3.3) haben wir bereits kennengelernt. Als drittes IPC-Objekt wird in
diesem Kontext die Message Queue behandelt. Da alle drei Objekte eine
hnliche Verwaltung besitzen, soll diese zunchst betrachtet werden.

9.3 IPC - Inter Process Communication 207

9.3.1

IPC-Grundlagen

Die Datenstrukturen und Funktionen sind im Directory include/linux in den


Dateien ipc.h, msg.h, sem.h, shm.h sowie im Directory ipc in den Dateien msg.c,
sem.c, shm.c und util.c zu finden.
Jedem IPC-Objekttyp sowie jedem IPC-Objekt liegen Datenstrukturen
zugrunde, die in util.h bzw. ipc.h definiert sind. Listing 9.6 zeigt die Verwaltung
der Objekttypen. Fr Message Queues, Semaphoren und Shared Memory wird
jeweils eine solche Datenstruktur angelegt, die im Feld entries auf ein Array
verweist, in dem Informationen ber die angelegten Objekte des jeweiligen Typs
verwaltet werden.
Die durch ipc_id vereinbarte Struktur lautet
struct ipc_id { struct kern_ipc_perm* p; }

In dem Array werden Zeiger auf Datenstrukturen kern_ipc_perm verwaltet,


deren Aufgabe es ist, den IPC-Schlssel und Zugriffsrechte fr das jeweilige IPCObjekt zu verwalten.
Die anderen Felder der Struktur ipc_ids beschreiben unter anderem die
maximal erlaubte Anzahl von Objekten dieses Typs (size) sowie die Anzahl
derzeit angelegter Objekte in_use. Die Semaphore sem dient zur Vermeidung von
Race Conditions bei der internen Verwaltung.
Listing 9.7 zeigt die Datenstruktur kern_ipc_perm. Die Zugriffsrechte
werden analog zu den Zugriffsrechten des Filesystems verwaltet. Dazu werden in
der Datenstruktur die User-ID (uid) und Group-ID (gid) des Besitzers, sowie die
User-ID (cuid) und Group-ID (cgid) des erzeugenden Prozesses gespeichert. Die
Zugriffsrechte werden in mode festgehalten.
struct ipc_ids { int
size ; int in_use; int
max_id; unsigned short
seq; unsigned short
seq_max; struct
semaphore sem; struct
ipc_id* entries;

};

Listing 9.6. Struktur zur Adressierung der IPC-Objekttypen

Frage: Fr ein Shared Memory Objekt wird mehr als nur der IPC-Schlssel und
die Zugriffsrechte bentigt. Wie kann nun von entries auf die weiteren bentigten
Informationen eines konkreten IPC-Objekts zugegriffen werden?
In den folgenden Abschnitten werden wir sehen, dass kern_ipc_perm jeweils zu
Beginn der gesamten Struktur eines konkreten IPC-Objektes steht. Ein

208

9 Kommunikation zwischen Prozessen


struct kern_ipc_perm {
spinlock_t lock;
int deleted;
key_t key;
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
unsigned long seq;
void *security;

>;

Listing 9.7. Grundlegende IPC-Datenstruktur


Zeiger auf diese Struktur zeigt somit zugleich auf den Anfang eines konkreten
IPC-Objektes.
9.3.2

Message Queues

Die Struktur eines Message Queue Objekts wird in msg.h beschrieben. Listing
9.8 zeigt diese Datenstruktur, an deren Anfang sich - wie eben erlutert - die
Struktur kern_ipc_perm befindet.

struct msg_queue {
struct kern_ipc_perm q_perm;
time_t q_stime; /* last msgsnd time*/
time_t q_rtime; /* last msgrcv time*/
time_t q_ctime; /* last change time*/
unsigned long q_cbytes; /* current number of bytes on queue */
unsigned long q_qnum; /* number of messages in queue */
unsigned long q_qbytes; /* max number of bytes on queue
*/ pid_t q_lspid; /* pid of last msgsnd */ pid_t
q_lrpid; /* last receive pid */
struct list_head q_messages;
struct list_head
q_receivers; struct
list_head q_senders;

>;

Listing 9.8. Struktur einer Message Queue


Auf die Rechtestruktur q_perm folgen drei Zeitangaben, die beschreiben, wann
zuletzt eine Nachricht in die Message Queue gestellt, wann zuletzt eine
Nachricht ausgelesen und wann zuletzt eine nderung vorgenommen wurde. Die

9.3 IPC - Inter Process Communication 209

nchsten drei Eintrge geben die derzeitige Lnge der Nachrichten in dieser
Message Queue sowie die Anzahl von Nachrichten und die maximale Lnge von
Zeichen an. Daran schliet sich die Information ber die IDs derjenigen Prozesse
an, die als letzte eine Nachricht in die Queue gestellt bzw. daraus gelesen haben.
Die letzten drei Eintrge sind Listen, die verwendet werden, um die
Nachrichten (q_messages), schlafende Empfnger (q_receivers) und schlafende
Sender (q_senders) zu verwalten.
Jeder Eintrag in qunessage entspricht einer msg_msg-Struktur, wie sie in
Listing 9.10 dargestellt ist. Dies kann z.B. den Zeilen in Listing 9.9 entnommen
werden.

tmp = msq->q_messages.next;
while (tmp != &msq->q_messages) {
msg = list_entry(tmp,struct msg_msg,m_list);

Listing 9.9. Code-Ausschnitt aus msg.c

Jede Nachricht wird dabei auf einer neuen Page angelegt. Deshalb ist auch kein
eigener Datenteil fr die eigentliche Nachricht ntig, denn die Daten folgen in der
Page direkt auf die msg_msg-Struktur. Die Felder dieser Struktur dienen zum
Verketten der Nachrichten (m_list), zur Beschreibung des Nachrichtentyps
(m_type) und zur Lngenangabe der Nachricht (m_ts). next wird fr lange
Nachrichten verwendet, die nicht auf eine Page passen.
struct msg_msg { struct
list_head m_list; long m_type;
int m_ts;

/* message text size */

struct msg_msgseg* next; void


*security;
/* the actual message follows immediately */

>;

Listing 9.10. Struktur der Nachrichten

9.3.3

Benutzung von Message Queues

Message Queues werden wie Semaphoren verwaltet: das Erzeugen bzw. der
Zugriff auf eine bereits angelegte Message Queue erfolgt mit dem Aufruf
msgget(), der den IPC-Schlssel und die Rechtestruktur bergeben bekommt.
Zurckgegeben wird die Programm-interne Message-ID, ber die dann das

210

9 Kommunikation zwischen Prozessen

Objekt identifiziert wird. Auch das Entfernen einer nicht mehr bentigten
Message Queue gleicht dem einer Semaphore: der Aufruf ist msgctl(). Schreiben
in die Queue und Lesen aus der Queue erfolgt mit den Funktionen msgsnd()
undmsgrcv().
Listing 9.11 zeigt ein Programm, das eine Message Queue neu anlegt und in
die angelegte Message Queue schreibt. Wie alle IPC-Objekte verbleibt die
angelegte Message Queue im System, bis sie gelscht oder das System
heruntergefahren wird.
Listing 9.12 zeigt ein Programm, das auf die angelegte Message Queue
zugreift und versucht, eine Nachricht auszulesen. Das Flag IPC_NOWAIT wird
hier benutzt, damit der Funktionsaufruf nicht blockiert. Der Rckgabewert der
Funktion msgrcv() gibt an, wie viele Zeichen die ausgelesene Nachricht enthlt,
die nach dem Auslesen aus der Message Queue entfernt wird.
/* Header Datei mymsg.h */
key_t MYKEY = (key_t) 4711;
struct msgbuf {
long mtype;
char mtext
[100];

>;

/* ======================= */

/* Programm sende.c zur Benutzungeiner Message Queue


*/ #include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/msg.h>
#include <sys/types.h>
#include
"mymsg.h" main()
{
int mymsgid;
struct msgbuf nachricht,
ergebnis; int laenge; int rc;
mymsgid = msgget(MYKEY, IPC_CREAT|0600);
nachricht.mtype = (long) 5;
strcpy(nachricht.mtext,"Dies ist ein
Test"); rc = msgsnd(mymsgid, &nachricht,
18, 0); printf("Ergebnis von msgsnd:
%d\n", rc);
/* ... */ exit(0);

>
Listing 9.11. Message Queue: Anlegen und Nachricht schreiben

9.3 IPC - Inter Process Communication 211


/* Programm lies.c benutzt eine existierende Message Queue */
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/msg.h>
#include <sys/types.h>
#include "mymsg.h" main() {
int mymsgid;
struct msgbuf nachricht, ergebnis; int laenge,
rc, mestyp; mymsgid = msgget(MYKEY, 0600);
printf("Test02, Message Queue ID: %d\n", mymsgid);
/* ... */ laenge = 100; mestyp = 0;
rc = msgrcv(mymsgid, &ergebnis, laenge, mestyp,
IPC_N0WAIT); printf("Ergebnis von msgrcv: #/,d; Lnge: /0d;
Text: %s\n", rc, laenge, ergebnis.mtext); msgctl(mymsgid,
IPC_RMID, 0); exit(0);

>
Listing 9.12. Message Queue: Auslesen und Entfernen

Bislang wurde nicht auf die Struktur einer Nachricht sowie auf das 4. Argument
der Funktion msgrcv() eingegangen. Diese knnen benutzt werden, um die
Nachrichten einer Message Queue zu strukturieren und in verschiedene
Klassen einzuteilen. In der Struktur einer Nachricht muss mtype eine Integer
mit einem Wert grer als 0 sein. Das 4. Argument der Funktion msgrcv() - vgl.
mestyp in Listing 9.12 - darf jedoch eine Integer mit beliebigem Wert sein. Das
Ergebnis des Aufrufs hngt entscheidend von diesem Argument mestyp ab:
mestyp = 0: Das Auslesen erfolgt FIFO, die zuerst eingestellte Nachricht wird

ausgelesen.

mestyp > 0: In diesem Fall wird die erste Nachricht mit Typ mtype = mestyp

ausgelesen. Falls jedoch die Option MSG_EXCEPT benutzt wird, ndert sich
das Verhalten dahingehend, dass die erste Nachricht eingelesen wird, deren
Typ nicht mit mestyp bereinstimmt.
mestyp < 0: Die erste Nachricht, deren Typ kleiner oder gleich dem Betrag von
mestyp ist, wird gelesen.
Die Funktion msgctl() kann benutzt werden, um - hnlich wie bei Semaphoren Informationen aus der Struktur q_perm (vgl. Listing 9.8) auszulesen bzw. zu
ndern.

212

9 Kommunikation zwischen Prozessen

9.3.4

Semaphoren

Frjede IPC-Semaphore wird eine Datenstruktur sem_array angelegt. Listing


9.13 zeigt, dass auch hier eine Struktur vom Typ kern_ipc_perm am Anfang
steht (vgl. Fragestellung und Lsung auf S. 207).
struct sem_array {
struct kern_ipc_perm sem_perm; /* permissions .. see ipc.h */
time_t
sem_otime; /* last semop time */
time_t
sem_ctime; /* last change time */
struct sem
*sem_base; /* ptr to first semaphore in array */
sem_queue *sem_pending;
/* pending operations to be processed */
sem_queue **sem_pending_last; /* last pending operation */
sem_undo *undo; /* undo requests on this array */
unsigned long sem_nsems; /* no. of semaphores in array */

};

Listing 9.13. sem_array: IPC-Semaphoren zugrunde liegende Datenstruktur

Die Eintrge sem_otime und sem_ctime entsprechen den Eintrgen der Message
Queue. Der letzte Eintrag semjnsems gibt an, wie viele Semaphoren-Werte in
dieser Semaphore zusammengefasst werden.
Der Eintrag *sem_base ist ein Zeiger auf das Array, das die SemaphorenWerte verwaltet. Die Struktur ist in Listing 9.14 dargestellt: der erste Teil
beschreibt den aktuellen Wert, der zweite enthlt die Prozess-ID desjenigen
Prozesses, der als letzter diesen Wert verndert hat.

};

struct sem {
int semval; /* current value */
int sempid; /* pid of last operation */

Listing 9.14. Struktur zur Beschreibung der einzelnen Semaphoren-Werte

Die nchsten beiden Eintrge in sem_array enthalten diejenigen Operationen


auf der Semaphore, die noch nicht ausgefhrt werden konnten, *sem_pending
bildet den Kopf einer verketteten Liste solcher Operationen, zum schnelleren
Zugriff zeigt **sem_pending_last auf den letzten Eintrag dieser Liste. Die
zugrundeliegende Struktur ist in Listing 9.15 dargestellt.
Die ersten beiden Eintrge in der Struktur sem_queue zeigen, dass hier die
doppelt verkettete Liste selbst verwaltet wird, d.h. es wird nicht auf die sonst
blichen Hilfsmittel aus include/linux/list.h zurckgegriffen. Der Eintrag sleeper
verweist auf den PCB des blockierten Prozesses (vgl. task_struct in

9.3 IPC - Inter Process Communication

213

struct sem_queue
{ struct sem_queue next; /* next entry in the queue */
* struct sem_queue prev; /* previous entry in the queue,
**
*(q->prev) == q */
sleeper; /* this process */
struct
undo; /* undo structure */
task_struct*
pid; /* process id of requesting process */
struct sem_undo * status;/* completion status of operation */
int int
sma; /* semaphore array for operations */
struct sem_array * id; /* internal sem id */
int
sops; /* array of pending operations */
struct sembuf *
nsops; /* number of operations */
int

>;

Listing 9.15. Struktur der auf die Semaphore wartenden Operationen

Listings 3.1-3.3). undo enthlt die Informationen, die fr das Betriebssystem


ntig sind, um die Operation bei Beendigung des Prozesses rckgngig zu
machen, wenn der Prozess es nicht schon selbst gemacht hat (vgl. S. 104). pid ist
die Prozess-ID des blockierten Prozesse, *sops zeigt auf ein Array, das die
durchzufhrenden Semaphoren-Operationen enthlt und nsops gibt die Lnge
dieses Arrays an, d.h. die Anzahl der Operationen.
Die Struktur von sem_undo wird in Listing 9.16 gezeigt. Auch hier handelt es sich
um eine verkettete Liste, die mit dem ersten Zeiger die Undo-Operationen eines
Prozesses verknpft und mit dem zweiten Zeiger die Undo-Operationen einer
Semaphore verbindet, semid gibt an, welche Semaphore angesprochen wird,
semadj ist ein Array, das die Korrekturen fr die vorliegende Semaphore und den
vorliegenden Prozess beschreibt. Die Lnge des Arrays richtet sich nach der
Anzahl der Semaphoren-Werte.
struct sem_undo {
struct sem_undo * proc_next; /* next entry on this process*/
struct sem_undo * id_next;
/* next
entry on this semaphore
set */
int
semid; /* semaphore set identifier */
short *
semadj; /* array of adjustments,
};
one per semaphore */
Listing 9.16. Struktur der Undo-Information

Aus Performance-Grnden enthlt der PCB eines Prozesses, task_struct, einen


Verweis auf die Undo-Liste: auf S. 23 befindet sich der Eintrag struct

214

9 Kommunikation zwischen Prozessen

9.4 Sockets 215

sysv_sem
sysvsem.
Dieebenfalls
Strukturmit
sysv_sem
ist in sem.h
definiert als struct
Listing
9.19)
beginnt
einer Struktur
kern_ipc_perm
(vgl. Prage
6
sem_undo_list
.
Listing
9.17
zeigt
den
Zusammenhang
zur
Undo-Liste.
auf S. 207). Die verwendeten Felder sind bis auf shm_file,
shmjnattch und
shm_segsz den bislang kennengelernten Strukturen vergleichbar, shm_nattch
enthlt
diesem_undo_list
Anzahl der auf
das Shared Segment zugreifenden Prozesse, das Feld
struct
{ atomic_t
shm_segsz gibt die Lnge
des Speicherbereiches an. Interessant ist shm_file,
refcnt;
das aufspinlock_t
eine Filestrukturlock;
verweist. Der Speicherbereich wird dadurch erzeugt,
struct
sem_undo
*proc_list;
dass ber
eine
Filestruktur
auf einen Dentry-Eintrag und von da aus auf eine
};
Inode-Struktur
verwiesen wird. Dieser Inode-Struktur wird ein Speicherbereich
zugeordnet.
Listing 9.17. Struktur sem_undo_list

struct shmid_kernel /* private to the kernel */


struct file *
shm_file
Die Struktur;
von sembuf ist in Listing 9.18 dargestellt. Sie entspricht der
int
id;
Struktur
Arguments der semop()-Funktion auf S. 103 (siehe auch
unsigned
long des zweiten
shm_natt
ch;
Programm
auf
S.
104).
unsigned long
shm_segs
z;
time_t
shm_atim
;
time_t
shm_dtim
struct sembuf ;{
time_t
shm_ctim
unsigned short
sem_num; /* semaphore index in array */ sem_op;
;
pid_t
shm_cpri
short
/* semaphore operation */ sem_flg; /* operation
d;
pid_t
shm_lpri
short
flags */
d;

>;

Listing 9.18. Die sembuf-Struktur

Frage: Wie knnen mehrere Semaphoren-Operationen atomar durchgefhrt


werden?
Eine Antwort liefert die Funktion sys_semtimedop()7, die immer aufgerufen
wird, wenn eine Semaphoren-Operation durchgefhrt werden soll. Nach einigen
berprfungen wird die Anweisung
sma = sem_lock(semid);

aufgerufen. Diese setzt auf sem_array->kern_ipc_perm->lock einen Spinlock. Die


Operationen erfolgen dann nicht direkt, sondern durch den Aufruf von
try_atomic_semop(). In dieser Funktion wird jede einzelne Anforderung
versuchsweise durchgefhrt. Gelingt dies, wird anschlieend der Spinlock wieder
freigegeben; andernfalls wird jede nderung zurckgenommen, der Prozess in
die Warteschlange eingefgt und blockiert. Der Spinlock wird anschlieend
wieder freigegeben.
9.3.5

Shared Memory

Das Shared Memory wird nach den gleichen Methoden verwaltet wie die anderen
beiden IPC-Objektarten. Der Anfang der Struktur shmid_kernel (siehe
6

struct sem_undo_list ist in sem.h definiert.


In ipc/sem.c definiert.

216

9 Kommunikation zwischen Prozessen

auf einen Rechner beschrnkt. Da die Entwicklung von Linux ganz wesentlich
durch die Nutzung des Internets beeinflusst worden ist, steht zu erwarten, dass
Linux die Kommunikation vernetzter Rechner gut untersttzt. Ein Blick auf den
Umfang der Kernel-Quellen gibt einen ersten Eindruck davon: die Sourcen fr
Netzwerk und Netzwerk-Treiber nehmen mehr als 30 MB von den insgesamt 203
MB ein. Linux untersttzt die gngige Internet-Protokollfamilie mit den
Transportprotokollen TCP und UDP und der Vermittlungsschicht IP in den
Versionen IPv4 und IPv6 ber diverse Verbindungen wie Ethernet, FDDI, TokenRing, ISDN-Karten usw. Aber auch andere Protokolle wie IPX (Novell),
AppleTalk, NetBios sind implementiert. Damit eignet sich Linux bestens als
Server in heterogenen Netzwerken.
Der vollstndige Umfang kann hier nicht dargestellt werden. Wir wollen uns
deshalb auf die Protokollfamilie TCP/IP beschrnken. Doch bevor auf die
Implementierung ansatzweise eingegangen wird, sollen zunchst die Grundlagen
des Internets und die Anwendung der Kommunikation in Form von System Calls
dargestellt werden.
9.4.1

Schichtenmodell

Fr die Implementierung von Netzwerkprotokollen hat die International


Organization for Standardisation (ISO) ein 7-Schichten Referenzmodell
entwickelt. Konkrete Implementierungen wie TCP/IP besitzen nicht alle
Schichten,
aber die TCP/IP-Schichten lassen sich dem Referenzmodell zuordnen,
>;
wie die Abb.
9.1 zeigt.
Listing 9.19. Die Struktur shmid_kernel
Jede Schicht darf nur mit der direkt benachbarten kommunizieren. Dadurch
werden Abhngigkeiten vermieden, und die Schichten lassen sich austauschen,
vorausgesetzt, die Schnittstellen bleiben erhalten.
Die Aufgaben
derDer
einzelnen
Schichten
des ISO/OSI-Modells sind im einzelnen:
9.3.6
System
Call sys_ipc()
Physical layer oder Bitbertragungsschicht: Sie regelt die physischen
Obwohl
im Vorhergehenden
dargestellt,
sind Aufrufe
msgget(),
Anforderungen,
die beimanders
bertragen
eingehalten
werdenwie
mssen.
Dazu
semctl()
usw.
keineFestlegung
System Calls
eigentlichen
Sinne.
Alle diese
Aufrufe
gehren
sowohl
der im
Stecker
sowie die
Definition,
mit welcher
werden
durch die
Bibliotheken
auf den
System Call
sys_ipc()
Spannung
eine
logische 1 bzw.
eineeigentlichen
logische 0 bertragen
werden
muss und
abgebildet.
Diese
Funktion
dient
als
Multiplexer:
das
erste
Argument
wieviele Mikrosekunden die bertragung dauern soll. Nur so kann dieses
Aufrufs
enthlt eine
Konstante,
die Empfnger
die jeweiligeauch
IPC-Funktion
Dies
sichergestellt
werden,
dass der
ein Bit mitidentifiziert.
der Wertigkeit
1
8
kannerkennt,
in der Funktion
sys_ipc()
nachvollzogen
werden.
In
einer
switchwenn der Sender ein solches abgeschickt hat.
Anweisung
werden
Grund des erstenDiese
Argumentes
die entsprechenden
Data
link layer
oderauf
Sicherungsschicht:
Schicht hat
die Aufgabe, Daten in
Funktionen
wie
z.B.
sys_msgget(),
sys_semcnt()
usw.
Datenbertragungsrahmen (data frames) zu verpackenaufgerufen.
und diese sequentiell
zu bertragen. Die Netzwerkadapter mssen eine eindeutige
Identifikationsnummer besitzen, die sogenannte MAC-Adresse (Media
Access Control).
Beim Empfang mssen die Datenrahmen erkannt und
9.4
Sockets
Quittungsmeldungen erzeugt werden. Ein Rahmen kann korrekt beim
Empfnger
ankommen,
er kann jedoch auch zerstrt
sein. Die
Die bisher
betrachteten
Kommunikationsmethoden
Pipe, worden
Named Pipe,
Message
Quittungsmeldung
deren
Ausbleiben
veranlasst
den Sender,
Queue,
Shared Memory bzw.
- sowie
Semaphoren
zur
Synchronisation
- sind
entsprechend zu reagieren, ggf. den Rahmen erneut zu schicken. Dabei
8
Definiert
in wiederum
arch/i386/kernel/sys_i386.c.
knnen
jedoch
Probleme auftreten, denn ein Rahmen kann

9.4 Sockets 217

Application
layer
Presentation
layer Session
layer
Transport
layer Network
DataRechner
link
Abb. 9.1. TCP/IP- und ISO/OSI-Referenzmodelle im Vergleichlayer
Der linke
schickt eine Nachricht an den rechten Rechner. Scheinbar kommunizieren entsprechende
Schichten miteinander, tatschlich aber wird die Nachricht von Schicht zu Schicht
durchgereicht, die physische Kommunikation findet erst auf der untersten Schicht statt.
Links neben dem ISO/OSI-Schichtenmodell ist im Vergleich das TCP/IP-Modell dargestellt.
Die Gegenberstellung zeigt, wie die Schichten der beiden Modelle aufeinander abgebildet
werden knnen.
Empfnger ankommen, wenn der Quittungsrahmen zerstrt wurde. Weiter
muss die Sicherungsschicht dafr sorgen, dass der Sender rechtzeitig die
bermittlung weiterer Rahmen stoppt, wenn der Pufferbereich, den der
Empfnger bereithlt, voll luft. Wird diese Regelung nicht vorgenommen,
besteht die Gefahr von Datenverlusten.
Bei Duplexbertragung tritt das Problem auf, dass Quittungsrahmen und
Daten miteinander in Konkurrenz treten. Auch dieses Problem ist von der
Sicherungsschicht zu behandeln.
Network layer oder Vermittlungsschicht: Der Network Layer dient der
Steuerung des Netzwerks. Hierhin gehrt die Auswahl des richtigen Weges
eines Pakets vom Sender zum Empfnger. Diese Auswahl kann statisch
durch Tabellen festgelegt werden oder aber dynamisch fr eine Sitzung an
Hand des Verkehrsaufkommens ermittelt werden. Es ist sogar mglich, die
Leitwegbestimmung fr jedes einzelne Paket dynamisch durchzufhren.
Befinden sich zu viele Pakete im Netz, kann es zu Stauungen - ggf. sogar
Deadlocks - kommen. Die Vermittlungsschicht muss rechtzeitig diese
Situation erkennen und die Versendung weiterer Pakete bremsen, bis die
Stauung wieder aufgelst ist.
Die Vergabe eindeutiger logischer (IP) Netzadressen gehrt ebenfalls zu den
Aufgaben. Dies ist ntig, damit sich die Rechner gegenseitig anspre-

218

9 Kommunikation zwischen Prozessen

chen knnen. Auf der Ebene der MAC-Adressen ist das nicht ohne weiteres
mglich.
Wenn das Paket von einem Netzwerk in ein anderes wechselt, muss die
Vermittlungsschicht an dieser Grenzstelle dafr sorgen, dass die ggf.
unterschiedlichen Protokolle der beiden Netze beachtet werden.
Transport layer oder Transportschicht: Diese Ende-zu-Ende-Schicht hat die
wesentliche Aufgabe, Daten, die sie von der Sitzungsschicht bekommt, an
die Vermittlungsschicht weiterzugeben und dafr zu sorgen, dass die Daten
richtig beim Empfnger ankommen. Whrend bei der Versendung der Daten
viele Knotenrechner durchlaufen werden knnen und dabei die Schichten 13 benutzt werden, wird die Schicht 4 nur beim Sender und Empfnger
bentigt. Sie dient also dazu, die unteren, physisch orientierten Schichten
von den oberen, anwendungsbezogenen Schichten zu isolieren. Hufig stellt
die Transportschicht den oberen Schichten als Dienst eine fehlerfreie
Standleitung (point-to-point channel) zur Verfgung, in der die Nachrichten
in der Reihenfolge ihres Absendens empfangen werden. Grere Rechner
arbeiten in der Regel im Multi-User-Betrieb. Damit entstehen viele
Verbindungen vorn und zum Host. Die Zuordnung von einer Nachricht zur
jeweiligen Verbindung muss im Transport-Nachrichtenkopf dokumentiert
werden, damit die Transportschicht die richtige Zuordnung vornehmen
kann. Auerdem gehrt an diese Stelle die Mglichkeit, ber logische
Namen den Kommunikationspartner anzusprechen. Die Zuordnung von
Namen zu Adressen erfolgt in Nameservern, in die die Information
eingetragen wird. Somit muss derjenige Prozess, der mit einer Datenbank
Kontakt aufnehmen will, nur noch wissen, wie er einen Nameserver
erreicht. Der Datenbank-Prozess kann dann dynamisch seine Verbindung in
den Nameserver eintragen.
Die Flussregulierung zwischen den Hosts ist ebenfalls Aufgabe der
Transportschicht. Dies ist von der Flussregulierung zwischen den
Knotenrechnern zu trennen.
Session layer oder Sitzungsschicht: Sie dient dazu, einem Anwender Zugang zu
einem rechnerfernen Timesharing System zu verschaffen oder Dateien
zwischen Maschinen zu bertragen. Hierzu gehrt:
Die Dialogsteuerung, die regelt, welcher der Partner gerade an der
Reihe ist, und
das Token-Management, wenn sicherzustellen ist, dass nur jeweils ein
Partner eine Operation ausfhren darf, und die Synchronisation, um
z.B. bei der bertragung einer langen Datei, bei der ein Absturz
passiert, an geeigneter Stelle wieder aufsetzen zu knnen.
Presentation layer oder Darstellungsschicht: Der Presentiation Layer dient dazu,
die Darstellung der Daten angemessen zu konvertieren. Hierhin gehrt
nicht nur zum Beispiel die Umwandlung von ASCII nach EBCDIC (und
umgekehrt). Es ist auch daran zu denken, dass einige Maschinen z.B. in der
Darstellung von Zahlen sehr unterschiedliche Formate haben. Das beginnt
schon bei ganzen Zahlen: das Einer-Komplement oder das

9.4 Sockets 219

hufiger angewendete Zweier-Komplement sind nur eine Unterscheidung,


Vertauschung der Byte-Order ist eine andere. Diejeweiligen Darstellungen
mssen vollstndig beschrieben und knnen dann ineinander umgewandelt
werden.
An diese Stelle gehren aber auch Themen wie Datenkompression, um das
zu bertragende Datenvolumen zu verringern, und die Kryptographie, um
Eindringlingen das Mitlesen zu erschweren. Auch die elektronische
Signatur ist in diesem Themenkreis angesiedelt.
Application layer oder Anwendungsschicht: Sie enthlt eine Vielzahl von
hufig benutzten Protokollen. Hierzu gehren:
Die Synchronisation von Transaktionen, bei denen mehrere Partner
beteiligt sind,
die Definition eines abstrakten Terminals, um die Vielzahl von
existierenden Terminals angemessen zu untersttzen,
der Dateitransfer, bei dem Unterschiede in den Konventionen bezglich
Dateinamen, Darstellung, Speicherung usw. behandelt werden mssen,
elektronische Post,
Remote Job Entry usw.
Im TCP/IP-Referenzmodell sind die Schichten folgendermaen aufgeteilt:

Die Datenbertragungsschicht fasst die ISO/OSI-Schichten Physical


layer und Data link layer zusammen. Die Gertetreiber fr die
Netzwerkkarten implementieren diese Schicht.
Die IP-Schicht vermittelt wie der Data link layer den Datenaustausch
zwischen zwei vernetzten Rechnern, die ggf. ber mehrere Knoten
miteinander vernetzt sind. Die Netzadressen werden durch die IPAdressen gebildet, z.B. 127.0.0.1 (IPv4) oder fe80: :210:a7ff :fela:3d79 (IPv6).
Der Transport layer untersttzt die beiden Protokolle TCP und UDP.
Das Protokoll TCP (Transmission Control Protocol) ist ein
verbindungsorientiertes Protokoll, das Client und Server in eine feste
Verbindung bringt: UDP (User Datagram Protocol) ist hingegen ein
Paket-orientierter Dienst. Sowohl TCP als auch UDP verwenden jeweils
fr sich eindeutige Portnummern, um auf dem Rechner mit dem
richtigen Prozess kommunizieren zu knnen.
Die Anwendungsschicht fasst die Schichten 5-7 des ISO/OSIReferenzmo- dells zusammen. Es gibt viele Standard-Protokolle wie
smtp, tftp, ftp, usw., die in Request for Comments (RCF) definiert sind
und die in Anwendungen implementiert werden mssen, um einen
Dienst zu nutzen.

Am Beispiel des Trivial File Transfer Protocol (TFTP) wird gezeigt, wie die
Schichten die Daten kapseln, um ihre Funktionen durchfhren zu knnen. Die
Anwendungsschicht ist durch den tftp-Dienst gegeben. Die zu bertragende
Datei wird in Datenblcke zerteilt, deren Lnge auch nach Hinzufgen der
Header durch die vier TCP/IP-Schichten 1500 Bytes9 nicht berschreiten
Dies ist der Default. Bei anderer MTU-Size knnen die Werte variieren.

220

9 Kommunikation zwischen Prozessen

Source Port Nr

Destination Port Nr

9.4 Sockets

221

darf. Abbildung 9.2 zeigt diese Kapselung, tftp setzt auf dem UDP-Dienst auf.
Da Datenpakete verloren gehen knnen, muss tftp selbst einen Header von 4
UDP Length
UDP Checksum
Byte erzeugen, damit die Datenpakete in ihrer Reihenfolge identifiziert werden
knnen. Die darunter liegende Schicht - in diesem Falle UDP - erzeugt einen
Header von 8 Byte, der in Abb. 9.3 dargestellt ist. Die darauf folgende InternetSchicht fgt einen mindestens 20 Byte groen IP-Header hinzu, der in Abb. 9.4
dargestellt ist. Fr das Ethernet wird noch zustzlich ein Header von 14 Byte
und ein Trailer von 4 Byte erzeugt, um die physische bertragung
sicherzustellen.
ve
rs

l
e
n

TOS

total length in
bytes

4 Byte tftp

Header 8 Byte

UDP Hcadcr

20 Byte lP Header

Abb. 9.2. Kapselung der Daten durch die TCP/IP-Schichten Jede Schicht fgt
beim Sender die fr sie wichtigen Informationen hinzu und entfernt sie auf der
Empfngerseite.

^---------------------- 32 bit -------------------------------^


Abb. 9.3. Der UDP-Header
Anmerkung: insgesamt werden also 2 x 32 Bit benutzt, das bedeutet 8 Byte.

222

9 Kommunikation zwischen Prozessen

fla
gs

identification
protoc
ol

TTL

fragment
offset

header checksum

source address
destination address

options

source port
number

destination port
number
sequence number

acknowledgement
number 32 bit ------------------------------^
^-------------------Abb. 9.4. Der IPv4-Header

len

vers: IP-Versionsnummer,
len: Lnge
res.
flags
window
size des Headers - auf Grund des variabel langen Feldes

options bentigt, protocol: z.B. TCP oder UDP, options: variabel langes Optionsfeld

TCP checksum
Urgent pointer
Wre anstelle von tftp z.B. telnet verwendet worden, also ein TCP-basierter
Dienst, so wre nicht nur der von tftp erzeugte Header gegen den von telnet
erzeugten ausgetauscht
worden, sondern auch der UDP-Header gegen den TCPoptions
Header, der in Abb. 9.5 dargestellt ist.
Im IP-Header sind somit die IP-Adressen der beteiligten Rechner sowie das
verwendete Protokoll enthalten. Im UDP- bzw. TCP-Header stehen die jeweiligen
Port-Adressen, die es ermglichen, die Nachricht dem richtigen Prozess auf dem
jeweiligen Rechner zuzuordnen.
9.4.2

System Calls

Fr die folgenden Betrachtungen soll als Beispiel ein einfacher Server dienen,
der eine empfangene Nachricht an den Client zurckschickt. Die
Programmierbeispiele werden - wie blich - in der einfachsten Form und ohne
jegliche Fehlerberprfung dargestellt, um die Struktur klar herauszustellen.
Es soll zunchst auf den Kommunikationsablauf der Datagram-orientierten
Kommunikation mittels UDP eingegangen werden, dessen Ablauf in Abb. 9.6 auf
S. 223 grafisch dargestellt ist. Das Programm verwendet zur Adressierung

9.4 Sockets

223

^----------------------- 32 bit ----------------------------------^

Client
Abb. 9.5. Der TCP-Header Server
Abb.
9.6.
Kommunikationsablauf
bei UDP
kleinen
Strich unter
len: Lnge des TCP-Headers, res.\ reserviert, flags: Typ
des Die
Pakets
- Datenpaket
bzw.
recvfrom
deuten
an:
Blockieren
bis
eine
Nachricht
eingetroffen
ist
Verbindungsaufbau, window size\ Anzahl der unbesttigten Pakete bevor TCP aufhrt zu
senden, options: variabel langes Optionsfeld
die Struktur
sockaddr_in,
die eine
Internet-Adresse
durch
dieund
IP-Nummer
Die Listings
9.21 und 9.22
zeigen
einen einfachen
Server
Client frund
diese
den
Port
eindeutig
festlegt
(vgl.
Listing
9.20).
Art der Kommunikation.

Der wesentliche Unterschied zwischen Client und Server liegt darin, dass der
Client
nichtsockaddr_in
nur die Socket{ binden, sondern auch beim sendto()-Aufruf die
struct
Adresse des Servers angeben muss. Formal
passiert dies zwar*/auch beim Server,
/*Adress-Familie
aber der
hat die Client-Adresse
im int
vorangehenden
recvfrom()-Aufruf
gerade
unsigned
short
/* Port Nummer
*/
sin_port;
struct
in_addr
/* Internet-Adresse*/
erhalten.
sin_addr;
Whrend
bei UDP-basierten Diensten einzelne Pakete verschickt werden,
wird bei TCP eine Verbindung hergestellt, die dann einen bidirektionalen
Listing 9.20. Hilfsstruktur
zurKommunikation
Adressierung
Datenstrom
bereitstellt. sockaddr_in
Die bei einer
durchzufhrenden
Aktionen unterscheiden sich bei UDP und TCP somit voneinander. Abbildung 9.7
auf S. 226 zeigt die notwendigen Aktionen fr TCP. Nach Bereitstellen der Socket
Mit socket() wird sowohl beim Server als auch beim Client ein
muss beim Server mit bind() ein bestimmter Port fr den Prozess angefordert
Verbindungsmechanismus - eine sogenannte Socket - bereitgestellt. Sowohl auf
werden. Die Funktion listen() aktiviert den Server-Modus der Socket. Mit
der Seite des Servers als auch auf der Client-Seite wird mit bind() ein
accept() wartet der Server auf eine eingehende Verbindungsanforderung des
bestimmter Port fr denjeweiligen Prozess angefordert und die Adresse somit
Clients und blockiert solange.
gebunden. Die Funktion recvfrom() blockiert, bis ein Datagram eintrifft; die
Nach dem Anlegen der Socket benutzt der Client die Funktion connect(), um dem
Funktion sendto() verschickt ein Datagram an eine vorgegebene Adresse.
Server seinen Verbindungswunsch mitzuteilen. Dabei muss er dem Auf-

224

9 Kommunikation zwischen Prozessen


#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
main () {
struct sockaddr_in server, client; int sockfd;
/* Server IP- und Port-Adresse setzen und binden */
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
server.sin_family = AF_INET;
server.sin_port = htons(4711);
server.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr*)&server, sizeof(struct sockaddr));
/* Datenstrukturen fr Nachricht mid Adresse des Clients */ int
n;
char buf [100];
socklen_t clientl = sizeof(client);
/* auf Nachricht warten */
n = recvfrom(sockfd, buf, 100, 0, (struct sockaddr*) &client,
&clientl);
/* Nachricht verschicken */
sendto(sockfd, buf, n, 0, (struct sockaddr*)&client, clientl);
close(sockfd); exit(0);

Listing 9.21. UDP Echo Server

ruf natrlich die IP-Adresse des Rechners und die Port-Nummer des
ServerProzesses mitgeben, connect() beantragt beim eigenen System zustzlich
eine beliebige freie Port-Nummer.
accept() bergibt dem Server die IP-Adresse und die Port-Nummer des
Clients nach Eintreffen einer Verbindungsanforderung.
Nachdem auf diese Weise die Verbindung hergestellt ist, kann der Client
mittels write() dem Server eine Nachricht zuschicken, die dieser mit Hilfe der
Funktion read() lesen kann. Die Verbindung bleibt bestehen und die
Kommunikation kann auf diese Weise weitergefhrt werden, bis auf dem Server
oder dem Client close() aufgerufen wird.
Quelltexte fr einen simplen Client und einen Server, der nur eine Nachricht
liest und zurckschickt, sind in den Listings 9.23 und 9.24 (S. 227,f.) zu finden.
Die Aufrufe knnen der Struktur der Abb. 9.7 direkt zugeordnet werden.

9.4 Sockets

225

#include <sys/types.h>
#include
<sys/socket.h>
#include
main () {
struct sockaddr_in dest; struct sockaddr_in me;
/* Socket erzeugen und binden
*/ int sockfd;
sockfd = socket(AF_INET, SOCK_DGRAM, 0); me.sin_family =
AF_INET; me.sin_port = htons(0);
me.sin_addr.s_addr = inet_addr("127.0.0.1");
bind(sockfd, (struct sockaddr*)&me, sizeof(struct sockaddr));
/* Nachricht vorbereiten */
char buf[100];
int n;
strcpy(buf,"Dies ist ein Test");
/* da finde ich den Server ! */ dest.sin_family = AF_INET;
dest.sin_port = htons(4711); dest.sin_addr.s_addr =
inet_addr("127.0.0.1"); int dest_l = sizeof(struct
sockaddr);

/* Nachricht verschicken und Rcksendung empfangen */


sendto(sockfd, buf, 18, 0, (struct sockaddr*)&dest,
dest_l); n = recvfrom(sockfd, buf, 100, 0, (struct
sockaddr*)&dest, &dest_l);
printf ( "Text : */,s\n" ,buf ) ; close(sockfd); exit(0);

Listing 9.22. Der UDP Client

Wie bei den IPC-Aufrufen sind die hier verwendeten Aufrufe keine System Calls
im eigentlichen Sinne. Die Bibliothek bildet die verwendeten Aufrufe auf den
System Call sys_socketcall() ab, der als Multiplexer dient. Das erste Argument
reprsentiert die jeweils gewnschte Funktion, wie dem Quelltext net/socket. c
entnommen werden kann.
9.4.3

Implementierung

Die Abb. 9.1 und 9.2 machen deutlich, dass beim Aufbereiten der Daten bei der
Socket-Kommunikation jede Schicht ihren Header (und ggf. Trailer) hinzufgt
oder entfernt. Da die Daten von Schicht zu Schicht weitergereicht wer-

9 Kommunikation zwischen Prozessen

226

close

Server

'

,
close
---------------

Client

Abb. 9.7. Kommunikationsablauf bei TCP Auch hier deutet der


Querstrich unter accept() darauf hin, dass blockiert wird, bis durch den connect()-Aufruf
des Clients eine Verbindung aufgebaut wird.

den, knnte die Verarbeitung mit viel Kopieren von Daten verbunden und damit
recht ineffizient sein. Einfacher wre es, die Daten an Ort und Stelle zu belassen
und mittels Zeiger-Operationen die Header und Trailer zu verwalten. Dazu wird
der Socket-Puffer sk_buff10 eingefhrt, der in den Listings 9.26 und 9.27 auf S.
229,f. dargestellt ist.
Der Kopf dieser Struktur zeigt, dass es sich um eine doppelt verkettete Liste
handelt, wobei der dritte Eintrag zustzlich noch einmal auf den Listenkopf
verweist, dessen Struktur in Listing 9.25 dargestellt ist, der ebenfalls in skbuff.h
definiert ist. Der Eintrag qlen enthlt die Anzahl der Listenelemente, lock dient
zur sicheren Verarbeitung.
Die Zeiger am Ende der sk_buff-Struktur dienen den angesprochenen ZeigerOperationen: data und tail verweisen auf den Anfang und das Ende des Pakets,
das durch das Durchreichen zur n-ten Schicht entstanden ist.
In include/linux/skbuff.h.

10

9.4 Sockets 227


#include <sys/types.h>
#include
<sys/socket.h>
#include
main () {
struct sockaddr_in destination; int sockfd;
/* da finde ich den Server ! */ sockfd =
socket(AF_INET, SOCK_STREAM, 0); destination.sin_family
= AF_INET; destination.sin_port = htons(4711);
destination.sin_addr.s_addr = inet_addr("127.0.0.1");

/* Verbindungswunsch stellen */ connect(sockfd, (struct


sockaddr*)&destination, sizeof(struct sockaddr));
/* Nachricht verschicken und Antwort lesen */
write(sockfd, "Hallo\n", 6);
char* buf = (char*)malloc(100);
int gelesen = read(sockfd, (void*) buf, 100);
printf("%u Zeichen empfangen, Text: %s\n", gelesen, buf);
close(sockfd);
exit(0);

Listing 9.23. Quelltext fr TCP-Kommunikation: Client


head und end zeigen auf den Anfang und das Ende des Speicherbereichs, in dem

die Daten gespeichert werden. Da Header und ggf. Trailer erzeugt werden
mssen, muss beim Anlegen eines solchen Bereiches hinreichend viel Platz vor
und nach den eigentlichen Nutzdaten gelassen werden. Es gibt drei weitere
Zeiger in dieser Struktur: h zeigt auf den Header des Transport Layer, nh
verweist auf den Network Layer Header, bei TCP/IP sind dies im ersten Fall der
TCP- oder UDP-Header, im zweiten der IP-Header. mac zeigt auf den MACHeader (Medium Access Control).
Sollen Daten gesendet werden, so passiert im Prinzip folgendes: die Daten
werden an geeigneter Stelle in einen Socket-Puffer kopiert und der TCP- bzw.
UDP-Header erzeugt. Dabei werden die Pointer im Socket-Puffer entsprechend
angepasst. Die Kontrolle wird an den Network Layer weitergeben, der den
Socket-Puffer bergeben bekommt und seinerseits den IP-Header hinzufgt.
Danach wird der Pointer data im Socket-Puffer durch die Punktion skb_push()11 so
gendert, dass er nun auf den Anfang des IP-Headers zeigt. Schlielich fgt noch
der Data Link Layer seinen Header und Trailer hin11

Enthalten in include/linux/skbuff.h.

228

9 Kommunikation zwischen Prozessen


#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
main () {
struct sockaddr_in server; int sockfd;
/* Server IP- und Port-Adresse setzen und binden */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
sefver.sin_family = AF_INET;
server.sin_port = htons(4711);
server.sin_addr.s_addr = htonl(INADDR_ANY);

bind(sockfd, (struct sockaddr*)&server, sizeof(struct sockaddr));


/* Server-Modus der Socket starten */ listen(sockfd,
S0MAXC0NN); int clientfd;
struct sockaddr_in client; int cl_size = sizeof(client); char*
buf = (char*)malloc(100);

/* auf Verbindungswunsch warten */


clientfd= accept(sockfd, (struct sockaddr*) &client, &cl_size);
/* Daten lesen und zurckschicken */ int gelesen =
read(clientfd, (void*) buf, 100); write(clientfd, buf,
gelesen); exit(0);

Listing 9.24. Quelltext fr TCP-Kommunikation: Server


struct sk_buff_head {
struct sk_buff
*next; struct
sk_buff *prev;
__u32
qlen;
spinlock_t lock;

>;

Listing 9.25. Kopf der doppelt verketteten Liste von Socket-Puffern

230

9 Kommunikation zwischen Prozessen

9.4 Sockets 229

struct sk_buff {
struct sk_buff
*next;
struct sk_buff
*prev;
struct sk_buff_head *list;
struct sock
*sk;
struct timeval
stamp;
struct net_device *dev;
struct net_device *real_dev;
union {
struct tcphdr
/* spezielle
Strukturen
ausgelassen */*th;
*uh;
struct udphdr
unsigned int
truesize;
*icmph;
struct
icmphdr
users;
atomic_t
*igmph;
unsigned
*head,
struct igmphdr
char
*ipiph;
struct iphdr
*data,
*tail,
*ipv6h;
struct ipv6hdr
*raw;
unsigned*end;
char >
h ; union {
struct iphdr
struct ipv6hdr
*iph;
*ipv6h;
struct arphdr
*arph;
unsigned char }
*raw;
nh; union {
struct ethhdr

unsigned char
} mac;
struct dst_entry
struct sec_path
char
unsigned int

unsigned char

*ethernet;
*raw;
*dst;
*sp;
cb [48];
len,
data_len,
mac_len,
csum; local_df, cloned, pkt_type,
ip_summed; priority; protocol,
security;
(*destructor)(struct sk_buff
*skb);

__u32
unsigned short
void

Listing 9.26. Socket-Puffer - Teil 1

232

9 Kommunikation zwischen Prozessen

9.4 Sockets 231

struct net_device

Innerhalb der Funktion tcp_push_one() wird zugegriffen auf die Funktionen


tcp_snd_test(), tcp_transmit_skb() und tcp_reset_xmit_timer(). Die Funktion
char
name[IFNAMSIZ];
tcp_snd_test() berprft, ob die Daten berhaupt gesendet werden drfen. Dies
/* I/O specific fields */
darf beispielsweise
nichtmem_end;
geschehen,/*
wenn
der Empfnger
berlastet ist und
unsigned long
shared
mem end */
folglichunsigned
keine weiteren
mehr /*
annehmen
kann,start
tcp_transmit_skb()
gibt die
long Pakete
mem_start;
shared mem
*/
aufbereiteten
Daten
den IP-Layer/*weiter.
DieI/O
verwendete
Funktion ist
unsigned
longan base_addr;
device
address*/
spezifisch
fr dieint
jeweilige
Adressen-Familie:
irq;
/* deviceip_queue_xmit()
IRQ number bei
*/ IPv4,
}; unsigned
/* spezifischwird
fr bei
manche
Hardware */
tcp_reset_xmit_timer()
TCP aufgerufen,
um vom Empfnger nicht
char if_port;
AUI,
Listingunsigned
9.27.Datenpakete
Socket-Puffer
- Teil einmal
2 /* Selectable
besttigte
noch
zu versenden.
Bei TP,..*/
der Datenbertragung
dma; Datenpaket
/* DMA
channel */
mittelsunsigned
TCP wirdchar
fr jedes
sichergestellt,
dass es beim Empfnger
17
unsigned
state;
ankommt.
Dazulong
gehrt,
dass jedes korrekt empfangene Paket mit einem Ack18
zu, korrigiert
Pointer im
Socket-Puffer und reiht den Socket-Puffer in die
struct die
net_device
*next;
dem Sender gemeldet wird. Bleibt dieses aus oder wird ein Nack19 vom
Queue/*
derThe
zu versendenden
Datenpakete
ein.
device initialization
function.
Called only once. */
Empfnger gesendet, um zu zeigen, dass das Paket zwar angekommen ist, die12
int
(*init)(struct
*dev);
Die
einzelnen Schritte
sollen fr TCPnet_device
etwas genauer
dargestellt werden:
Daten/*
aber
teilweise
zerstrt
sind, so muss das
Paket vom
Sender noch- einmal
------ Call
Fields
preinitialized
Space.c
finish here
- beim
ber den System
write()
wird auf diein
Methode
zugegriffen,
die dafr
*/
auf die Reise geschickt werden.
Socket File Deskriptor assoziiert20ist, nmlich sock_writev(). Die Funktion
Die Funktion
ip_queue_xmit()
bergibt
erzeugte Paket an den IP- Layer.
net_device
*next_sched
/* das
Interface
greift struct
auf sock_readv_writev()
zu; dieser
Aufruf dafr sorgt, dass die
Dort wird
noch
der IP-Header
fertiggestellt
und
das Paket dann letztendlich an
index
Unique
device
identifier
*/
int
Struktur msghdr richtig initialisiert wird. Nach der Initialisierung wird dann
den Gertetreiber zum
weitergereicht. Dazu wird im Aufruf NF_H00K
ifindex;
13 Versenden
auf sock_sendmsg() zugegriffen, in deren Verarbeitung diejenige Methode
neben int
dem Socket-Puffer usw.
auch eine Struktur net_dev bergeben, die das fr
iflink;
aufgerufen wird, die bei der
Socket fr sendmsg() assoziiert ist, nmlich sockstructverwendete
net_device_stats*
(*get_stats)(struct
net_device
*dev);
die Ausgabe
Netzwerkgerte-Device
reprsentiert
(siehe Listings
14
>ops->sendmsg(). Fr TCP wird dadurch auf tcp_sendmsg()15 zugegriffen,
struct
(*get_wireless_stats)
9.29 und
9.30).iw_statistics*
In der verkrzten
Darstellung
werden
im
zweiten
Teil
die
fr UDP auf udp_sendmsg()16.
(structdeutlich
net_device
*dev);
Funktionen zur Verwaltung der Hardware
sichtbar:
open() und stop()
In der Funktion tcp_sendmsg() wird mit tcp_alloc_pskb() ein neuer Socketdienen dem Initialisieren bzw. Abmelden der Karte, hard_stat_xmit() entfernt
Pufferstruct
angelegt.
Der Aufruf *ethtool_ops;
tcp_copy_to_page() kopiert die Daten an die
ethtool_ops
fertige
Pakete
aus
der
Warteschlange
und
verschickt
sie,
do_ioctl() leitet
/*
entsprechende Stelle des Socket-Puffers
und
berechnet
ggf.
die Checksum.
gertespezifische
Befehle
an
die
Karte.
*
This
marks zusammengesetzt
the end of the "visible"
of the
structure. All
Nachdem
das Paket
ist, wird diepart
Funktion
tcp_push_one()
Der
Empfang
von
Daten
gestaltet
sich
insofern
etwas
anders,
da die
Daten
*
fields
hereafter
are
internal
to
the
system,
and
may
change
aufgerufen. Dieser Funktion wird kein Socket-Puffer bergeben, sondern
die at
*
will
(read:
may be cleaned
upankommen.
at will). Hier finden wir ein
zu
einem
nicht
vorhersehbaren
Zeitpunkt
Socket,*/die zum Verschicken benutzt werden soll. Mit Hilfe der in Listing 9.28
typisches
Einsatzgebiet
von Interrupts
Kap. 7 und
Abschn.
6.1.1). Der
dargestellten
Datenstrukturen
kann in(vgl.
der Funktion
auf
den Socket-Puffer
Interrupt
wird
durch
die
Karte
erzeugt,
wenn
ein
Datenpaket
angekommen
ist.
zugegriffen werden.
Wird der Netzwerktreiber installiert, so wird dabei eine Handler- Routine fr
den von der Karte erzeugten Interrupt eingerichtet. Dieser Handler sorgt dafr,
dass die
Daten
von *tp
der =Karte
in den
Speicher
des Rechners
gelangen.
struct
tcp_opt
tcp_sk(sk);
/*sk
ist die bergebene
Socket*/
struct Dazu
sk_buffwird,
*skb
nachdem
die
Fehlerberprfung
ergeben
hat,
dass
ein
korrektes
Paket
= tp->send_head;
angekommen ist, zunchst ein Socket-Puffer angelegt und der Inhalt des Pakets
dahin
Beim
Analysieren der Header wird dann das richtige Protokoll
Listingbertragen.
9.28. Zugriff auf
Socket-Puffer
der Vermittlungsschicht erkannt. Danach setzt die Funktion netif_rx()21 ein, die
nicht mehr Hardware-spezifisch ist. Diese Funktion legt das Paket in einer
Warteschlange ab, die durch die Struktur
12
Auf Grund
der for
Komplexitt
des Umfangs wird diese
Darstellung einige Aspekte
/* These may
needed
future und
network-power-down
code.
sehr
kurz
fassen.
So
werden
auch
der
Verbindungsaufund
-abbau bei TCP nicht
be
*/
unsigned long
trans_sta ; /* Time (in jiffies) of last
17
dargestellt.
hat
die of
Verbindung
rt; der Empfnger
Tx/**/
unsigned
longEs sei denn,
last_rx;
Time
last Rxbeendet.
*
13 18
/
Funktionen
sind in net/socket.
c definiert.
unsigned AlleAcknowledge
flags;
/* interface
flags (a la
*
14 19
short
BSD)
/
davon ausgegangen, dass IP Version 4 benutzt wird.
No wird
Acknowledge
misigned Dabei
gflags;
15 20
short
In net/ipv4/tcp.c
definiert.
In net/ipv4/io_output. c definiert
21
unsigned 16Ebenfalls
*/
inmtu;
net/ipv4/udp.c.
In net/core/dev.c
definiert./* interface MTU value

unsigned
short

type;

/* interface hardware type

*/

unsigned
hard_head .len; /* hardware hdr length */
short
er.
/* Interface address info.
*/
unsigned char
broadcast[MAX_ADDR_LEN]; /* hw bcast add */
unsigned char
dev_addr[MAX_ADDR_LEN]; /* hw address
*/
misigned char
addr_len;
/* hardware address length */

9.4 Sockets 233


/* Interface address info. */
unsigned char
broadcast[MAX_ADDR_LEN]; /* hw bcast add */
misigned char
dev_addr[MAX_ADDR_LEN]; /* hw address */
unsigned char
addr_len;
/* hardware address length */
/* Protocol specific pointers */ void
*atalk_ptr; /* AppleTalk link */
void
*ip_ptr;
/*
IPv4 specific data */
void
*dn_ptr;
/*
DECnet specific data*/
void
*ip6_ptr; /*
IPv6 specific data */
void
*ec_ptr;
/*
Econet specific data*/
void
*ax25_ptr; /* AX.25 specific data */
/* Called after device is detached from network.
*/ void (*uninit)(struct net_device *dev);
/* Called after last user reference disappears.
*/ void (*destructor)(struct net_device *dev);
/* Pointers to interface service routines.
*/
int
(*open)(struct net_device *dev);
int
(*stop)(struct net_device *dev);
int
(*hard_start_xmit) (struct sk_buff *skb,
struct net_device *dev);
int
(*poll) (struct net_device *dev, int *quota);
int
(*hard_header) (struct sk_buff *skb,
struct net_device *dev,
unsigned short type,
void *daddr, void *saddr, unsigned
len); int (*rebuild_header)(struct sk_buff *skb);
void
(*set_multicast_list)(struct net_device *dev);
int
(*set_mac_address)(struct net_device *dev,
void *addr);
int

(*do_ioctl)(struct net_device *dev,

struct ifreq *ifr, int cmd); int


(*set_config)(struct net_device *dev,
struct ifmap *map);
int
(*hard_header_cache)(struct neighbour *neigh,
struct hh_cache *hh);
void
(*header_cache_update)(struct hh_cache *hh,
struct net_device *dev, unsigned char
* haddr);
int
(*change_mtu)(struct net_device *dev, int new_mtu);
void
(*tx_timeout) (struct net_device *dev);
/* ... vlan ... */
Listing 9.30. Struktur net_device: Reprsentation von Netzwerk-Devices, Teil 2
Listing 9.29. Struktur net_device: Reprsentation von Netzwerk-Devices, Teil 1 Die
Definition ist in include/linux/netdevice.h enthalten. . . . kennzeichnet Stellen mit greren
Auslassungen.

234

9 Kommunikation zwischen Prozessen


int
int
int
int

(*hard_header_parse)(struct sk_buff *skb,


unsigned char *haddr);
(*neigh_setup)(struct net_device *dev,
struct neigh_parms *);
(*accept_fastpath)(struct net_device *,
struct dst_entry*);
(*generate_eui64)(u8 *eui, struct net_device *dev);
9.5 Zusammenfassung

2
3
5

struct packet_type {
unsigned short
type
/* This is really
*
Listing 9.31. Struktur
net_device:
Reprsentation von Netzwerk-Devices, Teil
3
;
htons(ether_type).
/
struct
*dev
/* NULL is wildcarded here
*
net_device
;
/
int
(*fu
*
(struct sk_buff *, struct
nc)
,
22
net_device
struct
packet_type
softnet_data beschrieben ist (vgl. Listing 9.32 auf S.234). Mit dem Erzeugen des
void
*);
*af_packet_priv;
list ; in der Rckgabefunktion
Soft-IRQ (Softinterrupts)
NET_RX_SOFTIRQ
struct
netif_rx_schedule() von netif_rx() ist die Aufgabe des Handlers beendet.
list_head
struct softnet_data

>;

int
int
int
struct sk_buff_head
struct list_head
struct net_device
struct sk_buff
struct net_device

throttle; cng_level;
avg_blog;
input_pkt_queue;
poll_list;
*output_queue;
*completion_queue;
backlog_dev;

Listing 9.32. Struktur zur Verwaltung der Warteschlange ankommender Pakete

Auf diesen Soft-IRQ reagiert net_rx_action()23. Die Funktion ruft dev->poll()


auf; bei der Initialisierung wurde an dieser Stelle die Funktion process_backlog()
eingetragen. Diese nimmt mit __skb_dequeue()24 einen Socket-Puffer von der
Warteschlange und ruft netif_receive_skb() auf. In dieser Funktion wird der Typ
des Pakets an Hand der Header ermittelt und die diesem Pakettyp zugeordnete
Funktion zur Verarbeitung des Pakets aufgerufen. Das geschieht in einem
Aufruf der Form pt_prev->func(). pt_prev ist der ermittelte Typ packet_type und
func() die zur Verarbeitung zugeordnete Funkion. Damit wird das Paket der
Vermittlungsschicht bergeben. Listing 9.33 zeigt die dafr ntige Struktur
packet_type25.
Zu finden in include/linux/netdevice.c.

22

net_rx_actionO ist definiert in net/core/dev.c.


Definiert in include/linux/skbuff.h.
25
In include/linux/netdevice.h definiert.
23
24

236

9 Kommunikation zwischen Prozessen

zum Empfangen von Nachrichten benutzt. Fr Semaphoren wird der Aufruf


semop() bentigt, um die Semaphoren-Operationen zu untersttzen (vgl. S. 99).
Bei Shared Memory werden noch die Funktionen shmat() und shmdt() zum
Einfgen und Entfernen aus dem Prozessraum bentigt (vgl. Listing 5.7).
Sockets ermglichen unterschiedliche Arten der Kommunikation; sie knnen
verwendet werden, um Paket-orientiert Daten zu versenden oder um eine
Verbindung
zwischen zwei Teilnehmern aufzubauen, ber die die
};
Kommunikation dann als Datenstrom erfolgt. In jedem Falle muss eine Socket
mit dem Aufruf socket()
bereitgestellt
werden. Dabei
wird die
Listing
9.33. Struktur
packet_type
Kommunikationsart festgelegt. Durch bind() muss eine Port-Adresse beantragt
werden. Die Paket-orientierte Kommunikation nutzt dann sendto() und
Frage: Welche
der Implementierung
werden
auch
fr UDP-Kommunikarecvfrom(),
umTeile
Datenpakete
zu verschicken
bzw. zu
empfangen.
Im Falle des
tion
gelten,
wo
wird
diese
Implementierung
andere
Wege
Warum
Verbindungsaufbaus sind die Aktionen nicht symmetrisch:gehen?
der Server
ruftsind
nach
einige
Teile
einfacher
als
bei
TCP?
dem bind() die Funktion listen() auf, um auf eine Verbindungsanforderung zu
warten, und akzeptiert eine solche Anforderung im Aufruf accept(), der ein FileHandle zurckgibt, ber das der Server lesend und schreibend zugreifen kann.
Der Client
von bind() die Funktion connect() auf, in der er den
9.5 ruft anstelle
Zusammenfassung
Server adressiert. Diese Funktion ermittelt eine freie Port-Nummer, stellt beim
Server
die Anforderung
auf eine Verbindung
und liefert
bei Erfolg
ebenfalls
Unter dem
Oberbegriff Kommunikation
zwischen
Prozessen
haben
wir dreiein
File-Handle
zurkennengelernt:
Kommunikation zurck.
groe Bereiche
Eine
ganze
Reihe
von Fragestellungen
wurde bei den Sockets nicht erwhnt.
Die Pipes - einschlielich
Named Pipes,
So wurden
nur
die
Protokolle
TCP
und
UDP
in Ausschnitten angesprochen,
die IPC-Objekte und
Paketfragmentierung
und
Routing
blieben
ebenso
unerwhnt wie Netfilter. Trotz
Sockets.
der knappen Darstellung der Sockets sollte ein Verstndnis fr die Benutzung
Die drei IPC-Objekte, die zum Teil schon in anderen Zusammenhngen
dieser Kommunikationsart(en) und fr die internen Vorgnge entstanden sein.
betrachtet wurden, besitzen eine gemeinsame Verwaltung, die hier zusammen
mit den Message Queues vorgestellt wurde. Pipes sowie die drei IPC-Objekte
Message Queue, Semaphoren und Shared Memory knnen nur zwischen
Prozessen eines Rechners eingesetzt werden. Sockets hingegen ermglichen eine
Kommunikation ber Rechnergrenzen hinaus. Dafr gestaltet sich die
Implementierung von Sockets erheblich komplexer als bei den zuvor genannten
Objekten.
Die wesentlichen System Calls fr Pipes entsprechen den blichen
FileOperationen: der pipe()-Aufruf legt ein Array von File-Handles mit zwei
Eintrgen an; danach kann mit write() bzw. read() geschrieben bzw. gelesen
werden; close() dient zum Schlieen. Wichtig ist auch der dup()-Aufruf, der ein
File-Handle auf das erste nicht belegte File-Handle kopiert.
Die System Calls fr IPC-Objekte unterscheiden sich deutlich davon; obwohl
in Unix nahezu alles eine Datei ist, ist das hier nicht der Fall, denn identifiziert
werden sie nicht mit einem Namen, sondern mit einer eindeutigen Nummer.
Erzeugen bzw. Zugriff auf ein IPC-Objekt erfolgt mit ipcget(), wobei ipc fr msg,
sem bzw. shm - also Message Queue, Semaphore bzw. Shared Memory - steht.
Zurckgeliefert wird eine ID, ber die die weiteren Zugriffe erfolgen. Zum
Steuern und insbesondere Lschen von IPC-Objekten dient ipcctl(). Bei einer
Message Queue wird msgsnd() zum Versenden, msgrcv()

10
Der Bootvorgang

10.1 Grundlagen
Beim Starten eines Rechners liegen ungeklrte Verhltnisse vor: Whrend
ausfhrbare Programme auf der Platte gespeichert sind, herrscht in der CPU
und im Speicher des Rechners Chaos, das erst in geregelte Bahnen gelenkt
werden muss. Dabei kann man nicht erwarten, dass mit dem Anschalten sofort
das Betriebssystem geladen werden kann, vielmehr mssen eine Reihe von
Schritten durchlaufen werden, bis das Betriebssystem zur Verfgung steht.
Die durchlaufenen Schritte nennt man Bootstrapping - bei uralten Rechnern1
war dieser Prozess noch richtig sichtbar: da wurde zunchst ein Lochstreifen mit
einem durch Lchern kodierten minimalen Programm eingelegt und durch
Knopfdruck gestartet. Das eingestanzte Programm wurde dadurch in den
Speicher geladen. Anschlieend mussten ber Schalter die Register der CPU
richtig eingestellt und die CPU mit der richtigen Adresse gestartet werden. So
konnte die CPU dieses Minimalprogramm abarbeiten. Dieses war so ausgelegt, dass
der Rechner auf die erste Spur der Platte zugriff und von dort ein etwas greres
Programm lud. Dieses Programm wurde nach dem Laden in den Speicher
automatisch gestartet und konnte dann seinerseits das Betriebsystem laden.
Auch heutige Rechner funktionieren noch nach diesem Prinzip, nur mit dem
Unterschied, dass der Lochstreifen durch ein ROM* 2 im Rechner ersetzt ist, das
das BIOS3 enthlt, und dass keine Schalter mehr bentigt werden, um die
Register der CPU einzustellen.
Mit dem Laden des Betriebssystems ist man in der Regel jedoch noch nicht
zufrieden. Damit ein sinnvoller Betrieb der Anlage erfolgen kann, mssen noch

*Die folgende Beschreibung stammt von einem Prozessrechner Honeywell Bull DDP
512, der noch 1977 benutzt wurde.
2
3

Read Only Memory.


Basic Input/Output System.

238

10 Der Bootvorgang

eine Reihe administrativer Aufgaben erledigt werden, bevor der Benutzer das
vorfindet, was er zum Arbeiten bentigt:

Ggf. mssen temporre Plattenpltze gelscht werden,


falls der Rechner in einem Netzwerk eingebunden ist, muss eine
Initialisierung der entsprechenden Umgebung erfolgen, und
falls der Benutzer eine grafische Oberflche erwartet, muss diese
gestartet werden usw.

Natrlich kann man sich auf den Standpunkt stellen, dass dies nicht zum Kernel
gehrt; als Benutzer wre man aber mit dem nackten Betriebssystem - was ja
schon eine unglaubliche Verbesserung gegenber der reinen Hardware darstellt noch nicht zufrieden.

10.2

Vom BIOS zum Bootmanager

Die hier folgende Darstellung bezieht sich im Wesentlichen auf die x86Architektur. Sowohl beim Booten des Rechners als auch beim Drcken des ResetKnopfs wird das Reset-Signal erzeugt. Durch dieses Signal wird die CPU dazu
veranlasst, einige Register mit bestimmten vorgegebenen Werten zu laden und
den an der Stelle 0xfffffff0 befindlichen Code auszufhren. An dieser Stelle muss
die Hardware das ROM einblenden, in dem das BIOS enthalten ist. Das BIOS
enthlt elementare Gertetreiber fr den ersten Zugriff auf die wesentlichen
Gerte wie Platte, Floppy usw., auf denen das richtige Betriebssystem zu
finden ist.
Die CPU befindet sich im Real Mode, die Adressierung des Speichers erfolgt
mit 20Bit langen Adressen, jedoch knnen Code und Daten zusammen nur 64K
umfassen. Alle I/O-Operationen sind ungeschtzt mglich. Auf dem Wege zum
Betriebssystem muss also einiges geschehen: die CPU muss zum richtigen
Zeitpunkt in den Protected Mode und die Adressierung in den Virtual Mode
umgeschaltet werden, so dass z.B. Linux sein Adressierungsschema darauf
aufbauen kann. Doch bis es dazu kommt, muss das BIOS eine Reihe von
wichtigen Aufgaben durchfhren:

Im Power-On Self-Test (POST) werden die vorhandenen Gerte ermittelt


und die Hardware getestet,
danach wird die Hardware initialisiert; dabei werden bei PCI-basierten
Rechnern die IRQs und die I/O-Ports konfliktfrei zugeordnet,
dann wird in vorgegebener Folge auf dem Boot-Sektor von Floppies,
Platten und CD-ROMs - oder auch ber das Netz - nach einem
Betriebssystem gesucht, das gebootet werden kann,
schlielich wird der Bootsektor des ersten Gerts, auf dem ein
Betriebssystem gefunden wurde, von der Adresse 0x00007c00 in den
Speicher kopiert und danach der dort stehende Code durch einen
Sprung an diese Adresse ausgefhrt.

10.3 Der Kernel 239

Das Booten von einer Platte luft folgendermaen ab: im ersten Sektor der
Platte, dem MBR (Master Boot Record), befinden sich die Partitionseintrge und
ein kleines Programm, das es ermglicht, den ersten Sektor einer Partition zu
laden. Wird ein Partitionseintrag als aktiv gekennzeichnet, so wird mit dem eben
geladenen Programm an Hand der Information ber den Partitionseintrag der
ersten Sektor dieser Partition geladen.
Unter Verwendung eines Bootmanagers wie GRUB mssen
Partitionseintrge nicht mehr als aktiviert gekennzeichnet werden. Der
Bootmanager GRUB4 ist in zwei Teile aufgeteilt: der erste Teil ist sehr kurz und
dient nur dazu, den lngeren zweiten Teil zu laden und zur Ausfhrung zu
bringen. Dieser erste Teil kann sich im MBR oder im Bootsektor einer
aktivierten Partition befinden. Er muss insbesondere Information darber
haben, wo sich der zweite Teil des GRUB-Bootmanagers auf der Platte befindet,
der zur Ausfhrung gebracht werden soll. Der zweite Teil wiederum muss
Informationen darber haben, wo sich die Konfigurationsdateien auf der Platte
befinden. Er ldt und interpretiert diese. Die Konfigurationsdateien enthalten
fr jedes auf der Platte befindliche Betriebssystem die Stelle, an der der jeweilige
Kernel zu finden ist, ggf. die Initial Ramdisk und die Root-Partition und weitere
Optionen; ein Beispiel fr eine Konfigurationsdatei zeigt die Abb. auf S. 250.
Die wesentliche Aufgabe des 2. Teils des Bootmanagers besteht darin, die
Initial Ramdisk und den im Kernel integrierten Bootloader zu laden und zu
starten. Dabei muss dem Bootloader die Speicheradresse der Initial Ramdisk
mitgeteilt werden. Bzgl. der Adressen mssen vorgegebene Konventionen
eingehalten werden, damit die Start-Routine des Bootloaders gefunden werden
kann.

10.3

Der Kernel

Was nun folgt, ist in hchstem Mae Hardware-abhngig und in Assembler


geschrieben; wir finden diese Teile im Directory arch/i386/boot. Da in dieser
Abhandlung kaum auf Assembler-Code eingegangen werden kann, soll hier nur
in groben Zgen erlutert werden, was beim Booten des Linux-Kernels in der
x86-Welt geschieht.
In der Datei arch/i386/boot/setup. S findet man zu Beginn einige Adressen:
Dies sind die Adressen, die beim Laden bercksichtigt werden mssen, damit
GRUB auf die richtige Adresse zum Starten des Bootloaders zugreifen kann. In
dem Setup Header befinden sich weitere interessante Informationen: erwhnt sei
hier die Adresse der Ramdisk und der Pointer zur Kommandozeile, d.h.
derjenigen Informationen, die beim Starten mit bergeben und vom Kernel beim
Hochfahren ausgewertet werden.
Ein Blick auf den Assembler-Code offenbart die wesentlichen Aufgaben, die
hinter der Marke start_of_setup beginnen; der Kernel
Entsprechendes gilt fr den ebenfalls hufig eingesetzten Bootmanager LILO.

240

10 Der Bootvorgang

berprft die Signatur des Startsegments,


testet, ob der Loader fr den verwendeten Kernel passt,
ermittelt die Gre des RAM5,
initialisiert das Keyboard mit der hchsten Repeat-Rate6,
initialisiert die Grafik-Karte,
initialisiert die Platten-Controller und bestimmt die Geometrie der
Platte,
berprft und initialisiert den Micro Channel Bus7,
prft, ob eine PS/2 Maus (Bus-Mouse) vorhanden ist,
berprft, ob Advanced Power Management (APM) vom BIOS
untersttzt wird,
verschiebt - falls ntig - den Code an die richtige Stelle im Speicher,
erzeugt eine neue Interrupt Deskriptor Tabelle (IDT) und initialisiert
sie8,
erzeugt eine Global Deskriptor Tabelle (DGT), um die Adressierung, die
in Linux verwendet wird, zu untersttzen (vgl. Abschn. 5.4),
initialisiert den mathematischen Co-Prozessor, falls vorhanden,
wechselt in den Protected Mode und
ruft zum Schluss die Routine startup_32()9 auf.

Die nun folgenden Aktionen der Routine startup_32() bereiten das System weiter
vor. Zwar ist der Kernel geladen und die CPU befindet sich im Protected Mode
und es gibt eine Struktur, die fr die Anlage von Pagetables benutzt werden
kann; noch sind jedoch die Register nicht richtig initialisiert, deshalb fhrt der
Code im Weiteren folgende Aktionen aus:

Initialisierung der Segmentation Register und Pagetables und Start der


blichen Linux-Adressierung,
erneute Initialisierung der Interrupts10,
Kopieren der Daten, die beim Starten bergeben wurden, an eine
sichere Stelle,
Ermittlung des verwendeten CPU-Modells und Setzen der
entsprechenden Register, abhngig vom Modell der CPU,
Laden der entsprechenden Register mit den Adressen der Pagetable und

5
Dies wurde zwar schon beim Start des BIOS gemacht, aber Linux geht
vorsichtshalber seine eigenen Wege - wie auch bei den nchsten Schritten.
6

Hiermit wird die Wiederholungsrate eines Zeichens bei gedrckter Taste bezeichnet.

Micro Channel Bus (MCA) ist (war) eine von IBM initiierte Bus-Architektur, die die
Schwchen des ISA-Busses beheben sollte; heutzutage hat sich stattdessen im
Wesentlichen die PCI-Architektur durchgesetzt.
8
Dies ist ntig, weil das BIOS die Hardware-Interrupts anders zuordnet als Linux.
9
In arch/i386/kernel/head.S definiert.
10
Dies war zwar schon im BIOS erfolgt, aber im vorigen Schritt wurde fr Linux eine
Interrupt Deskriptor Tabelle angelegt.
7

10.3 Der Kernel 241

Aufruf der Funktion start_kernel(). Handelt es sich um ein


Mehrprozessor-System, so ruft nur ein Prozessor diese Funktion auf, die
anderen bedienen sich stattdessen initialize_secondary()11.

Die Funktion start_kernel()* 12 ist nicht lnger Hardware-orientiert, das nun


Folgende gilt fr alle Plattformen. Der Kopf dieser Funktion lautet
asmlinkage void _______init start_kernel(void)

asmlinkage ist eine klare Konsequenz der Tatsache, dass der Aufruf aus einem
Assemblerprogramm heraus erfolgt. Interessant und neu ist die Verwendung des
Makros __init(), das bewirkt, dass diese Funktion nach ihrer Beendigung aus
dem Speicher wieder entfernt werden kann. Da viele der
Initialisierungsfunktionen mit diesem Makro versehen sind, knnen damit ca.
250 KB Speicher nach erfolgter Initialisierung des Kernels wieder freigegeben
werden.
Es gilt, die Umgebung fr den ersten Prozessor - in Einprozessor-Systemen
den einzigen - einzurichten. Dazu gehren eine Reihe von Aktionen, die durch
einen Aufruf von lock_kernel() gegen Race Conditions gesichert werden (vgl.
Abschn. 6.3.5):

Initialisierung der Pagetables mit den Funktionen page_address_init()


und page_alloc_init(),
Ermittlung der Speicherstruktur in build_all_zonelists(),
Organisation des Speichers und Slabs in den Funktionen mem_init()
und kmem_cache_init(),
Ermittlung der beim Start bergebenen Kernelparameter in
setup_arch() und parse_args(),
Erzeugung der Datenstrukturen, die Prozessor-bezogen fr jede CPU
vorhanden sein mssen,
Initialisierung des Schedulers in sched_init(),
Initialisierung der Interrupt Deskriptor Tabelle durch die Aufrufe von
trap_init() und init_IRQ(),
Initialisierung der Timer in init_timers() und von System Time und
System Date in time_init(),
Initialisierung der Soft-IRQs in softirq_init(),
Zulassen lokaler Interrupts durch den Aufruf von local_irq_enable()
Bereitstellen der Initial Ramdisk,
Initialisierung eines Slab-Bereichs fr fork(),
Verndern der Prioritt usw. des aktuellen Prozesses, so dass er sich als
Idle-Thread eignet in init_idle().

Diese Funktion wird nicht weiter betrachtet.

Definiert in init/main.c. Interessant ist ein Blick auf den Anfang dieser Datei: da es
eine der ersten Dateien ist, die bei der Kompilation des Kernels vom Compiler bearbeitet
werden, findet man hier Tests, ob die Compiler-Version mit dem zu bauenden Kernel
vertrglich ist. Falls das nicht der Fall ist, kann frhzeitig eine Fehlermeldung ausgegeben
und der gesamte Prozess abgebrochen werden.
12

242

10 Der Bootvorgang

10.4 Die Runlevel 243

Danach
folgt ein und
Aufruf
von rest_init(),
um die
letzten Aufgaben
derProgramm
me
einzurichten
Prozesse
fr das Login
bereitzustellen.
Dieses
Initialisierung
in
einer
Funktion
vorzunehmen,
die
nicht
mit
dem
__initOluft als Prozess mit der Nummer 1 im Hintergrund und dient zugleich
zur
Makro
nach
Beendigung
aus
dem
Speicher
gelscht
wird.
Die
Aufgaben
berwachung und Steuerung der erzeugten Subsysteme und Prozesse. von
rest_init()
bestehen darin,
Die Konfiguration
wird in der Datei /etc/inittab beschrieben, die bei jedem
Startvoneinen
/sbin/init
ausgewertet
Als erstes
knnender
dieser
Datei zwei
Kernel-Thread
frwird.
Prozess
1 zu starten,
die init()-Funktion
Angabenausfhrt
entnommen
deranderen
voreingestellte
Runlevel initiiert,
- fr Desktops
undwerden:
damit alle
Kernel-Threads
blicherweise
5 - und das zu Beginn
Boot-Skript,
in derund
Regel die
mit unlock_kernel()
das Big auszufhrende
Kernel Lock wieder
aufzuheben,
Datei /etc/init.d/boot.
zum Schluss die Funktion cpu_idle() aufzurufen, die jetzt wirklich aus
Die Runlevel
beschreiben,
Eigenschaften
das fertig
dem aktuellen
Threadwelche
die Idle-Funktion
macht,
deren gestartete
Aufgabe esSystem
ist, im
hat. Abbildung
10.1
zeigt
eine
bliche
Beschreibung.
Hintergrund zu warten und den Prozessor zu bernehmen, wenn kein
anderer Thread lauffhig ist. Dabei tut der Idle-Thread nichts anderes
als auf einen anderen Thread zu warten, der lauffhig ist.

Noch ist der Kernel jedoch nicht vollstndig initialisiert: zu Beginn der Funktion
init() wird noch einmal ein Big Kernel Lock gesetzt und die Variable child_reaper
mit dem aktuellen Prozess initialisiert, damit klar ist, dass alle verwaisten
Prozesse diesem zugeordnet werden. Daran schliet sich bei einem
Mehrprozessor-System die Einrichtung der weiteren CPUs an. Die verwendeten
Aufrufe werden fr Einprozessor-Systeme mit Hilfe der Prcompiler-Anweisung
#define zu einer leeren Anweisung { } umdefiniert. Mit populate_rootfs() wird das
Filesystem der Initial Ramdisk bereitgestellt und danach do_basic_setup()
aufgerufen. Beim Aufruf dieser Funktion ist bzw. sind die CPUs, der Speicher
und das Prozess-Management initialisiert. Die Gerte sind aber noch nicht
eingebunden. Dieser Aufruf sorgt mit driver_init() fr das Einbinden der Gerte,
mit sock_init() fr eine geeignete Initialisierung des Netzwerk-Subsystems, mit
init_workqueues() fr die Initialisierung von Work-Queues und mit do_initcalls()
fr eine Initialisierung der Gertetreiber.
Nun ist das Booten des Kernels im Prinzip vollstndig fertig. Nicht mehr
bentigter Speicher, der durch das Makro __init() erzeugt wurde, wird jetzt
freigegeben, und der Big Kernel Lock wird zurckgesetzt. Doch wren wir mit
dem Ergebnis nicht zufrieden, und der Kernel wrde unsere Erwartungen kaum
erfllen. Deshalb erzeugt die Funktion init() zum Schluss drei File-Handles mit
den Nummern 0, 1 und 213 und ruft das Programm /sbin/init zur weiteren
Initialisierung mit Hilfe von run_init_process() auf. run_init_process() erzeugt
eine entsprechende Umgebung und berlagert den Prozess durch den Aufruf
execve() mit dem Programm /sbin/init. Durch diese berlagerung erfolgt die
weitere Ausfhrung im User-Space.
Runlev
el

1
3
5
6

Beschreibung
Stoppen des Systems
Single
User
10.4
Die Runlevel
Multiuser mit Netzwerk
Die
weiteren
des Systems liegen nun bei dem
Multiuser
mitVorgnge
Netzwerk der
undEinrichtung
grafischer Oberflche
Programm
/sbin/init,
das
dafr
sorgen
muss,
alle weiteren bentigten SubsysteReboot
Fr die Standard-Ein- und -Ausgabe und den Standard-Error.

13

244

10 Der Bootvorgang

Ende eines dieser Prozesse fr das damit freigegebene Device sofort wieder ein
solcher Prozess gestartet wird.
Uber die Datei /etc/inittab lsst sich natrlich noch viel mehr steuern, so
z.B.

was das System ausfhren soll, wenn es im Single User Mode


hochgefahren wird,
wie das System reagieren soll, wenn CTRL-ALT-DEL gedrckt wird,
was bei Spannungsabfall geschehen soll,
welche Skripte aufgerufen werden mssen, um in einen bestimmten
Run- level zu gelangen,
auf welchem Device ein Modem zur Datenbertragung bereitsteht usw.

Durch das Aufrufen von Skripts fr die verschiedenen Runlevel lassen sich auch
weitere Dienste einbinden. So kann ein Rechner als Fileserver mit NFS
konfiguriert werden. NFS-Server macht nur Sinn, wenn eine
Netzwerkanbindung vorhanden ist, also in den Runleveln 3 und 5. Die Maschine
soll automatisch Fileserver-Dienste anbieten, wenn sie in einem dieser beiden
Runlevel hochgefahren wird. Also wird man ein Skript zum Aktivieren des NFSServers
bereitstellen,
Abb.
10.1.
Runlevel das von demjenigen Skript aufgerufen wird, das in den
Runlevel 3 bzw. 5 schaltet. Doch muss auf noch mehr geachtet werden: das
Aktivieren des NFS-Servers ist erst dann sinnvoll, wenn bereits die
Eine
einfache Version
Datei /etc/inittab
knntenicht
wie in
Listing
10.1 aussehen:
Netzwerkdienste
vlligder
bereitstehen.
Dazu gehrt
nur
die whrend
des
Bootens des Kernels vorgenommene Initialisierung, sondern auch die Kenntnis
id : 3 :initdef ault:
si usw., die als ein spezieller Dienst beim
von Netzwerkadressen,
Routing
: :bootwait:/etc/init.d/boot
Hochfahren
der Runlevel 3 bzw. 5 gestartet wird. Es sind somit Abhngigkeiten
l:2345:respawn:/sbin/mingetty
noclear
zwischen
den Skripts zu beachten.
ttyl 2:2345 :respawn:/sbin/mingetty tty2
Das Umschalten zwischen den Runleveln kann auch whrend des laufenden
3:2345 :respawn:/sbin/mingetty tty3
Betriebs vorgenommen werden. Der Aufruf init i 15 fhrt zum Umschalten in den
Runlevel i Die einfachste Form des Umschaltens bestnde darin, alle im
aktuellen Runlevel gestarteten Dienste zu stoppen und anschlieend die Dienste
ListingRunlevel
10.1. Einfache
Form der
Datei /etc/inittab
fr den gewnschten
hochzufahren.
Geschickter
ist es, zu prfen,
welche Dienste im gewnschten Runlevel nicht mehr gebraucht werden und
welche hinzukommen mssen. Probleme bilden dabei wieder Abhngigkeiten.
Auf
Grund
der ersten Zeile wird
startet
Systemfolgendermaen
mit dem Runlevel
3. Zunchst
Bei der
SuSE-Distribution
dasdas
Problem
gelst:
wird jedoch wegen der zweiten Zeile die Datei /etc/init.d/boot als Bootskript
Alle Skripte fr die Dienste sind im Directory /etc/init.d vorhanden.
ausgefhrt. Der bootwait-Parameter bewirkt, dass das System auf das Ende
Jedes Skript hat eine Prambel, die ausgewertet wird, um die
dieses Skripts warten muss.14 Die Zeilen 3 bis 5 bewirken, dass nach Erreichen
Abhngigkeiten zu beschreiben. Listing 10.2 zeigt das am Beispiel des
des gewnschten Runlevel drei Terminal-Prozesse fr die Devices ttyl bis tty3
NFS-Servers.
gestartet werden. Der Parameter respawn veranlasst, dass nach dem
Auf Grund der Prambel trgt das Skript /sbin/SuSEconfig Links in
die Directories /etc/init.d/rc^.d ein, wobei i fr die Nummern der
14
runlevel
0-6 steht. Fr
wie nfsserver,
in Runlevel
3
Bei der
SuSE-Distribution
isteinen
diesesDienst
Boot-Skript
sehr allgemeinder
gehalten.
Will man
beim Booten
speziellewerden
Aktionensoll,
ausfhren
die nur das vorliegende
System
betreffen,
eingesetzt
werdenlassen,
in /etc/init.d/rc3.d
zwei
Links
sollten diese
in ein
Boot-Skript /etc/init.d/boot.local
geschrieben
Dies
erzeugt:
Sfcnfsserver
und Kinfsserver. Links,
die mit Swerden.
beginnen,
wird dann
automatisch
werden
zum 15am Ende des Skripts /etc/init.d/boot aufgerufen. Globale
Wartungen, die /etc/init.d/boot betreffen, verndern damit lokale Modifikationen
15
nicht. Dieser Aufruf erfordert Root-Rechte.

10.5 Module 245

Starten eingesetzt, solche mit K zum Herunterfahren. Die auf S bzw. K


folgenden Nummern k bzw. I bestimmen, in welcher Reihenfolge die Skripte
aufgerufen werden mssen.

### BEGIN INIT INFO


# Provides: nfsserver
# Required-Start: $network $remote_fs $named portmap
# Required-Stop: $network portmap
# X-UnitedLinux-Should-Start: ypbind
# X-UnitedLinux-Should-Stop:
# Default-Start: 3 5
# Default-Stop: 0 1 2 6
# Description: Start the kernel based NFS
daemon ### END INIT INFO

Listing 10.2. Prambel fr das Skript zum Starten von NFS

10.5

Module

Module dienen dazu, den Kernel mglichst schlank zu halten. Sie sollen erst
dann geladen werden, wenn sie wirklich bentigt werden. Da tut sich folgendes
Problem auf: Wenn z.B. alle Partitionen - mit Ausnahme der Boot-Partition - das
Filesystem reiserfs besitzen, wie kann dann ein Kernel ohne fest eingebundenes
reiserfs-Modul allein auf die Partition /, also die Wurzel des Filesystems,
zugreifen? Hier sehen wir, wie die Initial Ramdisk ins Spiel kommt: In der
Ramdisk muss das reiserfs-Modul gespeichert sein, so dass der Kernel sofort
darauf zugreifen kann, bevor er weitere Filesysteme einbindet.
An dieser Stelle sollen jetzt nicht Module und ihre Verwaltung im Detail
untersucht werden, sondern nur einige Probleme angesprochen werden, die die
Betriebssystem-Designer lsen mussten, damit Module verwendet werden
knnen.
Fr das Bereitstellen und Entfernen von Modulen stehen dem
Systemadministrator die Systemprogramme insmod und rmmod zur Verfgung.
Auch modprobe fgt Module ein, im Gegensatz zu insmod beachtet es dabei
jedoch Abhngigkeiten zwischen den Modulen und ldt ggf. zustzlich bentigte
Module in der richtigen Reihenfolge. Dabei greift modprobe fr jedes einzelne
Modul wieder auf insmod zurck, lsmod gibt eine Liste der derzeit geladenen
Module aus und zeigt die Abhngigkeiten.
Unter anderem mssen folgende Fragen gestellt werden:

Wie knnen Abhngigkeiten zwischen Modulen erkannt werden?


Wie exportiert ein geladenes Modul seine Symbole, so dass darauf
zugegriffen werden kann?

246

10 Der Bootvorgang

Was muss bei der Initialisierung eines Moduls geschehen?


was ist beim Entfernen eines Moduls zu beachten?
Wie knnen Module automatisch geladen werden?16

Antworten auf diese Fragen lassen sich im Wesentlichen in den folgenden


Dateien include/linux/module.h, kernel/module.c und kernel/kmod.c finden.

10.6

Zusammenfassung

Das Booten von Linux - oder allgemeiner: eines Betriebssystems - stellt sich als
mehrschichtiger Prozess dar, bei dem zunchst die BIOS-Routinen die Hardware
berprfen und einen Bootmanager laden. In der x86-Architektur kann der
Bootmanager nicht als ein Programm geladen werden, sondern ist in zwei Teile
geteilt. Dabei hat der erste Teil im Wesentlichen die Aufgabe, den viel
umfangreicheren zweiten Teil des Bootmanagers zu laden.
Dieser ldt nun seinerseits das Betriebssystem und ggf. die Inital Ramdisk,
bergibt Kernelparameter und ruft die Startroutine des Kernels auf. Erst hier
beginnt die eigentliche Arbeit von Linux - bzw. des gewhlten Betriebssystems.
Der folgende Abschnitt der Initialisierung ist sehr Hardware-abhngig und
deswegen weitestgehend in Assembler gehalten. In der x86-Architektur stellt
insbesondere die Geschichte der Prozessorentwicklung eine Schwierigkeit dar, da
im folgenden die Adressierung und der Mode des Prozessors gendert werden
mssen. Nach der erneuten berprfung der Hardware, dem Initialisieren eines
Teils der Gerte und der Umschaltung in den virtuellen Mode beginnt der
Hardware-unabhngige Part der Initialisierung durch den Aufruf von
start_kernel(). Diese Funktion richtet die Umgebung fr den ersten17 Prozessor
ein und startet nach Initialisierung der Subsysteme wie CPU, Speicher,
Prozessmanagement usw. den ersten Thread mit der Funktion init(). Hier
werden bei einem Mehrprozessor-System die anderen CPUs initialisiert, das
Netzwerk-Subsystem bereitgestellt, Gertetreiber eingebunden und initialisiert.
Nach Bereitstellung der drei Standard File-Handles wird dieser Prozess mit
einem execve()-Aufruf berlagert durch das Programm /sbin/init, das dazu dient,
den Runlevel und die Dienste so einzustellen, wie es vom Administrator
vorgesehen ist.

Dies sollte unter anderem beim Hotplugging passieren, wenn der Kernel erkannt hat,
dass ein neues Gert an den USB-Bus gesteckt worden ist. Das Modul mit dem korrekten
Treiber sollte geladen werden.
16

... und einzigen bei Einprozessor-Systemen.

17

A________________________
Kompilieren des Kernels

Auch wenn heute die Notwendigkeit in der Regel nicht mehr hoch ist, seinen
eigenen Kernel zu erzeugen, da viele Hardwaretreiber dynamisch in das laufende
System eingebunden werden knnen (vgl. 10.5), gibt es doch ein paar Grnde, die
zu einem eigenen Kernel fhren knnen:
Sicherheitserwgungen:
Loadable Modules knnen im Zweifelsfall missbraucht werden. Wenn alles,
was der Kernel fr eine bestimmte sehr sichere Produktionsumgebung
bentigt, bereits fest eingebunden ist, knnen an dieser Stelle keine
Schwachstellen mehr auftreten.
Dies bedeutet jedoch, dass alle Patches - alle sicherheitsrelevanten
nderungen am Kernel und den eingebundenen Modules - manuell
nachgehalten werden mssen und zu einer erneuten Kompilation des
Kernels fhren.
Mitarbeit an der Linux-Entwicklung:
Ohne Testen des jeweils modifizierten Kernels ist eine Mitarbeit nicht
mglich.
Interesse:
Um einige Aspekte besser zu verstehen sowie aus Neugier kann man
versuchsweise Kernel kompilieren.
Spezielle Hardware usw.:
In manchen Situationen, in denen Treiber fr neue Hardware noch nicht
zum Repertoire von Linux gehren, wohl aber schon als Testversionen bzw.
als Eigenentwicklung vorliegen, muss man den Kernel mit den Treibern
kompilieren.
Was ist zu beachten, damit man sich mit der Erzeugung eines neuen Kernels
keine Probleme einhandelt?

Es muss darauf geachtet werden, dass alle Entwicklungswerkzeuge in der


bentigten Version vorliegen. Dies betrifft zunchst den C-Compiler gcc. Als
weiteres unabdingbares Werkzeug wird make eingesetzt, make greift auf
eine Datei Makefile zu, die Regeln fr die Erzeugung des Kernel definiert.

248 A Kompilieren des Kernels

Die Kernel-Quellen mssen an der richtigen Stelle im Verzeichnisbaum,


nmlich /usr/src/linux-2.6.^-5^.5, installiert sein. Aus der
Namensgebung kann abgeleitet werden, dass es sich um die
Kernelversion 2, Pat- chlevel 6, Sublevel 4, Extraversion -54.5 handelt.
Mit Hilfe des Kommandos ln sollte usr/src/linux auf dieses
Directory verweisen.
Die verwendeten neuen Patches mssen geladen und mittels patch in
die Quellen eingefgt werden. Sollen eigene Vernderungen des
Quelltextes vorgenommen werden, so mssen sie jetzt vorgenommen
werden.
Mittels cd /usr/src/linux ist in das entsprechende Verzeichnis zu
wechseln. Die Datei Makefile ist anzupassen: die Variable
Extraversion sollte einen neuen Wert bekommen, z.B. -5^.5spc.
Damit werden bentigte Module in dem Directory
/lib/modules/2 . 6 . 4 ~ 5 4 - 5 s p c abgelegt und stren die zum
laufenden System gehrenden Module nicht. Durch geeignete
Namensgebung System.map-2.6.^-5^.5spc kann die neu erstellte
Systemmap neben der Systemmap des laufenden Systems existieren.
Nun ist die Konfiguration des Kernels festzulegen. Die Beschreibung
befindet sich in der Datei .config. Diese kann aus dem laufenden
System mit dem Aufruf
make oldconfig && make prepare

erzeugt werden. Um die Kernelkonfiguration geeignet zu modifizieren, kann


man mit dem Befehl
make xconfig

auf ein X-Fenster zur Konfiguration zugreifen. Die Konfiguration gliedert


sich in einzelne Unterabschnitte, dieje nach Anforderung verndert werden
mssen. Wichtige Abschnitte, die ggf. auch Querbezge haben, sind:
- Code maturity level options: Eine Vernderung der Eintrge wird ntig,
wenn an den Gertetreibern gearbeitet wird.
- General setup: Setzungen, die nur unter sehr bestimmten Umstnden
verndert werden sollten.
- Loadable module support: Wenn man darauf verzichten mchte, Module
dynamisch nachzuladen, mssen hier Vernderungen vorgenommen
werden. Dafr mssen aber die bentigten Module fest in den Kernel
einkompiliert werden.
- Processor type: Hier lsst sich der verwendete Prozessor genauer
festlegen und damit eine hhere Optimierung des Kernels erzielen.
- Bus options: Je nach verwendeten Bus-Systemen im Rechner knnen
einige Eintrge fallen gelassen oder mssen sogar eingeschaltet werden.
- Device drivers: Hier werden die unterschiedlichen HardwareAusstattungen beschrieben und damit die bentigten Gerte-Treiber zur
Kernel-Konfiguration angefordert.
- Executable file format: Dieser Eintrag beschreibt, welche Dateiformate

A Kompilieren des Kernels 249

- Auerdem gibt es eine Reihe von Optionen zur Sicherheit, Kryptographie,


Kernel Debugging und Profiling usw.
Viele der gesetzten Konfigurationsanforderungen lassen sich fest kompilieren
oder als Loadable Module erzeugen - sofern die Untersttzung dafr
vorgesehen ist.
Nach der Erzeugung der Konfiguration kann mit dem Kompilieren
begonnen werden:
make dep clean bzImage

trgt die Abhngigkeiten in die diversen Makefiles ein, auf die whrend der
Erzeugung automatisch zugegriffen wird, lscht alte Object-Dateien und
stt die Kompilation des Kernels an. Der neu erzeugte Kernel ist
anschlieend unter /usr/src/linux/arch/i386/boot/bzimage zu finden.
Die Module sind anschlieend mit
make modules && make modules_install

zu erzeugen. Sie werden dabei automatisch in das Verzeichnis mit dem


Namen lib/modules/2.6.^-5^.5spc kopiert.
Danach muss das neue Kernel-Image zusammen mit der Systemmap in
das Boot-Verzeichnis kopiert werden. Um Schwierigkeiten zu entgehen,
mssen dabei die Versionsnummern usw. verwendet werden. Da wir uns
im Verzeichnis /usr/src/linux befinden, lauten die Befehle
cp arch/i386/boot/bzimage /boot/vmlinuz-2.6.^-5^.5spc

und
cp arch/i386/boot/System.map /boot/System.map-2.6.^-5^.5spc

Ggf. muss noch eine Initial Ramdisk erzeugt werden. Diese wird unter
Umstnden gebraucht, weil der zu ladende Kernel vielleicht besondere
Treiber verwenden muss, um auf die Platten zugreifen zu knnen. Der
Aufruf lautet in diesem Beispiel
mkinitrd -k vmlinuz-2.6.^-5^.5spc -i initrd-2.5.^-5^.5spc

Als letzter Schritt ist der verwendete Bootloader anzupassen, damit


auch der neue Kernel geladen werden kann. Bei Verwendung von GRUB
knnte die Datei anschlieend wie in Abb. A.1 aussehen:

250 A Kompilieren des Kernels


# Modified by YaST2. Last modification on Tue May 18 15:03:25 2004
color white/blue black/light-gray default 0 timeout 8
gfxmenu (hdO,4)/message
#Dont change comment - YaST2 identifier: Original name: linux
title Linux
kernel (hdO,4)/vmlinuz root=/dev/hda6 vga=0x31a
splash=silent \ desktop resume=/dev/hda8 showopts initrd
(hdO,4)/initrd
#Dont change comment - YaST2 identifier: Original name: LinuxSPC
title TestVersion
kernel (hd0,4)/vmlinuz-2.6.4-54.5spc root=/dev/hda6
vga=0x31a \ splash=silent desktop resume=/dev/hda8 showopts
initrd (hd0,4)/initrd-2.6.4-54.5spc
#Dont change comment - YaST2 identifier: Original name: windows
title windows root (hd0,0) chainloader +1
#Dont change comment - YaST2 identifier: Original name: failsafe
title Failsafe
kernel (hd0,4)/vmlinuz root=/dev/hda6 showopts ide=nodma \
apm=off acpi=off vga=normal noresume nosmp noapic \ maxcpus=0
3 initrd (hdO,4)/initrd
#Dont change comment - YaST2 identifier: Original name: Linux-9.0
title Linux-9.0
kernel (hdO,12)/vmlinuz root=/dev/hdal4 vga=0x31a \
splash=silent desktop resume=/dev/hda8
ssowopts initrd (hdO,12)/initrd
#Dont change comment - YaST2 identifier: Original name: Mem-Test
title Speichertest
kernel (hdO,4)/memtest.bin

Listing A.1. Beispiel fr eine GRUB-Bootloader Datei

B
Lineare Listen in Linux

Neben der Verwendung von Strukturen gehren verkettete Listen zum wichtigen
Handwerkszeug, das bei Betriebssystemen - und hier bei Linux - eingesetzt wird.
Die Beschftigung mit Algorithmen und Datenstrukturen findet hier ihre
konkrete Anwendung. Nun knnten an jeder Stelle, an der Linux verkettete
Listen einsetzt, diese neu programmiert werden. Um diesem Wildwuchs
vorzubeugen, muss sich jeder Linux-Programmierer an das hierfr
bereitgestellte Werkzeug halten, das in include/linux/list.h enthalten ist.
Die typische Listenform, die Linux verwendet, ist die doppelt-verkettete
zirkulre Liste, die in der unten gezeigten Abb. schematisch dargestellt ist. Jeder
Eintrag ist zustzlich mit zwei Pointern versehen: next, der zum nchsten
Eintrag zeigt, prev, der auf den vorangehenden verweist. Der next-Pointer des
letzten Eintrags zeigt auf den ersten, der prev-Pointer des ersten Eintrags
verweist auf den letzten. Folgt man den Zeigern immer in einer Richtung, so
werden nacheinander alle Listeneintrge durchlaufen. Tatschlich ist es
gleichgltig, ob es einen ersten oder letzten Eintrag gibt, denn alle Eintrge
sind gleichberechtigt.

In der Header-Datei werden die Zeiger-Strukturen bereitgestellt durch die


Struktur listJiead. Dabei wird bewusst der in der Abb. B.1 angedeutete Datenteil
weggelassen, um eine allgemeine Form von Listen erzeugen zu knnen.

252 B Lineare Listen in Linux

Deshalb muss die Struktur list_head in andere Strukturen eingebettet werden,


wie wir es in vielen Beispielen gesehen haben, so beim PCB auf S. 21. Die
Definition von list_head hat folgende Form:

};

struct list_head {
struct list_head *next, *prev;

Es fehlen noch die entsprechenden Listen-Operationen. Auch diese werden in der


Header-Datei bereitgestellt.
Soll ein Listenelement hinter dem Listenkopf1 head eingefgt werden, so
steht dafr die inline-Funktion list_add() zur Verfgung. Die Definition sieht so
aus:
static inline void list_add(struct list_head *new,
struct list_head *head)

{
__list_add(new,
head,

head->next);

static inline void __list_add(struct list_head


*new,
struct list_head *prev,
struct list_head *next)
next->prev =
new; new->next =
next; new->prev
= prev; prev>next = new;

list_add() greift also auf die inline-Funktion __list_add() zu, die die notwendigen
Pointer-Operationen enthlt. Da es sich bei beiden Funktionen um ,,inlineFunktionen handelt, ersetzt der Compiler den Aufruf von list_add() zunchst
durch den Aufruf von __list_add() zusammen mit den Argumenten und
modifiziert diesen anschlieend durch den dort vereinbarten Code. Auf diese
Weise wird jeweils direkt entsprechender C-Code ohne Funktionsaufrufe
generiert; das Hinzufgen eines Listenelements wird dadurch sehr schnell. Die
Zeit fr diese Operation ist vom Typ 0(1), also unabhngig von der Anzahl der
Listenelemente.
Der Grund fr die Einfhrung der Funktion __list_add() liegt darin, dass
auch die Mglichkeit vorhanden sein sollte, einen Eintrag am Ende der Liste
anzufgen. Der Aufruf dafr lautet list_add_tail(). Auf Grund der
doppeltverketteten zyklischen Listenstruktur entspricht dies dem Einfgen
direkt vor dem Listenkopf. Und das kann ebenfalls mit __list_add() erreicht
werden, wenn man die Argumente anders whlt, es muss nur der Listenkopf
durch head->prev und der Nachfolger durch head ersetzt werden:
*Da alle Listenelemente gleichberechtigt sind, handelt es sich dabei um irgendein
Listenelement.

B Lineare Listen in Linux

253

static inline void list_add_tail(struct list_head *new,


struct list_head *head)

{
__list_add(new, head->prev, head);

Auch Stacks und Queues lassen sich auf diese Weise implementieren: werden mit
list_add() Elemente angefgt und das letzte Element weitergereicht, so haben wir
einen Stack; wenn list_add_tail() zum Anfgen verwendet wird und das erste
Element weitergereicht wird, so entspricht das einer Queue.
Funktionen zum Entnehmen aus der Queue mssen natrlich ebenfalls
bereit gestellt werden: list_del() nimmt das angegebene Element aus der Liste.
static inline void list_del(struct list_head *entry)

__list_del(entry->prev, entry->next);
entry->next =
LIST_P0IS0N1; entry->prev
= LIST_P0IS0N2;
static inline void __list_del(struct list_head * prev,
struct list_head * next)

next->prev =
prev; prev->next
= next;

list_del() berechnet den Vorgnger und Nachfolger und ruft mit dieser
Information die Funktion __list_del() auf, die ihrerseits die eigentlichen
Pointeroperationen in der Liste vornimmt. Danach bleibt nur noch die Aufgabe,
die Zeiger des aus der Liste entfernten Elements so zu setzen, dass sie nicht
mehr als gltige Zeiger interpretiert werden knnen, list_del() gibt jedoch nicht
den Speicher frei, den das herausgelste Element belegt; das wre auch fatal,
weil in der Regel mit dem herausgelsten Element noch etwas geschehen muss.
static inline void list_del_init(struct list_head *entry)

>

__list_del(entry->prev, entry->next);
INIT_LIST_HEAD(entry);

#define INIT_LIST_HEAD(ptr) do { \
(ptr)->next = (ptr); (ptr)->prev = (ptr); \
} while (0)

Die obige inline-Funktion list_del_init() zeigt eine zweite Funktion zum


Entnehmen aus einer Liste; im Unterschied zu list_del() wird die Struktur

254 B Lineare Listen in Linux

list_head des herausgelsten Elements so initialisiert, dass sie einen eigenen


Listenkopf bildet.
Um herauszufinden, ob eine Liste leer ist, wird list_empty() eingesetzt. Ist
der Nachfolger des Kopfes mit dem Kopf identisch, so gibt es kein Element in der
Liste:
static inline int list_empty(const struct list_head *head)

{
}

return head->next == head;

Auch das Entnehmen eines Elements und Einfgen in eine andere Liste wird
bentigt. Dies erledigt list_move() folgendermaen:
static inline void list_move(struct list_head *list,
struct list_head *head)

___list_del(list->prev, list->next);
list_add(list, head);

Entsprechend ist listjnove_tail() aufgebaut.


Zwei Listen lassen sich durch list_splice() zu einer zusammenfgen:
static inline void list_splice(struct list_head *list,
struct list_head *head)

{
if (!list_empty(list))

>

___list_splice(list, head);

static inline void __list_splice(struct list_head *list,


struct list_head *head)

struct list_head *first = list>next; struct list_head *last =


list->prev; struct list_head *at =
head->next;
first->prev = head; head->next =
first;

>

last->next = at; at->prev = last;

Nachdem geklrt ist, dass die einzufgende Liste nicht leer ist, wird sie hinter
dem Listenkopf head durch den Aufruf von __list_splice() eingefgt. Die
Pointeroperationen sind selbsterklrend.
Sollen alle Listenelemente durchlaufen werden, um eine bestimmte Aktion
mit ihnen durchzufhren, muss eine for-Schleife geschrieben werden,

B Lineare Listen in Linux 255

die - ausgehend von einem Listenelement - bei jedem Durchlauf den Zeiger auf
das nachfolgende Element benutzt, bis wir wieder beim ursprnglichen Element
angekommen sind. Dies erledigt ganz elegant fr uns das Makro list_for_each():
#define list_for_each(pos, head) \
for (pos = (head)->next, prefetch(pos->next); pos != (head);
\ pos = pos->next, prefetch(pos->next))

Aber damit haben wir noch nicht die zu bearbeitenden Daten! Tatschlich wollen
wir diejenige Struktur bearbeiten, in der der jeweilige Listenkopf eingebettet ist.
Dazu stellt die Datei list.h ein weiteres Makro bereit:
#define list_entry(ptr, type,
member) \ container_of(ptr, type,
member)

container_of()2 ist ein Makro, das die beiden Makros member_type()3 und
offsetof()4 benutzt:
#define container_of(ptr, type, member) ({
\
const member_type(type, member) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
#define member_type(type, member) __typeof__( ((type *)0)->member )
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

Ein Quelltext-Beispiel aus der Datei fs/inode.c lautet:


list_for_each(act_head, &inode_in_use) {
inode = list_entry(act_head, struct inode,
i_list); if (inode->i_sb == sb &&
IS_QUOTAINIT(inode))
remove_inode_dquot_ref(inode, type, tofree_head);

>

Was passiert nun mit diesem Quelltext. Schauen wir uns an, was der
MakroCompiler, d.h. die erste Phase des C-Compilers, daraus macht. Das Makro
list_for_each() wird durch eine for-Schleife ersetzt, act_head ist dabei der jeweils
betrachtete aktuelle Listenkopf, &inode_in_use derjenige Listenkopf, bei der der
Durchlauf startet. Wird er wieder erreicht, so wird die for-Schleife beendet. Die
Ersetzung, die der Compiler auf Grund des Makros list_for_each() vornimmt,
sieht folgendermaen aus:
2

Definiert in include/linux/kernel.h.

Ebenda.
offsetof ist in include/linux/stddef.h
definiert.
3
4

256 B Lineare Listen in Linux

for (act_inode_in_use = (inode_in_use)->next,


prefetch(act_inode_in_use->next); act_inode_in_use !=
(inode_in_use); \ act_inode_in_use = act_inode_in_use>next, prefetch(act_inode_in_use->next)) { inode =
list_entry(act_inode_in_use, struct inode, i_list); if
(inode->i_sb == sb && IS_QUOTAINIT(inode))
remove_inode_dquot_ref(inode, type, tofree_inode_in_use);

Die zweite Ersetzung erfolgt durch das Makro list_entry(), wobei diese Ersetzung
selbst noch weitere Ersetzungen durchluft. Zunchst wird list_entry() durch
container_of() ersetzt, sodann erfolgt die Ersetzung von member_type() und
offsetof (). Der endlich bersetzte Text sieht dann so aus:

for (act_inode_in_use = (inode_in_use)->next,


prefetch(act_inode_in_use->next);
act_inode_in_use != (inode_in_use);
act_inode_in_use = act_inode_in_use->next,
prefetch(act_inode_in_use->next)) { inode =
({
const __typeof__( ((inode *)0)->i_list ) *__mptr =
(act_inode_in_use);
(inode *)( (char *)__mptr ((size_t) &((inode *)0)->i_list )
);>) if (inode->i_sb == sb &&
IS_QUOTAINIT(inode))
remove_inode_dquot_ref(inode, type, tofree_inode_in_use);

_jnptr zeigt auf den Listenkopf i_list in der Struktur inode, dieser Listenkopf ist
die durch offsetof() berechnete Anzahl von Bytes vom Beginn der inode-Struktur
entfernt.

c____
Glossar

Access-Bit ist ein Bit, das bei Paging on demand in der Pagetable eingesetzt wird,
um fr eine Page anzuzeigen, ob in letzter Zeit ein Zugriff auf diese erfolgte.
Es wird bei Schreib- und Leseoperationen auf dieser Page von der CPU
gesetzt. Das Access-Bit wird in regelmigen Abstnden berprft und
anschlieend zurckgesetzt. Ist bei der berprfung das Bit gesetzt, wird
diese Page bei Linux an den Anfang der active-Liste eingetragen.
ACID (Atomicity, Consistency, Isolation, Durability) ist die Abktzung fr die
Anforderungen, die an eine Transaktion gestellt werden.
ACL (Access Control List: Zugriffskontroll-Liste) ist eine Verallgemeinerung der
Zugriffsrechte, die die Rechte fr jeden einzelnen Benutzer regelt.
Adressraum ist eine zusammenhngende Menge logischer Adressen, die zu
einem Prozess gehren.
Allocation bezeichnet das Bereitstellen von Platz im Speicher oder auf der Platte.
Altern bezeichnet eine Methode, um mit Starvation (Verhungern) umzugehen.
Scheduling, das aufPrioritten basiert, neigt dazu, gering priorisierte
Prozesse verhungern zu lassen. Altern erhht die Prioritt nicht bearbeiteter
Prozesse nach Ablauf einer vorgegebenen Zeitspanne, so dass nach mehreren
Perioden die Prioritt eines solchen Prozesses so hoch ist, dass auch er beim
Scheduling bercksichtigt wird.
Assembler bersetzt Programmtext, der aus logischen Adressen und lesbaren
Maschinenanweisungen besteht, in ausfhrbaren Code.
Atomar heit eine Folge von Maschinen-Instruktionen, die nicht durch einen
Interrupt unterbrochen werden kann.
Basisregister ermglicht es, eine logische auf eine physische Adresse abzubilden:
im Basisregister (Register) der CPU wird die physische Anfangsadresse
eines Bereichs gespeichert. Bei jedem Speicherzugriff auf diesen Bereich
wird zur logischen Adresse der Inhalt des Basisregisters hinzuaddiert.
Batch (Stapelverarbeitung) wird eine Verarbeitung von Jobs (Prozessen) genannt,
die ohne Benutzer-Interaktion erfolgt. Wichtiger als die Reaktionszeit ist bei
der Batch-Verarbeitung in der Regel der Durchsatz, weshalb

258 C Glossar

bei Batch-orientierten Betriebssystemen die Algorithmen des Schedulers


anders gewhlt werden mssen als z.B. bei Timesharing-orientierten
Betriebssystemen. Doch auch bei Timesharing-orientierten
Betriebssystemen gibt es hufig die Mglichkeit, Prozesse als Batch im
Hintergrund zu starten.
Benutzer ist ein Anwender (Person oder Programm, z.B.
Datenbankmanagementsystem), der dem System unter einer eindeutigen
Kennung (userid) bekannt ist. Ein Benutzer besitzt Rechte, die festlegen,
auf welche Dateien und welche Objekte er zugreifen darf.
Best Fit wird eine Strategie genannt, die die kleinste Speicherlcke sucht,
die fr die Speicheranforderung ausreicht. Vgl. auch First Fit und Worst Fit.
BIOS (Basic Input/Output System) ist das in einem ROM gespeicherte
elementare Bootprogramm zum Starten eines Rechners. Das BIOS ist in
seinen Fhigkeiten sehr beschrnkt, es kann nicht viel mehr, als den ersten
Block einer Platte einzulesen und das darin enthaltene Programm zu
starten.
BKL (Big Kernel Lock) ist eine veraltete Sperrtechnik unter Linux, die
inzwischen weitgehend aus dem Kernel verschwunden ist. Es handelt sich
um einen globalen Spinlock, der durch feiner granulierte Sperren abgelst
wird.
Block bezeichnet die Einheit, in der Daten auf Platten usw. abgelegt werden.
Eine Partition ist dabei in Blcke gleicher Gre aufgeteilt und enthlt ein
eigenes Filesystem. Ein Block kann direkt gelesen bzw. geschrieben werden.
Blockgruppe wird zur Fein-Einteilung einer Platte bzw. Partition benutzt,
um Datei-Informationen, Directories und zugehrige Dateien mglichst
dicht beieinander zu speichern und die Bewegung des Plattenarms beim
sequentiellen Zugriff auf eine in einer Blockgruppe gespeicherte Datei
mglichst klein zu halten.
Bootblock ist der erste Block auf einer Platte oder Partition, der unter
anderem Code zum Starten eines Betriebssystems enthalten kann.
Bootmanager dient zum Starten eines Betriebssystems. Dazu wird der
jeweilige Kernel von einer vorgegebenen Plattenposition geladen und
anschlieend die Kontrolle an den geladenen Kernel bergeben.
Bootmanager dienen in der Regel dazu, das Booten unterschiedlicher
Betriebssysteme zu ermglichen. Dabei kann hufig beim Start des
Bootmanagers das gewnschte Betriebssystem in einem Men ausgewhlt
werden; zustzlich lassen sich Optionen an den zu startenden Kernel
bergeben. Typische Bootmanager sind LILO oder GRUB.
Bootstrapping bezeichnet das schrittweise Laden immer leistungsfhigerer
Programme, bis zuletzt das Betriebssystem selbst geladen und initialisiert
werden kann.
Bottom Half ist der zweite Teil eines teilbaren Interrupts-Hndlers nach der
Top Half. Whrend im ersten Teil, der Top Half, die unverzglich ablaufen
muss, keine Interrupts zugelassen sind, kann in der Bottom Half nor-

C Glossar 259

mal auf Interrupts reagiert werden. Ziel der Aufteilung ist es, einerseits
mglichst schnell auf Interrupts zu reagieren, um die Hardware fr die
nchste Aufgabe frei zu machen, andererseits dem Interrupt-Handler zu
ermglichen, auch anspruchsvolle, Ressourcen-intensive Aufgaben
durchzufhren.
Buddy System Algorithmus zielt darauf ab, effizient zusammenhngende freie
Speicherblcke bereitstellen zu knnen. Dazu werden freigegebene
Speicherblcke daraufhin untersucht, ob ihr Buddy - ihr Nachbar ebenfalls frei ist. In diesem Falle werden die freien Blcke zu einer greren
Einheit zusammengefasst. Das Verfahren ist so konzipiert, dass die Anzahl
zusammengefasster Blcke jeweils eine 2er Potenz bildet. Diese
Zusammenfassung kann ber mehrere Stufen erfolgen.
Bus bezeichnet die physische Verbindung unterschiedlicher Bauteile, z.B. CPU
und Speicher. Damit die Bauteile miteinander kommunizieren knnen, muss
der jeweilige Bus vorgegebene Spezifikationen erfllen.
Busy Wait ist eine Methode, aktiv auf die Verfgbarkeit einer Ressource zu
warten. Mit aktiv ist dabei gemeint, dass das Programm in einer eng
gefhrten Endlosschleife die Ressource abfragt, ob sie bereit ist. Die Schleife
wird in dem Augenblick verlassen, in dem die Verfgbarkeit erkannt wird.
Dieses Verfahren kann bei Betriebssystemen sinnvoll eingesetzt werden, die
nur einen Task untersttzten. Bei Multitasking-Betriebssystemen ist dieser
Ansatz aber problematisch, weil die CPU Blindarbeit leistet, die keinem
Benutzer-Prozess zu Gute kommt.
Cache bezeichnet eine Hardware- und Software-untersttzte Organisation, um
schnell auf Information zuzugreifen, die andernfalls nur langsam zu
beschaffen ist. Dabei wird die gewnschte Information nach dem ersten
notwendigen Zugriff in einem schnelleren Speicher fr eine gewisse
Zeitspanne aufbewahrt. Findet in dieser Zeitspanne erneut ein Zugriff auf
diese Information statt, so gengt der Zugriff auf den schnellen Speicher, es
muss nicht mehr auf den langsamen Speicher zugegriffen werden. Beispiele
sind der auf der CPU integrierte Cache, der viele Zugriffe der CPU auf den
langsameren Hauptspeicher vermeidet, oder die I/O-Puffer, die es
ermglichen, langsame Plattenzugriffe zu vermeiden, wenn die gewnschte
Information bereits im Hauptspeicher ist.
Client-Server-Architektur siehe CS.
Compiler dient dazu, den lesbaren Quelltext eines Programms einer hheren
Programmiersprache in ausfhrbare Maschineninstruktionen umzusetzen.
Copy-on-Write ist eine Methode, das Erzeugen von Pages fr einen Prozess erst
dann durchzufhren, wenn es wirklich ntig ist: wird ein Prozess durch
fork() erzeugt, so knnen der Eltern- und der Kind-Prozess solange die
gleichen Pages benutzen, bis einer der beiden Prozesse durch Schreiben
nderungen daran vornimmt. In diesem Fall muss fr den Kind-Prozess die
Page zunchst neu angelegt und kopiert werden. Dieses Vorgehen erspart
das unntige Kopieren vieler Pages.

260 C Glossar

Contiguous ist eine Dateiorganisation, bei der die Blcke einer Datei
zusammenhngend mit aufsteigenden Blocknummern ohne Lcken
bereitgestellt werden.
CPU (Central Processing Unit) ist derjenige Teil des Rechners, in dem die
Maschinen-Instruktionen ausgefhrt werden. Neben dem Rechenwerk
(ALU) und einer Adressiereinheit sind Register und in der Regel ein
interner Cache integriert.
Critical Section (kritischer Abschnitt) bezeichnet bei kooperierenden
Prozessen einen atomar zu bearbeitenden Code-Abschnitt, in dem sich zu
einem gegebenen Zeitpunkt nur einer der Prozesse aufhalten darf
(gegenseitiger Ausschluss). Diese Code-Abschnitte sehen in der Regel bei
den Prozessen unterschiedlich aus. Sperren (Lock) werden eingesetzt, um
das Betreten des kritischen Abschnitts zu regeln. Indem garantiert wird,
dass sich jeweils nur ein Prozess in seinem kritischen Abschnitt befindet,
werden Race Conditions verhindert. Damit soll vermieden werden, dass zwei
Prozesse gleichzeitig im gleichen Bereich (z.B. Shared Memory oder Shared
Files) lesend oder schreibend zugreifen.
CS (Client-Server-Architektur) ist eine unsymmetrische Architektur, in der
ein Prozess - hufig auf einem entfernten Rechner - seine Dienste als Server
anbietet, die andere Rechner, die sogenannten Clients, anfordern.
Current Directory bezeichnet das momentane Arbeitsverzeichnis eines
Benutzers. In diesem werden alle erzeugten Dateien gespeichert, sofern kein
Pfad angegeben wird, und von diesem werden alle relativen Suchen (Pfad)
nach Dateien gestartet.
Current File Position bezeichnet die Position des Lese-/Schreibzeigers einer
geffneten Datei. Von dieser Stelle an werden die nchsten Bytes
bertragen.
Daemon ist ein im Hintergrund laufender Prozess, der Dienste anbietet. Es
gibt keine direkte Benutzer-Interaktion, vielmehr wartet ein Daemon
darauf, von einem anderen Programm aktiviert zu werden.
Datenkompression hilft, das Volumen gespeicherter oder bertragener
Daten zu reduzieren. Vertreter verlustfreier Kompression (das Original lsst
sich eindeutig ohne Verlust wieder herstellen) sind zip, rar, arj usw. Neben
den verlustfreien Verfahren knnen in bestimmten Fllen auch
verlustbehaftete Verfahren eingesetzt werden, so beispielsweise JPEG bei
Bildern.
Dateisystem siehe Filesystem.
Directory strukturiert ein Filesystem, indem dadurch logische Bereiche fr
die Dateien geschaffen werden. In der Regel werden darin Name, Rechte,
Speicherort ... der darin enthaltenen Dateien vermerkt.
Direkter Zugriff (direct I/O) wird ein Zugriff auf einen Block genannt, bei
dem auf Grund der Position in der logischen Datei die physische
Blocknummer ermittelt wird, ohne die davor (und dahinter) liegenden
Bereiche der Datei zu beachten.
DMA (Direct Memory Access) dient dazu, Daten zwischen Speicherbereichen
oder Speicher und Peripherie ohne CPU-Beteiligung zu kopieren. Damit

C Glossar 261

wird die CPU nur noch zum Initialisieren des entsprechenden Bausteines
bentigt und somit entlastet; sie kann sich damit anderen Aufgaben widmen.
DOS hie ein sehr populres Single User Single Tasking Betriebssystem fr die
PC-Architektur.
Durchsatz bezeichnet die pro Zeiteinheit erledigte Menge an Prozessen. Dieses
Ma ist besonders fr Batch-orientierte Systeme wichtig.
Echtzeit Unter diesem Schlagwort werden Anforderungen an ein Betriebssystem
verstanden, auf ein Ereignis innerhalb einer vorgegebenen (sehr kurzen)
Zeitspanne garantiert zu reagieren.
Environment (Umgebung) bezeichnet in Unix-Betriebssystemen eine Menge von
Null-terminierten Zeichenketten, die dazu dienen, Umgebungsvariablen und
deren Werte einem Prozess zu bergeben. Das Environment wird beim
Duplizieren eines Prozesses mittels fork()-Aufruf und einer nachfolgenden
berlagerung durch ein neues auszufhrendes Programm mit Hilfe der
exec()-Familie vererbt. Das Environment kann bei der berlagerung mit
dem speziellen Aufruf execve() neu gesetzt werden.
EXT2 (extended filesystem, version 2) ist ein unter Linux gngiges Filesystem.
Externe Fragmentierung ist eine Fragmentierung, bei der im Gegensatz zur
internen Fragmentierung die Lcken auf der Platte bzw. im Speicher keiner
Datei bzw. keinem Prozess zugeordnet werden knnen.
FCFS siehe FIFO.
Feedback wird bei manchen Schedulern eingesetzt, um auf ein sich vernderndes
Profile der Prozesse angemessen zu reagieren. Wechselt z.B. ein Prozess
nach interaktiver Eingabe in eine Phase intensiver Berechnung ohne weitere
Benutzereingaben, so kann ein Scheduler ohne Feedback dieses andere
Verhalten nicht bercksichtigen.
Fehlercode siehe Returncode.

FIFO (First In First Out, auch als FCFS - First Come First Serve bezeichnet) ist
eine Speicher-interne Organisation, bei der Nachrichten (oder auch
Arbeitsauftrge) eingestellt und in der Reihenfolge des Ankommens wieder
ausgelesen werden knnen.
Filedeskriptor oder auch File-Handle ist eine Integer, die das Betriebssystem
beim Offnen einer Datei als eindeutigen Wert vergibt. Sie identifiziert
innerhalb eines Prozesses eine geffnete Datei. Alle weiteren Zugriffe auf die
geffnete Datei erfolgen unter Nennung dieses Deskriptors.
Filesystem dient zur Bearbeitung und Speicherung von Dateien auf Platte (oder
hnlichen Datentrgern). Es muss die Benennung der Dateien, den Zugriff
auf die Dateien sowie die Wahrung der Zugriffsrechte untersttzen.
Firewire ist ein serieller Bus zum schnellen Datenaustausch zwischen
Peripheriegerten. Die Gerte knnen dabei direkt miteinander
kommunizieren.
First Fit eine Strategie, die nach dem ersten Vorkommen von gengend freiem
zusammenhngenden Speicher sucht (vgl. Best Fit und Worst Fit).

262 C Glossar

Fragmentierung entsteht, wenn im Speicher oder auf der Platte whrend der
Arbeit Blcke belegt bzw. freigegeben werden. Die freien Bereiche verndern
sich stndig; je nach Organisation kann es dazu kommen, dass zwar viel
freier Platz vorhanden ist, aber die Lcken nicht mehr zulassen, grere
zusammenhngende Bereiche zu belegen. Durch Defragmen- tierung wird
dann versucht, zumindest einen Teil der belegten Bereiche so zu
verschieben, dass wieder hinreichend groe Lcken entstehen. Manche
Filesysteme haben Strategien zur Vermeidung von Fragmentierung.
Frame teilt bei Paging den physischen Speicher in Blcke gleicher Gre. Die
Blockgre ist so auf die Adressierung ausgerichtet, dass der erste Teil der
Adresse den Frame adressiert, der zweite Teil eindeutig alle Adressen eines
Frames beschreibt. Die Frame-Gre ist auf Grund heutiger Architekturen
eine 2er Potenz, der Anfang jedes Frames liegt ebenfalls auf einer 2er
Potenz. Page- und Frame-Gre stimmen berein.
Freelist ist eine Organisation zur Verwaltung freier Speicherbereiche.
glibc ist eine C-Bibliothek, die dazu dient, Standard-C-Funktionen in
entsprechende System Calls umzusetzen.
Granularitt bezeichnet allgemein die Feinheit, in der eine Ressource wie zum
Beispiel der Speicher unterteilt werden kann. Hohe Granularitt bedeutet,
dass sich die Ressource besonders fein einteilen lsst.
Grenzregister ist eine Methode des Speicherschutzes: whrend das Basisregister
auf den Beginn des zum Prozess gehrigen Speicherbereiches zeigt, enthlt
das Grenzregister die obere Grenze fr die logische Adresse. berschreitet
die logische Adresse diese Grenze, so wird ein Illegal Address Interrupt
ausgelst. Whrend das Basisregister auch bei heutigen
RechnerArchitekturen noch recht deutlich zu sehen ist, ist die Funktion des
Grenzregisters blicherweise in das Paging verlagert worden.
GRUB ist ein Bootmanager.
Handler siehe Interrupt Handler.
Hashing ist eine Methode, einen groen, aber nur sprlich benutzten Adressraum durch einen Algorithmus auf einen kleinen Adressraum abzubilden,
der jedoch so gro sein muss, dass alle benutzten Adressen des groen
Adressraums darin eindeutig abgebildet werden knnen. Vorteil des
Hashings bei guter Hashfunktion ist der sehr schnelle Zugriff,
problematisch jedoch die Ermittlung einer guten Hashfunktion.
Header werden bei Datenpaketen verwendet, die im Netz verschickt werden
sollen. Durch die Header wird die Schichtenarchitektur der Netzsoftware
untersttzt: jede Ebene kapselt das an sie bergebene Paket mit einem
Header, d.h. fgt ein Byte-Array vor das Datenpaket, das die notwendigen
Informationen zur Verarbeitung durch diese Schicht enthlt. Beim
Hochreichen eines Datenpakets in eine bergeordnete Schicht wird dieser
Header natrlich entfernt. In manchen Fllen werden zustzlich Trailer
bentigt, Byte-Arrays, die hinten an das Datenpaket angehngt werden.

C Glossar 263

Heap ist eine Speicherstruktur, die von Prozessen benutzt wird, um lokale
Variablen und temporre Datenstrukturen aufzubewahren und beliebig
wieder zu lschen.
HOME heit eine Environment-Variable in Unix-Systemen, die den Pfad zum
Wurzelverzeichnis des Benutzers enthlt.
Hotplug bezeichnet die Mglichkeit, bei laufendem Betrieb Gerte hinzuzufgen
bzw. zu entfernen. Die Ereignisse werden berwacht, die Gerte werden
automatisch einschlielich bentigter Treiber eingebunden.
Indexed ist eine Dateiorganisation, bei der die Dateiblcke ber zustzliche
Indexblcke verwaltet werden. Die einzelnen Blcke knnen bei der
Allocation frei gewhlt werden.
Indirektion tritt auf, wenn der Zugriff auf einen Block ber ein- oder mehrstufige
Indexe erfolgt.
init-Prozess ist ein Prozess, der zu Beginn vom Kernel aufgerufen wird, und dazu
dient, in einem Unix-Betriebssystem die gesamte Prozess-Hierarchie zu
starten: jeder Prozess ist direkt oder ber mehrere Zwischenschritte durch
fork() aus dem init-Prozess hervorgegangen.
Initial Ramdisk oder RAM-Disk heit eine Datei, die whrend des Bootvorganges
in den Speicher geladen wird. Die Organisationform der Datei entspricht
dem Abbild einer Diskette, die gelesen werden kann, bevor weitergehende
Filetreiber geladen sind. Die RAM-Disk wird vom Bootmanager lesend zur
Verfgung gestellt, bevor der Kernel gestartet wird. Sie wird dazu benutzt,
um Treiber zu laden, die frh im Bootprozess zur Verfgung stehen mssen,
whrend noch keine komplexen Filesysteme gelesen werden knnen. Als
typische Module fr eine Initial Ramdisk sind SCSI-Treiber und das ReiserFilesystem zu nennen.
initrd siehe Initial Ramdisk.
inline ist eine Technik in der C-Programmierung, um einen Funktionsaufruf zu
beschleunigen. Es handelt sich dabei um eine Anweisung an den
Prcompiler. Anstatt die Funktion durch Call aufzurufen, wird der Quelltext
der Funktion vor dem bersetzen substituiert.
Inode ist eine Datenstruktur, die die wesentlichen Informationen ber eine Datei
enthlt, wie Rechte, Gre, Zeitpunkt des Erzeugens, Eigentmer, usw. - mit
Ausnahme des Dateinamens.
Interne Fragmentierung sind Lcken auf der Platte oder im Speicher, die
dadurch entstehen, dass der einer Datei bzw. einem Prozess zugeordnete
Platz nicht vollstndig ausgefllt wird.
Interrupt ist eine synchrone oder asynchrone Unterbrechung der gerade
bearbeiteten Instruktions-Folge. Eine synchrone Unterbrechung, auch
Signal genannt, liegt vor, wenn der Prozessor Ausnahmen im Rahmen seiner
Verarbeitung erkennt, z.B. Division durch Null. Whrend bei
KernelRoutinen diese Situationen intern verarbeitet und in Form eines
FehlerCodes zurckgegeben werden, dienen bei Bearbeitung von Prozessen
im User Mode Signale dazu, Prozess-spezifische Handler fr diese
Ausnahmen aufzurufen. Asynchrone Unterbrechungen kommen durch
externe Ereig-

264 C Glossar

nisse zustande, die nicht mit der CPU-Verarbeitung Zusammenhngen: die


Hardware erkennt das Eintreten eines solchen Ereignisses und erzeugt den
Interrupt, der Prozessor wechselt dabei in den Kernel Mode.
Interrupt Handler ist eine Prozedur, die auf Grund einer Unterbrechung sofort
ausgefhrt werden soll. Dem Interrupt liegt das Eintreten eines Ereignisses
zu Grunde - z.B. Ankunft eines Datenpaketes aus dem Netz -, auf das der
Handler entsprechend reagieren soll. Handler werden in der Regel darauf
achten mssen, whrend ihrer Verarbeitung Interrupts weitgehend zu
unterdrcken. Das bringt Probleme mit sich, wenn die zu bearbeitende
Aufgabe sehr Ressourcen-intensiv ist. Deshalb wird hufig versucht, den
Handler in eine Top Half, die sehr schnell ausgefhrt werden muss und
dabei Interrupts weitgehend unterdrckt, und in eine Bottom Half zu
unterteilen, die spter ausgefhrt werden kann und bzgl. der Interrupts
wesentlich unproblematischer ist. Damit Handler aufgerufen werden
knnen, mssen sie dem System bekannt gemacht werden.
Interrupt-Kontext ist ein Zustand, in den der Kernel durch das Eintreffen eines
Interrupts kommt. Befindet sich der Kernel im Interrupt-Kontext, drfen
keine Punktionen aufgerufen werden, die blockieren. Die normale
Verarbeitung hingegen erfolgt im Prozess-Kontext.
Illegal Address Interrupt wird erzeugt, wenn ein Prozess versucht, auf eine
Adresse zuzugreifen, die nicht zu seinem Adressraum gehrt.
IP (Internet Protocol) gehrt zur Internet-Protokoll Familie TCP/IP und
entspricht dem Data link layer des ISO/OSI-Modells. IP ist das Protokoll der
Vermittlungsschicht, TCP und UDP sind die Protokolle der
Transportschicht.
IPC (Inter Process Communication) ist eine Implementierung von
Kommunikationsmechanismen zwischen Prozessen eines Rechners, auf dem
ein Unix-Betriebssystem luft. Es werden Message Queues, Semaphoren
und Shared Memory bereitgestellt. Zur Kommunikation gehren zwar auch
Pipes, Named Pipes und Signale, jedoch gehren diese auf Grund ihrer
Implementation nicht zu IPC.
IRQ siehe Interrupt Request.
ISA-Bridge verbindet den PCI-Bus mit dem ISA-Bus.
ISA-Bus ist ein veralteter Bus, der in der PC-Architektur verwendet wurde und
auch heute noch zur Einbindung einiger Festplatten und ControllerKarten
benutzt wird.
ISAM (Indexed Sequential Access Method) bezeichnet eine Zugriffsmethode fr
Dateien, die sowohl sequentiellen als auch index-basierten Zugriff
untersttzt.
Kernel bezeichnet denjenigen Teil des Betriebssystems, der als eine Einheit
kompiliert wird. Er bietet grundlegende Dienste an. Je grer der Kernel,
desto hher in der Regel die Komplexitt der Software und die Gefahr
fehlerhafter Programmierung. Dies spricht fr mglichst schlanke Kernel,
bei denen viel Arbeit in ladbare Module verlagert wird.

C Glossar 265

Kernel Mode ist ein Zustand der CPU. Die CPU wechselt aus dem User Mode in
den Kernel Mode, wenn ein System Call ausgefhrt wird oder ein Interrupt
auftritt. Im Kernel Mode sind smtliche Instruktionen zulssig, das
Betriebssystem hat damit vollstndige Kontrolle ber den Rechner.
Kryptographie bezeichnet eine Verschlsselung zum Schutz gespeicherter oder
bertragener Daten gegen Aussphen. Es gibt eine Vielzahl von Verfahren,
dabei wird zwischen symmetrischen und asymmetrischen Verfahren
unterschieden. Zu den symmetrischen gehren z.B. DES, Twofish;
asymmetrische Verfahren werden auch als Public Key Verfahren bezeichnet.
Leichtgewichtiger Prozess bezeichnet in der Regel einen Thread, der alle
Ressourcen mit seinem Prozess teilt und nur eigenstndig dem Scheduling
unterworfen ist, einen eigenen Stack und einen Bereich besitzt, in dem die
Register der CPU gespeichert werden knnen, wenn der Thread die CPU
nicht besitzt. In Linux wird damit ein Prozess verstanden, der mit
geeigneten Flags mit Hilfe des clone()-System Calls erzeugt wird. Die Flags
beschreiben, welche Ressourcen die beiden Prozesse gemeinsam teilen. Sind
die Flags CLONE_FS, CLONE_FILES, CLONE_VM und
CLONE_THREAD gesetzt, so entspricht dies in der Auswirkung einem
Thread.
Library ist eine Sammlung von bersetzten Prozeduren und Funktionen. Der
Code, der z.B. vom C-Compiler gcc erzeugt wird, ist in der Regel erst dann
lauffhig, wenn er mit entsprechenden Bibliotheken gelinkt wird, da erst
darin die Umsetzung von Standard-Aufrufen wie z.B. open() in
entsprechende System Calls vorgenommen wird.
LIFO (Last In, First Out) ist eine Speicher-interne Organisation, bei der
Information eingestellt und in umgekehrter Reihenfolge wieder ausgelesen
werden kann (Beispiel: Stack).
LILO ist ein Bootmanager.
Link (Verknpfung) stellt eine Verbindung zwischen Filesystem-Objekten her.
Unix unterscheidet zwischen symbolischen und harten Links. Ein
symbolischer Link spiegelt das Objekt scheinbar an einer bestimmten Stelle
des Dateibaums. Link und das Ziel des Links sind nicht fest verbunden: wird
das Ziel gelscht, dann verbleibt der Link im System, zeigt aber ins Leere.
Ein harter Link hingegen greift auf eine bereits vorhandene Inode zu. Harte
Links knnen nur innerhalb eines Filesystems eingesetzt werden.
Liste - hier verkettete Liste - ist eine Datenstruktur, die im Code von
Betriebssystemen sehr intensiv genutzt wird. Bei einer einfach verketteten
Liste enthlt jeder Listeneintrag einen Zeiger auf den nachfolgenden
Eintrag. Der Eintrag des letzten Elements zeigt auf Null. Bei einer doppelt
verketteten Liste enthlt jeder Eintrag zwei Zeiger: zum Vorgnger und zum
Nachfolger. Listen besitzen einen Listenkopf, der einen Zeiger auf das erste
Listenelement bzw. auf das erste und das letzte Listenelement bei einer
doppelt verketteten Liste enthlt.
Listenkopf siehe Liste.
Loadable Module bezeichnet ein Modul, das der Kernel bei Bedarf nachladen
kann. Als Beispiel kann das Modul fat.ko dienen. Der Kernel ldt dieses

266 C Glossar

Modul, wenn eine FAT-partitionierte Partition, z.B. eine unter Windows


erstellte Diskette, in den Dateibaum eingefgt werden soll.
Lock bezeichnet eine Sperre, die verhindert, dass auf ein bestimmtes Objekt
zugegrifFen werden kann. Sperren im Linux-Kernel werden ber BKL
(veraltet), Semaphoren und Spinlocks erzeugt.
Logische Adresse ist diejenige Adresse, unter der eine Datenstruktur oder
eine Funktion dem Programm bekannt ist. Um zugreifen zu knnen, muss
die logische Adresse in die physische Adresse umgewandelt werden.
Longterm Scheduler ist ein Scheduler, der insbesondere bei Batch-Systemen
von Interesse ist. Aus einer Warteliste von Auftrgen whlt er denjenigen
aus, der als nchstes als Prozess in das System bernommen werden soll.
LRU (Least Recently Used) bezeichnet einen Ansatz beim Paging on
Demand, um die oft benutzten Pages mglichst im Speicher zu behalten. Da
ein Blick in die Zukunft nicht mglich ist, richtet man den Blick in die
Vergangenheit: eine Page, die lange nicht benutzt worden ist (least recently
used), wird wohl auch in naher Zukunft nicht bentigt.
Magic Signature, magic number oder Magische Zahl bezeichnet ein oder
mehrere Bytes, die an bestimmter Stelle eines Objekts gespeichert werden.
Aus dem dort gespeicherten Inhalt kann das Betriebssystem gewisse
Informationen ableiten: steht dort etwas anderes als erwartet, so handelt es
sich garantiert nicht um das gewnschte Objekt. So benutzt beispielsweise
das Kommando file unter anderem diesen Mechanismus, um den speziellen
Typ einer Datei zu bestimmen.
Makro (Macro) ist Code fr den Prcompiler, also nicht in der Quellsprache
selbst geschrieben. Er muss erst noch durch den Prcompiler in
entsprechenden Quelltext umgesetzt werden. Dies erfolgt auf Grund der
MacroDefinition durch Auswertung der verwendeten Argumente.
Maschineninstruktion ist eine ein oder mehrere Byte umfassende
Instruktion, die die CPU direkt verwerten kann. Assembler- oder Quell-Code
hingegen ist lesbar, kann aber vom Prozessor nicht direkt verarbeitet
werden, sondern muss erst durch einen Compiler in eine geeignete Folge von
Maschineninstruktionen umgewandelt werden.
MBR (Master Boot Record) ist der erste Sektor einer Platte, der die
Partitionstabelle und ein kleines Programm enthlt, das den ersten Sektor
einer Partition laden und das dortige Programm starten kann.
Mediumterm Scheduler beschftigt sich damit, unter den ausgelagerten
Prozessen denjenigen auszuwhlen, der wieder als rechenbereiter Prozess in
die Bereit-Schlange eingegliedert werden soll.
Message Queue gehrt zu IPC. Diese Organisation ermglicht es (mehreren)
nicht verwandten Prozessen, Nachrichten auszutauschen. Eine Message
Queue kann FIFO benutzt werden, es gibt jedoch auch die Mglichkeit,
Prioritten festzulegen und damit das FIFO zu umgehen.
MMU (Memory Management Unit) ist diejenige Einheit eines Rechners, die
die logische Adresse, die im Programm verwendet wird und mit der die CPU
arbeitet, in die physische Adresse des realen Speichers umwandelt.

C Glossar 267

Die MMU kann ein eigenstndiger Baustein oder in der CPU integriert sein.
Modul siehe loadable Module.
mount bezeichnet in Unix-Systemen das Einfgen einer Partition in den
Verzeichnisbaum.
Multiprozessor Systeme haben mehr als eine CPU. Sie knnen eng oder lose
gekoppelt sein: eng gekoppelt bedeutet gemeinsamen Zugriff der CPUs auf
einen Hauptspeicher, lose gekoppelt: jeder CPU ist ein Speicher zugeordnet,
die CPUs knnen nicht direkt auf den Speicher einer anderen CPU
zugreifen, die Kopplung erfolgt ber einen Kommunikationsbus. Linux
untersttzt enge Kopplung. Dabei kann der Zugriff einer CPU auf den
Speicher mit unterschiedlicher EfRzienz erfolgen, je nachdem, wie nahea
der Speicher der jeweiligen CPU zugeordnet ist (vgl. NUMA).
Multitasking bedeutet, dass das Betriebssystem die parallele - gleichzeitige Bearbeitung mehrerer Tasks untersttzt. Im eigentlichen Sinne kann es
Parallelverarbeitung nur bei Mehrprozessor-Systemen geben; parallel
bedeutet, dass die Tasks durch die Verwendung eines Schedulers scheinbar
gleichzeitig bearbeitet werden. Damit einher geht die Notwendigkeit,
Speicherschutz zu implementieren, damit ein Prozess nicht auf den
Speicherplatz eines anderen Prozesses zugreifen kann.
Multiuser heit, das Betriebssystem untersttzt mehrere Benutzer. Damit muss
zugleich ein Rechte-System bereitgestellt werden, damit Dateien gegen
unerlaubten Zugriff geschtzt und Programme nur von autorisierten
Benutzern gestartet werden knnen.
Named Pipe ist eine FIFO-Organisation im Speicher, die fr die Prozesse wie
eine Datei aussehen, jedoch werden keine Daten auf der Platte gespeichert;
es wird jedoch die Inode fr die Pipe auf der Platte abgelegt. Im Gegensatz
zu Pipes werden sie durch mkfifo() bzw. mknod -p angelegt, anschlieend
knnen Prozesse mit Hilfe des dabei vergebenen Namens darauf zugreifen.
Somit knnen auch Prozesse, die nicht mittels fork() miteinander verwandt
sind, ber diesen Mechanismus kommunizieren.
NFS (Network File System) ist ein von einem NFS-Server ber das Netz
bereitgestelltes Filesystem. Daneben untersttzt Linux noch CODA, NCP und
SMB.

NTFS (New Technology File System) ist ein in der Microsoft-Welt verwendetes
Filesystem.
NUMA (Non Uniform Memory Access) bezeichnet (Mehrprozessor-)
Architekturen, bei denen die Geschwindigkeit des Speicherzugriffs von der
CPU und der Adresse im Speicher abhngt.
0() ist eine in der Informatik gebruchliche Notation, um den Rechenaufwand
eines Algorithmus abzuschtzen. 0(1) besagt insbesondere, dass der
Rechenaufwand konstant ist, unabhngig von der Anzahl der Eingaben.
Page Fault ist ein Interrupt, der bei Paging on demand ausgelst wird, wenn auf
eine Page zugegriffen werden soll, die zwar zu dem Prozess gehrt, aber

268 C Glossar

im Augenblick des Zugriffs nicht im Speicher vorhanden ist. Das Valid Bit
zeigt an, ob die Page momentan im Speicher abgebildet ist.
Page untersttzt beim Paging eine Umsetzung des logischen Adressraums in
den physischen Adressraum. Der logische Adressraum wird in Blcke
gleicher Gre (Pages) unterteilt, die Gre stimmt mit der Frame-Gre
berein. Diese Blcke werden mit Hilfe der Pagetable auf Frames im
Speicher abgebildet.
Page Cache ist ein Cache, in dem Pages vorgehalten werden.
Pagetable wird bei Paging verwendet, um die Umsetzung von logischer zu
physischer Adresse zu erreichen.
Paging bezeichnet eine Speicherverwaltung, die externe Fragmentierung des
Speichers vermeidet. Der Speicher wird in Frames eingeteilt, der logische
Adressraum eines Prozesses in Pages. Die Pages werden unter Zuhilfenahme
einer Pagetable auf die Frames abgebildet.
Paging on demand liegt vor, wenn Paging verwendet wird und Speicherplatz
fr die Pages dynamisch bei Zugriff angefordert wird.
Partition ist eine Unterteilung einer Platte. Der Benutzer nimmt Partitionen
wie eigenstndige Platten wahr.
Patch ist eine nderung einer oder mehrerer (Quell-)Dateien des
Betriebssystems - bzw. allgemein eines Software-Systems -, um einen Fehler
zu beheben.
PATH heit in Unix-Betriebssystemen eine Umgebungsvariable, die die
Suche nach ausfhrbaren Programmen ermglicht. Wird ein Name so
benutzt, dass er als ausfhrbare Datei interpretiert wird, so wird nach einer
ausfhrbaren Datei gleichen Namens in den in dieser Variablen
aufgefhrten Directories gesucht und zwar in der Reihenfolge der
Nennungen. Die Suche wird beendet, wenn die Datei gefunden wird oder
wenn das letzte der Directories erfolglos durchsucht wurde.
PCB (process control block) ist eine systemabhngige Datenstruktur, in der
alle wichtigen Informationen ber einen Prozess gespeichert werden.
PCI-Bridge ist ein Baustein, der CPU, Speicher und PCI-Bus miteinander
verbindet. Heute werden statt dessen in der Regel zwei Bausteine eingesetzt:
North- und South-Bridge.
PCI-Bus ist ein paralleler Bus, um Rechner-intern Interface-Karten zu
integrieren.
Physische Adresse bezeichnet diejenige Adresse im Speicher, an der eine
Funktion oder Datenstruktur gespeichert ist. Whrend im Programm
logische Adressen verwendet werden, setzt die MMU die logische Adresse in
die physische Adresse des realen Speichers um.
PDA (Personal Digital Assistant) bezeichnet Gerte, die ursprnglich dafr
gedacht waren, Kalendereintrge, Adressen und Notizen zu verwalten. Auf
Grund der Gre - blicherweise fehlt eine Platte - und damit einher
gehender Speicherkapazitt muss das Betriebssystem minimal ausgelegt
werden.

C Glossar 269

pid (process identifier) ist in der Regel ein Wert vom Typ Integer oder Long,
der denjeweiligen Prozess identifiziert. Zujedem Zeitpunkt kann es
hchstens einen Prozess im System mit einer bestimmten pid geben.
Pipe ist eine FIFO-Organisation im Speicher. Im Gegensatz zur Named Pipe
knnen Pipes nur zwischen Prozessen verwendet werden, die durch fork()
(ggf. ber mehrere Stufen) miteinander verwandt sind. Auerdem mssen
sie im Gegensatz zu Named Pipes nicht geffnet werden, sondern stehen mit
dem Anlegen zur Verfgung.
Pfad beschreibt den Weg durch die Baumstruktur eines Filesystems zur
Datei. Der absolute Pfad beginnt beim Wurzelverzeichnis, der relative Pfad
beim Current Directory.
Platte ist ein gngiges Block-orientiertes Speichermedium fr Dateien, das
wahlfreien Zugriff erlaubt.
Plattenpartition siehe Partition.
Pointer (Zeiger) werden in Programmiersprachen wie C benutzt, um auf den
Speicherplatz zu verweisen, an dem eine Datenstruktur oder Funktion
gespeichert ist. Auch bei Platten knnen Pointer eingesetzt werden, sie
enthalten dann die Nummer eines physischen Blocks der Platte.
POST (Power-On Self-Test) wird bei der Initialisierung eines Rechners
durchgefhrt. Dabei werden die vorhandenen Gerte ermittelt und
berprft und anschlieend eine Zuordnung der Interrupts und I/O-Ports
vorgenommen.
Prcompiler ist eine insbesondere in der C-Programmierung verwendete
Technik, bersichtlichere Programme zu erstellen. Der Prcompiler luft vor
der eigentlichen bersetzungphase des C-Compilers. Er wertet die
MakroAnweisungen #include-, #define- und #if aus und ersetzt sie im
Quelltext durch C-Code.
Preemptiv bezeichnet Scheduling-Strategien, die Ressourcen - insbesondere
die CPU - einem Prozess entziehen.
Prioritt wird bei manchen Scheduling-Strategien verwendet, um Prozesse
mit hherer Prioritt solchen mit geringerer Prioritt vorzuziehen.
Privilegierter Modus siehe Kernel Mode.
Programm bezeichnet eine Datei, die ausfhrbaren Code erhlt. Dieser ist in
der Regel mit Hilfe eines Compilers sowie eines Linkers aus einem
SourceCode erzeugt worden.
Process Control Block siehe PCB.
Program Counter ist ein CPU-Register, das auf die gerade ausgefhrte
Maschineninstruktion zeigt. Whrend der Ausfhrung wird der Inhalt des
Registers in der Regel um die Anzahl der Bytes, die diese Instruktion
einnimmt, erhht; der Program Counter zeigt damit auf die folgende
Maschineninstruktion. Handelt es sich jedoch bei der bearbeiteten
Maschineninstruktion um eine Verzweigung, einen Prozeduraufruf oder
entsprechendes, so wird stattdessen diejenige Adresse im Program Counter
eingetragen, bei der die Verarbeitung fortgesetzt werden soll.

270 C Glossar

Prozess ist ein Programm in Ausfhrung. Genauer: PCB, Speicher, der den
ausfhrbaren Programm-Code und die Daten enthlt, und weitere
zugeordnete Ressourcen bilden einen Prozess, der beim Scheduling
bercksichtigt werden kann.
Prozess-Kontext ist der Zustand, in dem ein Prozess mit dem Kernel gekoppelt
ist. In diesem Zustand drfen Funktionen aufgerufen werden, die
blockieren, also z.B. sleep().
PSE (Page Size Extension oder auch Large Pages) verndert die Umsetzung der
logischen auf die physische Adresse: die Page- und Frame-Gre wird auf 4
MB (22 Bit bzgl. der Adressierung innerhalb der Page) angehoben.
Race Conditions knnen auftreten, wenn zwei oder mehr Prozesse lesend und
ndernd auf dieselbe Datenstruktur zugreifen. Je nach
Ausfhrungsreihenfolge kann das Ergebnis unterschiedlich ausfallen. Da
jedoch durch das Scheduling keine Aussage ber die Reihenfolge der
Zugriffe gemacht werden kann, muss in solchen Fllen ein korrekter Ablauf
durch Locks erzwungen werden.
Readahead (Vorablesen) dient der Performance-Steigerung beim sequentiellen
Lesen einer Datei: bei der Anforderung eines Blockes werden die folgenden
Blcke ebenfalls mitgelesen, da die Annahme zugrunde liegt, dass die Datei
nur gering fragmentiert ist und somit die nchsten physischen Blcke auf
dem Speichermedium auch den Inhalt der logisch folgenden Blcke
enthalten.
Reaktionszeit nennt man diejenige Zeit, die bei online-Bearbeitung zwischen der
Terminal-Eingabe und der ersten Reaktion des Systems darauf vergeht. Zu
lange Reaktionszeiten hemmen den Arbeitsfluss.
Realtime siehe Echtzeit.
Recht - bei Multiuser-Betriebssystemen mssen Rechte fr die Benutzer
eingefhrt werden, damit der Zugriff auf Dateien und ausfhrbare
Programme vom Betriebssystem kontrolliert werden kann.
Red-Black-Tree (auch RB-Tree) bezeichnet eine besondere binre Baumstruktur
mit gefrbten Knoten. Diese Struktur erfllt folgende Eigenschaften: alle
Knoten sind rot oder schwarz, wobei nie zwei rote Knoten aufeinander
folgen. Die Wurzel selbst ist schwarz, wie auch die Bltter. Die Anzahl der
schwarzen Knoten von einem beliebigen Knoten im Baum zu einem Blatt ist
auf jedem Pfad gleich. Damit lassen sich die guten Eigenschaften des
binren Suchens vereinen mit guten Algorithmen zur Balancierung. Die
Balancierung hat die Komplexitt 0(log n).
Reentrant bezeichnet man ausfhrbaren Code, der von mehreren Prozessen
gleichzeitig benutzt werden kann. Viele Programme sind in
UnixBetriebssystemen so ausgelegt. Dadurch wird bei einem System, das
mehrere Benutzer gleichzeitig bedient, in der Regel weniger Speicher belegt
als wenn die Programme nicht reentrant wren.
Register sind Speichereinheiten in der CPU. Die CPU kann ohne Verzug - im
Gegensatz zum integrierten Cache-Speicher oder zum Speicher - auf die

C Glossar 271

Register zugreifen. Anzahl, Gre und Verwendung der Register hngen von
der jeweiligen CPU-Familie ab.
Reiserfs ist ein unter Linux verwendetes Journaling Filesystem. Es ist
insbesondere fr die Verwaltung sehr vieler kleiner Dateien geeignet.
Relokabel (verschiebbar) ist ausfhrbarer Code, wenn er an andere Speicherorte
verschoben werden darf. Dies setzt zumindest voraus, dass bei der
Adressierung keine absoluten Adressen verwendet werden.
Ressourcen sind alle Objekte - wie Dateien, Gerte, Speicher usw. -, die ein
Betriebssystem den Prozessen zur Verfgung stellt.
Returncode bezeichnet den Rckgabewert einer Funktion, der vom Typ Integer
ist. (Fast) alle System Calls haben einen Returncode, der anzeigt, ob die
Verarbeitung erfolgreich war oder nicht. In der Regel bedeutet Returncode =
0 korrekte Verarbeitung, Returncode < 0, dass ein Fehler bei der
Bearbeitung des System Calls auftrat.
ROM (Read Only Memory) wird in der Rechnerarchitektur in der Regel dazu
benutzt, ein elementares Bootprogramm aufzunehmen, das den Bootprozess
startet.
Round Robin ist ein einfacher Seheduling-Algorithmus, der alle Prozesse fair - in
diesem Falle gleichmig - bedienen soll. Jeder Prozess bekommt, sobald er
vom Scheduler die CPU erhlt, eine Zeitscheibe zugeordnet, innerhalb derer
er rechnen darf. Gibt er durch einen Interrupt die Kontrolle vorzeitig ab,
wird der nchste Prozess ausgewhlt, ansonsten wird er mit Ablauf der
Zeitscheibe von der CPU verdrngt, der Scheduler whlt den nchsten
Prozess aus. Die Wahl der Lnge der Zeitscheibe beeinflusst dieses
Verfahren erheblich.
Scatter gather Technik bedeutet, dass bei einem I/O-Vorgang im Speicher
verstreute Blcke gemeinsam bearbeitet werden.
Scheduler ist diejenige Komponente des Betriebssystems, die einen
rechenbereiten Prozess auswhlt, um ihm die CPU zuzuteilen. Auch fr I/OAuftrge kann es I/O-Scheduler geben, deren Aufgabe es ist, aus den
Auftrgen nach vorgegebenen Kriterien den nchsten auszuwhlen.
Scheduling ist der Algorithmus, den der Scheduler bei seiner Auswahl benutzt.
SCSI ist ein paralleler Bus zur Datenbertragung zwischen Rechner und
Gerten.
Segmentierung ist eine Einteilung des Adressraums nach inhaltlichen
Kriterien. Whrend Paging den Adressraum formal in gleich groe Blcke
unterteilt, ist Segmentierung auf den Software-Entwicklungsprozess
ausgerichtet: Der Adressraum wird aufgeteilt in Code-, Stack-,
Datensegmente; diese Aufteilung kann auch auf Funktionsebene erfolgen.
Semaphore wurde von Djikstra eingefhrt, um den Zugriff auf Critical Sections
zu regeln und dadurch Race Conditions zu vermeiden. Bei der Semaphore
handelt es sich um einen abstrakten Datentyp, der zwei Zustnde
einnehmen kann und die beiden Operationen wait () und signal() anbietet.
wait() prft, ob bereits ein Prozess seine Critical Section betreten hat

272 C Glossar

und legt in diesem Falle den aufrufenden Prozess in einer Warteschlange ab.
signal() muss vom Prozess sofort nach Verlassen der Critical Section
aufgerufen werden, wartende Prozesse werden dann aufgeweckt. Die IPC
Implementierung von Semaphoren verallgemeinert diesen Ansatz in zwei
Richtungen:
- Semaphoren-Werte knnen nicht-negative Integer-Werte bis zu einer
vorgegebenen maximalen Gre annehmen,
- es knnen mehrere Semaphoren-Werte in einer Semaphore
zusammengefasst werden,
- in einer atomaren Operation kann auf mehrere Semaphoren-Werte einer
Semaphore zugegriffen werden.
Sequentieller Zugriff bezeichnet das aufeinander folgende Lesen bzw.
Schreiben aller Bytes einer Datei.
Server siehe CS.
Shared Memory ist Speicherbereich, auf den mehrere Prozesse gemeinsam
zugreifen knnen. IPC stellt neben Message Queues und Semaphoren auch
Shared Memory zur Verfgung.
Shell ist ein Prozess, der eine Kommando-orientierte Benutzeroberflche zur
Verfgung stellt. Es gibt mehrere unterschiedliche Implementierungen auf
Unix-hnlichen Betriebssystemen. Eine vielgebrauchte Implementierung ist
die Bash-Shell.
Shortterm Scheduler bezeichnet denjenigen Scheduler, der aus einer Menge
von rechenbereiten Prozessen den nchsten auswhlt, dem die CPU zugeteilt
wird. Der Algorithmus muss eine sehr kurze Laufzeit besitzen, da er extrem
hufig aufgerufen wird.
Signal ist eine synchrone Unterbrechung des Instruktionsflusses, siehe
Interrupt.
Slab dient als Cache fr Datenstrukturen, die vom Kernel bentigt werden.
Durch die Zusammenfassung gleicher Objekte knnen frei gewordene
Speicherpltze sofort wieder fr Objekte derselben Art benutzt werden;
damit wird zum einen der Fragmentierung des Speichers entgegengewirkt,
zum anderen knnen aufwndige Funktionen zur Initialisierung der Objekte
weitgehend vermieden werden.
SMP (Symmetrie Multi Processor) ist eine Multiprozessor-Architektur, in der
alle CPUs gleichwertig sind, d.h. laufende Prozesse knnen jeder CPU
zugewiesen werden.
Soft-IRQ (SoftInterrupt Request) ist in Linux ein Mechanismus zur
Ausfhrung der Bottom Half in der Bearbeitung eines Interrupts. Ein SoftIRQ muss statisch in den Kernel eingebunden werden. Damit er ausgefhrt
wird, muss er signalisiert werden.
Speicher wird synonym fr flchtigen, direkt adressierbaren Hauptspeicher
verwendet. Der Zugriff auf diesen Speicher ist um ein Vielfaches schneller
als der Zugriff auf nicht flchtigen Speicher wie z.B. Plattenspeicher, ist
jedoch langsam im Vergleich zum CPU internen Cache.

C Glossar 273

Speicheradresse zeigt auf einen Platz (Zelle) im Speicher, auf den


zugegriffen werden kann.
Sperre siehe Lock.
Spinlock ist eine bestimmte Art von Lock. Das Spinlock wartet aktiv auf die
Freigabe des gewnschten Objekts. Es wird bei Multiprozessor-Systemen
eingesetzt, um den gleichzeitigen Zugriff von mehreren Prozessoren aus zu
unterbinden.
Stack ist eine LIFO-Organisation, in der ein Prozess Objekte ablegen kann.
Ein Stack wird unter anderem in vielen Umgebungen eingesetzt, um bei
Funktionsaufrufen Argumente und Rcksprungadresse abzulegen.
Startadresse ist diejenige logische Adresse eines Adressraums, bei der die
Verarbeitung des Prozesses aufgenommen wird.
Starvation (Verhungern) ist ein Problem Prioritts-orientierten Schedulings.
Hufig wird Altern eingefhrt, um Starvation zu vermeiden: Prozessen wird
sehr spt oder schlimmstenfalls gar nicht die CPU zugeteilt (gilt analog fr
den I/O-Scheduler).
Superblock enthlt wichtige Informationen ber die jeweilige
Plattenpartition.
Swap Bereich ist eine besonders eingerichtete Plattenpartition zur
Aufnahme von aus dem Speicher verdrngten Pages. Anstelle einer
Plattenpartition kann ggf. auch eine zusammenhngende Datei verwendet
werden.
Swapping verlagert Teile eines Prozesses aus dem Speicher in den
wesentlich langsamer zugreifbaren Swap Bereich. Damit wird schneller
Speicher wieder frei.
Synchronisation ist ntig, wenn Prozesse auf gemeinsame Ressourcen - z.B.
Datenstrukturen - zugreifen. Damit der Eintritt in die Critical Section
kontrolliert erfolgt, werden an dieser Stelle die Prozesse synchronisiert.
Dazu werden unter anderem Semaphoren eingesetzt.
System Call ist ein Aufruf einer Kernel Routine von einem Prozess aus.
Bibliotheken dienen dazu, Standard-Funktionen in geeignete System Calls
und deren Aufruf-Struktur umzusetzen. Ein System Call entspricht einem
speziellen Interrupt, der Prozessor wechselt in den Kernel Mode.
Systemmap enthlt die Einstiegspunkte zu den Routinen des Kernels.
Task siehe Prozess.
Tasklet ist eine spezielle Form zur Ausfhrung der Bottom Half in der
Bearbeitung eines Interrupts. Ein Tasklet basiert auf dem Soft-IRQMechanismus, ist aber wesentlich flexibler. Der Handler kann dynamisch
angemeldet werden. Damit er ausgefhrt wird, muss er signalisiert werden.
TCP (Transmission Control Protocol) ist ein verbindungsorientiertes
Protokoll der TCP/IP Protokoll-Familie, das ein feste Verbindung zwischen
Client und Server herstellt (vgl. IP und UDP).
Thrashing tritt auf, wenn bei Paging on demand Speicher so knapp wird,
dass Pages, die gleich wieder bentigt werden, durch Swapping aus dem
Speicher verdrngt werden.
Thread siehe Leichtgewichtiger Prozess.

274 C Glossar

Timesharing nennt man System, das dafr ausgerichtet ist, mehrere interaktive
Benutzer gleichzeitig zu bedienen. Dazu muss insbesondere ein geeigneter
Scheduling-Algorithmus ausgewhlt werden. Typisch sind Varianten des
Round Robin.
Time slice siehe Zeitscheibe.
Top Half wird bei der Behandlung von Interrupts eingesetzt. Der InterruptHandler wird dazu, sofern mglich, aufgeteilt in die Top Half und die Bottom
Half. Die Top Half muss unverzglich ablaufen und unterdrckt in der Regel
lokal alle Interrupts oder global denjenigen Interrupt, auf den gerade
reagiert wird.
Trailer siehe Header.
Transaktions-orientiert: Die Bearbeitung muss die Kriterien Atomaritt,
Konsistenz, Isolation und Dauerhaftigkeit (englisch ACID) erfllen, also
entweder komplett ausgefhrt werden oder gar keine nderung
hinterlassen.
Treiber bezeichnet in der Regel Software, die fr Interface-Karten bzw. Gerte
spezifisch geschrieben ist. Dabei mssen die Besonderheiten der Hardware
bercksichtigt werden.
UDP (User Datagram Protocol) ist ein verbindungsloser Paket-orientierter
Dienst der Protokoll-Familie TCP/IP (vgl. IP und TCP).
Unterbrechung siehe Interrupt.
USB (Universal Serial Bus) ist ein serieller Bus, um einen Rechner mit externen
(USB-) Peripheriegerten zu verbinden.
User Mode Ein Zustand der CPU (vgl. Kernel Mode). Bei Bearbeitung von
Benutzer-Prozessen befindet sich die CPU im User Mode. In diesem Zustand
sind zumindest alle I/O-Operationen unzulssig. Damit kann ein BenutzerProzess nur mit Hilfe von System Calls - und somit mit Hilfe des
Betriebssystems - auf gewnschte Dienste des Rechners zugreifen.
Valid Bit ist ein Bit, das in der Pagetable benutzt wird, um anzuzeigen, ob die
jeweilige Page im Speicher abgebildet ist oder nicht. Ist das Valid Bit bei
einem Zugriff nicht gesetzt, so wird ein Page Fault Interrupt ausgelst. Der
Handler muss die gewnschte Page vom Swapping Space holen, wenn sie
bereits dorthin ausgelagert worden war, oder eine neue Page erzeugen und
mit den gewnschten Daten laden, wenn die Page noch nicht angelegt
worden ist.
Verhungern siehe Starvation.
VFAT (Virtual File Allocation Table) ist ein in der Microsoft-Welt eingesetztes
Filesystem.
VFS (Virtual File System) bezeichnet in Linux eine Verallgemeinerung eines
Filesystems. Diese Schnittstelle ermglicht es, auf unterschiedliche konkrete
Filesysteme wie ext2, reiserfs oder nfs zuzugreifen.
Virtuelle Speicherverwaltung dient dazu, den Speicher dynamisch zu verwalten.
Es wird Paging verwendet, der Speicher ist in Frames eingeteilt, die
Umsetzung von logischer zu physischer Adresse ist darauf eingerichtet.
Zustzlich wird die Hardware so erweitert, dass ein Page-Zugriff auf eine
zum Adressraum des Prozesses gehrige Page, die nicht im Speicher

C Glossar 275

abgebildet ist, zu einem Page Fault-Interrupt fhrt. Dadurch wird die


entsprechende Page im Speicher angelegt und - wenn ntig - die Daten
geladen. Das Verfahren wird als Paging on demand bezeichnet.
VMA (Virtual Machine Architecture) bezeichnet in der IBM-Architektur eine
Virtualisierung der Hardware. Damit wird es mglich, mehrere
Betriebssysteme auf einem Rechner gleichzeitig laufen zu lassen, wobei jedes
System den Rechner allein zu besitzen scheint. VMWare, Bochs und hnliche
Systeme bilden dieses Vorgehen heute fr die x86-Architektur nach.
VM/CMS ist (oder besser: war) ein einfaches Single User, Single Tasking DOShnliches Betriebssystem, das in der IBM-Grorechnerwelt zur DialogUntersttzung diente. Durch Einsatz unter VM/SP konnte dann eine
Vielzahl solcher Systeme bereitgestellt und damit viele Benutzer parallel
bedient werden.
VM/SP (Virtual Machine/System Product) siehe VMA.
Wait Queue ist eine bestimmte Datenstruktur zur Aufnahme von Prozessen
(genauer PCBs), die auf ein bestimmtes Ereignis warten. Ein in einer Wait
Queue aufgenommener Prozess wird beim Scheduling nicht bercksichtigt,
er muss erst durch das Eintreten des gewnschten Ereignisses geweckt und
aus der Wait Queue entfernt werden.
Warteschlange siehe Wait Queue.
Wrapper ist eine Punktion, die als Schnittstelle zwischen dem Aufrufer und dem
eigentlichen Aufruf benutzt wird. Dabei kann die Wrapper-Funktion z.B. die
Sicherheit durch weiteren Code erhhen. Die aufgerufene Funktion muss
einen entsprechenden Schutz dann nicht mehr implementieren. Dies ist
insbesondere dann interessant, wenn der Wrapper auf mehrere Funktionen
zugreift.
Work Queue ist in Linux ein besonderes Verfahren, bei der Interrupt-Behandlung
diejenigen Arbeitsschritte, die nicht sofort durchgefhrt werden mssen
(Bottom Half), in eine Queue einzutragen, aus der heraus sie zu einem
geeigneten Zeitpunkt von einem Kernel-Thread bearbeitet werden. Ein
wesentlicher Unterschied zu anderen Bottom Half-Methoden liegt darin,
dass die Bearbeitung im Prozess-Kontext und nicht im InterruptKontext
erfolgt.
Worst fit sucht den grten zusammenhngenden freien Speicherplatz, damit
wird bei der Speicherung die zurckbleibende Lcke am grten (vgl. Best
Fit und First Fit).
Wurzelverzeichnis ist in Unix-Systemen derjenige Dateiknoten, von dem aus das
ganze Filesystem in Baumform aufgespannt wird.
Zeitscheibe wird bei Round Robin Scheduling eingesetzt und ist diejenige Zeit,
die einem Prozess eingerumt wird, wenn er die CPU zugeordnet bekommt.
Sptestens mit Ablauf der Zeitscheibe wird dem Prozess die CPU entzogen
(preemptiv) und der nchste rechenbereite Prozess erhlt die CPU.
Zone beschreibt Speicherbereiche mit gleichen Eigenschaften: gerade in der PCArchitektur gibt es Bereiche, die (zumindest von alten Interface-

276 C Glossar

Karten) nicht ber DMA angesprochen werden knnen, sowie Bereiche, die
nicht permanent im Kernel-Adressraum eingebunden sein knnen. Verkettet ist
eine Dateiorganisation, wenn die Blcke einer Datei ber Pointer verkettet sind
und bei der Allocation eines Blockes ein beliebiger freier Block gewhlt werden
kann.

Interessante WWW-Adressen

Trotz der Gefahr, dass Links schnell veralten, mchte ich hier einige wichtige
www-Adressen angeben in der Hoffnung, dass sie eine Weile erhalten bleiben:

http://www.cs.cf.ac.Uk/Dave/C/

http
:/
/www.dit.upm.es/~jmseyas/linux/kernel/hac

Eine ausfhrliche Einfhrung in C einschlielich Prcompiler und Unix


System Calls.

kers-docs.html

Eine umfangreiche Liste von online-Dokumenten und Bchern zum


LinuxKernel.

http://www.kernel.org/

http ://www.linux.org/

http ://www.novell.com/de-de/linux/suse/

http ://www.inf.fh-dortmund.de

Die offizielle Quelle fr die Linux-Kernel.


Ein informatives Linux-Portal.

Ehemals http : / /www. suse. de


ein wichtiger Distributor, der mit seinen Mailinglisten und seiner Supportund Hardware-Datenbank eine Reihe von Problemen lsen hilft.
Von dort aus fhren die Links Personen, gefolgt von Prof . Dr. Albrecht
Achilles auf meine Homepage. Unter dem Link Betriebssysteme sind die

Literaturverzeichnis

1.
2.
3.
4.
5.
6.
7.
8.

Bovet D. P. &: Cesati M. (2003) Understanding the LINUX KERNEL. OReilly


Brey B. B. (1994) Assembly Language Programming, 8086-8088, 80286, 80386,
80486. Prentice Hall
Kernighan B. W., Ritchie D. M. (1990) Programmieren in C. Hanser
Love R. (2004) Linux Kernel Development. Sams Publishing
Mauerer W. (2004) Linux Kernelarchitektur. Hanser
Silberschatz, A. et. al. (2002) Operating System Concepts. John Wiley &: Sons
Inc.
Tanenbaum A. S. (2002) Moderne Betriebssysteme. Pearson Studium
Wirth N. (2000) Algorithmen und Datenstrukturen, Pascal-Version. Teubner

Sachverzeichnis

/etc/fstab, 80 /etc/init.d, 244 /etc/inittab,


243 /proc/interrupts, 108 /proc/sys,
79 /proc/sys/vm, 79 /sbin/init, 242
-_blockdev_direct_IO, 174 __do_softirq,
119, 120 __generic_file_aio_read, 173,
175, 176
__generic_file_aio_write_nolock, 77
__grab_cache_page, 77 __init, 241
__make_request, 174
__skb_dequeue, 234
__tasklet_schedule, 124
__wait_queue, 129
__wait_queue_head, 130
_syscall, 17 jsyscallWrapper, 18
accept, 223, 228 Access -Bit, 59,
79 -Liste, 143, 149 ack, 113, 231
ACL, siehe Access-Liste active
Array, 45, 49
add_to_page_cacheJru, 77, 180
add_wait_queue, 130
address_space, 76
address_space_operations, 77
Adresse, 12, 56, 72

Block, 195 Internet,


222 IP, siehe IPAdresse logisch, 55,
56, 59, 68 physisch, 56,
68 Start, 3, 56
Umsetzung, 12, 56, 57, 68, 69
Adressraum, 12, 17, 24-28, 30, 33, 35, 55,
57, 58, 63, 64, 66, 68, 77, 78, 83, 151, 179,
203 Kernel, 70, 73 linear, 57, 68 logisch,
56, 57, 62, 63 Operation, 77 alarm, 99
Altern, 42, 144, 181 Anwendungsschicht,
siehe Application layer
AppleTalk, 216
Application layer, 219
asmlinkage, 167, 241
asynchron, 94 atomar, 103
atomic_inc, 91
atomic_inc_and_test, 91
atomic_set, 91 Ausschluss,
90
background_writeout, 79
Barriere, 181
Basisregister, 56 Batch, 4,
39 bdev_get_queue, 174
Befehlszhler, 5

282 Sachverzeichnis
benannte Pipe, siehe Pipe BH,
siehe Bottom Half Big Kernel
Lock, 94, 105, 242 bind, 222225, 228 BIOS, 238
Bitbertragungsschicht, siehe Physical
layer
BKL, siehe Big Kernel Lock
blk_partition_remap, 174 Block, 10, 66,
135, 137-140, 143, 166, 167, 174, 181, 182,
184, 191, 195, 197
-gruppe, 183-186, 191-193, 197
-lnge, 76, 147, 152, 189, 191
-nummer, 134, 195 Boot, 180
Bootmanager, 239 Bootstrapping,
237 Bottom Half, 117 Buddy System
Algorithmus, 75 Bus, 7
Busy Wait, 10, 105
Cache, 5, 7, 8, 43, 53, 61, 62, 64, 70, 75, 77,
78, 82, 151, 155, 173, 177, 179, 180, 184,
194 call, 11
canceLdelayed_work, 128 change_bit, 91
chdir, 165
check_media_change, 151 chmod, 165
chown, 165 circular wait, 91 clear_bit, 91
clone, 34-36 CLONE_FILES, 34
CLONE_FS, 34 CLONE_SIGHAND, 34
CLONE_THREAD, 35 CLONE_VM, 35, 64
close, 164, 224 Compiler, 3 cond_resched,
125, 177 connect, 223, 227 context_switch,
47 contiguous, 138 Controller, 7, 9, 61 Copyon-write, 72

copy_process, 52
CPU, 1, 5, 7, 10, 12, 13, 20, 56, 61, 73, 107,
120, 126, 127, 237, 238, 240, 242
entziehen, 13, 14, 40, 41
Operationen, 87 Register, 19, 20
Unterbrechung, 112, 116
zugeordnet, 53, 80, 111 cpu_idle, 242
cpu_workqueue_struct, 127
cpu_wq, 127
creat, 164
create, 136, 148
create_workqueue, 127
Critical Region, 87, 89, 93
current
directory, 34, 142, 147, 157, 158, 165 file
position, 136, 138, 168 Makro, 115, 169
Darstellungsschicht, siehe Presentation
layer
Data link layer, 216, 227 Datagram, 221,
222 Datei, 30, 34, 133, 138, 142, 144, 147,
154, 161, 165, 168, 170, 172, 174, 183, 185,
192, 194 -baum, 140, 142, 148 -block, 135,
185, 189 anlegen, 136, 139, 158, 192
geffnet, 19, 24, 26, 28, 30, 151, 158, 167,
171
lschen, 136, 148, 189, 193, 194
Mapping, 68, 72, 160 Operationen,
136, 149 operationen, 172 Rechte,
133, 135 sequentiell, 134, 177, 185
Typ, 133, 134 vererbte, 29, 36
zurcksetzen, 136
Datenkompression, 219 Datenpaket,
231 Datenrahmen, 216
Datensegment, 19 Datenstrom
bidirektional, 223
Deadlock, 90, 91, 105, 217

Sachverzeichnis 283
DECLARE_TASKLET, 123 delete, 136
delete_inode, 152
dentry, 78, 148, 155-157, 170, 215
dentry_operations, 156 dio_bio_submit,
174 Direct Memory Access, siehe DMA
direct_io_worker, 174 Directory, 134-137,
140-142, 147, 151, 161, 162, 192, 193
lschen, 193 dirty, 147
dirty_background_ratio, 79
dirty_inode, 152
disable, 111
disable_irq, 116
disable_irq_nosync, 116
DMA, 10, 61, 70, 73, 75
do_follow_link, 158
do_generic_file_read, 176
do_generic_mapping_read, 77, 176, 177
do_ioctl, 231
do_lookup, 158
do_page_cache_readahead, 177 do_softirq,
119, 125 do_sync_read, 171 DOS, 3, 11,
56, 140, 142 down, 92
down_intermptible, 92, 93 down_trylock,
92 dup, 164, 199, 202 Duplexbertragung,
217 Durchsatz, 39
Echtzeit, siehe Realtime effective_prio, 51
elektronische Signatur, 219 Elevator, 181
elvtune, 183 enable, 111 enable_irq, 116
Entzug, 90 environ, 30
Ereignis, 4, 44, 94, 107, 129 Ethernet, 216
Exception, 107 exec, 27, 66, 159, 199 exit,
27, 28, 30, 31, 41 expired Array, 47, 49, 53

ext2, 171, 183


ext2_alloc_branch, 197
ext2_block_to_path, 195
ext2_dir_entry_2, 194
ext2_direct_IO, 174, 194
ext2_empty_dir, 193
ext2_find_goal, 195
ext2_get_block, 195, 196
ext2_get_blocks, 195
ext2_get_branch, 195
ext2_get_group_desc, 191
ext2_group_desc, 191
ext2_inode, 190
ext2_new_inode, 192
ext2_read_inode, 191
ext2_rmdir, 193
ext2_splice_branch, 197
ext2_super_block, 188
ext2_unlink, 193 ext3, 183
fastcall, 124, 168 fat, 183
fcheck, 169, 170 fcntl, 166,
205 FDDI, 216 Feedback, 42,
51, 54 Festhalten und
Warten, 90 fget_light, 169
FIFO, 48, 50, 54, 180, 200, 205, 211
fifo_open, 205 file, 160, 215
Handle, 199
Hole, 191
file_operations, 150
file_read_actor, 179
file_system_type, 161
Filedeskriptor, 158, 168
filemap_copy_from_user, 78
files_struct, 159
Filesystem, 2, 20, 75, 76, 133, 156, 171, 207
virtuell, 143
find_get_page, 77, 178-180
find_group_dir, 192
find_group_orlov, 192
find_group_other, 193
flush_scheduled_work, 128
Flussregulierung, 218

284 Sachverzeichnis
follow_link, 149
follow_mount, 158
fork, 25, 26, 28, 31, 33, 64, 72, 200, 203
Fragmentierung, 56, 62, 82, 135, 139, 185
Frame, 57-59, 67, 70-73, 75, 82
bereitstellen, 62 verdrngen, 59 free, 66, 67
free_area, 73, 75 free_irq, 113 free_pages,
73, 83 Freelist, 59 fs_struct, 159, 165
fsck.ext2, 187 fsync, 151
generic_file_read, 171, 173
generic_make_request, 174 Gertetreiber,
180 getxattr, 149 glibc, 15, 167
Grenzregister, 56 Gruppendeskriptor, 184,
186
handle_IRQ_event, 114 handle_ra_miss,
180 Handler, 34, 36, 96, 109, 111-113, 115117, 119, 120, 231 hard_stat_xmit, 231
Hardware-Interrupt, 119 Hashing, 24, 61,
137, 145, 156, 157 Header, 220, 225, 227,
231 IP, 221, 227 MAC, 227 TCP, 222, 227
UDP, 220, 227 Heap, 19 hold and wait, 90
HOME, 142 hook, 167 HPFS, 183
hw_interrupt_type, 111
in_interrupt, 116 Index, 184 indexed, 139
Indirektion, 185, 186, 191 inet_addr, 227

init, 33, 242, 244


init_sync_kiocb, 172
INIT_WORK, 126 Initial
Ramdisk, 239, 245 inline,
169
Inode, 77, 78, 145, 148, 151, 152, 156, 166,
170, 171, 176, 184-186, 189, 191-194, 197,
200, 215 Nummer, 65 Struktur, 146, 147
inode_in_use, 147 inode_operations, 149
inode_unused, 147 insmod, 245
Inter Process Communication, 95, 200, 215
Schlssel, 207, 209
ZugrifTsrechte,
207
Interrupt, 10, 12, 14, 41, 48, 87, 107, 231
abschalten, 110 Bearbeitung, 107,
130 Kontext, 115, 116, 119, 125, 132
Registrierung, 109 synchron, siehe
Exception unterbrechen, 109
unterdrcken, 87, 130 Interrupt
Handler, siehe Handler InterruptHandler, 114 Bottom half, 109
Struktur, 114 Top half, 109
Interrupt-Vektor, 113 IO-Scheduler,
180 as, 181, 182 cfq, 181
deadline, 181, 182 noop,181
Prioritt, 181 ioctl, 150 IP, 216, 221,
227 Adresse, 218, 219, 221, 223, 224
Header, 221, 231 Layer, 231
ip_queue_xmit, 231
IPC, siehe Inter Process Communication
ipc_id, 207
ipc_ids, 207

Sachverzeichnis
IPC_NOWAIT, 210
IPC_UNDO, 104, 105 IPX, 216
IRQ, 111 irq_desc, 110
irq_desc_t, 110 irqaction, 112,
113 ISDN, 216 ISO, 216
kern_ipc_perm, 208 Kernel
Mode, 11, 15, 16, 109 KernelSemaphore, 93 kill, 97-99
kritischer Abschnitt, 115
Kryptographie, 219
ksoftirq, 125 Thread, 125
lschen, 193 least recently
used, 79 Library, 15 Link, 148,
156, 165 link, 148, 165
link_path_walk, 157 linked,
139 listen, 223, 228 llseek, 151
Load Balancing, 53, 54
load_balance, 53
locaLirq_disable, 115
locaLirq_enable, 115
locaLirq_restore, 116
locaLirq_save, 116
local_softirq_pending, 119
Lock, 88, 89, 93, 115, 120
Contention, 89
Granulierbarkeit, 89
Reader/Writer, 93
Reihenfolge, 89 RW, 93
Scalability, 89 lock, 151
lock_kernel, 94 locked, 70
locks_verify_area, 170 login, 25
lokale Variablen, 66 Longterm,
40

lookup, 148
lseek, 163
lsmod, 245
MAC, siehe Media access control
magic byte, 134 malloc, 66, 67, 84
Mapping, 75 mapping, 177
mark_page_accessed, 179
Maschineninstruktionen, 5 MBR,
239
Media access control, 216, 227
Mehrprozessor, 52 -Architektur, 61
-system, 54, 73, 82, 87 Memory
Deskriptor, 63, 64 Anlegen, 64
Message Queue, 200, 206, 208 mkdir,
148, 165 mke2fs, 189 mknod, 200, 205
mlock, 72, 84 mlockall, 71 mm_struct, 63
mm_users, 64 mmap, 67, 68, 84, 151
mmlist, 64 modprobe, 245 Module owner,
150 mount, 151, 166 mpage_readpage,
177, 195 msg.c, 209 MSG_EXCEPT, 211
msg_msg, 209 msg_queue, 208 msgctl,
210, 211 msgget, 209, 210 msghdr, 230
msgrcv, 210, 211 msgsnd, 210
Multitasking, 12, 19, 25 Multiuser, 4, 42,
133, 140, 142, 243 munlock, 72, 84
munmap, 67, 68, 84 mutual exclusion, 90
mv, 148

286 Sachverzeichnis
Nack, 231
named pipe, siehe Pipe
nameidata, 157 Nameserver,
218 net_dev, 231 net_device,
232-234 net_rx_action, 234
NetBios, 216
netif_receive_skb, 234
netif_rx, 231
netif_rx_schedule, 119, 234
Network Layer, 217, 227
Netzwerk, 118 nice, 51, 52,
54 NTFS, 183 NUMA, 73, 75
open, 16, 151, 162, 205, 231
open_softirq, 117, 118
Operation atomar, 87, 91, 105
Orlov-Allocation, 192
packet_type, 235
Page, 33, 57, 59-62, 67, 70, 72, 73, 77-79, 81,
177, 179, 200, 209 -table, 7, 33, 47, 57-60,
68, 70, 72, 73, 81, 83, 240, 241 Adresse, 57,
59 aktualisieren, 179 Alterungsprozess, 79
ausgelagert, 81
Cache, 61, 70, 75-77, 179, 180
dirty, 60, 62, 70, 77, 78 Gre,
72, 76, 201 Index-, 195 lock, 71
Locking, 71, 72
logisch, 72 OfFset,
57 Size Extension,
72 Slot, 81
zurckschreiben, 78
page, 71
page_cache_alloc_cold, 77, 180
page_cache_get, 77 page_cache_readahead,
177 Pagefault, 12, 59, 60, 71, 72, 83, 107
Pages

ausgelagert, 80 PageUptodate, 179


Paging, 57, 61 on demand, 59, 70, 72
Partition, 2, 3, 80, 151, 152, 166, 171, 174,
183, 187, 191-193, 195, 239, 245
path_lookup, 157
pause, 99
PCB, siehe Process Control Block
PCI-Bridge, 7 pdflush, 62, 77
pdflush_operation, 79 Personality,
24, 158 pg_data_t, 75 Physical Layer,
216 PID, 24-26, 31, 32 Pipe, 199, 200,
215 Benutzung, 202, 203 Lesen, 204
Named, 200, 205, 206
nicht-blockierend, 205
schlieen, 205 pipe, 199,
200 pipe_inode_info, 201
pipe_read_release, 205
pipe_release, 205
pipe_write_release, 205
Platte, 183 point-to-point,
218 Port, 221-224 POST,
238
preemption, 42, 44, 87, 90
Presentation layer, 218
prio_array, 46
Prioritt, 14, 19, 39, 40, 42, 48, 50, 51, 53,
177
dynamisch, 43, 46, 51
Priorittsarray, 44, 54
active, 45 Priorittstufe,
47 proc, 154, 183
Process Control Block, 19, 20, 24, 25, 28,
32, 33, 51, 62, 64, 158, 169, 212, 213
process_backlog, 234
Program Counter, 5
Programmcode, 19
Protected Mode, 238, 240

Sachverzeichnis 287
Prozess, 13, 14, 19, 24, 35, 40, 44, 46, 47,
49, 52, 55, 59, 62, 64, 72, 115, 129,
130, 163, 168, 199-201, 214, 242
-gruppe, 25, 50
-verwaltung, 2
-wechsel, 47
Adressraum, 56, 63, 64, 66, 68, 78, 83,
151
beenden, 28, 30 blockiert, 212
erzeugen, 25 ID, siehe PID
Kontext, 115, 116, 125, 132
Lebenszyklus, 40
leichtgewichtig, 33 Programm
laden, 27 Startadresse, 56 PSE,
72 pthread, 37 ptrace, 17
put_inode, 152
q_perm, 211
queue_delayed_work, 128 queue_work, 127
Quittungsmeldung, 216
Rckgabewert, 31
Race Condition, 86, 105, 119, 123, 130, 201,
207 Radix Tree, 178 raise, 97 raiseJrq, 119
rand_initialize_irq, 112 RCF, 219
read, 77, 136, 137, 151, 162, 167, 171, 200,
203, 205, 224 sequential, 185 read_inode,
152 read_pages, 177 Readahead, 176, 177
readdir, 151 readpage, 77 readv, 151
Reaktionszeit, 39 Real Mode, 238
Realtime, 4, 39, 44, 48, 49, 51, 182

Rechte, 3, 12-15, 19, 49, 52, 63, 68, 71, 99,


133, 135, 142, 147, 158, 160, 165,
170, 187, 189, 200, 207, 208, 244
IPC, 207
recvfrom, 222, 224, 225
Red-Black-Tree, 64
Register, 6, 48 reiserfs,
183 release, 151
relokabel, 56
remove_wait_queue, 130
rename, 148
Request for Comments, siehe RCF
Request-Queue, 180 request_irq, 112,
113 reset, 136
Ressource, 13, 14, 39, 90 vererbte, 28
rmdir, 148, 165, 193 rmmod, 245 root
directory, 140 Round Robin, 42, 4750, 54, 80 RR, siehe Round Robin
run_workqueue, 128, 129 Runqueue,
44, 53, 54 runqueue, 45
SA_INTERRUPT, 112
SA_SAMPLE_RANDOM, 112
SA_SHIRQ, 112, 113 ScatterGather, 151 sched_exit, 52
SCHED_FIFO, 49 sched_getparam,
50 sched_getscheduler, 50
SCHED_RR, 49 sched_setaffinity, 53,
54 sched_setparam, 49
sched_setscheduler, 49, 50, 54
sched_yield, 49, 51, 52, 54 schedule,
47, 48, 53, 125, 128, 177 Scheduler,
42, 46-48, 180 Scheduling, 13, 14, 20,
39, 53 Longterm, 53 Mediumterm, 53
priorittsgesteuert, 44, 53 Round
Robin, 54 SCHED_FIFO, 49

288 Sachverzeichnis
Shortterm, 53
Schichtenmodell, 216
SCSI, 118
security_file_permission, 170
Segmentierung, 57 sem, 212 sem_array,
212 sem_queue, 213 sem_undo, 213
sem_undo_list, 214 sema_init, 93
Semaphore, 93, 95, 100, 105, 106, 200, 201,
206, 212 initialisieren, 105 IPCErweiterung, 100 Kernel, 92 Pseudocode,
101 undo, 104, 213 semaphore, 93 sembuf,
104, 214 semctl, 101-103, 105 semget, 101,
102, 104, 105 semop, 103, 104 semun, 102
sendto, 222, 224, 225 set_affinity, 111
set_bit, 91 set_page_dirty, 77 setup, 239
setxattr, 149
Shared Memory, 68, 200, 206, 214
Shell, 3, 25
shmat, 68, 69, 84
shmctl, 68, 69
shmdt, 68, 69
shmget, 68, 69, 84, 101
shmid_kernel, 215
Shortterm, 40
show_options, 154
Sicherungsschicht, siehe Data link layer
sigaction, 96, 97
SIGALRM, 95, 99
SIGBUS, 95
SIGCHLD, 95
SIGCONT, 95
SIGFPE, 95
SIGHUP, 95
SIGILL, 95
SIGINT, 95, 96

SIGIO, 95, 205


SIGKILL, 95
Signal, 31, 40, 41, 94, 95, 106 blockieren,
96 signal, 96, 100, 103 Signal Handler,
siehe Handler Signatur, 219 SIGPIPE, 95
SIGSEGV, 95 SIGSTOP, 95 SIGSYS, 95
SIGTERM, 95 SIGUSR1, 95, 97
SIGUSR2, 95 sk_buff, 229, 230
sk_buff_head, 228 skb_push, 227 Slab
Allocator, 62, 84
Cache, 62, 64, 82 Layer,
82 SMP-Lock, 82
sock_readv_writev, 230
sock_sendmsg, 230
sock_writev, 230
sockaddr_in, 222 Socket,
200
socket, 222, 224, 225, 227, 228 Soft-IRQ,
117, 121, 124, 125, 131, 234
Einschrnkungen, 120 Handler, 120, 121
softirq_action, 118 softirqJnit, 121
softirq_vec, 117, 118 softnet_data, 234
Speicher, 9 Hierarchie, 9 inhomogen, 73
logisch, 67 Zonen, 75
Speicherbereich, 63, 65, 66
anfordern, 66 Speicherbus,
61 Speicherengpass, 62
Speicherschutz, 56
Speichersegment, 19
Speicherverwaltung, 2
virtuell, 57 Sperre, siehe
Lock

Sachverzeichnis 289
spin_lock, 115
spin_lock_irqsave, 92, 115
Spinlock, 91, 93, 105, 115, 130, 214
Stack, 19, 35, 48
start_kernel, 241
start_of_setup, 239
start_up, 240
Startadresse, 56
startup, 111
startup_32, 240
startup_irq, 112
starvation, siehe Verhungern
statfs, 152
status, 31
stop, 231
submit_bio, 174, 177 super_block, 153, 186
super_operations, 154 Superblock, 145, 147,
184, 186 Swap, 71, 73, 80, 179 Auswahl, 80
swap_info_struct, 80, 81 swapoff, 80
swapon, 80 swappiness, 80 Swapping, 40
symlink, 149 sync, 166 sync_fs, 152
Synchronisation, 61, 62, 85, 91, 94, 105,
199, 201
synchronize_irq, 116 sys, 183 sys_ipc, 215
sys_read, 167, 168 sys_semtimedop, 214
sys_socketcall, 225
System Call, 11, 14, 16, 17, 48, 96, 225
Fehler, 96 system_call, 16, 17
TASK_INTERRUPTIBLE, 128, 129
TASK_RUNNING, 128 task_struct, 20
task_timeslice, 52
TASK_UNINTERRUPTIBLE, 129 Tasklet,
117, 118, 121, 123, 125, 131
Einschrnkungen, 123 tasklet_action, 121,
122

tasklet_disable, 124 tasklet_enable, 124


tasklet_hi_schedule, 123 tasklet_init, 123
tasklet_schedule, 123
TASKLET_STATE_RUN, 122
tasklet_struct, 121 tasklet_trylock, 122
tasklet_unlock, 123 tasklet_vec, 123
TCP, 216, 219, 222, 223, 227, 231 Header,
221 TCP-Client, 227 TCP-Server, 228
tcp_alloc_pskb, 230 tcp_copy_to_page, 230
tcp_push_one, 230 tcp_reset_xmit_timer,
231 tcp_sendmsg, 230 tcp_snd_test, 231
tcp_transmit_skb, 231 test_and_clear_bit,
91 test_and_set, 87 Thrashing, 60
Thread, 20, 33, 35, 64, 85, 89, 125
THREAD_NEED_RESCHED, 48 time slice,
siehe Zeitscheibe Timer, 118, 129
Timesharing, 4, 39, 44, 51, 52, 54 Timing
Error, 99, 100 Token-Ring, 216 Trailer, 227
Transaktions-orientiert, 4 Transmission
control protocol, siehe TCP
Transport Layer, 218, 227 Transportschicht,
siehe Transport Layer try_atomic_semop,
214
UDP, 216, 219-221, 223, 227 Client, 225
Header, 221 Server, 224, 225 udp_sendmsg,
230 umask, 158, 165 Umgebung vererbte, 30
umount, 166 unlink, 148, 193

290 Sachverzeichnis
unlock_kernel, 94
Unterbrechungen unterdrcken, 115 up,
92, 93
User datagram protocol, siehe UDP User
Mode, 11, 12 user_struct, 24
Valid-Bit, 59 Verbindung, 223
verdrngen, 44 Verdrngung, 42
Vererbung Datei, 29 pipe, 199
Ressource, 28 Umgebung, 30
Verhungern, 42, 47, 94, 124, 144, 181
Verklemmung, 90 Verknpfung, siehe
Link Vermittlungsschicht, siehe Network
layer VFAT, 183 vfork, 33, 34 VFS, 183,
199, 205 vfs_read, 171 vfsmount, 152
Virtual File System, siehe VFS Virtual
Mode, 238 vm_area_struct, 64, 65
VM_EXEC, 66 VM_READ, 66
VM_SHARED, 66 VM_WRITE, 66 VMA,
65

wait, 31, 32, 100, 103 waitpid, 32, 35


wake_up, 130 Warten im Kreis, 91
Warteschlange, 40, 48, 51, 129, 130, 132,
231 wb_kupdate, 79 Windows, 1, 3, 12, 143
wipe, 194
Work Queue, 117, 125, 126, 128, 132
work_queue, 126 work_struct, 126 Worker
Thread, 128 workqueue_struct, 127 write,
136, 137, 151, 163, 200, 203, 205, 224, 230
sequential, 195 write_super, 152 writepage,
77 writev, 151
Wurzelverzeichnis, 140, 158
Zeitscheibe, 42, 43, 45-48, 51 Zombie, 33
Zone, 70 zone, 74
ZONE_DMA, 73 ZONE_HIGHMEM, 73
ZONE_NORMAL, 73 Zugriff direkt, 138
sequentiell, 138 zur Vererbung, 28

Vous aimerez peut-être aussi