%*
%* Copyright (c) 2007 by Lbukas Ruf (lukas.ruf@lpr.ch)
%*
%* Create: 2007-01-30:1549
%* Modify: 2007-01-30:1626
%* Commit: $Id: design.tex 3053 2007-08-20 19:32:27Z sryffel $
%*

\chapter{\label{arch}Architektur}


% After the reader was fetched in \chapref{problem} clearly to
% understand your approach, you are going to present \emph{your}
% \textbf{model} and your \textbf{architecture} in this chapter.
% 
% According to this statement of my own, we start this introduction
% by the following text:
% \begin{quote}
% \end{quote}

Dieses Kapitel erklärt verschiedene Lösungsansätze zu den im vorherigen Kapitel 
eingeführten Problemen und begründet die gewählten Design-Entscheide. Dabei
werden die Subsysteme ``Memory Management'', ``Thread Management'' und
``Interprozesskommunikation'' in den folgenden drei Unterkapiteln separat
behandelt. 


% \section{\label{introduction:essentials}Essentials}
% 
% Please note: headings at the level of and below sections may not
% be appropriate for your document.
% 
% Well this section can be further subdivided into subsection.  We present
% this in subsection \ref{introduction:essentials:subsections}.
% 
% \subsection{\label{introduction:essentials:subsections}Subsections}
% 
% \paragraph{\label{introduction:essentials:subsections:paragraph}Paragraphs}
% can be specially referenced as well.
% 


% \section{Übersicht}
% \label{arch:essentials}
% 
% \ldots

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\section{Memory Management}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\label{arch:memorymanagement}

Das Memory Management der ia32-Architektur baut auf den zwei Konzepten Segmente
und Paging auf. Deren Funktionsweisen, Vor- und Nachteile werden zuerst erklärt
und nachher wird auf ihre Verwendung in Topsy v3 eingegangen. 


\subsection{Einführung ins Memory Management der ia32-Architektur}

\begin{figure}[!ht]
	\centering
	\includegraphics[width=0.75\textwidth]{segmentation_and_paging.png}
	\caption{Übersicht über Segmente und Paging der ia32-Architektur (aus
	\cite{bib:ia32spec})} 
	\label{fig:segnpaging}
\end{figure}

Die Abbildung \ref{fig:segnpaging} gibt einen Überblick über das Memory
Management der ia32-Architektur. Für die Übersetzung von Adressen in den
physikalischen Adressraum werden zwei Mechanismen verwendet: Segmente und
Paging. 

\subsubsection{Segmente}

Segmentierung ist ein Mechanismus, mit dem der verfügbare Speicher in kleinere,
geschützte Bereiche, genannt Segmente, eingeteilt werden kann. Diese können
für den Code, die Daten und den Stack eines Programms verwendet werden. Indem
zum Beispiel jedem Prozess (oder Thread) eigene Segmente zugeteilt werden, kann
der Prozessor garantieren, dass keiner ausserhalb seiner Segmente in den Bereich
der anderen Prozesse schreibt. Segmente können als Code, Daten oder Stack
deklariert und so die auf ihnen erlaubten Operationen eingeschränkt werden.


\subsubsection{Paging}

Wenn Paging aktiviert ist, unterteilt der Prozessor den adressierbaren 
Adressraum in kleine Einheiten, genannt Pages. Diese können dann an eine 
beliebige Stelle im RAM oder auf einem anderen Gerät gemappt werden.

Paging erlaubt nicht nur mehrere virtuelle Adressräume. Es kann zusätzlich 
für jede Page definiert werden, ob sie auch mit User  oder nur mit Kernel 
Privilegien lesbar und/oder schreibbar ist. Berechtigungen können somit viel
feingranularer gesetzt werden als bei Segmenten.
% Natürlich können im linearen Adressraum zusammenhängende Bereiche beliebig auf
% den physikalischen Speicher verteilt sein. 

\subsubsection{Adressierung}

