Buffl

BS 2

CR
by Cha R.

Was ist „Paging“?

Der physische Speicher wird statisch in gleich große aneinander liegender Blöcke, sog.  Seitenrahmen (frames) aufgeteilt. Dadurch entfällt hier das Problem der externen Hauptspeicher-Fragmentierung. Ungenutzten Speicher innerhalb eines jeweils „letzten“ Blocks, sog. interne Fragmentierung kann es natürlich nach wie vor geben (durchschnittlich eine halbe Blockgröße im jeweils letzten Block) (Im Gegensatz zur Segmentierung besitzt jeder Prozess beim Paging nur einen logischen Adressraum so daß logisch unterschiedliche Blöcke beim Wachsen im logischen/virtuellen Adressraum miteinander kollidieren können). Der logische Adressraum ist ebenfalls in gleich große „Seiten“ (pages) aufgeteilt und über eine (ggf. mehrstufige) Seitentabelle werden z.B. 4k-Worte große logische Seiten-Adressbereiche jeweils auf gleichgroße physische Rahmen-Adressbereiche abgebildet. Dabei werden aus Effizienzgründen z.B. die niederwertigen 12 logischen Adress-Offset-Bits (=4k) direkt auf die 12 niederwertigen physischen Adress-Offset-Bits abgebildet, wodurch sowohl logische Seiten-Adressbereiche als auch physische Seitenrahmen-Adressblöcke gleich (z.B. auf 4k-Grenzen) ausgerichtet werden. Die Bits einer logischen Adresse setzen sich somit aus Seitennummer und Offset zusammen und die Bits einer physischen Adresse aus Seitenrahmennummer und Offset. Ein Index in die Seitentabelle ist eine Seitennummer und ein Eintrag in der Seitentabelle ist eine Seitenrahmennummer. Die Seitentabelle selbst darf nicht ausgelagert werden (zumindest nicht solange sie nur einstufig ist).




Wer kümmert sich um die Zuteilung des Hauptspeichers und welche Strategien gibt es? Wie erfolgt die Seitenallokation in Linux?

Der Langzeit-Scheduler verwendet eine dynamische Allokierungs-Strategie (z.B. first fit, best fit) für die zusammenhängende Hauptspeicherzuweisung. Speziell bei Linux wird dazu ein Seiten-Allokierer verwendet, der nach der sogenannten Buddy-Zuweisungs-strategie auf Anfrage physisch aufeinanderfolgende Seitenrahmen bereitstellt. Die Buddy Strategie ist ein Kompromiss zwischen statischer und dynamischer Allokierung; d.h. auch hier kann externe Fragmentierung auftreten. Der Gesamte Speicher wird über eine Partitionierungs-Schablone in „überlappende“ Bereiche jeweils von der Größe einer Zweier-potenz unterteilt; d.h. der größte Bereich entspricht der vorhandenen physischen Speicher-größe. Wenn der Allokierer einen zusammenhängenden Bereich einer bestimmten Länge benötigt, nimmt er das kleinste freie Stück, das mindestens die erforderliche Länge aufweist. Wenn es mehr als doppelt so lang ist wie der benötigte Bereich, so wird es halbiert; das eine halbe Stück wird benutzt, das andere, sein Buddy, bleibt frei. Wann immer ein Stück frei wird und sein Buddy bereits frei ist, werden sie wieder verschmolzen. Dieses Verfahren ist recht einfach zu implementieren, kann aber zu erhöhter interner Fragmentierung führen, denn wenn ein Prozess z.B. 33 Seiten benötigt, belegt er schon 64; d.h. die entstehende interne Fragmentierung ist immer der Rest ungenutzten Speichers bis zur nächsten Zweierpotenz. Der Vorteil dieses Systems ist die die schnelle Zusammenfassung von freien  Speicherblöcken, da nur die entsprechen-den Listen durchsucht werden müssen. Wird z.B. ein 32 KB-Block frei, muß nur die 32 KB-Liste nach freien Blöcken durchsucht werden, damit festgestellt werden kann, ob eine Zusammenfassung möglich ist.




Was versteht man unter Synchronisation? Warum ist die Prozess-Synchronisation

wichtig? Wie kann man Prozess-Synchronisation realisieren? Wie funktioniert

Synchronisation?

Parallele Prozesse verwenden gemeinsame Daten, dabei greifen sie in unvorhersehbarer

Reihenfolge quasi-gleichzeitig z.B. auf eine gemeinsame Variable zu. Enthält z.B. eine

Operation auf diese gemeinsame Variable eine atomar auszuführende Befehls-

Sequenz aus mehreren Instruktionen (z.B. einen „Read-Modify-Write“-Zugriff), so kann durch einen unvorhersehbaren parallelen Variablen-Fremdzugriff inmitten der atomaren

