Prozesse und Systemaufrufe
MittelEinheit 1 von 9 in Kurs: Betriebssysteme – Prozesse, Speicher, Threads und Dateisysteme
Was du lernst
- Beschreibe die Hauptaufgaben des Betriebssystems sowie die Geschichte und Entwurfsideen, die erklären, warum es den Rechner auf diese Weise aufteilt und schützt.
- Beschreibe die Hauptaufgaben eines BetriebssystemsErkläre, wie das Betriebssystem aus der zugrunde liegenden Hardware leichter nutzbare, gemeinsam genutzte und geschützte Ressourcen für Programme macht.
- Erkläre, wie Mehrprogrammbetrieb und Zeitteilverfahren die Entwicklung des Betriebssystems geprägt habenVerfolge die Entwicklung von der Stapelverarbeitung über den Mehrprogrammbetrieb zum Zeitteilverfahren, erkenne, warum jeder Schritt neuen Schutz erforderte, und vergleiche die wichtigsten Organisationsformen eines Kernels.
- Beschreibe die Hauptaufgaben eines Betriebssystems
- Erkläre, was ein Prozess ist, was das Betriebssystem über jeden Prozess speichert und wie Prozesse zwischen den Zuständen „laufend“, „bereit“ und „blockiert“ wechseln.
- Erkläre, was ein Prozess ist und welche Informationen das Betriebssystem über ihn speichertUnterscheide ein Programm von einem Prozess, beschreibe den Maschinenzustand und den Adressraum eines Prozesses und erkläre, wie das Betriebssystem einen Prozess erzeugt und verwaltet.
- Verfolge die Zustandswechsel eines ProzessesVerfolge anhand der Zustände „laufend“, „bereit“ und „blockiert“ und ihrer Übergänge, wie Prozesse eine CPU gemeinsam nutzen, und erkenne die zusätzlichen Zustände, die reale Systeme melden.
- Erkläre, was ein Prozess ist und welche Informationen das Betriebssystem über ihn speichert
- Nutze die Unix-Prozessschnittstelle in C, um Kindprozesse zu erzeugen, in ihnen neue Programme auszuführen, ihre Ergebnisse abzurufen, sie zu beenden und diese Aufrufe zu einer einfachen Shell zu verbinden.
- Erzeuge mit fork einen KindprozessRufe fork auf, unterscheide Eltern- und Kindprozess anhand des Rückgabewerts und sage voraus, was jeder Prozess sieht und ausgibt.
- Führe mit exec ein anderes Programm ausErsetze mit exec das Programm in einem Prozess, übergib Argumente korrekt und erkläre, warum Unix das Erzeugen eines Prozesses vom Laden eines Programms trennt.
- Warte auf Kindprozesse, lies ihren Beendigungsstatus aus und beende ProzesseBeende Prozesse mit exit und Signalen, rufe das Ergebnis eines Kindprozesses mit wait und waitpid ab, dekodiere seinen Status und verhindere Probleme durch Zombieprozesse und verwaiste Prozesse.
- Entwickle eine einfache Shell mit fork, exec, wait und UmleitungenVerbinde die Prozessaufrufe zu einer Shell-Schleife, die Befehle ausführt, Ein- und Ausgabe umleitet, Befehle mit Pipes verbindet und Aufträge im Hintergrund ausführt.
- Erzeuge mit fork einen Kindprozess
- Erkläre, wie die Hardware privilegierten Kernelcode von Benutzerprogrammen trennt und wie Programme diese Grenze über Systemaufrufe, Ausnahmen und Interrupts kontrolliert überschreiten.
- Unterscheide Benutzermodus und KernelmodusErkläre, was jeder CPU-Modus erlaubt, welche Operationen privilegiert sind und was geschieht, wenn ein Programm die Grenzen seines Modus überschreitet.
- Erkläre Schritt für Schritt den Ablauf eines SystemaufrufsVerfolge einen Systemaufruf vom Bibliothekswrapper über den Trap in den Kernel und zurück und deute seinen Rückgabewert und seine Fehler.
- Unterscheide Traps, Ausnahmen und InterruptsOrdne die Ereignisse ein, die die CPU in den Kernel wechseln lassen, erkläre die Funktion der Tabelle der Kernelbehandlungsroutinen und sage voraus, was nach jeder Art von Ereignis geschieht.
- Unterscheide Benutzermodus und Kernelmodus
- Erkläre, wie das Betriebssystem Programme direkt und mit voller Geschwindigkeit auf der CPU ausführt und dabei die Kontrolle behält, und wie es die CPU von einem Prozess an einen anderen übergibt.
- Erkläre die eingeschränkte direkte AusführungErkläre die zwei Probleme bei der direkten Ausführung von Programmen auf der CPU und wie Benutzermodus, Traps und Timerinterrupt sie lösen, Schritt für Schritt vom Systemstart bis zum Prozessende.
- Verfolge einen Kontextwechsel und schätze seinen Aufwand abVerfolge jeden Schritt beim Wechsel der CPU von einem Prozess zu einem anderen, unterscheide einen Kontextwechsel von einem Moduswechsel und schätze ab, wie viel CPU-Zeit ein Wechsel kostet.
- Erkläre die eingeschränkte direkte Ausführung
Prozesse und Systemaufrufe einfach erklärt
1Erkläre, was ein Betriebssystem macht
Das Betriebssystem bietet gewöhnlichen Programmen nützliche Möglichkeiten, Berechnungen auszuführen, Daten zu speichern und Geräte zu nutzen, während sie sich den Rechner sicher teilen. Seine Aufgaben erklären, warum eine Desktopanwendung keinen eigenen Festplattentreiber braucht und warum ein fehlerhaftes Programm nicht alles zum Stillstand bringen sollte. Verfolge die Entwicklung von Rechnern mit nur einem Auftrag zu interaktiven Systemen und vergleiche dann, wie verschiedene Kernelentwürfe privilegierte Aufgaben aufteilen.
1.1 Beschreibe die Hauptaufgaben eines Betriebssystems
Ein Browser, ein Compiler und ein Musikplayer können auf einem Computer alle Fortschritte machen. Jeder braucht CPU-Zeit, Speicher und Zugriff auf Geräte. Das Betriebssystem stellt diese Ressourcen über Dienste bereit, die Programme nutzen können, ohne den ganzen Rechner zu kontrollieren.
Stelle Programmen nützliche Ressourcen bereit
Virtualisierung macht aus einer physischen Ressource eine bequem nutzbare virtuelle Ressource. Durch CPU-Virtualisierung können sich viele Prozesse so verhalten, als hätte jeder einen eigenen Prozessor. Das Betriebssystem lässt jeden eine Zeit lang laufen und bewahrt seinen Zustand zwischen den Abschnitten. Die Speichervirtualisierung gibt jedem Prozess einen eigenen Adressraum: die Adressen, die seine Befehle nutzen können.
Eine Abstraktion verbirgt Details hinter einer nützlichen Schnittstelle. Eine Datei ist eine Abstraktion für gespeicherte Bytes. Ein Programm kann write aufrufen, ohne zu wissen, ob diese Bytes auf eine SSD oder ein Netzlaufwerk gehen. Ein Systemaufruf bietet eine kontrollierte Möglichkeit, einen Kerneldienst anzufordern. Die gerätespezifische Arbeit bleibt hinter der Schnittstelle.
Teile Ressourcen auf und schütze Programme
Als Ressourcenverwalter entscheidet das Betriebssystem, wer CPU-Zeit, Speicher und Gerätedienste erhält. Es versucht, die Hardware sinnvoll auszulasten und Programmen zugleich angemessenen Zugriff zu geben. Die gemeinsame Nutzung braucht auch Schutz: Ein Programm darf weder den privaten Speicher eines anderen überschreiben noch den Timer deaktivieren, um die CPU zu behalten.
Die Hardware hilft, diese Grenze durchzusetzen. Anwendungsbefehle laufen im Benutzermodus mit eingeschränkten Berechtigungen. Kernelcode läuft mit den Privilegien, die zur Verwaltung des Rechners nötig sind. Ein Timer kann ein Programm auch dann unterbrechen, wenn es nie etwas vom Betriebssystem anfordert.
Betrachte bei einem Laptop, der eine Datei herunterlädt und gleichzeitig Code kompiliert, drei getrennte Aufgaben:
- Das Netzwerkgerät meldet eintreffende Daten. Der Kernel stellt sie dem herunterladenden Prozess zur Verfügung.
- Das Betriebssystem teilt die CPU-Zeit zwischen Browser, Compiler und anderen bereiten Prozessen auf.
- Der Compiler fordert Dateioperationen an, um seine Ausgabe zu speichern. Sein Code steuert den Speichercontroller nicht direkt an.
Trenne den Kernel von der Oberfläche
Der Kernel ist der privilegierte Kern. Shell, Desktop und die meisten Systembibliotheken laufen als gewöhnliche Programme, selbst wenn sie mit dem Betriebssystem ausgeliefert werden. Das Schließen eines Desktopfensters entfernt den Kernel nicht. Ein Server ohne grafische Oberfläche braucht weiterhin Prozessverwaltung, Speicherschutz und Gerätedienste.
Auf einer logischen CPU wechseln sich Benutzercode und Kernelcode ab. Während der Compiler gewöhnliche Benutzerbefehle ausführt, läuft der Kernel nicht gleichzeitig auf dieser CPU. Systemaufrufe, Ausnahmen und Hardwareinterrupts geben ihm die Kontrolle zurück.
Wähle erst das Wie, dann das Wer
Ein Mechanismus stellt eine Operation bereit; eine Strategie wählt, wie sie genutzt wird. Die Register eines Prozesses zu sichern und die eines anderen wiederherzustellen, ist ein Mechanismus. Zu wählen, welcher bereite Prozess den nächsten Abschnitt erhält, ist eine Strategie. Diese Trennung erlaubt es dem Betriebssystem, seine Schedulingentscheidungen zu ändern, ohne den Wechselmechanismus zu ersetzen.
Beim Betriebssystementwurf werden nützliche Abstraktionen, Leistung, Schutz, Zuverlässigkeit, Sicherheit und Energieverbrauch gegeneinander abgewogen. Die zusätzliche CPU-Zeit und der zusätzliche Speicher für die Verwaltung sind Verwaltungsaufwand. Drei wiederkehrende Themen des Fachgebiets sind Virtualisierung, Nebenläufigkeit (gleichzeitig laufende Aktivitäten) und Persistenz, durch die gespeicherte Daten das Ende eines Programms oder einen Stromausfall überdauern.
Wichtige Erkenntnisse
- Virtualisierung gibt Programmen bequem nutzbare Sichten auf gemeinsam genutzte Hardware.
- Abstraktionen wie Dateien verbergen Gerätedetails hinter nützlichen Operationen.
- Der Kernel ist privilegiert; Shell und Desktop sind gewöhnliche Programme.
- Ein Mechanismus stellt eine Operation bereit; eine Strategie wählt, wann oder für wen sie genutzt wird.
- Schutz und geringer Verwaltungsaufwand sind beides Ziele beim Entwurf von Betriebssystemen.
1.2 Erkläre, wie Mehrprogrammbetrieb und Zeitteilverfahren die Entwicklung des Betriebssystems geprägt haben
Betriebssysteme entwickelten sich weiter, als Computer von der Ausführung einzelner Aufträge dazu übergingen, viele Aktivitäten gleichzeitig zu bedienen. Jede Änderung löste ein praktisches Problem: untätige Hardware, langsame Interaktion oder Fehler, die sich zwischen Programmen ausbreiten.
Von Stapeln zu überlappender Arbeit
Bei der Stapelverarbeitung laufen eingereichte Aufträge ohne Interaktion mit der einreichenden Person nacheinander ab. Frühe Unterstützungssoftware half hauptsächlich beim Laden von Aufträgen und bei üblichen Ein-/Ausgabeoperationen. Musste der aktuelle Auftrag auf ein Gerät warten, konnte die CPU untätig bleiben.
Der Mehrprogrammbetrieb hält mehrere Aufträge im Speicher. Blockiert einer wegen Ein-/Ausgabe, kann ein anderer rechnen. Angenommen, ein Auftrag rechnet wiederholt 20 ms und wartet 80 ms auf die Festplatten-Ein-/Ausgabe. Allein beträgt seine CPU-Auslastung 20 / 100 = 20 %. Bei zwei solchen Aufträgen und geeigneter Überlappung können nützliche Berechnungen 40 % der verstrichenen Zeit ausfüllen. Die Verzögerung des Geräts ist nicht kürzer geworden; stattdessen wurde CPU-Zeit gefüllt, die sonst ungenutzt bliebe.
Der Gewinn hängt davon ab, dass Arbeit bereitsteht. Zusätzliche Aufträge können die CPU-Auslastung nicht über 100 % erhöhen, und bei Aufträgen, die auf dasselbe Gerät warten, kann weiterhin ein Engpass entstehen. Mehrere Aufträge im Speicher brauchen auch Schutz: Keiner darf einen anderen überschreiben oder die eigenen Daten des Betriebssystems beschädigen.
Sorge für schnelle Reaktionen
Das Zeitteilverfahren gibt Prozessen kurze Abschnitte auf der CPU. Ein Terminal oder Desktop kann reagieren, während andere Programme weiterarbeiten. Müsste man warten, bis ein Stapelauftrag endet, bevor der Tastendruck eines anderen Benutzers bedient wird, würde sich das ganz anders anfühlen.
| Aufteilungsverfahren | Was aufgeteilt wird | Beispiel |
|---|---|---|
| Zeitteilverfahren | Nutzung einer Ressource über aufeinanderfolgende Zeitabschnitte | CPU-Abschnitte |
| Raumteilverfahren | Kapazität, die verschiedene Benutzer gleichzeitig belegen | Speicherbereiche oder Festplattenplatz |
Nebenläufigkeit bedeutet, dass mehrere Aktivitäten im selben Zeitraum ablaufen. Ihre Befehle können auf einer logischen CPU verzahnt werden. Parallelität bedeutet, dass Befehle tatsächlich gleichzeitig auf unterschiedlichen Verarbeitungsressourcen ausgeführt werden. Mehrprogrammbetrieb setzt keine parallele Hardware voraus.
Platziere Dienste innerhalb oder außerhalb des Kernels
| Entwurf | Wo die meisten Dienste laufen | Wichtigster Zielkonflikt |
|---|---|---|
| Monolithischer Kernel | Dateisysteme, Treiber und Netzwerkfunktionen laufen im Kernelmodus | Direkte Aufrufe sind effizient; ein Treiberfehler kann den Kernel beschädigen |
| Mikrokernel | Ein kleiner privilegierter Kern unterstützt Dienstprozesse im Benutzermodus | Die Isolation der Dienste hilft, Fehler einzugrenzen; Kommunikation verursacht zusätzlichen Aufwand |
| Hybrider Kernel | Verbindet Elemente beider Ansätze | Die genaue Grenze zwischen den Diensten hängt vom Entwurf ab |
Linux ist monolithisch und kann Kernelmodule laden. MINIX 3, QNX und seL4 veranschaulichen Mikrokernelentwürfe. Windows NT und Apples XNU werden üblicherweise als hybride Entwürfe beschrieben. Ein Treiber im Benutzermodus lässt sich nach einem Absturz oft neu starten, während ein privilegierter Treiber Zugriff hat, durch den sein Ausfall das ganze System betreffen kann.
Frühen Personal-Computer-Systemen wie MS-DOS und dem klassischen Mac OS fehlte der heute erwartete Speicherschutz für Prozesse. Moderne Systeme verbinden Betriebssystemregeln mit ihrer Durchsetzung durch Hardware. Kurze CPU-Abschnitte allein würden ein Programm nicht daran hindern, den Speicher eines anderen Programms zu verändern. Aufteilung und Schutz lösen unterschiedliche Teile des Problems.
Wichtige Erkenntnisse
- Der Mehrprogrammbetrieb füllt Ein-/Ausgabewartezeiten mit Berechnungen eines anderen Auftrags.
- Das Zeitteilverfahren unterstützt die interaktive Nutzung durch kurze CPU-Abschnitte.
- Nebenläufige Arbeit kann auf einer CPU verzahnt werden; parallele Arbeit wird gleichzeitig ausgeführt.
- Das Zeitteilverfahren teilt Nutzungsabschnitte auf; das Raumteilverfahren teilt Kapazität auf.
- Bei der Kernelorganisation stehen direkte Dienstaufrufe und eine stärkere Isolation der Dienste in einem Zielkonflikt.
- Mehrere im Speicher befindliche Aufträge erfordern Speicher- und Privilegienschutz.
2Beschreibe Prozesse und ihre Zustände
Eine Programmdatei ist nur der Ausgangspunkt. Um sie auszuführen, erstellt und verwaltet das Betriebssystem einen aktiven Prozess mit Speicher, Registern, offenen Deskriptoren und einem Platz in der Prozessliste. Lerne, diesen Zustand zu lesen, und verfolge dann zwei Prozesse, während sich CPU-Arbeit und Wartezeiten auf Geräte überlappen. Der Unterschied zwischen dem Warten auf eine CPU und dem Warten auf ein Ereignis erklärt, welchen Prozess der Scheduler auswählen kann.
2.1 Erkläre, was ein Prozess ist und welche Informationen das Betriebssystem über ihn speichert
Eine ausführbare Datei auf einem Datenträger kann wiederholt gestartet werden. Jeder Start braucht einen eigenen veränderlichen Zustand, damit eine Instanz unabhängig pausieren, fortgesetzt werden oder ausfallen kann. Diese aktive Instanz ist ein Prozess.
Erkenne die laufende Instanz
Ein Programm besteht aus gespeicherten Befehlen und statischen Daten. Ein Prozess verbindet ein Programm mit seinem aktuellen Speicher, seinen CPU-Registern und seinem Ein-/Ausgabezustand. Drei Shells können dieselbe ausführbare Datei ausführen und dabei unterschiedliche Variablen, aktuelle Verzeichnisse und Befehlsverläufe haben.
Der Befehlszähler (PC) bezeichnet den nächsten Befehl. Der Stapelzeiger gibt die Position des aktiven Stacks an. Allgemeine Register enthalten Zwischenwerte. Wenn ein Prozess nicht mehr läuft, muss genug Zustand erhalten bleiben, damit er beim richtigen Befehl mit den richtigen Werten weitermachen kann.
Lies die Darstellung des Adressraums
Der Adressraum eines Prozesses ist der Speicher, den seine Befehle adressieren können. Eine übliche Darstellung für Lehrzwecke platziert Code und statische Daten bei niedrigeren Adressen, darüber einen Heap und bei höheren Adressen einen Stack. Der Heap dient dynamisch reservierten Objekten; der Stack unterstützt Funktionsaufrufe und lokalen Zustand. In dieser Darstellung wächst der Heap zu höheren und der Stack zu niedrigeren Adressen hin. Der tatsächliche Aufbau hängt vom System ab, und Implementierungen von malloc können Speicher auf mehr als eine Weise erhalten.
Zwei Prozesse können für eine Variable dieselbe virtuelle Adresse ausgeben und dort trotzdem unterschiedliche Werte halten. Virtuelle Adressen gehören zur Speichersicht eines Prozesses. Übereinstimmende Adresszahlen bedeuten keinen gemeinsam genutzten beschreibbaren Speicher.
Starte ein Programm
Um ein neues Programmabbild zu starten, richtet das Betriebssystem diese Teile ein:
- Den ausführbaren Code und die Daten, wobei auf modernen Systemen Teile bei Bedarf geladen werden.
- Einen anfänglichen Benutzerstack und Startinformationen, darunter die Argumentzeichenketten und den Argumentvektor, mit denen die Laufzeitumgebung
mainaufruft. - Speicher für dynamische Zuweisungen und den anfänglichen Registerzustand.
- Eine Ein-/Ausgabeumgebung einschließlich geerbter Dateideskriptoren.
- Einen Einstiegspunkt, an dem der Programmstartcode beginnt.
Unter Unix sind Dateideskriptoren kleine ganze Zahlen, die offene Ein-/Ausgaberessourcen bezeichnen. Nach Shell-Konvention steht 0 für die Standardeingabe, 1 für die Standardausgabe und 2 für die Standardfehlerausgabe. Sie sind oft mit einem Terminal verbunden. Ein Prozess kann weitere Deskriptoren erben; die drei vertrauten Nummern sind weder eine Obergrenze noch eine Zusage, dass jeder Prozess sie immer geöffnet hat.
Untersuche den Datensatz des Kernels
Der Prozesskontrollblock (PCB) speichert Informationen, die der Kernel zur Verwaltung eines Prozesses braucht: PID, Zustand, gesicherten Ausführungskontext, Speicherinformationen, offene Deskriptoren, Elternprozess, Eigentümer, aktuelles Verzeichnis und Abrechnungsinformationen. Linux verwendet task_struct; xv6 verwendet struct proc. Eine Prozesstabelle oder Prozessliste organisiert diese Datensätze.
Während ein Prozess läuft, befinden sich seine aktuellen Register in der CPU. Beim Eintritt in den Kernel werden seine Benutzerregister auf seinem Kernelstack gesichert; ein Kontextwechsel sichert außerdem den Kernelausführungskontext. Der PCB ist kein fortlaufend aktualisiertes Spiegelbild jedes Benutzerregisters.
Eine Linux-Ausgabe von ps könnte zum Beispiel 8240 8200 S bash und 8291 8240 R ps enthalten. Der zweite Prozess hat die PID 8291 und die Eltern-PID 8240, ist also ein Kindprozess der Shell. R umfasst sowohl laufende als auch bereite Prozesse. Eine PID bezeichnet einen existierenden Prozess innerhalb seines PID-Namensraums, aber nach der Bereinigung kann diese Nummer erneut vergeben werden.
Wichtige Erkenntnisse
- Ein Prozess ist eine Programminstanz mit Speicher-, Register- und Ein-/Ausgabezustand.
- Gleiche virtuelle Adressen in unterschiedlichen Prozessen können unterschiedliche Daten bezeichnen.
- Der PCB enthält den Verwaltungsdatensatz des Kernels und nicht die fortlaufend aktuellen CPU-Register.
- Dateideskriptoren werden geerbt; 0, 1 und 2 haben konventionelle Rollen.
- PIDs können erneut vergeben werden, nachdem ein Prozess beendet und bereinigt wurde.
2.2 Verfolge die Zustandswechsel eines Prozesses
Ein Prozess kann existieren, ohne gerade einen Befehl auszuführen. Sein Zustand zeigt dem Betriebssystem, ob er eine CPU nutzen kann oder erst auf etwas anderes warten muss. Dieser Unterschied erklärt, warum ein ausgelasteter Computer viele Prozesse haben kann, aber nur wenige in einem Augenblick ausgeführt werden.
Bestimme, was der Prozess braucht
| Zustand | Bedeutung | Beispiel |
|---|---|---|
| Laufend | Wird auf einer CPU ausgeführt | Ein Compiler erzeugt Befehle |
| Bereit | Kann ausgeführt werden und wartet auf CPU-Zeit | Eine Berechnung, deren Abschnitt gerade endete |
| Blockiert | Wartet auf ein Ereignis, bevor er fortfahren kann | Ein Lesevorgang am Terminal wartet auf Eingabe |
Ein blockierter Prozess verbraucht keine CPU-Zeit, solange er blockiert ist. Sein Gerät kann trotzdem weiterarbeiten oder das Ereignis eintreten. Ein bereiter Prozess hat bereits alles, was er braucht, außer einer CPU.
Die grundlegenden Übergänge sind bereit → laufend bei Auswahl, laufend → bereit, wenn der Prozess von der CPU genommen wird, laufend → blockiert, wenn er warten muss, und blockiert → bereit, wenn sein Ereignis eintritt. Der Abschluss eines Ereignisses macht einen Prozess ausführungsbereit; er gibt ihm nicht sofort die CPU. Zwischen ausführungsbereiten Prozessen auszuwählen, ist die Aufgabe des Schedulers.
Verfolge überlappende Arbeit
Nutze eine logische CPU und vernachlässige den Kontextwechselaufwand. Prozess A rechnet 2 ms, wartet 3 ms auf Festplatten-Ein-/Ausgabe und rechnet dann 1 ms. Prozess B braucht 4 ms CPU-Zeit. A beginnt zuerst.
| Intervall | CPU | Zustand von A | Zustand von B |
|---|---|---|---|
| 0–2 ms | A | Laufend | Bereit |
| 2–5 ms | B | Blockiert | Laufend |
| 5–6 ms | B | Bereit | Laufend |
| 6–7 ms | A | Laufend | Beendet |
Bei 2 ms startet A die Ein-/Ausgabe und blockiert. B rechnet, während die Festplatte arbeitet. Bei 5 ms endet die Ein-/Ausgabe von A, aber das Betriebssystem lässt B seine vierte CPU-Millisekunde beenden, bevor es A ausführt. Das ist eine Strategieentscheidung und kein zusätzlicher Zustandsübergang direkt von blockiert zu laufend.
Die gesamte CPU-Arbeit beträgt 2 + 1 + 4 = 7 ms, und beide sind nach 7 ms fertig. Liefe A bis zum Ende, bevor B beginnt, betrüge die verstrichene Zeit 2 + 3 + 1 + 4 = 10 ms. Die CPU-Auslastung läge bei 7 / 10 = 70 %, weil keine andere Arbeit die Ein-/Ausgabewartezeit ausfüllen würde.
Lies Zustände auf einem realen System
Linux ps fasst laufend und bereit als R zusammen. Es zeigt unterbrechbaren Schlaf als S, nicht unterbrechbaren Schlaf, oft wegen Ein-/Ausgabe, als D, einen angehaltenen Prozess als T und einen beendeten Prozess, dessen Status noch nicht abgerufen wurde, als Z. Ein angehaltener Prozess wartet nicht einfach auf den Abschluss einer gewöhnlichen Ein-/Ausgabe. Ein Zombieprozess hat seine Ausführung beendet und behält nur den Datensatz, der zum Melden seines Ergebnisses nötig ist.
Ein neuer Prozess durchläuft außerdem seine Erzeugung, bevor er bereit ist. Auf einem System mit vier logischen CPUs können höchstens vier Prozesse mit je einem Thread gleichzeitig ausgeführt werden. Bereitschaftswarteschlangen ordnen ausführungsbereite Arbeit; Listen blockierter Prozesse erfassen die Ereignisse, die Prozesse benötigen. Ein Kontextwechsel sichert den Ausführungszustand eines Prozesses und stellt den eines anderen wieder her, wenn das Betriebssystem wechselt, welcher Prozess läuft.
Wichtige Erkenntnisse
- Bereit bedeutet, auf eine CPU zu warten; blockiert bedeutet, auf ein Ereignis zu warten.
- Der Abschluss einer Ein-/Ausgabe versetzt einen blockierten Prozess in den Zustand „bereit“.
- Die Überlappung von Gerätewartezeiten mit nützlicher CPU-Arbeit kann die insgesamt verstrichene Zeit verkürzen.
- Linux meldet „bereit“ und „laufend“ mit demselben Kürzel
R. - Ein Zombieprozess ist beendet, aber sein Status wurde noch nicht abgerufen.
- Die Anzahl logischer CPUs begrenzt, wie viele Prozesse mit je einem Thread gleichzeitig ausgeführt werden können.
3Erzeuge und steuere Prozesse mit fork, exec und wait
Die Unix-Prozessschnittstelle trennt das Erzeugen eines Kindprozesses, das Ersetzen seines Programms und das Abrufen seines Ergebnisses. Verfolge, was jeder Aufruf ändert und was er beibehält, darunter private Variablen und den gemeinsam genutzten Zustand offener Dateien. Setze die Aufrufe dann zum Verhalten einer Shell zusammen: Vordergrundbefehle, Umleitungen, Pipelines und Hintergrundaufträge. Jeder Schritt hängt davon ab, welcher Prozess ihn ausführt.
3.1 Erzeuge mit fork einen Kindprozess
fork() erzeugt aus dem aufrufenden Elternprozess einen Kindprozess. Beide setzen ihre Ausführung unmittelbar nach dem Aufruf fort, sodass aus einem Quellcodepfad zwei Ausführungen werden. Der Rückgabewert zeigt jedem Prozess, welche Rolle er hat.
Verzweige anhand aller drei Ergebnisse
pid_t rc = fork();
if (rc < 0) {
perror("fork");
exit(1);
} else if (rc == 0) {
printf("child %ld\n", (long) getpid());
} else {
printf("parent of %ld\n", (long) rc);
}
Verwende <unistd.h> für fork und getpid, <stdio.h> für Ausgaben und <stdlib.h> für exit. Bei Erfolg erhält der Elternprozess die positive PID des Kindprozesses; der Kindprozess erhält 0. Bei einem Fehler erhält der Aufrufer -1, errno gibt den Fehler an, und es existiert kein Kindprozess.
Hat der Elternprozess die PID 5000 und der Kindprozess die PID 5001, gibt der Elternprozess „parent of 5001“ und der Kindprozess „child 5001“ aus. Es ist nicht garantiert, welcher Prozess zuerst läuft. getpid() gibt die eigene PID des Aufrufers zurück; getppid() gibt die PID seines Elternprozesses zurück. Der Kindprozess startet main nicht neu und wiederholt keine Befehle vor dem fork.
Verfolge privaten Speicher und gemeinsame Ein-/Ausgabe
Eltern- und Kindprozess beginnen mit übereinstimmenden gewöhnlichen Speicherinhalten in getrennten Adressräumen. Ist x vor dem Aufruf 10 und setzt der Kindprozess seine Kopie auf 20, bleibt die Kopie des Elternprozesses 10. Selbst die ausgegebene Adresse von x kann übereinstimmen: Diese gleichen virtuellen Adressen gehören zu unterschiedlichen Prozessadressräumen.
Copy-on-Write vermeidet das sofortige Kopieren jeder Speicherseite. Eltern- und Kindprozess nutzen zunächst schreibgeschützte physische Seiten gemeinsam. Schreibt einer auf eine private Seite, stellt der Kernel eine private Kopie bereit, bevor er den Schreibvorgang erlaubt. So bleibt das Verhalten getrennter Speicher erhalten, während der Aufwand für einen Kindprozess sinkt, der bald exec aufruft.
Dateideskriptoren verhalten sich anders. Ihre kopierten Einträge verweisen auf dieselbe zugrunde liegende offene Dateibeschreibung, die den aktuellen Dateioffset enthält. Aufeinanderfolgende Lesevorgänge über einen geerbten Deskriptor setzen diesen gemeinsamen Offset weiter. Die Deskriptortabelle wird kopiert, aber der zugrunde liegende Zustand der offenen Datei im Kernel kann gemeinsam genutzt bleiben.
Zähle nur fork-Aufrufe, die ein Prozess erreicht
Sind alle Aufrufe erfolgreich, gibt es nach fork(); fork(); vier Prozesse: Beide durch den ersten Aufruf entstandenen Prozesse führen den zweiten aus. Eine Schleife mit drei Durchläufen, die alle Prozesse erreichen, hinterlässt Prozesse, einschließlich des ursprünglichen. Ein fork in einem Zweig, den nur der Elternprozess ausführt, wird von weniger Prozessen erreicht. Wiederholtes Verdoppeln beschreibt dann nicht mehr das ganze Programm.
Gib gepufferten Text bewusst aus
printf kann Text in einem Puffer im Benutzerraum zurücklassen. fork kopiert diesen Puffer mit dem übrigen Speicher. Wird die Ausgabe auf einen vollständig gepufferten Datenstrom umgeleitet, kann Text, der vor dem fork ausgegeben wurde, beim normalen Beenden von beiden Prozessen ausgegeben werden.
Bei printf("start\n"); fork(); gibt das üblicherweise zeilengepufferte Terminal den Zeilenumbruch vor dem fork aus. Bei der Umleitung in eine Datei kann der Text im Puffer bleiben, sodass die Datei zwei Kopien erhält. fflush(stdout) vor fork() gibt den ausstehenden Text einmal aus. Die doppelte Ausgabe entsteht durch kopierte gepufferte Daten, nicht dadurch, dass das frühere printf zweimal ausgeführt wird.
Wichtige Erkenntnisse
fork()gibt im Elternprozess die PID des Kindprozesses, im Kindprozess 0 und bei einem Fehler -1 zurück.- Beide erfolgreichen Ausführungspfade laufen direkt nach
fork()weiter. - Änderungen am gewöhnlichen privaten Speicher des Kindprozesses ändern die Werte des Elternprozesses nicht.
- Geerbte Deskriptoren können dieselbe offene Dateibeschreibung und denselben Offset gemeinsam nutzen.
- Die Anzahl der Prozesse hängt davon ab, welche Prozesse den jeweiligen fork-Aufruf erreichen.
- Gib ausstehende Ausgaben vor einem fork aus, wenn eine doppelte Ausgabe unerwünscht wäre.
3.2 Führe mit exec ein anderes Programm aus
Ein Prozess kann seine Identität behalten und zugleich das Programm wechseln, das er ausführt. Die exec-Familie nimmt diesen Austausch vor. In Verbindung mit fork kann ein Elternprozess seine Arbeit fortsetzen, während im Kindprozess ein anderes Programm gestartet wird.
Ersetze das Abbild und behalte den Prozess
Ein erfolgreiches exec ersetzt den alten Code, die statischen Daten, den Heap und den Stack. Der Kernel bereitet die Argumente und den Anfangszustand des neuen Programms vor und beginnt dann dessen Startcode. Die alte Aufrufstelle existiert nicht mehr, sodass ein erfolgreiches exec nie dorthin zurückkehrt.
PID, Elternbeziehung und aktuelles Verzeichnis bleiben erhalten. Offene Deskriptoren bleiben erhalten, ausgenommen die mit Close-on-Exec markierten. Für abgefangene Signale gilt wieder die Standardaktion; ignorierte Signale bleiben im Allgemeinen ignoriert. Die Berechtigungsdaten bleiben gewöhnlich gleich, aber die Set-User-ID- oder Set-Group-ID-Berechtigung einer ausführbaren Datei kann die effektiven Berechtigungsdaten ändern, wenn das System es erlaubt.
| Operation | Neuer Prozess? | Hauptwirkung |
|---|---|---|
fork() | Ja | Erzeugt einen Kindprozess mit kopiertem Ausführungszustand |
| Erfolgreiches exec | Nein | Ersetzt das Programmabbild des aufrufenden Prozesses |
Erstelle einen gültigen Argumentvektor
Um wc -l notes.txt auszuführen, kann der Kindprozess Folgendes verwenden:
char *args[] = {"wc", "-l", "notes.txt", NULL};
execvp(args[0], args);
perror("execvp");
_exit(127);
Die Zeichenketten sind getrennte Argumente und keine zusammengefasste Befehlszeile. Üblicherweise bezeichnet args[0] das Programm. Das abschließende NULL ist ein Zeiger, der das Ende des Arrays markiert; es ist nicht die Zeichenkette „NULL“. Ohne einen gültigen Abschluss kann der Aufruf über das Array hinauslesen.
Die Zeilen nach execvp behandeln einen Fehler. Kann das Programm nicht gefunden werden, gibt exec -1 zurück und setzt errno, zum Beispiel auf ENOENT. _exit beendet dann den Kindprozess, ohne geerbte C-Ausgabepuffer auszugeben. Würde nach einem Fehler in die Shell-Schleife zurückgekehrt, entstünde versehentlich ein weiterer Shell-Ausführungspfad.
Lies den Variantennamen
Das v in execv bedeutet, dass Argumente als Vektor ankommen; l-Varianten erhalten eine Liste. Eine p-Variante sucht einen Programmnamen in PATH. Eine e-Variante übergibt eine ausdrücklich angegebene Umgebung. Der zugrunde liegende Linux-Systemaufruf ist execve; die Bibliothek stellt die bequem nutzbaren Varianten bereit.
execvp("wc", args) durchsucht PATH. execv("wc", args) behandelt „wc“ als relativen Pfadnamen im aktuellen Verzeichnis. Ein absoluter Pfadname wie /usr/bin/wc kann dort verwendet werden, wo diese ausführbare Datei installiert ist. Exec wertet keine Shell-Syntax aus: Eine Zeichenkette > in args ist nur ein Argument, sofern ein Programm sie nicht selbst interpretiert.
Nutze die Zeit vor dem Austausch
Im Beispiel mit Eltern-PID 6000 und Kind-PID 6001 führt der Kindprozess nun wc aus, behält aber die PID 6001. Sein Elternprozess kann auf diese PID warten. Vor exec kann der Kindprozess Deskriptoren umleiten, eine Pipe verbinden oder das Verzeichnis wechseln; das neue Programm erbt die vorbereitete Umgebung.
Diese Trennung ist zentral für die Unix-Shell. Das Windows-Verfahren CreateProcess verbindet Erzeugung und Programmladen und konfiguriert den neuen Prozess über Parameter. POSIX bietet für eine kombinierte Operation außerdem posix_spawn.
Wichtige Erkenntnisse
- Exec ersetzt ein Programmabbild, ohne einen Prozess zu erzeugen oder seine PID zu ändern.
- Bei Erfolg kehrt exec nie zurück; nachfolgende Anweisungen behandeln Fehler.
- Schließe Argumentvektoren mit einem
NULL-Zeiger ab. p-Varianten durchsuchenPATH; eine Variante, die nur einen Pfadnamen nutzt, tut das nicht.- Durch das Einrichten von Deskriptoren vor exec kann ein unverändertes Programm an Umleitungen und Pipes teilnehmen.
3.3 Warte auf Kindprozesse, lies ihren Beendigungsstatus aus und beende Prozesse
Ein Elternprozess muss erfahren, ob und wie ein Kindprozess beendet wurde. Prozessende und Ergebnisabruf sind getrennte Schritte: Der Kindprozess endet, anschließend ruft sein Elternprozess den verbleibenden Datensatz des Kernels ab.
Beende einen Prozess
Die Rückkehr aus main oder der Aufruf von exit(status) beendet die Ausführung normal. exit gibt C-Ausgabepuffer aus und führt registrierte Behandlungsroutinen für die Beendigung aus. _exit(status) beendet den Prozess ohne diese Bereinigung durch die Bibliothek, was nach einem fehlgeschlagenen exec in einem mit fork erzeugten Kindprozess nützlich ist. Üblicherweise bedeutet 0 Erfolg und ein Wert ungleich null einen Fehler.
Auch ein Signal kann einen Prozess beenden. SIGTERM fordert die Beendigung an und kann behandelt werden; SIGKILL kann weder abgefangen noch ignoriert werden. SIGSTOP hält die Ausführung an und kann ebenfalls weder abgefangen noch ignoriert werden; SIGCONT setzt einen angehaltenen Prozess fort. Ein Terminal sendet normalerweise SIGINT bei Strg-C und SIGTSTP bei Strg-Z an den Vordergrundauftrag. Ein Signal ist eine Softwarebenachrichtigung an einen Prozess, kein Hardwareinterrupt.
Warte auf ein Ergebnis
wait(&status) wartet auf einen Kindprozess und gibt dessen PID zurück. waitpid(pid, &status, 0) wählt eine bestimmte positive Kind-PID aus. Ein beendeter Kindprozess, dessen Status noch nicht abgerufen wurde, kann sofort abgerufen werden. Andernfalls hält ein gewöhnliches blockierendes Warten den Elternprozess an, bis ein passendes Ergebnis verfügbar ist.
Mit WNOHANG gibt waitpid 0 zurück, wenn passende Kindprozesse existieren, aber keiner ein Ergebnis bereit hat. Ein positives Ergebnis bezeichnet den Kindprozess, dessen Status abgerufen wurde. Ein Ergebnis von -1 ist ein Fehler: ECHILD bedeutet, dass kein passender Kindprozess mehr vorhanden ist; EINTR bedeutet, dass eine Signalbehandlungsroutine das Warten unterbrochen hat.
Dekodiere den Status vor der Verwendung
int status;
pid_t r;
do {
r = waitpid(child, &status, 0);
} while (r == -1 && errno == EINTR);
if (r == -1) {
perror("waitpid");
} else if (WIFEXITED(status)) {
printf("exit %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("signal %d\n", WTERMSIG(status));
}
Dies verwendet <sys/wait.h>, <errno.h> und <stdio.h>. Der gespeicherte Status kodiert sowohl die Art der Beendigung als auch ein Ergebnis. WEXITSTATUS ist aussagekräftig, nachdem WIFEXITED wahr ist; WTERMSIG folgt auf WIFSIGNALED.
Nur die unteren acht Bits eines normalen Beendigungswerts sind verfügbar. exit(300) ergibt daher 44, während exit(-1) 255 ergibt. Unter Linux erzeugt exit(3) gewöhnlich den rohen Wartestatus 768; die Ausgabe dieser rohen ganzen Zahl dekodiert den Beendigungscode nicht.
Rufe den Status von Zombieprozessen ab und erkenne verwaiste Prozesse
Ein Zombieprozess ist beendet, behält aber seine PID und sein Ergebnis, bis ein Warteaufruf sie abruft. Er führt keinen Code aus, sodass das Senden von SIGKILL ihn nicht entfernen kann. Ein langlebiger Elternprozess muss den Status jedes Kindprozesses abrufen. Um alle Kindprozesse abzurufen, warte nach jeder positiven PID weiter, versuche es bei EINTR erneut und höre bei ECHILD auf; melde andere Fehler.
Ein verwaister Prozess hat seinen Elternprozess verloren und kann weiterlaufen. Linux gibt ihm einen geeigneten Subreaper oder den Init-Prozess des Namensraums als neuen Elternprozess, der sein Ergebnis später abrufen kann. Gewöhnliche Signalberechtigungen hängen von Benutzeridentitäten und Privilegien ab. Die Ausführung als root gewährt Berechtigungen, bedeutet aber keine Ausführung im Kernelmodus.
Wichtige Erkenntnisse
- Jeder erfolgreiche Warteaufruf ruft das Ergebnis eines Kindprozesses ab.
- Dekodiere normale Beendigung und Beendigung durch ein Signal mit den jeweils passenden Statusmakros.
WNOHANGgibt 0 zurück, wenn ein passender Kindprozess existiert, aber noch kein Ergebnis bereithält.- Wiederhole ein bei
EINTRunterbrochenes Warten;ECHILDbedeutet, dass kein passender Kindprozess mehr vorhanden ist. - Ein Zombieprozess ist beendet; ein verwaister Prozess hat seinen Elternprozess verloren und kann noch laufen.
SIGKILLundSIGSTOPkönnen weder abgefangen noch ignoriert werden.exitgibt C-Puffer aus;_exittut das nicht.
3.4 Entwickle eine einfache Shell mit fork, exec, wait und Umleitungen
Eine Shell setzt Befehlstext in Prozesserzeugung, Deskriptoreinrichtung und Ergebnisabruf um. Sie ist ein gewöhnliches Benutzerprogramm und nutzt dieselben geschützten Betriebssystemdienste, die anderen Programmen zur Verfügung stehen.
Starte einen externen Befehl
Für wc -l notes.txt erstellt die Shell den Argumentvektor und startet den Befehl. Die zerlegten Wörter bilden einen mit einem Nullzeiger abgeschlossenen Argumentvektor args:
pid_t child = fork();
if (child == -1) {
perror("fork");
} else if (child == 0) {
execvp(args[0], args);
perror("execvp");
_exit(127);
} else {
int status;
pid_t result;
do {
result = waitpid(child, &status, 0);
} while (result == -1 && errno == EINTR);
if (result == -1) perror("waitpid");
else if (WIFEXITED(status)) {
printf("exit %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("signal %d\n", WTERMSIG(status));
}
}
Dies verwendet <unistd.h>, <sys/wait.h>, <errno.h> und <stdio.h>. Die umgebende Shell liest und zerlegt jeden Befehl, führt diesen Startpfad aus und zeigt dann ihre nächste Eingabeaufforderung an.
Nur der Kindprozess ersetzt sein Programm; die Shell kann einen Befehlsabsturz als Beendigungsergebnis abrufen. Eingebaute Befehle wie cd müssen den eigenen Zustand der Shell ändern: Eine Änderung des Verzeichnisses eines Kindprozesses ließe die Eltern-Shell in ihrem bisherigen Verzeichnis.
Leite vor exec einen Deskriptor um
Für ls -l > out.txt öffnet der Kindprozess das Ziel zum Schreiben und erzeugt oder leert es dabei. Dann lässt er die Standardausgabe auf diese Datei verweisen:
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) { perror("open"); _exit(1); }
if (dup2(fd, STDOUT_FILENO) == -1) {
perror("dup2"); _exit(1);
}
if (fd != STDOUT_FILENO && close(fd) == -1) {
perror("close"); _exit(1);
}
open braucht <fcntl.h>; Deskriptoraufrufe und _exit verwenden <unistd.h>. Der Erstellungsmodus wird durch die Dateierstellungsmaske des Prozesses gefiltert. Die Prüfung fd != STDOUT_FILENO hält Deskriptor 1 offen, selbst wenn open selbst 1 zurückgegeben hat. Nach erfolgreicher Einrichtung behält exec Deskriptor 1 bei, und ls schreibt normal, ohne von der Umleitung zu wissen.
Ein älterer Ansatz schließt Deskriptor 1 vor dem Öffnen der Datei: open gibt den unbenutzten Deskriptor mit der niedrigsten Nummer zurück. dup2 gibt das Ziel ausdrücklich an. Die Eingabeumleitung verbindet entsprechend eine lesbare Datei mit Deskriptor 0.
Verbinde zwei Befehle
Für ls | wc -l erzeugt pipe(fds) ein Leseende fds[0] und ein Schreibende fds[1]. Der erzeugende Kindprozess verbindet das Schreibende mit Deskriptor 1. Der verbrauchende Kindprozess verbindet das Leseende mit Deskriptor 0. Nach dem Duplizieren schließt jeder die ursprünglichen Pipe-Deskriptoren, die er nicht benötigt. Der Elternprozess schließt seine Kopien und wartet auf beide Kindprozesse.
Erzeuge beide Kindprozesse, bevor du auf einen von ihnen wartest. Ein Erzeuger kann die Pipe füllen und blockieren; der Leser muss existieren, um sie zu leeren. Der Leser erreicht das Dateiende erst, wenn alle Schreibdeskriptoren geschlossen und gepufferte Pipe-Daten verbraucht sind. Ein vom Elternprozess offengehaltenes ungenutztes Schreibende kann wc unbegrenzt warten lassen.
Halte die Eingabeaufforderung verfügbar
Bei sleep 60 & startet die Shell den Kindprozess und zeigt die nächste Eingabeaufforderung, ohne sofort zu warten. Später nutzt sie nicht blockierende Warteaufrufe oder reagiert auf SIGCHLD, um den Status beendeter Kindprozesse abzurufen.
Der Beendigungsstatus steuert auch Befehlskombinationen. make && ./app startet den zweiten Befehl nur nach Erfolg; make || echo failed startet den zweiten nur nach einem Fehler. Diese Entscheidungen nutzen das dekodierte Ergebnis, nicht den rohen kodierten Wartestatus.
Wichtige Erkenntnisse
- Ein externer Befehl läuft in einem Kindprozess; ein eingebauter Befehl, der den Zustand ändert, läuft in der Shell.
- Eine Umleitung ändert Deskriptoren im Kindprozess vor exec.
- Eine Pipe verbindet die Standardausgabe eines Erzeugers mit der Standardeingabe eines Verbrauchers.
- Starte beide Seiten, bevor du wartest, und schließe ungenutzte Pipe-Enden.
- Das Dateiende setzt voraus, dass nach dem Verbrauch gepufferter Daten jedes Pipe-Schreibende geschlossen ist.
- Die Ausführung im Hintergrund verschiebt das Warten; die Shell muss den Status des Kindprozesses später dennoch abrufen.
4Erkläre Benutzermodus, Kernelmodus und Systemaufrufe
Benutzerprogramme brauchen Betriebssystemdienste, ohne die Kontrolle über den Rechner zu erhalten. Privilegienmodi der Hardware schaffen diese Grenze, und kontrollierte Eintrittspfade ermöglichen dem Kernel, auf Anfragen und Ereignisse zu reagieren. Verfolge einen Systemaufruf durch seinen Bibliothekswrapper und seine Kernelbehandlungsroutine und unterscheide dann absichtliche Traps, Befehlsausnahmen und Geräteinterrupts anhand ihrer Ursachen und möglichen Folgen.
4.1 Unterscheide Benutzermodus und Kernelmodus
Der Kernel muss Programme schnell rechnen lassen und zugleich Operationen verhindern, die den Rechner übernehmen könnten. Der Privilegienmodus der CPU ist die Hardwaregrenze, die beides ermöglicht.
Trenne Berechnung von privilegierter Steuerung
Im Benutzermodus können Befehle auf erlaubten Prozessspeicher zugreifen und gewöhnliche Berechnungen ausführen. Sie können die Hardwarekonfiguration, die andere Programme schützt, nicht beliebig ändern. Im Kernelmodus, auch Supervisormodus oder privilegierter Modus genannt, kann das Betriebssystem die geschützten Operationen zur Verwaltung des Rechners ausführen.
Zu den privilegierten Operationen gehören das Programmieren des Timers, das Deaktivieren von Interrupts, das Einrichten von Interruptbehandlungsroutinen, das Ändern von Steuerregistern der Speicherübersetzung, die direkte Steuerung geschützter Geräte und das Anhalten der CPU. Könnte jede Anwendung den Timer deaktivieren, wären kurze CPU-Abschnitte von der Kooperation dieser Anwendung abhängig.
| Plattform | Anwendungsmodus | Kernelmodus |
|---|---|---|
| x86-64 unter Linux oder Windows | Ring 3 | Ring 0 |
| AArch64 | EL0 | EL1 |
AArch64 bietet außerdem EL2 für Hypervisoren und EL3 für sichere Firmware. Diese Namen bezeichnen Privilegienstufen der Hardware, keine unterschiedlichen Arten von Benutzerkonten.
Tritt an einer kontrollierten Adresse ein
Benutzercode kann seine eigenen Privilegien nicht erhöhen und keine beliebige Kerneladresse zur Ausführung wählen. Der Eintritt erfolgt durch einen Systemaufruf, eine durch einen Befehl verursachte Ausnahme oder einen Hardwareinterrupt. Die CPU überträgt die Kontrolle an einen vom Betriebssystem konfigurierten Einstiegspunkt. Der Rückkehrcode des Kernels stellt den gesicherten Benutzerzustand wieder her und senkt die Privilegienstufe, um den Anwendungscode fortzusetzen.
Der Kernel schützt seine Daten auch durch Speicherberechtigungen. Ein Prozess kann Kerneladressen in seinem virtuellen Adressraum haben, ohne im Benutzermodus die Berechtigung zum Zugriff darauf zu besitzen. Die Kenntnis einer Adresse gewährt keinen Zugriff auf die Bytes dort.
Betrachte eine x86-64-Linux-Anwendung, die hlt ausführt, einen Befehl, der eine CPU bis zu einem Ereignis anhalten soll. Im Benutzermodus verursacht der Befehl eine Ausnahme. Linux sendet normalerweise SIGSEGV, dessen Standardaktion diesen Prozess beendet. Die Anwendung hält nicht den ganzen Computer an; die Schutzgrenze macht aus der versuchten privilegierten Operation ein Ereignis, das der Kernel behandelt.
Trenne Identität und Modus
Ein Root- oder Administratorprozess ist weiterhin ein Programm im Benutzermodus, während seine eigenen Befehle laufen. Seine Benutzeridentität gibt ihm umfassendere Berechtigungen, Dienste anzufordern. Er tritt weiterhin über kontrollierte Pfade in den Kernel ein, und der Kernel prüft die Anfrage.
Ein privilegiertes Wartungsprogramm darf zum Beispiel möglicherweise eine geschützte Datei öffnen. Das Programm fragt über einen Systemaufruf an; es wird nicht allein deshalb zu Kernelcode, weil die Anfrage erlaubt wird. Umgekehrt wird Kernelcode, der ein read eines gewöhnlichen Benutzers bedient, im Kernelmodus ausgeführt, obwohl der Aufrufer keine Administratoridentität hat.
Auf einer logischen CPU ist dies eine Abfolge: Benutzerbefehle, Kerneleintritt, Kernelarbeit, Rückkehr zu Benutzerbefehlen. Ein Programm und eine zweite Betriebssystemaufgabe werden nicht gleichzeitig auf derselben CPU ausgeführt. Getrennte logische CPUs können im selben Augenblick unterschiedliche Modi ausführen.
Wichtige Erkenntnisse
- Der Benutzermodus erlaubt gewöhnliche Berechnungen und schränkt zugleich Operationen zur Steuerung des Rechners ein.
- Der Kerneleintritt nutzt vom Betriebssystem konfigurierte Einstiegspunkte.
- Root ist eine Benutzeridentität; der Kernelmodus ist ein Privilegienzustand der CPU.
- Ein verbotener Benutzerbefehl löst eine Ausnahme aus, sodass der Kernel reagieren kann.
4.2 Erkläre Schritt für Schritt den Ablauf eines Systemaufrufs
Ein C-Programm scheint häufig eine gewöhnliche Funktion aufzurufen, wenn es einen Betriebssystemdienst braucht. Unterhalb eines Systemaufruf-Wrappers ermöglicht ein kontrollierter CPU-Übergang dem Kernelcode, diese Anfrage zu prüfen und auszuführen.
Unterscheide einen Bibliotheksaufruf von einer Kernelanfrage
Die C-Bibliothek implementiert strlen ohne Systemaufruf im Benutzerraum. printf formatiert Daten und kann sie puffern; ein späteres write überträgt Bytes über den Kernel. Ein schlanker read-Wrapper ordnet Argumente nach der Systemaufrufkonvention der Plattform an und führt den Eintrittsbefehl aus.
POSIX beschreibt Schnittstellen für Unix-ähnliche Systeme. Linux implementiert viele Dienste, die grob in Prozesssteuerung, Datei- und Geräteoperationen, Information, Kommunikation und Schutz gegliedert werden. Eine Systemaufrufnummer wählt einen Dienst aus der Zuordnungstabelle des Kernels. Die Nummern sind plattformabhängig: Linux auf x86-64 verwendet 1 für write und 39 für getpid; AArch64 verwendet 64 für write.
Verfolge eine Schreibanfrage
Verfolge bei write(1, "hi\n", 3) unter Linux auf x86-64 die Anfrage der Reihe nach:
- Der Wrapper legt die Dienstnummer 1 in
rax, Deskriptor 1 inrdi, die Pufferadresse inrsiund die Anzahl 3 inrdxab. syscallsichert die Rücksprungadresse und die Flags in Registern, wechselt in den Kernelmodus und springt zum konfigurierten Einstiegspunkt. Dabei wird der Stack nicht automatisch gewechselt.- Der Kerneleinstiegscode richtet den Kernelstack des Prozesses ein und sichert den übrigen Benutzerzustand.
- Die Zuordnung im Kernel wählt
write; die Implementierung prüft den Deskriptor, den Zugriff auf den Benutzerpuffer und die Anfrage, bevor sie Bytes an die Ausgaberessource weiterleitet. - Bei einem vollständig erfolgreichen Schreibvorgang beträgt das Ergebnis 3 Bytes. Der Rückkehrcode des Kernels stellt den Benutzerzustand wieder her und setzt den Wrapper fort, der zum Aufrufer zurückkehrt.
Deskriptor 1 kann ein Terminal, eine Datei oder eine Pipe bezeichnen. Die Nummer allein bestimmt das Ausgabegerät nicht. Ein Schreibvorgang kann auch erfolgreich eine kleinere Byteanzahl zurückgeben; Aufrufer, die jedes Byte übertragen müssen, verfolgen die tatsächlich geschriebene Menge.
Prüfe Fehler an der Schnittstelle
Der Kernel behandelt Benutzerzeiger und andere Argumente als nicht vertrauenswürdig. Eine Anfrage, aus einem nicht zugänglichen Puffer zu kopieren, kann mit EFAULT fehlschlagen; kontrolliertes Kopieren verhindert, dass der Aufrufer Zugriff auf Kernelspeicher erhält.
Unter Linux ist ein Kernelfehler ein negativer Fehlercode. Der übliche C-Wrapper meldet -1 und setzt errno. Schlägt zum Beispiel open("missing.txt", O_RDONLY) fehl, weil die Datei nicht existiert, ist errno gleich ENOENT. perror("missing.txt") kann „missing.txt: No such file or directory“ anzeigen. Prüfe errno nach einem gemeldeten Fehler, nicht als primären Erfolgstest: Ein erfolgreicher Aufruf muss einen früheren Fehler nicht zurücksetzen.
Berücksichtige die Zeit im Kernel
Ein Aufruf, der sofort abgeschlossen werden kann, kehrt möglicherweise zum selben Prozess zurück. Ein blockierender Lesevorgang am Terminal kann diesen Prozess dagegen in den Zustand „blockiert“ versetzen und einen anderen laufen lassen. Das Eingabeereignis macht den Leser bereit; sobald er eingeplant wird, schließt er den Rückweg ab.
Der Kerneleintritt verursacht gegenüber einem normalen Funktionsaufruf zusätzliche Arbeit: Zustand sichern, Privilegien ändern, Argumente prüfen und die Ausführung wiederherstellen. Pufferung durch die Bibliothek kann kleine Ausgaben zu weniger Schreibvorgängen zusammenfassen. Unter Linux führt strace ls den Befehl ls aus und meldet dabei dessen Systemaufrufe, Argumente und Ergebnisse.
Wichtige Erkenntnisse
- Eine Bibliotheksfunktion kann ihre gesamte Arbeit im Benutzerraum erledigen oder Systemaufrufe ausführen.
- Eine Dienstnummer wählt eine Kerneloperation; der Aufrufer wählt keine beliebige Kerneladresse.
- Kernelcode prüft Deskriptoren, Zeiger und andere Argumente.
- Erkenne einen Fehler am Rückgabewert und prüfe dann
errno. - Ein blockierender Systemaufruf kann einen Kontextwechsel verursachen; der Kerneleintritt allein muss das nicht.
- Systemaufrufnummern und Registerkonventionen hängen vom Betriebssystem und von der Architektur ab.
4.3 Unterscheide Traps, Ausnahmen und Interrupts
Ein Kerneleintritt kann eine Dienstanfrage, ein Befehlsproblem oder ein Geräteereignis bedeuten. Wenn du die Ursache einordnest, kannst du vorhersagen, was das Betriebssystem tun wird und ob derselbe Befehl erneut ausgeführt werden soll.
Frage nach der Ursache des Ereignisses
| Ereignis | Ursache | Zeitpunkt in Bezug auf den aktuellen Befehl |
|---|---|---|
| Systemaufruf-Trap | Das Programm fordert absichtlich einen Dienst an | Synchron |
| Ausnahme oder Fehler | Der aktuelle Befehl braucht eine besondere Behandlung | Synchron |
| Hardwareinterrupt | Ein Timer oder Gerät signalisiert der CPU ein Ereignis | Asynchron |
Synchron bedeutet, dass das Ereignis an den ausgeführten Befehl gebunden ist. Es bedeutet nicht, dass dieselbe Eingabe bei jedem Durchlauf genau dieselben Ausnahmen verursacht. Ein Ladebefehl kann eine Ausnahme auslösen, wenn seine Seite fehlt, und später erfolgreich sein, wenn diese Seite im Speicher liegt. Asynchron bedeutet, dass die Ursache des Ereignisses vom aktuellen Befehl unabhängig ist, etwa wenn eine Festplatte eine frühere Arbeit abschließt.
CPU-Handbücher verwenden diese Begriffe unterschiedlich. Achte auf die Ursache: absichtliche Dienstanfrage, durch einen Befehl verursachtes Ereignis oder Hardwarebenachrichtigung.
Leite an die richtige Behandlungsroutine weiter
Beim Systemstart richtet das Betriebssystem geschützte Adressen für Behandlungsroutinen ein. Eine Trap- oder Interrupttabelle ordnet Ereignisarten Kernelroutinen zu. Auf x86-64 dient die Interruptdeskriptortabelle (IDT) für Interrupts und Ausnahmen; syscall nutzt eine Einstiegsadresse in einem speziellen Prozessorregister. Nur privilegierter Code kann diese Ziele konfigurieren.
Bei einem Interrupt aus dem Benutzermodus sichert die x86-64-Hardware einen Rückkehrrahmen auf einem Kernelstack. Der Einstiegscode sichert zusätzliche Register, bevor er die Arbeit erledigt. Der syscall-Pfad sichert seinen anfänglichen Rückkehrzustand in Registern und ist darauf angewiesen, dass der Einstiegscode den Kernelstack einrichtet. Alle Eintrittspfade als gleiche Hardwareaktionen zu behandeln, verdeckt einen wichtigen Implementierungsunterschied.
Wähle die Folge
Eine gültige Seite, die nicht im Speicher liegt, kann geladen werden. Danach wird der Befehl, der die Ausnahme auslöste, erneut versucht. Ein Zugriff auf Speicher, den der Prozess nicht nutzen darf, führt unter Linux normalerweise dazu, dass SIGSEGV gesendet wird. Eine Ausnahme ist daher nicht immer fatal und auch nicht immer eine Aufforderung, den aktuellen Befehl zu überspringen.
Bei einem tatsächlichen x86-64-Ganzzahlbefehl idiv mit Divisor null löst die CPU eine Division-Error-Ausnahme aus. Linux übermittelt normalerweise SIGFPE; die bekannte Meldung erwähnt Gleitkommazahlen, selbst bei diesem Ganzzahlbefehl. Die Ganzzahldivision von AArch64 liefert dagegen bei einem Divisor von null das Ergebnis null. Eine Division durch null in C hat undefiniertes Verhalten, sodass portabler C-Code sich auf keines der beiden Maschinenergebnisse verlassen kann.
Wenn ein Festplattenlesevorgang endet, vermerkt die Interruptbehandlungsroutine den Abschluss und macht den wartenden Prozess bereit. Dieser Prozess kann später laufen. Ein Timerinterrupt gibt dem Betriebssystem die Gelegenheit, die aktuelle CPU-Belegung zu überdenken, aber die Behandlungsroutine kann auch zu demselben Prozess zurückkehren.
Unterscheide ein Signal von einem Interrupt
Ein Hardwareinterrupt benachrichtigt die CPU; ein Signal benachrichtigt einen Prozess über das Betriebssystem. Eine Ausnahme kann den Kernel veranlassen, ein Signal zu senden, und ein Prozess kann über kill ein Signal für einen anderen anfordern. Normale Geräteabschlüsse sind Interrupts und bedeuten keine Programmfehler.
Kernelcode muss auch gemeinsam genutzte Daten schützen, während Ereignisse auftreten. Er kann Interrupts auf einer CPU kurz deaktivieren und bei Bedarf Sperren nutzen. Lange Zeiträume mit deaktivierten Interrupts verzögern die Gerätebehandlung; das Deaktivieren lokaler Interrupts allein hindert eine andere CPU nicht am Zugriff auf gemeinsame Daten.
Wichtige Erkenntnisse
- Traps und Ausnahmen entstehen durch den aktuellen Befehl; Hardwareinterrupts haben eine externe Ursache.
- Eine behebbare Ausnahme kann behoben und ihr Befehl erneut ausgeführt werden.
- Geschützte Eintrittstabellen verhindern, dass Benutzerprogramme Kernelbehandlungsroutinen ersetzen.
- Ein Signal richtet sich an einen Prozess; ein Hardwareinterrupt führt zur Behandlung durch die CPU.
- Ein Geräteabschluss macht einen blockierten Prozess bereit, und ein Timerinterrupt muss keinen Prozesswechsel auslösen.
5Erkläre, wie das Betriebssystem die CPU durch eingeschränkte direkte Ausführung aufteilt
Schnelle Ausführung und Kontrolle durch das Betriebssystem müssen zusammenwirken. Die eingeschränkte direkte Ausführung lässt gewöhnliche Befehle auf der Hardware laufen, während Modi, geschützte Einstiegspunkte und ein Timer dafür sorgen, dass der Kernel die Kontrolle behält. Verfolge, was geschieht, wenn das Betriebssystem zum selben Prozess zurückkehrt und wenn es einen anderen fortsetzt. Der gesicherte Zustand und der Wechselaufwand erklären, wie kurze CPU-Abschnitte interaktives Arbeiten unterstützen können.
5.1 Erkläre die eingeschränkte direkte Ausführung
Jeden Anwendungsbefehl in einem Softwaresimulator auszuführen, würde die gemeinsame CPU-Nutzung aufwendig machen. Einer Anwendung unbegrenzte Kontrolle über den Rechner zu geben, würde die gemeinsame Nutzung unsicher machen. Die eingeschränkte direkte Ausführung (LDE) nutzt gewöhnliche Hardwareausführung mit einem geschützten Rückweg zum Betriebssystem.
Löse zwei unterschiedliche Steuerungsprobleme
Direkte Ausführung bedeutet, dass die gewöhnlichen Befehle eines Prozesses auf der CPU selbst ausgeführt werden. Das Betriebssystem untersucht und genehmigt nicht jeden arithmetischen Befehl. Zwei Probleme bleiben: Operationen einzuschränken, die den Rechner beschädigen könnten, und die Kontrolle zurückzugewinnen, wenn ein Programm nicht freiwillig anhält.
Der Benutzermodus löst das Problem der Einschränkung. Systemaufruf-Traps erlauben einem Programm, Dienste anzufordern, ohne beliebigen privilegierten Code zu wählen. Ein Hardwaretimer löst das Steuerungsproblem: Er kann eine Anwendung unterbrechen, die überhaupt keine Aufrufe ausführt. Zusammen unterstützen diese Mechanismen die CPU-Virtualisierung mit geringem Verwaltungsaufwand.
Bereite alles vor, bevor Anwendungen laufen
Beim Systemstart konfiguriert der Kernel Ereignisbehandlungsroutinen und initialisiert den Timer. Diese Einrichtungsoperationen sind privilegiert. Spätere Benutzerprogramme können weder die Behandlungsroutinen umlenken noch den Timer deaktivieren, um ihre CPU-Abschnitte zu behalten.
Um einen Prozess zu starten, erzeugt das Betriebssystem seinen Verwaltungsdatensatz und sein Speicherabbild, bereitet Startargumente und einen Benutzerstack vor und legt anfängliche Registerwerte fest. Es richtet einen anfänglich gesicherten Kontext so ein, dass der normale Rückweg vom Kernel zum Benutzer das Programm starten kann. Vom Konzept her beginnt der Prozess mit der Rückkehr aus einem Trap, den er nie ausgelöst hat.
Der genaue Aufbau des gesicherten Rahmens hängt von CPU und Betriebssystem ab. Die nützliche Entwurfsidee ist die Wiederverwendung: Derselbe Wiederherstellungsmechanismus kann einen angehaltenen Prozess fortsetzen oder einen neu vorbereiteten starten.
Kehre freiwillig zum Kernel zurück
Bei einem Systemaufruf sichert die Hardware den anfänglichen Rückkehrzustand und überträgt die Kontrolle. Der Kerneleinstiegscode sichert die zusätzlichen Register auf dem Kernelstack des Prozesses. Nach Abschluss einer nicht blockierenden Anfrage stellt der Kernel den Prozess wieder her und kehrt in den Benutzermodus zurück. Blockiert die Anfrage, kann zunächst ein anderer bereiter Prozess laufen.
Wenn main zurückkehrt, fordert die Beendigungsverarbeitung der Laufzeitumgebung schließlich den Kernel auf, den Prozess zu beenden. Sein Speicher kann freigegeben werden, während sein Beendigungsdatensatz erhalten bleibt, bis ein Elternprozess ihn abruft.
Kehre ohne Kooperation zurück
Ein kooperatives System ist darauf angewiesen, dass Programme Aufrufe ausführen, die CPU freiwillig abgeben oder Ausnahmen verursachen. Eine Endlosschleife, die nichts davon tut, kann eine CPU für sich beanspruchen. Das klassische Mac OS veranschaulicht diese Schwäche.
Ein nicht kooperativer Entwurf nutzt einen Timerinterrupt, um den Kerneleintritt zu erzwingen. Betrachte ein Lehrmodell mit einer logischen CPU und einem periodischen Timer alle 4 ms. Der Timer erzeugt 1000 / 4 = 250 Taktimpulse pro Sekunde. Ein Benutzerprozess, der in while (1) { } feststeckt, wird beim nächsten Taktimpuls unterbrochen. Der Kernel kann dann einen anderen bereiten Prozess ausführen; die Schleife muss nicht erst einen Systemaufruf erreichen.
Der Taktimpuls ist eine Gelegenheit zu handeln, keine Pflicht zum Prozesswechsel. Der Scheduler kann den unterbrochenen Prozess weiterlaufen lassen. Timerbehandlung und Wiederherstellung des gesicherten Zustands sind Mechanismen; die Wahl des Schedulers ist eine Strategie. Die tatsächliche Antwortzeit hängt auch von der Kernelarbeit und davon ab, wann Interrupts behandelt werden können. Die Timerperiode allein garantiert daher keine bestimmte Antwortzeit einer Anwendung.
Wichtige Erkenntnisse
- LDE führt gewöhnliche Benutzerbefehle direkt mit durch Hardware durchgesetzten Einschränkungen aus.
- Der Benutzermodus schränkt Operationen ein; Systemaufrufe stellen kontrollierte privilegierte Dienste bereit.
- Ein Timer lässt das Betriebssystem ohne Kooperation des Benutzercodes die Kontrolle zurückgewinnen.
- Die Einrichtung beim Systemstart schützt die Adressen der Behandlungsroutinen und die Timerkonfiguration.
- Ein Timerimpuls gibt dem Scheduler eine Wahl; er verlangt keinen Prozesswechsel.
5.2 Verfolge einen Kontextwechsel und schätze seinen Aufwand ab
Der Eintritt in den Kernelmodus ändert nicht zwangsläufig den laufenden Prozess. Ein Kontextwechsel findet statt, wenn der Kernel den Ausführungszustand eines Prozesses sichert und einen anderen fortsetzt. Dazu muss sowohl der Benutzerzustand gesichert als auch der eigene Ausführungskontext des Kernels gewechselt werden.
Verfolge den Wechsel von A zu B
Verfolge bei einem timergesteuerten Wechsel auf x86-64 die Ebenen:
- A führt Benutzerbefehle aus. Der Timer unterbricht den Prozess.
- Die Hardware wechselt in den Kernelmodus und sichert den Interruptrückkehrrahmen von A auf dem Kernelstack von A. Der Einstiegscode sichert dort die übrigen Benutzerregister von A.
- Der Scheduler wählt B. Die Wechselroutine sichert den Kernelausführungskontext von A und stellt den gesicherten Kernelkontext von B wieder her, einschließlich dessen Kernelstapelzeigers.
- Der Kernel wechselt bei Bedarf zum Adressraumkontext von B. Er läuft nun im Auftrag von B und nutzt dessen Kernelstack.
- Der Kernelaustrittscode stellt die Benutzerregister von B wieder her. Der Rückkehrbefehl stellt den Rückkehrzustand von B wieder her und wechselt in den Benutzermodus. B läuft dort weiter, wo der Prozess zuvor angehalten wurde.
Die erste Sicherung bewahrt die unterbrochene Benutzerausführung von A. Die zweite bewahrt die Stelle des Kernels bei der Behandlung von A. Es handelt sich nicht um zwei identische vollständige Kopien desselben Registersatzes. Ein PCB oder eine zugehörige Kontextstruktur und der prozessbezogene Kernelstack wirken zusammen, um den Zustand zu sichern.
Erkenne das Ereignis und die Entscheidung
Ein blockierender Aufruf, ein Prozessende oder eine Timerentscheidung kann zu einem Wechsel führen. Ein Geräteinterrupt kann einen anderen Prozess bereit machen, den das Scheduling dann auswählen kann. Der gesamte Wechselmechanismus läuft im Kernel; eine Anwendung sichert ihre eigenen Register nicht, um sich freiwillig unterbrechen zu lassen.
| Situation | Modusübergang? | Prozesswechsel? |
|---|---|---|
| Nicht blockierender Dienst kehrt zum Aufrufer zurück | Benutzer → Kernel → Benutzer | Nein |
| Timer wird behandelt, derselbe Prozess läuft weiter | Benutzer → Kernel → Benutzer | Nein |
| A blockiert und B wird fortgesetzt | Kerneleintritt, dann Rückweg von B | Ja |
Ein Kontextwechsel sichert den für die Fortsetzung nötigen Ausführungszustand. Zeit vergeht und die Außenwelt kann sich ändern: Gemeinsam genutzte Dateioffsets und externe Eingaben werden nicht eingefroren, während ein Prozess pausiert.
Berücksichtige direkten und indirekten Aufwand
Zur direkten Arbeit gehören das Ausführen des Schedulers, das Sichern und Wiederherstellen von Registern und die Auswahl eines Adressraums. Übersetzungscaches müssen möglicherweise angepasst werden; gekennzeichnete Übersetzungen können verhindern, dass alle alten Einträge verworfen werden. Indirekter Aufwand folgt, wenn B CPU-Caches oder zwischengespeicherte Adressübersetzungen neu füllen muss, die A verdrängt hat. Der Aufwand hängt von Hardware und Arbeitslast ab. Verwende daher bei der Berechnung des Verwaltungsaufwands gemessene oder angegebene Werte.
Angenommen, ein Wechsel kostet einschließlich seiner relevanten Auswirkungen 5 µs und nutzbare CPU-Abschnitte dauern 10 ms. Rechne 10 ms in 10.000 µs um. Der Wechselanteil beträgt 5 / (10.000 + 5), etwa 0,05 %. Bei nutzbaren Abschnitten von 1 ms sind es 5 / 1005, etwa 0,50 %; bei 0,1 ms sind es 5 / 105, etwa 4,8 %.
Der Nenner umfasst die nutzbare Arbeit plus die Wechselzeit für einen wiederholten Zyklus. Ein kürzerer Abschnitt kann die Möglichkeiten für interaktive Arbeit verbessern, beansprucht aber einen größeren Anteil für wiederholte Wechsel. Timer und Wechselcode ermöglichen kurze Abschnitte; die Schedulingstrategie muss diesen Aufwand gegen die Reaktionsfähigkeit abwägen.
Wichtige Erkenntnisse
- Nach einem Kerneleintritt kann die Ausführung ohne Kontextwechsel zum selben Prozess zurückkehren.
- Das Sichern des Benutzerzustands beim Eintritt und des Kernelkontexts während eines Wechsels dient unterschiedlichen Zwecken.
- Die Fortsetzung von B braucht den Kernelstack, die gesicherten Register und den Adressraumkontext von B.
- Der Wechselaufwand umfasst direkte Verwaltungsarbeit und das spätere Neufüllen von Caches oder Adressübersetzungen.
- Der Verwaltungsanteil ist der Wechselaufwand geteilt durch die Summe aus nutzbarem Abschnitt und Wechselaufwand.
- Kürzere Abschnitte erhöhen bei festen Kosten pro Wechsel den Wechselanteil.
Prozesse und Systemaufrufe: Beispiele
1Erkenne den Beitrag des Betriebssystems
12 Schritte
- 1EinführungBeginne mit mehreren Programmen
Stell dir einen Laptop vor, der ein Projekt kompiliert, während er Musik abspielt und Netzwerkdaten empfängt. Jede Anwendung braucht CPU-Zeit, Speicher und Ein-/Ausgabe, aber keine kontrolliert den ganzen Rechner.
Verfolge, wer diese Ressourcen bereitstellt, wie mehrere Aufträge Fortschritte machen und wo die privilegierten Dienste laufen. Die wiederkehrende Frage ist konkret: Welche Arbeit fordert die Anwendung an und welche muss das Betriebssystem verwalten?
- 2ErklärungVerbirg die Gerätedetails
Das Betriebssystem bietet Abstraktionen: nützliche Schnittstellen, die darunterliegende Mechanismen verbergen. Ein Programm öffnet eine Datei und liest Bytes, ohne Befehle für jedes Speichergerät zu implementieren.
Ein Systemaufruf fordert über eine kontrollierte Schnittstelle einen Kerneldienst an. Der Kernel prüft und erledigt die geschützte Arbeit. Die Anwendung entscheidet weiterhin, welche Daten sie möchte; sie erhält keine Berechtigung, das Gerät direkt anzusteuern.
- 3BeispielVerfolge eine Ausgabedatei des Compilers
Ein Compiler erzeugt Bytes für eine ausführbare Datei und fordert einen Schreibvorgang an. Der Kernel prüft die Ausgaberessource und behandelt die Speicherarbeit hinter der Dateischnittstelle.
Gleichzeitig verwaltet das Betriebssystem den Speicher des Compilers und teilt CPU-Zeit mit dem Musikplayer. Eine Handlung, das Speichern einer Datei, liegt somit innerhalb mehrerer Aufgaben des Betriebssystems: Abstraktion, Ressourcenverwaltung und Schutz der anderen Programme.
- 4KernaussageGib jedem Prozess einen nutzbaren Rechner
Virtualisierung stellt eine physische Ressource über eine nützliche private Sicht dar. Ein Prozess kann Fortschritte machen, als hätte er einen eigenen Prozessor, obwohl seine Befehle sich mit denen anderer Prozesse abwechseln.
Auch seine Speichersicht ist privat: Eine von ihm genutzte Adresse muss nicht dieselben physischen Bytes wie in einem anderen Prozess bezeichnen. CPU- und Speichervirtualisierung geben Programmen bequeme Modelle, während das Betriebssystem die tatsächlich gemeinsam genutzte Hardware verwaltet.
- 5HerausforderungFinde den privilegierten Teil
Auf einem Computer ist kein grafischer Desktop installiert, aber er führt Dienste aus, speichert Dateien und teilt CPU-Zeit auf. Hat er den wesentlichen Kern des Betriebssystems verloren? Erkläre.
---reveal---
Nein. Der Kernel verwaltet weiterhin Prozesse, Speicher und Gerätedienste. Ein grafischer Desktop ist ein Benutzerprogramm, das auf diesen Diensten aufbaut. Sein Fehlen ändert die Oberfläche, die ein Mensch sieht, nicht aber den Bedarf an der Ressourcenverwaltung des Kernels.
- 6ErklärungTrenne die Operation von der Wahl
Ein Mechanismus ermöglicht eine Handlung. Das Sichern der Register eines Prozesses und das Wiederherstellen der Register eines anderen stellt den Mechanismus für einen Wechsel bereit.
Eine Strategie wählt, wie er genutzt wird, indem sie zum Beispiel entscheidet, welcher bereite Prozess den nächsten CPU-Abschnitt erhält. Diese Unterscheidung ist bei einer Entwurfsänderung wichtig: Eine neue Entscheidungsregel kann denselben zuverlässigen Wechselcode wiederverwenden. Effiziente Mechanismen allein entscheiden nicht, was fair ist.
- 7ErklärungFülle eine sonst ungenutzte Wartezeit
Ein Stapelverarbeitungssystem kann die CPU ungenutzt lassen, während sein aktueller Auftrag auf Ein-/Ausgabe wartet. Der Mehrprogrammbetrieb hält andere Aufträge verfügbar und führt während dieser Wartezeit einen davon aus.
Der wartende Auftrag rechnet nicht und das Gerät ist nicht schneller geworden. Der Gewinn entsteht durch die Überlappung unabhängiger Arbeit: Ein Prozess wartet, während ein anderer die CPU nutzt. Mehrere im Speicher befindliche Programme brauchen außerdem Schutz, damit keines den Speicher eines anderen überschreiben kann.
- 8BeispielBerechne den Auslastungsgewinn
Ein Auftrag rechnet 20 ms und wartet dann 80 ms auf Ein-/Ausgabe. Allein enthält ein Zyklus von 100 ms nur 20 ms CPU-Arbeit: Die Auslastung beträgt 20 %.
Mit einem zweiten solchen Auftrag und geeigneter Überlappung können weitere 20 ms Berechnung sonst ungenutzte Zeit füllen. Das ergibt in diesem Beispiel bis zu 40 %. Die Verbesserung nutzt Wartezeit produktiv; zusätzliche Aufträge können die CPU-Auslastung nicht über 100 % erhöhen.
- 9VerbindungGib interaktiver Arbeit kurze Abschnitte
Das Zeitteilverfahren gibt Prozessen kurze CPU-Abschnitte, sodass interaktive Arbeit nicht auf einen ganzen Stapelauftrag warten muss. Das ist selbst auf einer logischen CPU Nebenläufigkeit: Mehrere Aktivitäten kommen durch Verzahnung voran.
Parallelität braucht tatsächliche gleichzeitige Ausführung auf getrennten Verarbeitungsressourcen. Arbeits- und Festplattenspeicher veranschaulichen eine andere Aufteilung, das Raumteilverfahren, bei dem verschiedene Benutzer gleichzeitig getrennte Teile belegen.
- 10ErklärungWähle eine Dienstgrenze
Ein monolithischer Kernel wie Linux führt die meisten Dienste gemeinsam und privilegiert aus. Direkte Aufrufe sind effizient, aber ein fehlerhafter Treiber kann den Kernelzustand beschädigen.
Ein Mikrokernel wie seL4 belässt mehr Dienste in getrennten Benutzerprozessen. Isolation kann den Ausfall eines Dienstes eingrenzen, verursacht aber Aufwand für Kommunikation und Moduswechsel. Windows NT und XNU verbinden Ansätze. Die Grenze bestimmt, welche Fehler sich ausbreiten und welche Dienste neu gestartet werden können.
- 11HerausforderungVerbinde Aufteilung und Schutz
Ein System mit einer CPU führt mehrere Aufträge aus und wechselt zu einem anderen, sobald einer auf Festplatten-Ein-/Ausgabe wartet. Ein Vorschlag würde jedem Auftrag erlauben, jede Speicheradresse zu überschreiben. Erkläre den Auslastungsvorteil und den Fehler dieses Vorschlags.
---reveal---
Der Mehrprogrammbetrieb füllt Ein-/Ausgabewartezeiten mit Berechnungen eines anderen Auftrags. Uneingeschränktes Schreiben würde einem Auftrag erlauben, einen anderen oder das Betriebssystem zu beschädigen. Überlappende Arbeit verbessert die Ressourcennutzung, aber gemeinsame Nutzung braucht weiterhin durchgesetzten Speicher- und Privilegienschutz.
- 12ZusammenfassungBetrachte einen Entwurf mit drei Fragen
Frag dich bei einem Betriebssystementwurf, welche Ressource aufgeteilt wird, welche Schnittstelle ein Programm sieht und welche Grenze andere Arbeit schützt.
Diese Fragen verbinden CPU-Virtualisierung mit Prozessabschnitten, Dateien mit verborgenen Gerätedetails und Kernelorganisation mit Fehlerisolation. Mechanismen führen geschützte Operationen aus; Strategien entscheiden, wie ihre Nutzung aufgeteilt wird. Nebenläufigkeit kann durch Verzahnung auf einer CPU entstehen.
2Verfolge einen Prozess bei CPU- und Ein-/Ausgabearbeit
13 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungVerfolge eine aktive Instanz
- 2ErklärungMache aus einer Datei einen Prozess
- 3ErklärungSichere den nächsten Befehl
- 4BeispielLies eine Prozessliste
- 5HerausforderungDeute gleiche Adressen
- 6TippLies Deskriptoren als geerbte Ressourcen
- 7ErklärungFrag dich, was fehlt
- 8BeispielBeginne den Zeitablauf
- 9BeispielVervollständige den Zeitablauf
- 10HerausforderungÄndere die Entscheidung für einen bereiten Prozess
- 11VerbindungErkenne reale Zustandsnamen
- 12HerausforderungVerbinde Datensätze mit CPU-Kapazität
- 13ZusammenfassungLies den Zustand als Ausführungsbereitschaft
3Setze einen Befehl aus Prozessaufrufen zusammen
23 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungGib jedem Aufruf eine Aufgabe
- 2ErklärungTeile eine Ausführung in zwei
- 3BeispielVerfolge einen privaten Wert
- 4BeispielZähle einen Zweig, nicht die Quellcodeaufrufe
- 5HerausforderungZähle die Ausgabepfade
- 6TippPrüfe, was kopiert wurde
- 7ErklärungErsetze das Programm des Kindprozesses
- 8BeispielÜbergib getrennte Argumentzeichenketten
- 9HerausforderungSage einen erfolgreichen Austausch voraus
- 10KernaussageNutze die Zeit vor exec
- 11ErklärungRufe das Ergebnis eines Kindprozesses ab
- 12BeispielDekodiere eine normale Beendigung
- 13ErklärungLass keine Ergebnisse unabgerufen
- 14HerausforderungBehandle eine Unterbrechung richtig
- 15TippWähle eine Beendigungsanfrage
- 16ErklärungKehre zur Vordergrund-Eingabeaufforderung zurück
- 17BeispielLeite die Ausgabe vor dem Austausch um
- 18HerausforderungErhalte das Ausgabeziel
- 19BeispielVerbinde die beiden Pipe-Enden
- 20KernaussageStarte vor dem Warten
- 21VerbindungBehalte Hintergrundarbeit im Blick
- 22HerausforderungVerfolge eine leere Pipeline
- 23ZusammenfassungVerfolge Identität, Abbild und Ergebnis
4Verfolge eine Anfrage über die Privilegiengrenze
15 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungFrag dich, warum der Kernel betreten wurde
- 2ErklärungBegrenze, was Anwendungen steuern können
- 3BeispielTrenne Identität und Privilegien
- 4HerausforderungSage die Folge eines verbotenen Befehls voraus
- 5ErklärungFinde den eigentlichen Systemaufruf
- 6BeispielBereite einen Schreibvorgang vor
- 7BeispielTritt ein und prüfe
- 8BeispielDeute die Rückkehr
- 9HerausforderungVerfolge einen blockierten Lesevorgang
- 10ErklärungOrdne nach der Ursache ein
- 11BeispielBehebe oder beende
- 12TippOrdne Signale der Prozessebene zu
- 13HerausforderungOrdne einen Geräteabschluss ein
- 14HerausforderungUnterscheide zwei Arten des Eintritts
- 15ZusammenfassungVerfolge Ursache, Prüfungen und Ergebnis
5Behalte die Kontrolle bei direkter Programmausführung
15 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungVerbinde Geschwindigkeit und Kontrolle
- 2ErklärungLöse zwei Steuerungsprobleme
- 3BeispielBereite Rechner und Prozess vor
- 4VerbindungErkenne die Schwäche der Kooperation
- 5BeispielUnterbrich eine Endlosschleife
- 6HerausforderungBestimme den fehlenden Mechanismus
- 7ErklärungSichere den unterbrochenen Benutzerpfad
- 8BeispielSichere auch den Kernelpfad
- 9BeispielKehre über den gesicherten Zustand von B zurück
- 10HerausforderungErkenne einen reinen Moduswechsel
- 11ErklärungErfasse den Aufwand nach einem Wechsel
- 12BeispielBilde den Nenner für den vollständigen Zyklus
- 13HerausforderungSchätze eine veränderte Abschnittsdauer ab
- 14HerausforderungVerbinde einen blockierten Aufruf mit seiner Fortsetzung
- 15ZusammenfassungVerfolge, zu wem der Rückweg gehört
Prozesse und Systemaufrufe: Übungsaufgaben
Set 1: Ressourcen, Prozesse und Schutz10 Übungsfragen
Frage 1 von 10
Antwort zeigenAntwort ausblenden
Frage 2 von 10
Antwort zeigenAntwort ausblenden
Frage 3 von 10
Antwort zeigenAntwort ausblenden
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Frage 6 von 10
Antwort zeigenAntwort ausblenden
Frage 7 von 10
Antwort zeigenAntwort ausblenden
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Set 2: Einen Prozess erzeugen, ersetzen und sein Ergebnis abrufen10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
Jeder fork-Aufruf ist erfolgreich. Wie viele Prozesse erreichen puts("done") in diesem Code?
if (fork() == 0) {
fork();
}
puts("done");
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
/opt/course/bin/checker installiert, und PATH enthält /opt/course/bin. Im aktuellen Verzeichnis gibt es keine Datei namens checker. Warum findet execv("checker", args) das Programm nicht?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
exit(515) auf. Was meldet WEXITSTATUS nach einem erfolgreichen Warteaufruf und einem wahren WIFEXITED-Test?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
waitpid(child, &status, WNOHANG) gibt 0 zurück. Was ist geschehen?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
SIGKILL bleibt der Eintrag sichtbar. Welche Handlung gibt den aufbewahrten Ergebnisdatensatz dieses Kindprozesses frei?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
producer | consumer. Der Erzeuger endet, aber der Verbraucher wartet unbegrenzt, nachdem er alle verfügbaren Bytes gelesen hat. Die Shell hat ihre Pipe-Kopien geschlossen; der Verbraucher hält noch seinen geerbten ursprünglichen Schreibende-Deskriptor offen. Was behebt das Hängen?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 3: Verfolge Kerneleintritt und CPU-Wechsel10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
errno ist ENOENT. Was sollte der Aufrufer daraus schließen?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 4: Verfolge Prozessverhalten und Ressourcenaufteilung10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
Ein Prozess mit einem Thread hat ein privates x mit dem Wert 6. Jeder fork-Aufruf ist erfolgreich. Alle Prozesse erreichen puts. Wie viele Zeilen enthalten „9“?
pid_t p = fork();
if (p == 0) x += 3;
fork();
if (x == 9) puts("9");
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
execvp für ein Programm auf, das nicht existiert. Seine nächste Anweisung gibt einen Fehler aus. Warum kann diese Anweisung ausgeführt werden?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
errno EINTR liefert. Was sollte der Elternprozess tun, um weiterhin den Status der Kindprozesse abzurufen?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
chdir nur in einem mit fork erzeugten Kindprozess auf, wartet auf diesen und startet dann einen weiteren Befehl. Warum beginnt der nächste Befehl im alten Verzeichnis?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 5: Diagnostiziere Wechselwirkungen zwischen Prozess und Kernel10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
waitpid den Test WIFSIGNALED. Welches Makro liefert die Nummer des beendenden Signals?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
SIGSEGV. Welche Reihenfolge unterscheidet die beiden Ereignisse?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
dup2(fd, 0) auf, schließt fd bedingungslos und führt den Befehl mit exec aus. Warum kann die Standardeingabe des Befehls geschlossen sein?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
Ein Programm hat ein privates x mit dem Wert 2. Jeder fork-Aufruf ist erfolgreich. Wie viele Prozesse geben „hit“ aus?
pid_t p = fork();
if (p > 0) {
x = 7;
fork();
}
if (x == 7) puts("hit");
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe
Schlüsselbegriffe
A
Abrufen des Beendigungsstatus
Abstraktion
Adressraum
Argumentvektor
Ausgabepuffer
Ausnahme
Ausnahmeebene
B
Beendigungsstatus
Befehlszähler
Benutzermodus
Betriebssystem
Bibliothekswrapper
C
Close-on-Exec
Copy-on-Write
CPU-Auslastung
D
Dateideskriptor
Dateiende (EOF)
E
Eingebauter Shell-Befehl
Eingeschränkte direkte Ausführung
Elternprozess
errno
errno ist ein Fehlerindikator der C-Bibliothek, der von vielen Schnittstellen bei einem Fehler gesetzt und geprüft wird, nachdem der Rückgabewert einen Fehler gemeldet hat.exec
F
fork
H
Hardwareinterrupt
Hintergrundauftrag
K
Kernel
Kernelkontext
Kernelmodus
Kernelstack
Kindprozess
Kontextwechsel
Kooperative Steuerung
M
Mechanismus
Mehrprogrammbetrieb
Mikrokernel
Moduswechsel
Monolithischer Kernel
N
Nebenläufigkeit
Nicht blockierendes Warten
O
Offene Dateibeschreibung
P
Parallelität
PATH
execvp nach Namen ausführbarer Dateien suchen.Pipe
Privilegierter Befehl
Programm
Prozess
Prozesskennung (PID)
Prozesskontrollblock (PCB)
R
Raumteilverfahren
S
Scheduler
Schutzring
Seitenfehler
Shell
Signal
Stapelverarbeitung
Stapelzeiger
Strategie
Systemaufruf
Systemaufrufnummer
T
Timerinterrupt
Translation Lookaside Buffer (TLB)
Trap
Trap-Rückkehr
Trap-Tabelle
U
Umleitung
V
Verwaister Prozess
Verwaltungsaufwand (Overhead)
Virtualisierung
W
Wartestatus
Z
Zeitscheibe
Zeitteilverfahren
Zombieprozess
Zustand „bereit“
Zustand „blockiert“
Zustand „laufend“
Über diese Einheit
Erstelle deine eigenen Lernmaterialien
Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.