Um ein Byte in einem Segment zu lokalisieren, muss eine \emph{logische Adresse}
angegeben werden. Diese besteht aus einem 16-Bit breiten Segment Selektor und
einem 32-Bit-Offset. Der Selektor identifiziert unter anderem eindeutig ein
Segment, dessen Basis-Adresse in der Global Descriptor Table steht. Dazu wird
der Offset addiert und so die 32-Bit breite \emph{lineare Adresse} gebildet.
Bei aktiviertem Paging übersetzt der Prozessor diese mit Hilfe des Page
Directories und der Page Tables in eine \emph{physikalische Adresse}. Sonst
wird die lineare direkt auf die physikalische Adresse gemappt. 


\subsection{Segmente}
\label{arch:memorymanagement:segments}


\begin{figure}[!ht] 
	\centering
	\includegraphics[width=0.75\textwidth]{topsy-i386-mm.pdf}
	\caption{Kernel und User Segmente in Topsy i386}
	\label{fig:topsyi386mm}
\end{figure}

Topsy i386 benutzt Segmente, um einen minimalen Speicherschutz zu garantieren. Die
logischen Adressräume des Kernel-  und des Userspaces werden durch zwei
verschiedene Segmente definiert. Die Abbildung \ref{fig:topsyi386mm} zeigt auf
der linken Seite das Segment des Kernels, welches den ganzen linearen Adressraum
ausser die ersten 128KB umfasst. Das User Segment geht von der
physikalischen Adresse 1MB bis zum Ende des vorhandenen Speichers. 

% Ein Zugriff
% auf die logischeAdresse bei 0 liefert also den Inhalt des physikalischen
% Speichers bei 1MB.  

Segmente sind ein Relikt aus der Zeit, als es noch kein Paging gab. 
Speicherschutz kann mittels Paging viel eleganter und flexibler realisiert und 
Berechtigungen können feingranularer gesetzt werden. Die maximale Anzahl Segmente ist 
durch die Grösse der Global Descriptor Table limitiert. Für Paging gibt es 
keine solche Einschränkung.

Topsy v3 kennt, wie die meisten anderen Architekturen, keine Segmente. Es müsste
also ein zusätzlicher Abstraktionslayer ins Memory Management von Topsy v3
eingeführt werden, um im ia32-Port Segmente zu benutzen.

Dies würde keine Vorteile bieten, weshalb ich mich gegen die Verwendung von 
Segmenten entschieden habe. Segmente können nicht wie Paging deaktiviert 
werden. Sie können aber so gewählt werden, dass sie den gesamten linearen 
Adressraum umfassen. Dann entspricht die logische immer der linearen Adresse.



\subsection{Paging}

% Gewähltes Memory Layout, siehe Notizen \?

Die Architektur des Paging-Mechanismus wurde durch Topsy v3 und die
ia32-Architektur definiert. Die Herausforderungen lagen deshalb mehr in der
Implementation.




%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\section{Thread Management}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\label{arch:threadmanagement}

Das Thread Management ist ein Schlüsselgebiet jedes Betriebssystems. Dabei
stehen Sicherheit und Geschwindigkeit an oberster Stelle. Aus verschiedenen, im
Kapitel \ref{problem:challenges} erläuterten Gründen musste es grundlegend
überdacht und verändert werden.

\subsection{Einführung}

In Topsy v3 gibt es zwei grundsätzlich verschiedene Arten von Threads: \emph{User Threads} und
\emph{Kernel Threads} (siehe Kapitel \ref{related:topsyi386} und
\ref{related:topsyv3}). Kernel Threads sind Teil des Kernels und haben
dementsprechend mehr Privilegien. Während der regulären Ausführung 
laufen User Threads im unprivilegierten \emph{User Mode}. Wenn eine
Unterbrechung in Form eines Interrupts oder einer Ausnahmebedingung stattfindet,
springt der Thread in den Interrupt Handler und wechselt in den privilegierten
\emph{Kernel Mode}. Kernel Threads laufen immer im Kernel Mode.

\subsubsection{Unterbrechungen}