Instruktions-Sequenz die unterbrochene Operation inkonsistent ausgeführt werden,

wodurch der Variablenwert ggf. verfälscht wird. Man kann diese sog. „race conditions“

vermeiden, indem gleichzeitig immer nur eine atomar auszuführende Sequenz einer

Operation auf ein und dieselbe Variable durchlaufen wird, also immer nur einem Thread

bzw. immer nur einem kritischen Abschnitt „exklusiven Zugriff“ auf die gemeinsame

Ressource gestattet wird. Diese „Serialisierung“ der „kritischen Abschnitte“ bzgl. z.B. einer

gemeinsamen Variablen nennt man Synchronisation. Sie verhindert gleichzeitige Zugriffe

und somit die Unterbrechung atomar auszuführender Operationen und der dabei

entstehenden Inkonsistenzen innerhalb gemeinsamer Variablen. Somit hängt das

Ergebnis der Variablen-Operationen nicht mehr von der unvorhersehbaren Ausführungs-

reihenfolge der Prozess-Zugriffe ab, da diese verhindert werden.

Kurzform: Die Synchronisierung soll race conditions verhindern, indem konkurrierenden

Threads/Prozessen ein exklusiver Zugriff garantiert wird, solange sie sich in einem kritischen

Abschnitt befinden; d.h. das Betreten und Verlassen eines kritischen Abschnitts muß

zwischen Prozessen abgestimmt werden, z.B. durch zwei den Abschnitt umschließende

Betriebssystem-Funktionen „enter_critical_section()“ und „leave_critical_section()“.

Welche Probleme können entstehen, wenn z.B. die Prozesse Beobachter und Berichterstatter nicht synchronisiert werden? Welche Probleme gibt es, wenn man die Prozesse Beobachter und Berichterstatter mit einer Synchronisationsvariablen switch synchronisiert? Was passiert, wenn die Variable switch mit Eins initialisiert wird und der Prozess Beobachter eine höhere Priorität beim Scheduling als der Prozess Berichterstatter hat?

Der kritische Abschnitt im Thread Beobachter liegt inmitten der Zeile „Zähler:=Zähler+1“, da der Berichter-statter vor dem Zurückschreiben des inkremetierten Wertes Zähler aus-geben und sein „Zähler:=0“ ausführen könnte. Der dann richtige Wert 0 würde später durch den inkrementierten Wert überschrieben werden. Der kritische Abschnitt im Thread Bericherstatter umfasst die beiden Zeilen „print(Zähler)“ und „Zähler:=0“, da der Beobachter zwischen den beiden Operationen Zähler (ggf. öfters) inkrementieren könnte. Diese Inkremente gingen dann allerdings aufgrund des danach ausgeführten „Zähler:=0“ verloren. Die Einführung einer Synchronisationsvariable bewirkt zwar „exklusiven Zugriff“ und somit Synchronisation, erlaubt aber nur eine echte abwechselnde Vergabe (birgt Starvation Gefahr) des exklusiven Zugriffs und hat zudem ein „geschäftiges Warten“ (busy waiting) im rechnenden Zustand des jeweils wartenden Threads zur Folge. Wenn die Variable switch mit 1 initialisiert wird und der Prozess Beobachter eine höhere Priorität beim Scheduling als der Prozess Berichterstatter hat, dann kann der Berichterstatter aufgrund seiner geringeren Priorität Switch nie auf 0 setzten und der höherpriore Beobachter wartet ewig aktiv (via busy waiting) in der Zeile „while switch = 1 do no-op“. Zwischen den beiden Prozessen/Threads existiert dann also ein Deadlock.




Was ist ein Semaphor (Kurstext)?

Das Konzept des Semaphors (Zeichenträger) wurde von Dijkstra zur Lösung von Problemen entwickelt, bei denen mehrere Prozesse oder Threads ein Betriebsmittel belegen wollen, von dem insgesamt n Stück zur Verfügung stehen. Dabei kann es sich um n freie Speicherplätze handeln, um n CPUs oder um das Recht, in den kritischen Abschnitt eintreten zu dürfen. Im letzten Fall ist n = 1, weil ja zu einem Zeitpunkt immer nur ein Prozess seinen kritischen Abschnitt betreten darf. Ein Semaphor S kann als abstrakter Datentyp spezifiziert werden. Der Zustand von S besteht aus der Anzahl freier Betriebsmittel, gespeichert in einer Zählvariablen count, und einer Prozessmenge W. Falls count ≠ 0, so ist W leer, ansonsten enthält W alle Prozesse, die sich bisher vergeblich um ein Betriebsmittel bemüht haben und darauf warten, daß wieder eines frei wird. Auf S sind zwei Operationen definiert, down und up. Wenn ein Prozess ein Betriebsmittel benutzen will, ruft er die Operation down(S) auf. Wenn ein Prozess sein Betriebsmittel wieder freigibt, ruft er die Operation up(S) auf (Die Variable S.count darf nach der Initialisierung nur über die down- und up-Anweisungen benutzt, aber nie direkt abgefragt oder gar geändert werden):



 

 

