Académique Documents
Professionnel Documents
Culture Documents
Betriebssystem
e
4^ Springer
eXamen.press
AlbrechtAchilles
Betriebssystem
e
Mit 31 Abbildungen
^ Springer
achilles@fh-dortmund.de
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
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
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
Vor allem danken mchte ich meiner Familie, die mir Zeit fr dieses
Vorhaben lie und manchmal sicher unter meinem Druck gelitten hat.
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
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
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
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
Glossar............................................................................................................257
Interessante WWW-Adressen...............................................................................277
Literaturverzeichnis ............................................................................................279
Sachverzeichnis.....................................................................................................281
Einfhrung
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:
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 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
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
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
1.2
Computer-Hardware
1.2.1
CPU
1 Einfhrung
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.
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.
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"
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,
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:
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:
11
1.3
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:
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
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
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 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
= ?
*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
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)
Definiert in fs/open.c.
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); \
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
20
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
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.
Vgl. Kap. 9.
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 */
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;
22
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;
23
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
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
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
3.2.1
26
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:
>
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
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.
27
Ausgabe:
Prozesse
Eltern
Kind
Zeitpunk
t des
forkAufrufs
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
bergeben, (char *)0 markiert das Ende. Die Bedeutung der einzelnen
Argumente ist:
1.
2.
3.
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
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.
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
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);
./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
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
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
31
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;
32
3.3.2
Synchronisation
Durch wait() wird der Elternprozess angehalten und wartet auf das Ende eines
Kindprozesses. Damit entstehen mehrere Fragen:
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.
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
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
34
}
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:
Flags sind spezielle Bits, die die Ausfhrung des Aufrufs beeinflussen.
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
// 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);
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.
37
--------------------------------------*/
#include <pthread.h> void
*thread_fc(void *arg)
>
main()
int rc;
pthread_t thrd; void *thread_rc;
>
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.
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:
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
4.1 Grundlagen 41
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
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
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
4.1.5
Mehrprozessor-Systeme
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
46
4 Scheduling
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.
Der Begriff Zeitscheibe wird also etwas anders verwendet, als die vorausgehenden
allgemeinen Bemerkungen erwarten lassen.
5
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)) {
/*
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
4.3 Realtime
49
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
11
50
4 Scheduling
}
}
>
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
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
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
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
P3
P2
P1
physischer
Adressraum =
Speicher
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
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
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
5.2
Ziele fr Linux
5.2.1
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
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:
Seit der vorliegenden aktuellen Version 2.6 wird versucht, diese Problematik mit
einer Radix-Tree-Verwaltung zu lindern.
62
5 Speicherverwaltung
5.2.5
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
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
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
64
5 Speicherverwaltung
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
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
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;
Definiert in include/linux/mm.h.
5 Speicherverwaltung
66
System Calls
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);
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
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;
/* ----- */
*/
exit(0);
>
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
Page locking
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;
};
>
exit(0);
5 Speicherverwaltung
72
5.4.2
PSE
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
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
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
74
5 Speicherverwaltung
struct zone {
spinlock_t lock; unsigned long free_pages;
unsigned long pages_min, pages_low, pages_high;
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.
75
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 */
>;
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);
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
5.7
Swapping
5.7.1
Kernel Caches
pdflush
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
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 */
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
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
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
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
b)
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:
6.1.2
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:
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
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
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
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. *
6 Synchronisation
92
6.3.3
Semaphoren
93
struct semaphore {
atomic_t count; int
sleepers;
wait_queue_head_t
wait;
6.3.4
Reader-/Writer-Locks
94
6 Synchronisation
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
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
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);
main () {
sigaction(SIGINT, &neu, &alt);
while (1) { raise(SIGINT);
printf("und noch eine
Zeile\n"); sleep(5);
>
exit(0);
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);
Die Anzahl der Zeilen, die das Kindprogramm ausgibt, kann von Lauf zu Lauf variieren.
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);
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:
signal(sem):
sem.wert := sem.wert +
1; if sem.wert > 0 then
begin
// entferne ersten PCB aus
sem.liste; // wecke zugehrigen
Prozess P; end;
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);
>
Wert = 0
Wert *= 2
enthalten.
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,
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);
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.
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
106
6 Synchronisation
108
7 Interrupts
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
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
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
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:
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
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;
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);
>;
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;
};
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
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)
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);
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
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)
xx_hole_daten_von_controller();
spin_unlock_irqrestore(&xx_lock, flags);
/* ab hier brauchen wir den Schutz nicht mehr */
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
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
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;
118
7 Interrupts
struct softirq_action
>;
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);
softirq_vec[nr].data = data;
softirq_vec[nr].action =
action;
Der Aufruf eines solchen Handlers erfolgt immer aus do_softirq()10 heraus. Diese
Funktion hat die in Listing 7.12 gezeigte Gestalt.
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. *
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
>;
/*
/*
/*
/*
/*
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)
{
}
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.
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 );
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)
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);
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
18
>
set_current_state(TASK_INTERRUPTIBLE);
7.4.5
Work Queues
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;
>;
\
\
\
\
\
\
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
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 */
>;
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;
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);
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.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;
};
130
7 Interrupts
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
>;
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
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
8
Dateisysteme und Plattenverwaltung
8.1
Grundlagen
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
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:
Dies schliet nicht aus, dass er auch andere Programme einsetzt, jedoch bietet ihm
die Oberflche zunchst nur bestimmte Programme an.
1
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
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.
136
8.1.4
Implementierungen fr Directories
Zugriffsmethoden
138
- 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
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:
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:
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
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
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.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
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
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
10
146
i_state;
dirtied_when;
i_flags;
i_sock;
i_writecount;
*i_security;
i_generation;
147
*generic_ip;
> u;
>;
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).
Fr die beiden Eintrge in der Struktur gilt i_count > 0, ijilink > 0.
12
148
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.
13
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
>;
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);
8.2.2
VFS-Datenstrukturen fr Partitionen
17
152
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;
154
>;
>;
22
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 */
23
156
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 *);
};
24
25
26
struct nameidata {
struct dentry *dentry;
struct vfsmount *mnt;
struct qstr
last ;
unsigned int
flags;
int
last_type;
struct lookup_intent intent;
>;
158
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
>;
};
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.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
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
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;
};
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.
162
vfsmount
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
>
exit(0);
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
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);
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.
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);
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
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", "");
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
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:
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
48
168
>
>
EXPORT_SYMBOL_GPL(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
>
spin_unlock(&files->file_lock);
return file;
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
}
/*
*/
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:
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
>
>
return ret;
EXPORT_SYMBOL(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
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
__set_current_state(TASK_RUNNING);
return iocb->ki_user_data;
Ist ein direkter Zugriff ohne Bercksichtigung des Caches gefordert62 (siehe
Code unter dem Kommentar coalesce the iovecs . .. in Listing 8.27),
61
Dies wird beim ffnen einer Datei, d.h. beim Aufruf von open(), durch das Flag
O_DIRECT bewirkt.
62
174
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:
63
Definiert in include/linux/fs.h.
*/
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;
176
>
out :
return retval;
8.4.2
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
/* . . . */
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;
>
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;
>
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;
Definiert in mm/swap.c.
74
180
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
75
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
182
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
Blockgruppe 0
184
Blockgruppe 1
Blockgruppe n
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
185
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
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.
8.6.1
Der Superblock
187
188
/*
/*
/*
/*
/*
/*
/*
/*
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;
/*
>;
Listing 8.31. Ausschnitte aus der Struktur ext2-Superblock
189
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
>
>
>;
osd2;
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];
94
Definiert in fs/ext2/inode.c.
Diese Punktion ist in fs/ext2/balloc.c enthalten.
95
192
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
193
"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
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
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
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),
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
103
104
Definiert in fs/direct-io.c.
Beide sind in der Datei fs/ext2/inode.c enthalten.
196
{
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;
benachbarten Block mit Hilfe von ext2_find_near() zu finden. Die Kriterien dafr
knnen so beschrieben werden:
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
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
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 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
Die Listings 9.2 und 9.3 zeigen den Datenaustausch mit Hilfe einer Pipe. Die
Pipe wird im Elternprozess angelegt, der anschlieend zwei Kindprozesse
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);
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);
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
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;
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
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 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,
206
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
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.1
IPC-Grundlagen
};
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
>;
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;
>;
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);
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;
>;
9.3.3
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
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];
>;
/* ======================= */
>
Listing 9.11. Message Queue: Anlegen und Nachricht schreiben
>
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.3.4
Semaphoren
};
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 */
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
>;
214
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
>;
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
216
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
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
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
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
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.
222
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
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
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);
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-
226
close
Server
'
,
close
---------------
Client
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
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
>;
230
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
232
struct net_device
unsigned
short
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 */
234
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;
22
236
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
238
10 Der Bootvorgang
eine Reihe administrativer Aufgaben erledigt werden, bevor der Benutzer das
vorfindet, was er zum Arbeiten bentigt:
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
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:
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
240
10 Der Bootvorgang
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:
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
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):
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
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.
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
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:
246
10 Der Bootvorgang
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
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?
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
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
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.
};
struct list_head {
struct list_head *next, *prev;
{
__list_add(new,
head,
head->next);
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.
253
{
__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)
{
}
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);
{
if (!list_empty(list))
>
___list_splice(list, head);
>
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,
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)
>
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
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:
_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
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
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
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
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
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
kers-docs.html
http://www.kernel.org/
http ://www.linux.org/
http ://www.novell.com/de-de/linux/suse/
http ://www.inf.fh-dortmund.de
Literaturverzeichnis
1.
2.
3.
4.
5.
6.
7.
8.
Sachverzeichnis
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
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
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
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
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
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
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