Betriebssysteme I
C++ und Koroutinen
Prinzip der Koroutinen
asymmetrisches Aufrufmodell | symmetrisches
Aufrufmodell |
Prozedur | Koroutine |
|
|
Beispiel:
class Loop : public Coroutine
{
private:
Coroutine* nextone;
int my_id;
public:
Loop (void *tos, int id) : Coroutine (tos), my_id (id) {}
void action ();
void remember (Coroutine* next) { nextone = next; }
};
void Loop::action ()
{
unsigned int i = 0;
while (true)
{
i++;
kout << "Coroutine " << my_id << ": " << i << endl;
resume (*nextone);
}
}
unsigned char stack[2][1024];
int main ()
{
Loop a (stack[0] + 1024, 1);
Loop b (stack[1] + 1024, 2);
a.remember (&b);
b.remember (&a);
a.go ();
}
Ergebnis:
Coroutine 1: 0
Coroutine 2: 0
Coroutine 1: 1
Coroutine 2: 1
Coroutine 1: 2
Coroutine 2: 2
Coroutine 1: 3
Coroutine 2: 3
...
Wenn die Ausgaben nicht jeweils auf die
nächste Zeile, sondern mit direkter Positionierung auf getrennte
Bildschirmbereiche erfolgt, entsteht der Eindruck, daß die
beiden Koroutinen gleichzeitig (parallel) ausgeführt werden.
Wozu Koroutinen?
Wenn ein Programm quasi mehrere Dinge gleichzeitig tun soll, kann dies
mit Koroutinen häufig sehr viel einfacher und eleganter
gelöst werden, als wenn nur ein einziger Kontrollfluß
angegeben werden kann. Beispiel: Päckmännchen.
Realisierung
Prozeduren
push [Par2]
push [Par1]
call proc
...
proc: push ebp
mov ebp,esp
sub esp,12 ; Platz fuer lokale Variablen
mov eax,[ebp+8] ; Parameter 1
mov ebx,[ebp+12] ; Parameter 2
mov [ebp-4], eax ; lokale Variable 1
mov [ebp-8], eax ; lokale Variable 2
...
ret
activation record:
... |
... |
Parameter 2 |
Parameter 1 |
Rücksprungadresse |
Base-Pointer |
lokale Variable 1 |
lokale Variable 2 |
lokale Variable 3 |
Bei geschachtelten oder rekursiven Prozeduraufrufen wird der Stack mit
jedem Prozeduraufruf voller und erst beim Verlassen der Prozeduren
wieder abgebaut.
Koroutinen
Problem:
Wenn Koroutinen wie Prozeduren aufgerufen würden,
müßte der Stack bei jedem Aufruf wachsen und nie abgebaut
werden. Ein Sprung mitten in eine Koroutine hinein ginge nicht, da die
Rücksprungadresse von anderen Stackeinträgen verdeckt worden
wäre.
Lösungsidee:
- Jede Koroutine bekommt einen eigenen Stack.
- Vor der Prozessorabgabe müssen die nicht-flüchtigen
Register der gerade aktiven Koroutine gesichert werden.
- Die Register der neu aktivierten Koroutine müssen wieder
hergestellt werden, bevor diese mit ihrer Arbeit fortfahren
kann.
Verwirklichung:
- Abtrakter Datentyp für den Laufzeitkontext:
struct
toc
- Für jede Koroutine eine Instanz dieses Datentyps
- Koroutinenwechsel:
Coroutine::resume (Coroutine& next)
- Speichern der nicht-flüchtigen Register (inklusive esp),
Laden der nicht-flüchtigen Register des neuen Kontextes
(inklusive esp),
ret
- Initialisierung:
Coroutine::Coroutine (void* tos)
- Nichtflüchtige Register (sofern eine Initialisierung
sinnvoll ist) in den Laufzeitkontext eintragen, den esp auf
den Stack verweisen lassen, auf den Stack die
Anfangsadresse der Funktion schreiben, mit der die
Ausführung beginnen soll (
kickoff (Coroutine*
object)
). Damit kickoff (Coroutine* object)
die action()
Methode der Koroutine aufrufen kann,
muß der this Zeiger der Koroutine als Parameter für
kickoff (...)
ebenfalls auf den Stack geschrieben werden.
- erste Aktivierung der ersten Coroutine:
Coroutine::go()
- Stackpointer laden,
ret