Semaphore verlagern das Problem der Synchronisation des Durchlaufens kritischer Abschnitte von der Anwenderebene auf das Betriebssystem. Die Semaphor-Operationen down und up zur Realisierung kritischer Abschnitteenthalten jeweils wiederum einen kritischen Abschnitt bestehend aus den beiden Zeilen „if S.count > 0“ und „then S.count:= S.count – 1“ in down(S) welche beide eine einzige atomare auszu-führenden Operation darstellen und auch die Zeilen: „if S.W ist nicht leer“ mit “S.count := S.count +1“ in up(S). Letzterer Schadensfall: S.W werde als leer detektiert; bevor S.count inkrementiert wird, durchläuft ein anderer Prozess down(S), blockiert und wird nicht aufgeweckt, obwohl S.count inzwischen inkrementiert wurde. Diese beiden Abschnitte sind allerdings laufzeittechnisch (z.B. durch atomare Assembler-Instruktionen) optimiert und führen zu keinem oder wenig Jitter: Kritische Abschnitte eines Anwenderprogramms, welche durch eine Semaphore synchronisiert/realisiert werden, dauern i.a. wesentlich länger als das kurze Durchlaufen der kritischen Abschnitte der Operationen down und up. Die effektiven Scheduling-Blockaden sind somit, falls überhaupt vorhanden, wesentlich kürzer.

Prozess-Synchronisation: Das Problem der dinierenden Philosophen

Eine Anzahl von n Philosophen die jeweils denken, hungrig sind oder essen sitzen an einem Tisch und teilen sich n Eßstäbchen, wovon jeder Philosoph immer zwei Stäbchen zum essen benötigt. Eine erste Idee zur Implementierung einer funktionierenden Lösung dieses Aufgabe wäre wie folgt:


Bei dieser Lösung gibt es aber einen Nachteil: Ein Deadlock aller Philosophen-Prozesse tritt auf, wenn jeder Philosoph via down(Slinks(i)) zeitnah zu den anderen Prozessen sein linkes Stäbchen aufnimmt: dann kann keiner ein rechtes erhalten, das ja sein rechter Nachbar hat, der ebenfalls auf sein rechtes Stäbchen wartet. Alle Prozesse blockieren hierbei also in down(Srechts(i)). Man löst das Problem dieser Blockade, indem ein Philosoph nur dann Stäbchen aufnehmen darf, wenn beide frei sind. Dazu braucht man einen binären Semaphor „mutex“, der mit 1 initialisiert wird. Bevor ein Philosoph versucht, die Stäbchen zu nehmen oder zurückzulegen, führt er eine down-Operation auf mutex aus. Nachdem er die Stäbchen genommen oder zurückgelegt hat, führt er eine up-Operation auf „mutex“ aus. Es kann also immer nur ein Philosoph gleichzeitig Stäbchen nehmen oder zurücklegen. Kann aber ein Philosoph nicht beide Stäbchen aufnehmen, so wird er schlafen gelegt. Das wird realisiert, indem jeder Philosoph i einen Semaphor p[i] hat, der mit 0 initialisiert wird. Das Ziel des Semaphors p[i] ist, den i-ten hungrigen Philosoph zu blockieren, d. h. warten zu lassen, falls eins seiner beiden Stäbchen nicht frei ist. Jeder Philosoph i benötigt noch eine Statusvariable status[i], um zu verfolgen, ob er gerade hungrig ist oder denkt oder ißt. Ein Philosoph kann nur in den Zustand von Essen übergehen, wenn seine beiden Nachbarn nicht beim Essen sind. Dieser status[i] wird zu Anfang auf Denken gesetzt. Die Prozedur teste(i) stellt fest, ob der Philosoph i hungrig ist und die zwei Stäbchen bekommen kann. Wenn ja, dann geht er in den Zustand „Essen“ über. Man beachten, daß die Prozedur teste sowohl von Philosophen für sich selbst aufgerufen wird (in stäbchen_nehmen) als auch für seine Nachbarn (in stäbchen_weglegen). Außerdem wird es hier nicht automatisch „gerecht“ zugehen, was die Verteilung der Essenszeiten angeht.



Author

Cha R.

Information

Last changed