Es gibt 4 verschiedene Klassen von Unterbrechungen:
\begin{enumerate}
  \item \emph{Timer Interrupt}: Der Timer löst periodisch einen Interrupt aus
  und unterbricht damit den laufenden Thread. Der Scheduler bestimmt dann, welcher
  Thread als nächster die CPU bekommt.
  \item \emph{Exceptions}: sind zumeist die Folge von fehlerhaftem Verhalten.
  Der Thread wird blockiert und meistens vom TM-Thread gelöscht. Bei einem
  Pagefault muss
  der MM-Thread über die Rechtmässigkeit des Zugriffs entscheiden und dann
  entweder die Page mappen und den Thread wieder aktivieren oder ihn
  beenden. 
  \item \emph{IPC und Syscalls}: Um Nachrichten zu versenden oder zu empfangen,
  muss ein Thread einen speziellen Software-Interrupt auslösen. Beim Senden darf
  der Thread nachher weiter arbeiten. Beim Empfangen wird er blockiert, sofern die
  Nachricht noch nicht eingetroffen ist.
  \item \emph{Übrige Interrupts}: Für alle übrigen Interrupts, die zum Beispiel
  von der Tastatur oder anderen Geräten kommen, wird der entsprechende Handler
  ausgeführt und eine neue Scheduler-Entscheidung gefällt. 
\end{enumerate}


\subsubsection{Ausführungszustände}

\begin{figure}[!ht] 
	\centering
	\includegraphics[width=0.75\textwidth]{proc-states.pdf}
	\caption{Mögliche Zustände eines User Threads}
	\label{fig:procstates}
\end{figure}

Jeder Thread im System ist in mindestens einem der in Abbildung
\ref{fig:procstates} für User Threads dargestellten Zustände. Kernel Threads
sind auch in den Zuständen (1) bis (3) im privilegierten Kernel Mode. Sie führen
dann aber im Gegensatz zu den Zuständen (4) bis (6) nicht den Interrupt Handler,
sondern den Code des Kernel Threads aus. 

Die Pfeile in der Abbildung \ref{fig:procstates} kennzeichnen die
möglichen Übergänge und ihre Bedingungen. Wenn ein neuer Thread kreiert
wird, ist er im Zustand (2): ready. Wenn der Scheduler ihm die CPU
zuteilt, wechselt er nach (1) und läuft, bis er durch eine Unterbrechung (Int)
in den Interrupt Handler (4) im Kernel Mode gezwungen wird. 

Bei einer Unterbrechung muss der Zustand des Threads (Kontext) gespeichert
werden, um diesen nachher wieder herzustellen, damit der Thread nachher an der
gleichen Stelle weiterarbeiten kann. Dieser Vorgang wird Kontext Wechsel genannt.

Das Ende der Abarbeitung der Unterbrechung ist im Diagramm mit End of Interrupt
(EOI) bezeichnet. Der Thread wird entweder blockiert (3), wenn er momentan
nicht lauffähig ist, oder als bereit markiert (2), so dass ihm der Scheduler bei
Gelegenheit wieder eine CPU zuteilen kann (1). 

In dieser Arbeit werden mit Nested Exceptions zwei weitere Zustände eingeführt. 
Neu können Threads im Zustand (4) unterbrochen werden. Dann muss, vergleichbar
mit dem Übergang von (1) zu (4), ein weiterer Kontext gespeichert werden. Dies
ist beliebig oft möglich, es müssen also dementsprechend viele Kontexte
verwaltet werden können



\subsection{Kontext Wechsel}

Für Microkernel-Designs sind Kontext Wechsel besonders kritisch, da sie viel
häufiger auftreten als bei monolithischen Systemen. Die Anforderungen an den
Mechanismus bestehen in hoher Geschwindigkeit, Sicherheit und Unterstützung für
beliebig viele Kontexte pro Thread.

Das neue Design sollte unnötige Kopieroperationen, wie sie in Topsy i386
vorkommen, vermeiden.
Es sollte nur gesichert und wieder hergestellt werden, was auch wirklich
gebraucht wird. 

Zur Gewährleistung der Sicherheit darf kein Thread im User Mode auf Kontexte
zugreifen können. Dies gilt auch für seinen eigenen. Kontexte müssen auf ihre
Gültigkeit überprüft werden können.

Die ia32-Architektur bietet zwei Alternativen: hardware-unterstützte und
software-gesteuerte Kontext Wechsel. Kontext Wechsel sind in Topsy i386 
vollständig in Software implementiert. Die Unterstützung durch die Hardware
speichert den gesamten Zustand des Prozessors und ist zum einen unflexibel und
ersetzt zum andern die Software nicht vollständig, da zum Kontext mehr als die
Register der CPU gehören. Die Realisierung in Software ist einfacher, bietet
mehr Kontrolle über den Vorgang und wurde deshalb favorisiert. 


