3. Prozesse in UNIX - Fakultät für Mathematik und Informatik

Transcription

3. Prozesse in UNIX - Fakultät für Mathematik und Informatik
Friedrich-Schiller-Universität Jena
Fakultät für Mathematik und Informatik
Ausarbeitung zum Referat
UNIX
im Proseminar „Moderne Betriebssysteme“, Wintersemester 06/07
vorgelegt von Christian Bayer
Matrikel: 84543
Email: [email protected]
Betreuer: Volker Dörsing
1
Inhalt
1. Geschichtlicher Überblick
2. Überblick über das Betriebssystem UNIX
2.1 Grundlagen
2.2 Schichtenmodell
2.3 Designkonzepte
3. Prozesse in UNIX
3.1 Datenstrukturen zur Prozessverwaltung
3.2 Prozesserstellung mit fork()
3.3 Beispiel: Eine einfache Shell
3.4 Scheduling
3.5 Interprozesskommunikation
3.6 Threads
4. Speicherverwaltung in UNIX
4.1 Datenstrukturen zur Speicherverwaltung
4.2 Swapping
4.3 Paging
4.4 Beispiel: Speicherverwaltung unter Linux (Buddy-Algorithmus)
4.5 Vergleich zu Windows: Working-Set-Algorithmus
5. Ein-/Ausgabe in UNIX
5.1 Grundlagen/Konzepte
5.2 Blockorientierte Geräte
5.3 Zeichenorientierte Geräte
5.4 Beispiel: Netzwerk
6. Dateisystem(e)
6.1 Grundlagen/Konzepte
6.2 Beispiel: Linux-Dateisystem ext2
6.3 Vergleich zu Windows: NTFS
7. Sicherheit unter UNIX
8. Quellen
2
1. Geschichte
Mit Aufkommen der elektronischen Datenverarbeitung wurden anfangs Lochkarten verwendet, um
mehrere Jobs hintereinander auszuführen. Da Rechenzeit teuer war, konnte so eine kontinuierliche
Auslastung des Systems sichergestellt werden.
In den 60er Jahren hingegen waren die Mehrzahl aller Computerarbeitsplätze Terminals. Diese
wurden aus Kosten- und Lokalitätsgründen an Großrechnern angeschlossen, denn die meisten dieser
Arbeitsplätze waren zu dieser Zeit an militärischen Einrichtungen und Universitäten zu finden.
Beide stellten ähnliche Anforderungen an ihr Betriebssystem: Auf der einen Seite gab es mitunter
sehr viele Benutzer des Systems (z.B. Studenten) und auf der anderen mussten mehrere Benutzer
gleichzeitig mit dem System arbeiten können, ohne sich gegenseitig (absichtlich oder unabsichtlich)
zu behindern.
Diese Forderung geht auf die Organisation der Rechnerhardware zurück, denn es gibt nur einen
Hauptrechner, dafür aber mehrere („dumme“) Terminals, die keinerlei Rechenbefähigung besitzen.
Ergo musste der Hauptrechner mehrere Benutzer simultan bedienen können, was eine ganze Reihe
anderer Anforderungen an das Betriebssystem wie Multiprogramming und Mutitasking-Fähigkeiten
nach sich zog.
Diese Anforderungen mündeten schließlich bei den Bell-Labs in der Entwicklung des
Betriebssystems MULTICS, welches bereits die meisten dieser Anforderungen erfüllte. Nachdem
jedoch die Arbeit an diesem System eingestellt worden war, beschäftigte sich Kenneth Thompson,
ein Mitarbeiter des Projekts, mit einer Neuimplementierung des MULTICS-Systems für die PDP/7,
mehr oder weniger „Just for Fun“. Er wollte die ausgreifte Programmierumgebung des MULTICSSystems für ein eigenes System übernehmen, welches fortan scherzhaft „UNICS“ genannt wurde.
UNICS wurde dann auf eine PDP/11 20 portiert, und bereits hier wurde klar, dass das komplett in
Assembler geschriebene System für eine Portierung auf eine andere Architektur ein komplettes
Neuschreiben des Systems nach sich zog.
In der Folge stießen Dennis Ritchie und Brian W. Kernighan zum Entwicklerteam hinzu und
schrieben UNICS in ihrer neuen Programmiersprache C neu.
Damit wurde die Entwicklungsarbeit und Portabilität des Systems drastisch verbessert, es folgten
weiter Portierungen auf damals übliche Rechner, wie die PDP/11-45 und die PDP/11-70. Da die
meisten Universitäten einen solchen Rechnertyp besaßen, das mit ihr ausgelieferte System in
vielerlei Hinsicht nicht den Anforderungen entsprach und UNIX für einen geringen Lizenzbetrag zu
bekommen war, breitete es sich rasant aus und wurde das Betriebssystem der Wahl an den
Universitäten. Hinzu kommt, dass der Mutterkonzern der Bell Labs, AT&T, regulierter Monopolist
war, und kein Geld im Computersektor verdienen durfte. Daher waren nicht nur die Lizenzkosten
gering, sondern es wurde auch der Quelltext mitgeliefert, was zum Versändnis des Systems beitrug
und natürlich zu Veränderungen und vielfältig zu Verbesserungen anregte.
3
Eine solche Verbesserung war beispielsweise eine TCP/IP Netzwerkschnittstelle, mit der UNIXRechner verbunden werden konnten. Diese Schnittstelle wurde von der Berkeley-Universität
implementiert und stellt die Grundlage vieler heutiger Netzwerkstacks dar.
Die Verbesserungen dieser Universität, der Universität von Berkeley, führte schließlich zur
Veröfferntlichung ihrer eigenen UNIX-Distribution, der Berkeley Software Distribution (BSD).
Parallel dazu wurde die originale AT&T-Version verbessert und verändert, Software-Firmen wie
Sun, IBM und HP kauften Lizenzen und modifizierten ebenfalls das System und brachten
schließlich ihre eigene, zu den meisten anderen UNIX-Varianten inkompatible Version auf den
Markt. Um die vielen inkompatiblen Varianten wieder zu vereinen, wurde ein Industriestandard
verabschiedet, der POSIX 1003.1-Standard. Dieser beschreibt, welche Anforderungen und
Systemrufe ein UNIX-kompatibles Betriebssystem haben muss, und teilweise auch, wie sie
implementiert werden müssen.
Diesem Standard folgend entwickelten sich Ende der achtziger Jahre des vergangenen Jahrhunderts
verschiedene freie Implementierungen des POSIX-Standards und damit freie UNIX-Varianten. Die
bekannteste davon ist sicherlich Linux, wenngleich Linux nicht 100%ig POSIX-kompatibel ist.
Weiterhin gibt es die direkten Nachfahren von UNIX, die BSD-Derivate Net-, Open- und FreeBSD
oder auch das für Ausbildungszwecke entwickelte MINIX.
1
1 Grafik: Verwandschaftsbeziehungen der gängigsten UNIX-Varianten, http://en.wikipedia.org/wiki/Image:Unix.svg
4
2. Überblick
2.1 Grundlagen
UNIX kennzeichnet also eine ganze Familie von Betriebssystemen, die aber über den POSIXStandard definierte Gemeinsamkeiten aufweisen. Allen heutigen UNIX-Systemen gemein ist
beispielsweise ihre Multiuser-, Multiprogramming- und Multitasking-Fähigkeit, es können also
gleichzeitig mehrere Benutzer eingeloggt sein, die gleichzeitig verschiedene Aufgaben oder
Programme ausführen können.
Das Betriebssystem UNIX stellt ein Interface zur Abstraktion der Hardware bereit, die sogenannten
Systemrufe. Diese Systemrufe sind die einzige Möglichkeit für Programme, die vorhandene
Hardware (wie z.B. Tastatur, Monitor, Drucker, Festplatte) zu benutzen. UNIX ist also zum einen
der Betriebssystemkern mit Treibern, Speicherverwaltung Dateisystem, Prozess-verwaltung und
Netzwerk.
2.2 Schichtenmodell
Dazu möchte ich folgende Grafik erläutern:
Im Inneren des Bildes befindet sich
das Speziellste, die Hardware. So
werden von innen nach außen die
Ressource Speicher und Prozessor
durch den Scheduler zur Verfügung
gestellt. Zeichenorientierte Geräte wie
Tastatur
durch
und
Grafikkarte
Gerätetreiber
werden
abstrahiert,
Blockorientierte Geräte durch Puffer.
Durch diese Puffer kann wiederum ein
Dateisystem auf eine Festplatte gelegt
werden. Für jede dieser vier Bereiche
existieren in UNIX Systemrufe, um
die geforderte Aufgabe auszuführen.
2
Zum anderen aber ist UNIX auch eine Sammlung von Programmen, deren Namen, Syntax und
Verwendungszweck ebenfalls im POSIX-Standard festgeschrieben ist. Beispielsweise besitzt jedes
UNIX eine Shell (sh), Tools zur Dateiverwaltung (mv, cp, mkdir), einen C-Compiler (cc),
Utilities zur Textverarbeitung (ed) und vieles mehr. Für eine vollständige Liste sei auf 3 verwiesen.
2 Grafik: UNIX-Schichtenmodell, http://cs.uni-muenster.de/u/ruckema/x/dis/dis3/node82.html
3 http://www.unix.org/version3/ieee_std.html
5
2.3 Designkonzepte
Beim Design von UNIX kommen vor allem zweckgebundene Konzepte zum Tragen. So sollte
UNIX Thompson eine veritable Programmierumgebung bieten. Programmierer sind in der Regel
keine Novizen am Computer, deshalb kann man als unerfahrener Benutzer den Eindruck
bekommen, UNIX wäre ein unfreundliches, kryptisches und stures System. Tatsächlich aber
spiegelt sich hier der Grundgedanke des UNIX-Designs wieder: „small is beautiful“. Alle
Programme unter UNIX sind nur für eine einzige Aufgabe gemacht und können nicht mehr, diese
Aufgabe aber machen sie gut. Das ist nur logisch, da die meisten Leute, die mit dem System
arbeiten, bereits wissen, wie es funktioniert und was das System tut, wenn man beispielsweise „rm
-Rf /“ eingibt. Für die Systemrufe gilt das Gleiche, und daher ist es schon fast erstaunlich, dass
ein UNIX-System trotzdem mit nur ca. 100 Systemrufen auskommt, wohingegen Windows etwa das
zehnfache an Systemrufen auf die Waage bringt. Begründet ist diese Fülle jedoch in dem WindowsDesign-"Konzept", für die gleiche Aufgabe viele verschiedene Wege benutzen zu können. „Small is
beautiful“ gilt unter UNIX auch unter den Systemrufen, daher seien die wichtigsten hier kurz
genannt, da viele weitere Systemrufe wieder auf diesen grundlegenden basieren.
Systemruf
Zweck
open()
Öffnet eine Datei
close()
Schließt eine Datei
read()
Liest aus einer Datei
write()
Schreibt in eine Datei
fork()
Tabelle 1: Systemrufe
Erzeugt einen neuen Prozess
So zum Beispiel der Systemruf creat(), der eine Datei erst mit open() öffnet und neu anlegt,
falls sie nicht existiert und anschließend mit write() in diese Datei schreibt. Dieses Konzept ist
wiederum schlüssig, da unter UNIX jedes E/A-Gerät durch eine Datei repräsentiert wird, siehe dazu
unter 5. Ein-/Ausgabe.
6
3. Prozesse in UNIX
UNIX ist von Anfang an ein Mehrbenutzersystem. Daher muss es in der Lage sein, mehrere
Programme semi-parallel ablaufen zu lassen. Ein laufender Prozess kann also in seiner Ausführung
unterbrochen werden, durch Swapping kann er sogar vom Speicher auf die Festplatte ausgelagert
werden, wenn er gerade nicht läuft.
Ein UNIX-Prozess besteht aus seinem Textsegment, also dem ausführbaren Code, einem
Datensegment mit initialisierten und nicht initialisierten Daten und einem Stacksegment mit aktuell
benutzeten Daten, beispielsweise Parametern für einen Systemruf.
Unter UNIX werden Prozesse mithilfe der Datenstrukturen Prozesstabelle und Benutzerstruktur
verwaltet.
3.1 Datenstrukturen zur Prozessverwaltung
In der Prozesstabelle werden Informationen über alle Prozesse verwaltet. Jeder Prozess ist hier mit
Parametern fürs Scheduling wie Schlaf- und Laufzeit sowie einer Priorität eingetragen. Weiterhin
enthält sie ein Speicherabbild mit Zeigern auf das Text-, Daten- und das Stacksegment eines
Prozesses, bei Paging auf deren Seitentabellen. Sind diese Segemente ausgelagert, so enthält die
Prozesstabelle Informationen, wie sie wiedereingelagert werden können.
Die Prozesstabelle enthält außerdem eine Maske für Signale, die aussagt, welche Signale
abgefangen und welche ignoriert werden. Außerdem finden sich hier der aktuelle Prozesszustand,
die Prozess-ID des Vaters und die Benutzer- und Gruppen-ID. Die Prozesstabelle selbst kann nicht
ausgelagert werden, da ja mit ihrer Hilfe entschieden wird, welcher Prozess als nächstes rechnen
darf. Um sich die Prozesstabelle unter UNIX anzuschauen, gibt man auf der Kommandozeile ps
aux ein und bekommt z.B. folgende Ausgabe:
USER
PID %CPU %MEM
VSZ
RSS TTY
STAT START
TIME COMMAND
S
11:04
0:05 init [5]
SW
11:05
0:00 [kjournald]
root
1
0.1
0.0
1392
476 ?
root
11
0.0
0.0
0
0 ?
root
24
0.0
0.1
1604
896 ?
S
11:05
0:00 /sbin/devfsd /dev
root
155
0.0
0.1
1664
624 ?
S
11:05
0:00 /usr/sbin/cron
root
251
0.0
0.1
1448
596 ?
S
11:05
0:00 /sbin/syslogd
root
270
0.0
0.2
2132 1336 ?
S
11:05
0:00 /sbin/klogd -c 1
#1
297
0.0
0.0
1480
S
11:05
0:00 /sbin/portmap
root
341
1.5
2.9 82668 15372 ?
R
11:05
0:57 /usr/X11R6/bin/X
root
344
0.0
0.0
0
0 ?
SW
11:05
0:00 [nfsd]]
root
346
0.0
0.0
0
0 ?
SW
11:05
0:00 [rpciod]
root
406
0.0
0.2
3328 1336 ?
S
11:05
0:00 /usr/sbin/sshd
root
576
0.0
0.0
1392
S
11:05
0:00 /usr/sbin/acpid
root
596
0.0
0.3
4864 1720 tty1
S
11:05
0:00 -bash
root
597
0.0
0.0
1392
S
11:05
0:00 /sbin/agetty
380 ?
496 ?
4
484 tty2
4 UNIX-Prozesstabelle: Typische Ausgabe des Kommandos ps aux
7
Ein Prozess kann entweder „für sich selbst“ rechnen, oder aber er kann seine Ergebnisse der
Außenwelt mitteilen. Dies geschieht wie bereits erwähnt über die Systemrufe.
Wird unter UNIX ein Systemruf ausgeführt, so wird der laufende Prozess unterbrochen und es wird
in den Kern-Modus gewechselt. Der Prozess weiß nicht, dass er unterbrochen wird, daher müssen
vor dem Wechsel die aktuellen Benutzerdaten des Prozesses, wie Register und Stack, gesichert
werden, bevor der Systemruf ausgeführt wird.
Die Benutzerstruktur eines Prozesses enthält nun die Verweise auf seine Benutzerdaten, so z.B.
seine Maschinenregister, Informationen über den aktuell ausgeführten Systemruf, offene
Dateideskriptoren, aber auch seine verbrauchte CPU-Zeit und einen Kern-Stack, der den KernAnteil des Prozesses repräsentiert. Die Benutzerstruktur eines Prozesses kann ausgelagert werden,
falls der Prozess nicht im Speicher ist.
3.2 Prozesserstellung mit fork()
Der wichtigste Systemruf zur Prozessverwaltung unter UNIX ist der Systemruf fork(). Dieser
Systemruf erzeugt einen neuen Prozess. Unter UNIX ist dieser Systemruf, ganz nach dem
Paradigma „small ist beautiful“, ganz einfach und effizient implementiert. Er erzeugt eine komplette
Kopie des Prozesses, der ihn aufruft, mit gleichem Code, allen offenen Dateien, dem Daten- und
dem Stacksegment. Warum ist das besonders einfach? Und warum effizient, wenn er doch eine
komplette Kopie erzeugt? Zuallererst ist es einfach und konsistent, da man einen lauffähigen
Prozess hat, und ein neuer Prozess, der eine exakte Kopie des alten ist, auch auf jeden Fall lauffähig
ist. Effizient wird fork() durch die Technik „Copy on Write“: Es werden zuerst gar keine Daten
kopiert, sondern für den Kindprozess nur Zeiger auf Daten-, Stack- und Textsegment des Vaters
gesetzt. Da meistens ohnehin ein anderer als der aktuelle Prozess gestartet werden soll, werden
diese Zeiger mit dem Systemruf exec() mit den Zeigern für das neue Programm überschrieben.
Wenn doch das gleiche Programm ausgeführt werden soll, und es schreibt in sein Datensegment, so
kommt es zu einem Seitenfehler, da unter UNIX jeder Prozess nur in seinem eigenen Adressraum
schreiben darf. Das Betriebssystem merkt das und kopiert erst jetzt die Daten, wenn sie tatsächlich
gebraucht werden.
Beim Aufruf des Systemrufs fork() wechselt also der Prozess in den Kern. Es wird in der
Prozesstabelle ein freier Eintrag gesucht und an diese Stelle der Tabelleneintrag des Vaters kopiert.
Das Textsegment ist nur lesbar und wird deshalb durch Setzen von Zeigern gemeinsam genutzt. Die
Benutzerstruktur wird mit dem Stack zusammen kopiert – den einzigen Ressourcen, die kopiert
werden müssen, bevor das Kind lauffähig ist.
8
3.3 Beispiel: Eine einfache Shell
Zur Verdeutlichung möchte ich nun eine ganz
while (1) {
read_command(command);
pid = fork();
if (pid < 0) {
printf ("error: unable to fork");
continue;
}
if (pid != 0) {
waitpid (-1, &status, 0);
}
else {
execve (command, 0, 0);
}
}
einfache Shell mithilfe von fork() erläutern.
Die Shell wartet auf Eingaben des Benutzers mit
read_command()
und
interpretiert
die
Eingaben als Programme, die sie ausführen soll.
Dann wird mit fork()
ein neuer Prozess
erzeugt und mithilfe des Systemrufs execve()
überschreibt sich der Kindprozess mit dem
Programm aus command, während der Vater mit
waitpid() auf die Beendigung des Kindes wartet um dann das nächste Kommando einzulesen.
3.4 Scheduling
Für UNIX gibt es (fast) so viele Scheduling-Algorithmen wie UNIX-Varianten. Allen gemein ist
jedoch das Prinzip des Zeitscheiben-Schedulings. Jeder Prozess bekommt eine bestimmte Zeit zum
rechnen, dann wird er unterbrochen. Dabei gibt es zwei Stufen, auf denen das Scheduling abläuft:
Die höhere Stufe verschiebt Prozesse zwischen Speicher und Platte. Die untere Stufe wählt aus den
rechenbereiten Prozessen im Speicher den nächsten aus. In dieser unteren Stufe läuft der RoundRobin-Algorithmus. Dabei existieren mehrere Warteschlangen, jeder ist eine Priorität zugeordnet,
doch die Prioritätswerte überschneiden sich nicht. Beim Scheduling wird jede Warteschlange
durchsucht, der Prozess mit der höchsten Priorität darf rechnen. Ist er fertig, so wird er ans Ende der
Warteschlange gestellt.
Die einzeln den Prioritäten zugeordneten Warteschlangen entsprechen einer speziellen Aufgabe und
die sich daraus ergebende Anordnung entspricht der Reihenfolge ihrer Dringlichkeit. So ist die
Abarbeitung von Disk-Ein-/Ausgabe dringender als das Warten auf Terminal-Ein-/Ausgabe.
Die entsprechende Priorität eines Prozesses errechnet sich aus der Formel priority =
CPU_usage + nice + base, wobei sich die Warteschlange aus priority / (negative)
Konstante ergibt. Die Priorität wird einmal pro Sekunde neu berechnet, CPU_usage verfällt mit
der Zeit, sodass auch rechenintensive Prozesse wieder Rechenzeit bekommen. Der nice-Wert kann
von -20 bis +20 gesetzt werden, er erhöht die Chance eines Prozesses, wieder rechnen zu können.
Je höher die Priorität der Warteschlange ist, desto „tiefer“ ist er im Kern-Modus und desto schneller
soll er wieder heraus, da im Kernmodus wieder Systemrufe z.B. für E-/A-Aufgaben wie read()
und write() ausgeführt werden, die schnell bearbeitet werden sollen.
9
5
3.5 Interprozesskommunikation
Die Interprozesskommunikation wird unter UNIX traditionell über Spezialdateien und Signale
abgewickelt. Diese Spezialdateien sind Pipes oder Sockets (entfernte Pipes), mit denen die Ausgabe
eines Prozesses an die Eingabe eines anderen Prozesses geleitet werden kann.
Ein Prozess kann aber auch einem anderen einfach nur eine Nachricht schicken, das geschieht
mithilfe von Signalen. Ein Signal ist eine Art Software-Interrupt, den ein Programm abfangen oder
auch ignorieren kann.
Signal
Funktion
SIGABRT
Beenden + Core-Datei
SIGALRM
Zeitgeber-Alarm
SIGFPE
Gleitkommafehler (z.B. Div 0)
SIGHUP
Restart (Hangup)
SIGKILL
Abbruch des Prozesses (nicht abfangbar)
SIGSEGV
Referenz auf ungültige Speicheradresse
6
3.6 Threads
Threads werden wieder nach einer POSIX-Richtline spezifiziert, jedes UNIX muss binäre
Semaphoren (Mutexe) und Condition-Variablen für die Synrchronisation unterstützen. Jedoch ist
nicht festgelegt, ob Threads im Kern oder im Userspace implementiert sein müssen, als Folge hat
beispielsweise Solaris Threads im Kern, wohingegen Linux sie im Userspace verwaltet.
5 UNIX-Scheduler, Andrew S. Tannenbaum, Modern Operatin Systems
6 Einige von POSIX geforderte Signale
10
4. Speicherverwaltung
UNIX musste von Anfang an portabel sein, was zu einer sehr geradlinigen Speicherverwaltung
geführt hat. UNIX war damit lauffähig auf Rechnern mit praktisch keiner (Hardware-)
Speicherverwaltung, aber auch auf Rechnern mit einer sehr ausgefeilten.
4.1 Datenstrukturen zur Speicherverwaltung
Wie schon erwähnt bestehen UNIX-Prozesse aus Text-, Daten- und Stacksegment. Dabei ist das
Textsegment nur lesend, da der Programmcode im Nachhinein nicht mehr verändert werden kann.
Daher nutzen mehrere Instanzen (Prozesse) des gleichen Programms dasselbe Textsegment.
Eine Methode des Speichersparens, welche unter Windows nicht per default möglich ist, sondern
nur über den Umweg der DLLs und dann auch nicht immer.
Das Datensegment enthält initialisierte und uninititalisierte Daten. Da eine nicht initialisierte
Variable unter C semantisch immer 0 ist, würde ein Programm mit vielen solcher Variablen sehr
viel Speicher verschwenden. Daher wird nur die Größe gespeichert und die Nullen werden
zusammengefasst.
Im Stacksegment werden Umgebungsvariablen und aktuelle Parameter eines Prozeduraufrufs
abgelegt. Es wächst dynamisch, bis es auf eine bereits belegte Seite trifft. Dann wird ein Seitenfehler ausgelöst, und das Betriebssystem alloziert mithilfe des Systemrufs brk() eine neue Seite.
Prozesse können mithilfe der Systemrufe mmap() Dateien in ihren Speicherbereich einblenden.
Auch das geschieht nur einmal pro Datei, solange nur aus der Datei gelesen wird.
7
7 Speicherbereich von Prozessen unter UNIX, Andrew S. Tannenbaum, Moderne Betriebssysteme, Prentice Hall
11
4.2 Swapping
Im Rechner ist der vohandene Arbeitsspeicher meist die wichtigste Ressource. Um aber auch auf
Rechnern mit wenig Speicher große Programme laufen zu lassen, oder aber sehr viele Programme
gleichzeitig auszuführen, gibt es die Technik des Swappings. Dabei werden Prozesse auf eine
Festplatte ausgelagert und nach einer bestimmten Zeit wieder eingebracht. Der Prozess, der unter
UNIX dafür zuständig ist, heißt Swapper. Zuerst werden von ihm blockierte Prozesse ausgelagert,
die ohnehin nicht rechenbereit sind. Dabei werden die mit der niedrigsten Priorität ausgelagert,
bekanntlich ja die, die nicht so tief im Kern-Modus stecken und dabei die höchste CPU-Zeit
verbraucht haben. Sollten keine blockierten Prozesse vorhanden sein, so muss wohl oder übel ein
rechenbereiter ausgelagert werden.
Eingebracht werden die Prozesse, die am längsten ausgelagert waren und für die genügend freier
Speicher vorhanden ist. Solche Prozesse werden durch sogenanntes „einfaches Einlagern“
eingebracht. Prozesse, die größer sind als der vorhandene Speicher, werden durch „schwieriges
Einlagern“ wiedereingebracht, da für sie erst noch andere Prozesse ausgelagert werden müssen.
4.3 Paging
Die Idee hinter dem Paging ist einfach: Ein Prozess muss nicht vollständig im Speicher sein, um
rechnen zu können. Ausreichend für seine Lauffähigkeit ist das Vorhandensein der Benutzerstruktur
und der Seitentabellen. Jede Code-Seite und jede Daten- oder Stackseite kann dynamisch
eingebracht werden, wenn sie benötigt wird. Oft arbeitet ein Programm in einer Schleife, in der nur
wenige Variablen und nur sehr wenig Programmcode wirklich zum Rechnen gebraucht wird.
Dabei kommt wieder ein intelligentes Verfahren zum Speichersparen zum Tragen: In der Regel
werden unter UNIX aus Performanzgründen Auslagerungspartitionen benutzt, statt – wie z.B. unter
Windows üblich – Auslagerungsdateien. Das spart zum einen Dateisystem-Overhead bei der
Auslagerung und zum anderen Blockzugriffe auf einen eigentlich wahlfreien Speicher.
Wird nun aber das Textsegment eines Prozesses oder eine speicherabgebildete Datei ausgelagert, so
wandert sie nicht in die Auslagerungspartition, sondern wird direkt über die Inode-Nummer im
Dateisystem angesprochen. Es wird also auch hier wieder kein Speicher verschwendet.
4.4 Beispiel: Speicherverwaltung unter Linux
Linux verfolgt wiederum einen etwas anderen Ansatz bei der Speicherverwaltung. Unter Linux ging
man von der einfachen Kalkulation aus, dass freier Speicher eine ungenutzte Ressource und damit
verschwendeter Speicher ist. Daher ist unter Linux fast nie mehr als 10% des Speichers frei.
Genutzt wird der Speicher durch Plattencache, denn bei jedem Zugriff werden immer ein paar mehr
Blöcke gelesen als eigentlich angefordert worden sind. Dieser Plattencache teilt sich einen Pool
zusammen mit den Benutzerseiten, so dass nie die vielleicht unnötigerweise eingelagerten Seiten
Grund dafür sind, dass ein Programm nicht in den Speicher passt. Es werden dann einfach Seiten
aus diesem Pool verworfen.
12
Der Seitenersetzungsalgorithmus wird immer dann aktiv, wenn eine neue Seite angefordert wird.
Unter Linux läuft dafür der Buddy-Algorithmus. Der Speicher wird dabei in Blöcke zu je 64 Seiten
eingeteilt. Wenn neuer Speicher angefordert wird, so wird die Anforderung auf die nächste
Zweierpotenz aufgerundet und der 64-Seiten-Block solange geteilt, bis die entsprechende Seitenzahl
erreicht ist. Das kann natürlich zu erheblicher Fragmentierung führen, falls beispielsweise 33 Seiten
angefordert werden. Dann verbleiben im schlimmsten Fall 31 Seiten ungenutzt. Der BuddyAlgorithmus hat seinen Namen von der Art, wie er die Seiten behandelt, die wieder freigegeben
wurden. Diese verschmelzen nämlich wieder zum nächstgrößeren Block.
8
4.5 Vergleich zu Windows: Working-Set-Algorithmus
Windows verfolgt bei der Speicherverwaltung den genau entgegengesetzten Ansatz zu Linux. Statt
keinen Speicher ungenutzt zu lassen, geht man bei Windows davon aus, dass das Auslagern von
Prozessen ein sehr aufwändiger Vorgang ist, der unbedingt zu vermeiden ist. Daher soll bei
Windows ein gewisser Anteil des Speichers immer frei bleiben. Windows verwendet zur
Verwirklichung den Working-Set-Algorithmus. Dabei ist das Working Set eines Prozesses eine
Teilmenge aller seiner eingelagerten Seiten. Es hat eine minimale und eine maximale Größe. Tritt
nun ein Seitenfehler auf, wird eine Seite zum Working Set hinzugefügt, falls die Minimalgröße
noch unterschritten ist. Wird die maximale Größe überschritten, so wird die Seite aus dem Working
Set entfernt, jedoch nicht aus dem Speicher. Wird mehr Speicher benötigt, dann werden Prozesse
ausgelagert, die lange unbeschäftigt waren oder viel Speicher benutzen. Interessant ist nun, dass
Prozesse, deren Working Set kleiner als das Minimum ist, gar nicht ausgelagert werden. Das führt
zu einer hohen Reakionsgeschwindigkeit kleiner Prozesse, aber leider auch zu einer „Zumüllung
des Speichers mit der Zeit, so dass möglicherweise hin und wieder neugestartet werden muss.9
8 Der Buddy-Algorithmus, Andrew S. Tannenbaum, Modern Operating Systems
9 Quelle Working Set: http://www4.informatik.uni-erlangen.de/Lehre/SS05/PS_KVBK/talks/Usik_Folien.pdf
13
5. Ein-/Ausgabe
5.1 Grundlagen/Konzepte
Unter UNIX gibt es bekanntlich für die Ein- und Ausgabe mit jeglicher Hardware nur die
Systemrufe open(), read(), write(), und close(). Mit diesen Systemrufen jedoch lassen
sich schon allein subjektiv die meisten hardwaregebundenen Aufgaben erledigen. Diese Idee greift
auch wieder das UNIX-Mantra „klein ist schön“ auf, denn das sind alle Systemrufe zur Ein- und
Ausgabe mit der Hardware. Die Systemrufe sind so universell, dass sie überall funktionieren, was
unter UNIX aber auch am Konzept der Spezialdateien liegt. Unter UNIX ist alles eine Datei. Auch
Geräte werden durch Spezialdateien repräsentiert, diese sind mit einer Major- und einer MinorNummer gekennzeichnet. Die Major-Nummer entspricht der Geräteklasse, der Minor der Nummer
des Gerätes in der Reihenfolg, in der es vom Kern gefunden wurde. So entspricht die erste im
System gefundene Ethernet-Netzwerkkarte unter FreeBSD dem Gerät en0, das zweite en1 und so
weiter. Solche Spezialdateien werden in der Regel ins Dateisystem eingeblendet (außer gerade die
Netzwerkkarten), Geräte befinden sich im Verzeichnis /dev.
Unter Windows gibt es häufig für ein Gerät von jedem Hersteller einen Treiber. Jedoch ist es
möglich, dass ein Gerät baugleich oder sehr ähnlich zu einem anderen Gerät eines anderen
Herstellers ist. Da wäre es Unsinn, wenn jeder Hersteller für das gleiche Gerät einen eigenen
Treiber schreibt. Diesen Ansatz verfolgt UNIX und heute notgedrungen umso mehr Linux, weil die
Hersteller häufig gar keine Treiber zur Verfügung stellen. Daher wird versucht, eine Geräteklasse
(beispielsweise alle NE2000-kompatiblen Netzwerkkarten) mit ein und demselben Treiber
anzusprechen, egal welcher Hersteller nun die Karte genau hergestellt hat.
Durch die Einteilung und Benennung aller Geräte in Geräteklassen wird eine hohe Abstraktion der
Hardware duch den Kernel erreicht. Es ist für das Internet-Protokoll nicht notwendig, zu wissen, ob
es auf einem Ethernet oder einem Token-Ring aufsetzt. Das Protokoll hat für alle Netze die gleichen
Schnittstellen.
Ein weiterer Unterschied zu Windows ist die Unterteilung aller Geräte in blockorientierte Geräte,
wie z.B. Platten- und Bandlaufwerke, und zeichenorientierte Geräte, wie z.B. Grafikkarte und
Drucker. Wiederum sind die Netzwerkkarten hier eine Ausnahme, da sie Eigenschaften beider
Gerätetypen aufweisen.
5.2 Blockorientierte Geräte
Diese Einteilung macht aufgrund der unterschiedlichen Gegebenheiten der Hardware Sinn, denn ein
Byte auf eine Festplatte zu kopieren, zu überprüfen, ob es angekommen ist und dann das nächste
Byte zu schreiben, ist nicht sonderlich effektiv. Statt dessen werden auf blockorientierte Geräte die
Daten blockweise geschrieben und gelesen. Wenn ein Byte angefordert wird, wird der ganze Block
gelesen, in dem sich dieses Byte befindet. Dieser Block wird in einen Puffercache gelesen, um für
spätere Zugriffe an dieselbe Stelle bereit zu stehen. Wird ein Block angefordert, so wird auch erst
14
überprüft, ob er sich im Puffer befindet. Wenn ja, wird er direkt aus dem Puffer gelesen, es findet
kein Plattenzugriff statt.
Der Puffercache hat eigentlich eine feste Größe, soll jedoch auch wachsen können. Daher wird er
über eine doppelt verkettete Liste verwaltet. Ein angeforderter Block wird an den Anfang der Liste
verschoben, kann die Liste nicht mehr wachsen, so wird der letzte Block in der Liste entfernt, bevor
ein neuer eingefügt wird. Dadurch befinden sich immer die zuletzt benutzten Blöcke in der Liste.
Um Datenverlust vorzubeugen, werden „verschmutzte“ Blöcke, also solche, die verändert wurden,
alle 30 Sekunden auf die Festplatte geschrieben.
5.3 Zeichenorientierte Geräte
Auch Zeichenorientierte Geräte sind, gemesssen am Prozessor, sehr langsam. Eine Ansteuerung
dieser Geräte mittels eines Puffers macht Sinn, allerdings nicht für Blöcke, sondern byteweise.
Diese Puffer heißen Streams. So kann man beim Lesen vom Terminal beispielsweise einen Stream
für die Eingabe, einen für die Ausgabe und einen für die Fehlerausgabe öffnen. Ähnlich funktioniert
die Ein- und Ausgabe über die serielle Schnittstelle, die ja im Prinzip auch nur ein Terminal ist.
Für das Lesen vom Terminal gibt es jedoch zwei unterschiedliche Modi. Einmal den Modus der
Zeilendisziplin, bei dem der Zeichenstrom „gekocht“ wird, also zeilenweise die Eingabe des Users
entgegengenommen wird, wobei Tabulatoren expandiert und gelöschte Zeichen entfernt werden. Es
gibt jedoch auch einen „rohen“ Modus, bei dem jedes eingegebene Zeichen des Nutzers an das
verarbeitende Programm weitergegeben wird. Hier muss das Programm selbsttätig den
Zeichenstrom interpretieren.
5.4 Netzwerk
Netzwerkkommunikation findet unter UNIX ausschließlich über sogenannte Sockets statt. Ein
Socket ist – wie kann es anders sein – eine spezielle Datei, die wieder mit den bekannten
Systemrufen angesprochen werden kann. Ein Socket ist im Prinzip nur eine Pipe, deren Ausgang
aber auf einem anderen Rechner ist. Insofern ist er wieder nur ein Mittel der
Interprozesskommunikation. Von den Sockets gibt es 3 Typen: Erstens den zuverlässigen,
verbindungsorientierten Byte-Strom, der weitestgehend der UNIX-Pipe und dem TCP-Protokoll
entspricht. Zweitens gibt es den zuverlässigen, aber packetorientierten Strom und drittens wäre da
noch der unzuverlässige Paketstrom, der durch das UDP-Protokoll modelliert werden kann. Dieser
Socket-Typ wird beispielsweise für Streaming-Inhalte verwendet, bei denen es auf Datenrate
ankommt und Einzelpakete nicht so wichtig sind.
Ein Socket wird durch den Systemruf listen() in den „Horch-“Zustand versetzt, ein Puffer wird
erstellt und solange blockiert, bis Daten eintreffen. Die Gegenseite erstellt ebenfalls einen Socket,
führt aber connect() darauf aus. An diesem Punkt ist die Pipe verbunden und kann als solche
genutzt werden.
15
6. Dateisystem
6.1 Grundlagen und Konzepte
Das UNIX-Dateisystem behandelt alle Dateien gleich – sie werden nicht nach MIME-Typen oder
Erweiterung oder anderen Kriterien unterschieden, wie das Beispielsweise nativ unter MacOS und
durch die Erweiterung unter Windows getan wird. UNIX kann solche Dateisysteme lesen – aber mit
den speziellen Dateitypen eigentlich nichts anfangen. Es unterscheidet einzig und allein die (Datei-)
Typen 'Verzeichnis', 'normale Datei' und diverse Spezialdateien. Dateinamen können bis zu 255
Zeichen lang werden, beliebig viele Erweiterungen tragen.
Das Besondere am UNIX-Dateisystemkonzept ist das Einhängen oder Mounten von Verzeichnissen
unterhalb der Wurzel „/“. Damit existieren unter UNIX keine „Laufwerke“, wie sie von anderen
Betriebssystemen her bekannt sind. Alle Laufwerke werden in ein Verzeichnis unterhalb der Wurzel
eingehangen. Der Grund dafür ist profan – es war die Faulheit der Entwickler, die sich nicht mit
einem Laufwerksbuchstaben vor Verzeichnisangaben belasten wollten. Verstehen kann man das
heute nur noch, wenn man eine Kommandozeile mag.
Das UNIX-Dateisystem unterstützt weiterhin symbolische und harte Links auf Dateien und
Verzeichnisse. Eine Datei kann also in mehreren Verzeichnissen vorhanden sein, ohne dass
zusätzlicher Speicherplatz benötigt wird. Außerdem muss man sich nicht um Aktualisierungen der
Datei kümmern, alles landet auf demselben Inode.
Inodes sind Zeigertabellen, die eine Datei repräsentieren. Ein Inode enthält mehrere Zeiger auf
direkte Datenblöcke und Zeiger auf indirekte Blöcke. Diese zeigen dann wieder auf direkte und
indirekte Blöcke, so lassen sich bei 32 Bit Zeigerlänge bis zu 2 GB große Dateien verwalten.
UNIX unterstützt weiterhin das Locking von Dateien oder einzelnen Bereichen daraus, so dass ein
Programm exklusiv in eine Datei schreiben kann. Währenddessen kann kein anderes Programm in
die Datei schreiben, zumindest nicht an die Stelle, die gelockt ist. Das Besondere unter UNIX ist,
dass eine Datei immer gelesen werden kann, auch wenn auf die Datei geschrieben wird oder wenn
sie gerade geöffnet ist. Nötig wurde das, um Backups des kompletten Systems machen zu können,
ohne das System herunterzufahren. Unter Windows kann man deshalb mit professioneller BackupSoftware richtig Geld machen...
6.2 Beispiel: Linux-Dateisystem ext2
Das Linux-Dateisystem ext2 teilt eine Partition in Blockgruppen ein. Dabei hat jede Blockgruppe
ihren eigenen Superblock. Dieser enthält die Anzahl der Blöcke und der Inodes in dieser
Blockgruppe. Ohne den Superblock kann diese Blockgruppe nicht mehr gelesen werden.
Jeder Inode unter Linux ist 128 Byte groß und enthält 12 direkte Zeiger auf Datenblöcke und 3
indirekte Blöcke. Datenblöcke sind unter ext2 standardmäßig 1 KB groß. Damit lassen sich Dateien,
die kleiner als 12 KB sind, ohne indirekte Blöcke ansprechen. Das stimmt insofern mit dem UNIXKonzept überein, dass UNIX und damit auch Linux sehr viele kleine Dateien hat.
16
6.3 Vergleich zu Windows: NTFS
Das Windows-Dateisystem NTFS verfolgt einen gänzlich anderen Ansatz als viele UNIXDateisysteme. Es unterstützt 255 Zeichen lange Dateinamen, die in Unicode codiert sind. Das heißt,
dass Dateinamen beliebige Zeichen enthalten können, da Unicode fast jede Schriftsprache
unterstützt, zum Beispiel auch Chinesisch. Jedoch ist die Win32-Api nur auf ASCII-Zeichen
ausgelegt, was in der Praxis bedeutet, dass wieder nur ASCII-Zeichen verwendet werden können.
Das Dateisystem selbst ist aber problemlos dazu in der Lage, Unicode-Zeichen zu unterstützen.
Ein weiterer, gänzlich anderer Ansatz ist das Stream-Konzept. Unter NTFS werden Dateien nicht
als lineare Sequenzen von Blöcken aufgefasst, sondern als Streams. Eine Datei ist ein Objekt im
Dateisystem und besteht aus verschiedenen Streams, im Einzelnen wäre das der Stream „Name“,
der Datenstream und beispielsweise eine Objekt-ID. Dieses Konzept stammt wieder vom MacOSDateisystem HFS, das Dateien nach ihren Typen unterscheidet. Handelt es sich bei der Objekt-ID
um ein Bild, so kann man einen Datenstream für das Bild selbst und einen weiteren Datenstream für
einen Thumbnail des Bildes in der Datei selbst speichern.
Die zentrale Datenstruktur im NTFS-Dateisystem ist die sogenannte Master File Table oder MFT.
Jede MFT ist änhlich der File Allocation Table oder FAT und beschreibt alle Dateien und
Verzeichnisse. Sie besteht aus 1 KB Records, enthält alle Dateiattribute und ist selbst auch eine
Datei. Das hilft vor allem beim Auffinden der MFT, vor allem beim Bootvorgang. Ihre Eigenschaft
als Datei unterliegt nicht etwaigen Beschränkungen des BIOS, sie kann also auch außerhalb der
1024-Zylinder-Grenze liegen und trotzdem aufgefunden werden.
Das sicherlich wichtigste Dateiattribut ist das „Data“-Attribut. Es verweist auf Datenblöcke der
Datei und damit auf die entsprechenden Streams.
Die MFT speichert kleine Verzeichnisse direkt als Dateiliste in ihrem Head. Größere Verzeichnisse
werden als B-Bäume implementiert. Ähnliches gilt auch für Dateien. Besonders kleine Dateien, also
solche unter 1 KB, werden direkt in den Head der MFT geschrieben, die Daten sind also bereits in
den Records des MFT vorhanden. Sollte eine Datei größer sein als 1 KB, so wird sie über eine Liste
von Blockadressen verwaltet.
Wenn unter NTFS eine Datei erstellt wird, so werden immer gleich mehrere Blöcke angefordert,
damit die Datei möglichst sequentiell auf die Platte geschrieben werden kann. Dabei beschreibt ein
Record immer einen zusammenhängenden Bereich von Blöcken.
Das Dateisystem NTFS weist signifikante Unterschiede zu den meisten UNIX-Dateisystemen auf,
aber auch einige Paralellen, wie die effiziente Speicherung von kleinen Dateien. Auch ist es
offenkundig besser gerüstet für die Zukunft, denn eine Dateinamenunterstützung in der nativen
Landessprache gibt es für UNIX bisher nur durch das von IBM erfundene Dateisystem JFS2 oder
das MacOS X-Dateisystem HFS+. Auch die Objektorientierung ist unter UNIX-Dateisystemen noch
nicht sehr verbreitet, einen ersten Ansatz dazu gibt das neue Dateisystem ext4 für Linux.
17
7. Sicherheit in UNIX
Da UNIX bereits sehr früh für mehrere Benutzer ausgelegt war, wurde es auch sehr früh nötig,
bestimmte Sicherheitsmechanismen einzubauen, um das System selbst, aber auch die Benutzerdaten
vor Fremdzugriffen (und manchmal die Benutzer vor sich selbst) zu schützen.
Unter UNIX muss sich jeder Benutzer am System authentifizieren. Wenn man kein Passwort hat,
kann man sich nicht anmelden. Die Anmeldung übernimmt das Programm login, welches den
Nutzer zuerst nach seinem Login-Namen fragt und dann nach seinem Passwort. Dieses Passwort
wird gehasht und mit dem Hash in der Datei /etc/passwd bzw. heute üblicher mit dem Hash in
der Datei /etc/shadow verglichen. Das hat den Vorteil, dass ein Kennwort nicht im Klartext
gespeichert werden muss. Die Speicherung in der Datei /etc/shadow hat den Vorteil, dass der
Nutzer auch die gehashten Kennwörter nicht lesen kann, da kurze Kennwörter mit einem BruteForce-Angriff leicht erraten werden können.
Stimmt das eingegebene Passwort mit dem Hash überein, so wechselt das Programm login seine
effektive Benutzer-ID mit setuid() und die Gruppen-ID mit setgid() auf die des
anzumeldenden Users und startet mit fork() und execve() eine neue Shell, mit der das loginProgramm im Speicher überschrieben wird. Damit hat der Benutzer eine Shell, die unter seiner
Benutzer-ID läuft und nicht mehr unter dem root-Account, unter der das login-Programm lief.
Der Benutzer root ist der Superuser auf einem UNIX-System, er darf traditionell alles. Daher sollte
man immer, wenn man als root angemeldet ist, äußerste Vorsicht walten lassen, da die UNIXKommandos nicht nur sehr mächtig sind, sondern auch (wieder traditionell) bei keiner Aktion
nachfragen. So ist schnell mal die ganze Festplatte mit Nullen überschrieben oder eine Partition
„aufgeräumt“. Gerade weil der root-Benutzer so mächtig ist, gibt es für normales Arbeiten mit dem
System unprivilegierte Benutzer, mit denen man in aller Regel auch arbeiten sollte. Gleiches gilt
übrigens auch für luftigere Betriebssysteme.
Um Dateien vor unberechtigten Zugriffen zu schützen, gibt es einen einfachen Mechanismus: Eine
Datei gehört in der Regel dem, der sie erstellt hat. Für ihn lassen sich die Dateirechte „schreiben“,
„lesen“ und „ausführen“ setzen. Der Ersteller ist Mitglied einer Benutzergruppe. Für diese
Benutzergruppe lassen sich nun wieder dieselben Dateirechte setzen. Schlussendlich gibt es noch
den „Rest der Welt“, also alle anderen Benutzer im System, für die sich ebenfalls diese Dateirechte
setzen lassen. Da jeder Benutzer in einer oder mehreren Gruppen sein kann, lassen sich schon mit
diesem einfachen Mechanismus komplizierte Gruppenbeziehungen und Dateirechtsfragen
nachbilden und effektiv umsetzen. Da das Prinzip recht einfach ist, ist es im Gegensatz zu
Benutzerbeziehungen in einer Windows-Domäne schnell zu verstehen und häufig auch konfliktfrei
umzusetzen. Natürlich bietet es nicht die Möglichkeiten des Feintunings wie unter Windows, aber
gerade das sollte den Laien ermutigen, sich einmal mit dem System auseinanderzusetzen.
18
8. Quellen
Soweit nicht durch Fußnoten angeben, wurde als Quelle Andrew S. Tannenbaum, Moderne
Betriebssysteme verwendet. Das Kapitel zum Windows-Dateisystem NTFS stammt aus der
englischen Ausgabe, die im Internet unter
ftp://ftp.prenhall.com/pub/esm/sample_chapters/engineering_computer_science/tanenbaum/mod_op
_sys_2e/pdf/sample-11.pdf zu finden ist.
19

Documents pareils