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