\subsection{Stacks}
\label{arch:threadmanagement:stacks}

Die wichtigsten Bestandteile eines Threads sind neben dem Code und den Daten
der Stack, die Thread-Datenstruktur des Betriebssystems und die Kontexte.
Diese Daten sollten im Interrupt Handler möglichst früh zur Verfügung stehen.
Dieser wird im Protection Domain des TM-Threads ausgeführt, denn nur er hat
Zugriff auf die Informationen, welche zum Beispiel für das Scheduling benötigt
werden.

In Topsy i386 hat jeder Thread einen Stack. Auf ihm wird bei einer Unterbrechung
der Kontext gespeichert und nachher in die dafür vorgesehene Datenstruktur des
TM-Threads kopiert. Dies ist kompliziert, da für Kernel Threads jedes Mal spezielle
Anpassungen nötig sind, und langsam, da das Kopieren des Kontextes vermeidbar
wäre. Dieses Konzept lässt sich nicht einfach auf Nested Exceptions erweitern.

Stacks werden in Topsy erst bei Bedarf gemappt. Wenn einer über den gemappten
Bereich wächst, gibt es einen Page Fault. Der Memory Manager mappt dann die
nächste Page. Dies könnte auch im Interrupt Handler passieren, sogar bevor
der Kontext vollständig gespeichert ist. Dies gilt es zu vermeiden, da dann der
Zustand des Threads nicht konsistent ist. Der ganze Stack muss also immer schon zu Beginn
gemappt werden. 

\begin{figure}[!ht]
	\centering
	\includegraphics[width=0.8\textwidth]{stacks-arch.pdf}
	\caption{User Mode und Kernel Mode Stack eines Threads}
	\label{fig:stacksarch}
\end{figure}

Das gewählte Stackdesign wird in der Abbildung \ref{fig:stacksarch} illustriert:
Durch die Verwendung eines separaten Kernel Mode Stacks werden alle oben
genannten Probleme gelöst. Die ia32-Architektur kann bei
einer Privilegienänderung automatisch auf einen anderen Stack wechseln. Dieser
Stack ist auch der ideale Ort zum Speichern der Kontexte. Er ist vom
Usermode nicht zugreifbar, die gespeicherten Kontexte können also
gerade dort belassen werden. Der Kernel Code ist nicht rekursiv und benötigt nur
einen kleinen Stack. Diesen schon zu Beginn zu mappen ist kein Problem. 

Die Thread-Struktur wird neu am Ende des Stacks abgelegt. Da dieser an einem
Vielfachen seiner Grösse ausgerichtet wird, kann im Kernel Mode jederzeit aus
dem Stackpointer die Position der Thread-Struktur abgeleitet werden. Zu Beginn
des Interrupt Handlers wird in die Protection Domain des TM-Threads gewechselt,
da nur dort alle benötigten Daten verfügbar sind. Dass der
Stack- und andere Pointer auf den Stack nach dem Wechsel nicht an einen anderen Ort zeigen, muss
der Kernel Mode Stack im Protection Domain des Threads an die gleiche Stelle
gemappt werden wie im TM-Thread.



\subsubsection{Limitierungen}

Ein Problem sind Stack Overflows im Kernel. Bei einem Stack Overflow im User
Mode gibt es einen Page Fault, und der Thread wird vom MM-Thread beendet. Ein
Stack Overflow auf dem Kernel Stack überschreibt zuerst die Thread Struktur, was
vielfältige Probleme verursachen könnte. Es ist sogar vorstellbar, dass Topsy
abstürzt. Dies ist aber vertretbar, da der Overflow nur durch einen Fehler im
Kernel verursacht werden könnte.





\subsection{Reentranter Interrupt Handler}
\label{arch:threadmanagement:reentrantinthandler}

Mit dem im Kapitel \ref{arch:threadmanagement:stacks} beschriebenen Stack- und
Kontextwechsel-Design sind wichtige Voraussetzungen für Nested Exceptions
gegeben. Neu können nicht nur ein, sondern beliebig viele Kontexte verwaltet
werden. Sobald der Kontext vollständig gespeichert ist, können im Interrupt
Handler Nested Exceptions zugelassen werden. 

Für jeden Kontext muss der Schedulerstatus gespeichert werden, welcher besagt, ob
der Kontext wieder hergestellt werden darf oder ob er blockiert ist. 
% Der Thread als Ganzes bleibt bis zum Ende des Interrupt Handlers lauffähig,
% dann wird sein Schedulerstatus entsprechend dem aktuellen Kontext verändert.

Der Scheduler selber interessiert sich nur für den Status des aktuellsten
Kontextes jedes Threads und muss nichts von den übrigen wissen. Eine Ausnahme
sind die Funktionen, welche den Schedulerstatus des Threads verändern
wollen. Beispielsweise muss ein Thread aufgeweckt werden, wenn die Nachricht
eintrifft, auf die er gewartet hat. Dies aber nur unter der Bedingung,
dass der aktive Kontext auf die Nachricht wartet. Denn wenn der Thread
zusätzlich wegen einer Nested Exception auf die Lösung eines Page Faults durch
den MM-Thread wartet, darf nur der Schedulerstatus des auf die Nachricht
wartenden Kontextes angepasst und der Thread selber auf keinen Fall geweckt
werden, da der Page Fault ja zuerst noch behandelt werden muss.

Da Nested Exceptions in Topsy ein neues Konzept sind, musste dafür ein
zusätzlicher Abstraktionslayer eingeführt werden. Dieser ist 
architektur-abhängig, da eventuell die entsprechenden architektur-abhängigen
Kontexte verändert werden müssen.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\section{Interprozesskommunikation}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\label{arch:ipc}

Interprozesskommunikation mittels Nachrichten ist ein häufig verwendetes 
Merkmal in Microkernel-Betriebssystemen und in Topsy einer der zentralsten 
Mechanismen. In Topsy v3 musste er, besonders für die Kommunikation zwischen 
Protection Domains und wie im Kapitel \ref{related:topsyv3} beschrieben,
grundlegend überarbeitet werden.


% \subsection{Boot-Prozess}
% \label{arch:bootup}
% \? 


\subsection{Speicherverwaltung}
\label{arch:ipcmm}

Wie schon angedeutet, finden das Versenden und Empfangen von Nachrichten in
einem speziellen Interrupt Handler, dem Message Dispatcher, statt. Dies
geschieht in der Protection Domain des TM-Threads, welcher nur Zugriff auf die vom
Microkernel bereitgestellten Memory-Funktionen, aber nicht auf die Daten und den
Code des MM-Threads hat. 

Die Nachricht muss zuerst in die Protection Domain des TM-Threads gemappt
werden. Später muss sie an die richtige Stelle in der PD des Zielthreads kopiert
werden. Dazu muss die entsprechende Page des Zielthreads auch in den PD des
TM-Threads gemappt werden. 

Dafür werden in Topsy i386 fixe Pages im virtuellen Adressraum des TM-Threads
verwendet. Dies funktioniert nicht mehr, wenn der Vorgang unterbrochen
und von einem anderen Thread eine andere Nachricht gesendet werden kann. Während
dem ganzen Message Dispatcher keine Interrupts zuzulassen, kommt nicht
in Frage.

Eine Möglichkeit wäre, den MM-Thread zu bitten, die Pages an eine beliebige freie
Stelle zu mappen. Das wäre
sicher und würde auch funktionieren. Es wäre aber nicht sehr performant, da dazu
eine Nachricht und ein Kontextwechsel zum MM-Thread und nachher eine Antwort und
ein Kontextwechsel zurück nötig wären.

Die gewählte Lösung teilt dem TM-Thread zu diesem Zweck einen Teil seines virtuellen
Speichers zur eigenen Verwaltung zu. Dieser wird in einem durch ein Lock
geschützten Puffer verwaltet und nachher vom TM-Thread selber an die richtige
physikalische Stelle gemappt. So kann er schnell und unkompliziert eine Page
mappen. Der kritische Punkt ist die Grösse des Puffers, sie ist aber gut
abschätzbar und muss nicht gross sein.




