DaĂ die Computer der Atari ST/TT-Reihe mit einer komfortablen BenutzeroberflĂ€che ausgerĂŒstet sind, ist allgemein bekannt. Trotzdem gibt es immer wieder Kritik am fehlenden Multitasking des GEM, der Eigenschaft also, mehrere Programme quasi gleichzeitig ablaufen zu lassen.
Zwar konnte man in diversen Zeitschriften immer wieder von einem geheimnisvollen GEM/XM hören, das angeblich bei Digital Research in Entwicklung sei, und genau diese FĂ€higkeiten (bei voller KompatibilitĂ€t zum ânormalenâ GEM) haben solle, doch bis heute warten die Atari-Anwender vergeblich auf diesen Leckerbissen.
Einen ersten Lichtblick gab es dann auf der letztjĂ€hrigen Atari-Messe in DĂŒsseldorf, als man am MAXON-Stand ein Produkt begutachten konnte, das genau diese fehlende Eigenschaft ânachliefernâ soll: MULTIGEM.
FĂŒr viele Benutzer dĂŒrfte es daher interessant sein, ein wenig mehr ĂŒber die Techniken des Mehrprogrammbetriebs (Multiprogramming) bzw. Multitaskings zu erfahren. Der folgende Artikel soll einen Einblick in die Möglichkeiten der Implementation eines solchen Programms geben, indem ein Themenbereich etwas genauer untersucht wird: Scheduling.
Ich muĂ die Leser, die jetzt erwarten, eine komplette âBauanleitungâ zu einem Multitasking-GEM quasi frei Haus geliefert zu bekommen, aber gleich enttĂ€uschen: dies wĂŒrde den Rahmen dieses Artikels bei weitem (!) sprengen; erstens wĂ€re solch ein Programmierprojekt eine recht knifflige Angelegenheit, und zweitens fĂŒllt alleine die vorhandene âGrundlagenliteraturâ mittlerweile ganze Regale. Es kann sich also nur um eine kleine EinfĂŒhrung (in die EinfĂŒhrung) der Thematik handeln. So weit alles klar? Gut, dann bitte anschnallen und das Rauchen einstellen, es geht los..
Prozesse
Vorab einige BegriffsklĂ€rungen: Unter einem ProzeĂ soll hier ein Programm verstanden werden, das sich (konkurrierend mit anderen Programmen) um die Gunst des Prozessors bewirbt. Auf einer Ein-Prozessor-Maschine wie dem Atari ST kann zu einem bestimmten Zeitpunkt nĂ€mlich immer nur ein Programm aktiv sein. Die Aufgabe eines Schedulers besteht nun darin, festzulegen, in welcher Reihenfolge bestimmte Prozesse (d.h. Programme) der CPU zugeteilt werden sollen bzw. welche erst einmal warten mĂŒssen. Dementsprechend befindet sich jeder ProzeĂ in genau einem der folgenden ZustĂ€nde:
Bereit: Der ProzeĂ âbewirbtâ sich um die Zuteilung der CPU. Im allgemeinen sind ihm alle anderen Betriebsmittel, wie z.B. bestimmte Speicherbereiche oder PeripheriegerĂ€te (Drucker, Festplatte), vom Betriebssystem bereits zugeteilt worden.
Laufend: Der ProzeĂ ist gerade aktiv, ist also im Besitz der CPU, die diesen ausfĂŒhrt; davon gibt es auf dem Atari wie gesagt höchstens einen.
Blockiert: Das ist i.a. dann der Fall, wenn der ProzeĂ auf ein bestimmtes Betriebsmittel wartet, wie z.B. auf das Eintreffen eines Interrupt-Signals, das ihm das Ende einer Ein-/Ausgabeoperation (IO) anzeigt.
Beendet: In diesem Fall hat der ProzeĂ die CPU schon benutzt, und alle seine Berechnungen auch durchgefĂŒhrt. AuĂerdem hat er alle vorher in seinem Besitz befindlichen Betriebsmittel zurĂŒckgegeben, die jetzt von einem anderen ProzeĂ benutzt werden können.
Falls der Scheduler nun der Meinung ist, der gerade aktive ProzeĂ mĂŒsse unterbrochen werden, weil ein anderer (wichtigerer) aufgetreten ist, stoppt er kurzerhand den arbeitenden ProzeĂ und legt alle wichtigen Daten ĂŒber diesen im sogenannten ProzeĂkontrollblock ab. Diesen Kontrollblock kann man als eine Art Pseudo-CPU bezeichnen; d.h. es handelt sich um eine Struktur, in der alle Registerinhalte des Prozessors zum Zeitpunkt der Unterbrechung sowie die Kennziffer (PID) des laufenden Programms, dessen Zustand und die von diesem benutzten Betriebsmittel festgehalten werden. Abbildung 1 zeigt eine mögliche Implementierung dieses Kontrollblocks in Pascal.
Um einen anderen ProzeĂ ans Ruder (hier: CPU) zu lassen, genĂŒgt es also, den zu diesem ProzeĂ gehörenden Kontrollblock auszuwerten, die CPU-Register mit den alten Werten und den ProgrammzĂ€hler neu zu laden. Obwohl dieses Vorgehen an sich recht trivial ist, kann es dennoch zu einigen Problemen kommen...
Deadlocks
So kann es z.B. passieren, daĂ verschiedene Prozesse sich (gleichzeitig) um bestimmte Betriebsmittel bewerben, diese aber nur exklusiv an einen ProzeĂ vergeben werden können. Im allgemeinen werden diese Betriebsmittel ĂŒber sogenannte Semaphoren verteilt, spezielle Datenstrukturen, die genau festlegen, wieviele Prozesse maximal auf sie zugreifen dĂŒrfen. Falls es dann noch weitere Programme gibt, die diese Mittel nutzen wollen, werden sie erst einmal abgewiesen und in eine Warteschlange (Queue) eingereiht, bis das Betriebsmittel wieder zur VerfĂŒgung steht. Diese Prozesse gehen also in den Zustand âblockiertâ ĂŒber (s.o.)
Ganz Ă€rgerlich wird es, wenn ein Programm mehrere Betriebsmittel fĂŒr sich beansprucht, und zwar gerade die, die auch von einem anderen ProzeĂ angefordert werden. In einer solchen Situation kann es sehr leicht zu einer ProzeĂverklemmung (DEADLOCK) kommen, und dann geht oft nichts mehr! Beispiel: Zwei Prozesse A und B benötigen exklusiv zwei Betriebsmittel M1 und M2. Nehmen wir an, daĂ A bereits im Besitz von M1 und B im Besitz von M2 ist. Wie soll weiterverfahren werden, vor allem dann, wenn es nicht (oder nur schwer) möglich ist, einem ProzeĂ ein Betriebsmittel wieder zu entziehen?
Zur Entstehung eines solchen Deadlocks sind grundsÀtzlich die folgenden Bedingungen notwendig:
- Die beteiligten Prozesse (Programme) verlangen einen exklusiven Zugriff auf bestimmte Betriebsmittel.
- Es gibt Prozesse, die schon ĂŒber einige Betriebsmittel verfĂŒgen, aber noch auf weitere warten.
- Es ist dem Betriebssystem nicht möglich, einem Prozeà einmal zugeteilte Betriebsmittel (BMs) wieder wegzunehmen, ohne daà dieser sie benutzt hat.
- Es existiert eine (geschlossene!) Kette von Prozessen, in der jeder ProzeĂ ĂŒber gewisse BM verfĂŒgt, die vom nĂ€chsten ProzeĂ in dieser Kette benötigt werden.
Um einen Deadlock zu vermeiden, mĂŒĂte man also mindestens eine der letzten drei Bedingungen durchbrechen, z.B. in dem man einem ProzeĂ alle BMs entzieht, wenn ihm ein bestimmtes nicht zugesprochen werden konnte.
Type kontrollblock =
record
data : array[0..7] of Longint; (* CPU-Datenregister )
adres : array[0..6] of Longint; ( AdreĂregister )
stack : array[0..1] of Longint; ( Stackpointer )
pc : Longint; ( Programm-Counter )
status, ( Statusregister )
pid, ( ProzeĂkennung )
prior : integer; ( PrioritÀt )
time : Longint; ( (Rest-)Laufzeit *)
end;
Abb. 1: Ein ProzeĂkontrollblock in Pascal
Scheduling-Strategien
Kommen wir aber jetzt zu dem bereits oben angedeuteten Problem der Zuteilung der CPU an ein bestimmtes Programm, d.h. der Frage, in welcher Reihenfolge die vorhandenen Prozesse abgearbeitet werden sollen.
Es sind diesbezĂŒglich eine Reihe von Verfahren entwickelt worden, von denen ich einige hier kurz erlĂ€utern und auch durch kleine Pascal-Programme simulieren möchte. Zur Programmentwicklung habe ich Maxon-Pascal benutzt, das sich vor allem durch das Unit-Konzept vorteilhaft von anderen Systemen abhebt; selbstverstĂ€ndlich kann aber auch jeder andere Pascal-Compiler (wie z.B. ST-Pascal) verwendet werden.
PrioritÀt: Klassen: Scheduling:
hoch system jobs SRTF
interaktive RR
"" editing RR
batch SRTF
niedrig student-jobs FCFS
Abb. 5: Klasseneinteilung bei Multi-Level-Queues
First Come First Serve (FCFS)
Eine der einfachsten Möglichkeiten, die eintreffenden Prozesse in eine abzuarbeitende Reihenfolge zu bringen, ist das FCFS-Prinzip. Hierbei werden alle AuftrĂ€ge einfach in der Reihenfolge ihres Eintreffens bearbeitet. Da die CPU natĂŒrlich immer nur einen ProzeĂ zu einem bestimmten Zeitpunkt bearbeiten kann, werden alle anderen Prozesse, die wĂ€hrend dieser Abarbeitungsphase eintreffen, in eine Warteschlange (FIFO-Queue) eingereiht. Sobald dann der aktive ProzeĂ beendet wird, holt sich der Scheduler den nĂ€chsten ProzeĂ aus dieser Schlange und ĂŒbergibt ihn der CPU zur Bearbeitung. Zwar ist dieses Verfahren extrem einfach zu implementieren, es besitzt aber auch einen gravierenden Nachteil: eine groĂe Varianz bei der mittleren Turnaround-Zeit. Hierunter versteht man die Zeit von der Abgabe eines Prozesses an die CPU bis zu dessen Fertigstellung. Es ist wichtig zu wissen, daĂ der Scheduler nur die Zeit beeinfluĂt, in der die AuftrĂ€ge in der Warteschlange sind, denn alle anderen ZeitaufwĂ€nde wie z.B. die AusfĂŒhrungszeit der CPU oder Zeiten fĂŒr Ein-/Ausgabeoperationen sind ja stets und unabhĂ€ngig von der Reihenfolge der Bearbeitung gegeben.
Abbildung 2 soll dieses Problem noch einmal veranschaulichen. Es werden dort drei verschiedene Prozesse (Tasks) in unterschiedlicher Reihenfolge bearbeitet und die mittlere Turnaround-Zeit berechnet.
Abb. 2: Varianzproblem beim FCFS
Shortest Job First (SJF)
Diese Form des Schedulings versucht stets, den ProzeĂ als nĂ€chsten zu starten, der die kĂŒrzeste CPU-Anforderung, also die kĂŒrzeste Rechenzeit besitzt. Das Problem ist aber, daĂ dieser Zeitbedarf normalerweise gar nicht bekannt ist (und auch gar nicht bekannt sein kann!).
Hier hilft dann nur einfaches SchĂ€tzen: in der Praxis wird diese SchĂ€tzung i.d.R. mit Hilfe des exponentiellen Mittels vorgenommen. Dabei geht man von der Annahme aus, daĂ kĂŒnftige Prozesse einen Ă€hnlichen Zeitaufwand haben werden wie die bisherigen. Durch das exponentielle Mittel wird dann ein einfacher Durchschnitt berechnet, wobei aber jĂŒngere Werte (d.h. aktuellere) stĂ€rker gewichtet werden als Ă€ltere. Somit lassen sich etwaige Trends berĂŒcksichtigen.
Ein arges Problem darf aber auch hier nicht verschwiegen werden: Da beim SJF ja immer die Prozesse mit der kĂŒrzesten Rechenzeit gestartet werden, kann es leicht passieren, daĂ einige Tasks einfach nicht bedient werden, weil nĂ€mlich stĂ€ndig andere Prozesse mit einer vermeintlich kĂŒrzeren Laufzeit eintreffen. Diesen Vorgang bezeichnet man in der Informatik auch als STARVATION, was aussagen soll, daĂ Prozesse mit einer hohen Laufzeit schlichtweg âverhungernâ mĂŒssen.
DaĂ dieses Problem durchaus ernst zu nehmen ist, wurde spĂ€testens im Jahre 1973 deutlich, als beim Massachusetts Institute of Technology (MIT), dem wohl bekanntesten Forschungszentrum der Welt, eine IBM 7094 abgeschaltet wurde: auf ihr fand man einen ProzeĂ aus dem Jahr 1967, der immer noch darauf wartete, das ihm die CPU zugesprochen wĂŒrde!!!
NatĂŒrlich gibt es kein Problem, das nicht zu meistern wĂ€re. So kann man sich vor einer Starvation beispielsweise recht wirkungsvoll durch das sogenannte AGING (Alterung) schĂŒtzen: dabei werden nach Ablauf eines bestimmten Zeitintervalls einfach die PrioritĂ€ten der noch nicht zum Zuge gekommenen Prozesse erhöht, beim SJF also z.B. die geschĂ€tzte AusfĂŒhrungszeit verkĂŒrzt.
Anzumerken bliebe noch, daĂ sich viele Scheduler dieses Verfahrens bedienen, da SJF die optimale mittlere Turnaround-Zeit liefert (auf den mathematischen Beweis soll hier aber, wenn er auch recht simpel ist, verzichtet werden). In Abbildung 3 kann die Bearbeitungsreihenfolge (am Beispiel von 4 Prozessen) noch einmal nachvollzogen werden.
Abb. 3: SJF-Scheduling
Shortest Remaining Time First (SRTF)
Die beiden bisher vorgestellten Verfahren zeichneten sich jeweils dadurch aus, daĂ die zu bearbeitenden Prozesse in eine bestimmte Reihenfolge gebracht wurden, und - wenn das erst mal geschehen war, just in dieser Folge bearbeitet wurden. DaĂ solch ein Vorgehen jedoch nicht optimal sein kann, wird klar, wenn man sich folgende Situation vorstellt:
Zu einem bestimmten Zeitpunkt gebe es auf einem Computer nur einen einzigen ProzeĂ (mit einer relativ langen AusfĂŒhrungszeit). Da kein anderer ProzeĂ sich um die CPU bewirbt, wird diesem die CPU zugeteilt und das entsprechende Programm gestartet. WĂ€hrend der Abarbeitungsphase treten dann plötzlich andere Prozesse auf, die aber, da die Zentraleinheit beschĂ€ftigt ist, erst einmal warten mĂŒssen. Denn sowohl FCFS als auch SJF starten einen neuen ProzeĂ erst dann, wenn der gerade aktive seine Arbeit beendet hat.
Sehr Ă€rgerlich, vor allem, wenn die wartenden Tasks eine wesentlich höhere PrioritĂ€t besitzen (also eigentlich âwichtigerâ sind) als die gerade aktive. Man brĂ€uchte also ein Verfahren, das den gerade aktiven ProzeĂ unterbricht, wenn ein wichtigerer auftritt.
Beim Shortest Remaining Time First bezieht sich diese Auswahl auf die Restlaufzeit eines Prozesses. Dabei wird das gerade laufende Programm immer dann vom Scheduler unterbrochen, wenn ein neuer ProzeĂ mit einer kĂŒrzeren Laufzeit als die Restlaufzeit der aktiven Task eintrifft. Die so deaktivierten Programme werden dann als quasi neue Prozesse betrachtet, indem ihre Laufzeit auf die noch verbleibende Restzeit gesetzt wird. (Dies setzt natĂŒrlich wie beim SJF voraus, daĂ man eine grobe Vorstellung davon hat, wie lange ein bestimmter ProzeĂ die CPU benutzt). Die Reaktivierung dieser Prozesse erfolgt dann nach dem bereits oben beschriebenen Shortest-Job-First-Prinzip. Auch dieser Sachverhalt ist zum besseren VerstĂ€ndnis noch einmal in einer Abbildung (4) zusammengefaĂt.
Task-Eintreffen: t0: (T1,8), t1: (T2,4), t2: (T3,9),
t3: (T4,5)
==> Bearbeitung in folgender Reihenfolge:
t0: (T1,8)
t1: (T1,7), (T2,4)
t2: (T1,7), (T2,3), (T3,9)
t3: (T1,7), (T2,2), (T3,9), (T4,5)
...
...
Zeitaufwand: (5+10+17+26)74 = 14,5
Abb. 4: ProzeĂverarbeitung beim SRTF
Round Robin (RR)
Beim Round-Robin-Verfahren wird die genaue Festlegung der Bearbeitungsreihenfolge und -zeit noch etwas âgerechterâ durchgefĂŒhrt. Hier kommen alle Tasks wie beim FCFS zunĂ€chst in eine FIFO-Queue (den letzten beiĂen die Hunde). ZusĂ€tzlich existiert jedoch ein fĂŒr alle Prozesse fest vorgegebenes Zeitintervall, so daĂ bei einer ProzeĂbearbeitung die folgenden zwei FĂ€lle auftreten können:
- Die (tatsĂ€chliche) Rechenzeit einer Task ist kleiner (oder gleich) dem vorgegebenen Intervall. Dann fĂŒhrt das Programm einfach seine Berechnungen durch und deaktiviert sich anschlieĂend selbst. (Der Scheduler startet dann das nĂ€chste Programm aus der Warteschlange).
- Der Zeitbedarf eines Programms ist gröĂer als das festgelegte Intervall. In diesem Fall wird die Task nach Ablauf der Zeitscheibe vom Scheduler unterbrochen und wieder hinten in die Schlange eingereiht. Danach wird das nĂ€chste Programm aus der Schlange geholt, gestartet, und das ganze Spiel beginnt von neuem.
Die Kunst dieses Verfahrens liegt also eigentlich darin, einen geeigneten Wert fĂŒr die Zeitscheibe zu finden. Setzt man den Wert zu hoch an, wird aus dem Round-Robin schnell das bekannte First-Come-First-Serve. Setzt man ihn hingegen zu niedrig an, kommt es zu hĂ€ufigen ProzeĂumschaltungen (Context-Switches), was wiederum unnötig viel Zeit kostet, da ja jedesmal der Prozessorstatus im Kontrollblock gesichert und ein neuer geladen werden muĂ. Es gibt zwar Rechner, die dieses Problem nicht so hart trifft, da sie mit mehreren RegistersĂ€tzen ausgestattet sind (Beispiel: SPARC der Sun-4: 8 SĂ€tze), nur nĂŒtzt uns das auf dem Atari natĂŒrlich herzlich wenig. Der VollstĂ€ndigkeit halber sei noch erwĂ€hnt, daĂ die in der Praxis benutzten Scheduling-Algorithmen i.d.R. mit Zeitscheiben von 10-20ms arbeiten.
Multi Level Queues
Eine weitere (und beliebte) Scheduling-Strategie ist die der Multi Level Queues. Es handelt sich hierbei im Prinzip um eine Mischung aus PrioritĂ€ten-Scheduling und anderen Algorithmen. Bei diesem Verfahren werden alle auftretenden Tasks in spezielle Klassen eingeteilt. So gibt es etwa Klassen fĂŒr System-Jobs, Stapelverarbeitung (Batch-Processing), allgemeine User-Jobs oder Ă€hnliches. Jeder Klasse wird dabei eine festgelegte PrioritĂ€t zugeordnet (vgl. Abbildung 5), so daĂ bestimmte Prozesse stets Vorrang vor solchen in einer niedrigeren priorisierten Klasse haben. Innerhalb einer solchen Klasse können die Prozesse dann z.B. nach dem gerade beschriebenen Round-Robin-Verfahren der CPU zugeteilt werden.
Dabei ist es wichtig zu betonen, daĂ die einmal getroffene PrioritĂ€tenfestlegung strikt ist. Auf Abbildung 5 bezogen, bedeutet das beispielsweise, daĂ âStudenten-Jobsâ nur dann bearbeitet werden können, wenn alle darĂŒber liegenden Warteschlangen leer sind.
Multi Level Feedback Queues
Das letzte Verfahren, das ich Ihnen vorstellen möchte, geht noch einen etwas anderen Weg: Hier werden werden Ă€hnlich wie beim Round-Robin Zeitscheiben definiert, die die Verarbeitung eines Prozesses auf eine bestimmte Zeit begrenzen. Im Gegensatz zum Round-Robin existiert hier jedoch nicht nur eine solche Schranke, sondern mehrere. Konkret: Man konstruiert sich verschiedene Warteschlangen mit jeweils festgelegter maximaler CPU-Benutzung. Jeder auftretende ProzeĂ wird dann zunĂ€chst in die Schlange mit der kleinsten Bearbeitungszeit eingereiht. Entweder reicht das dort gesetzte Limit zur Bearbeitung des Jobs aus, oder die Task wandert in die nĂ€chste Schlange (mit einer groĂzĂŒgiger ausgelegten Zeitspanne) usw.
In der letzten dieser Schlangen könnten die Prozesse dann z.B. nach dem zu Anfang beschriebenen FCFS-Prinzip verwaltet werden; somit ist sichergestellt, daà alle Programme auch tatsÀchlich komplett bearbeitet werden.
Ran an die Tasten
Nach so viel Theorie wenden wir uns nun den Programm-Listings zu. Bei den Listings 3 und 4 handelt es sich um die eigentlichen Hauptprogramme, die eine ProzeĂverarbeitung nach dem Round-Robin- bzw. Multi-Level-Queue-Verfahren simulieren. Aber der Reihe nach: In Listing 1 finden Sie eine Pascal-Unit, die verschiedene Prozeduren zur Erstellung und Verwaltung von Warteschlangen enthĂ€lt. (ST-Pascal-Benutzer und solche, deren Compiler keine Units unterstĂŒtzen, mĂŒssen diese in das Hauptprogramm mit einbinden). Ich gehe an dieser Stelle davon aus, daĂ Sie die Arbeitsweise einer solchen Schlange bereits kennen; Einzelheiten zur Funktion der einzelnen Routinen entnehmen Sie bitte dem kommentierten Listing (1).
Die zur Bearbeitung der vorhandenen Prozesse notwendigen Prozeduren wurden ebenfalls in eine eigene Unit ausgelagert (Listing 2). task_state gibt den aktuellen Status eines Prozesses aus, schedule_rr steuert die Abarbeitung der in den Warteschlangen vorhandenen Prozesse nach dem oben beschriebenen Round-Robin-Verfahren. Bei der Funktion work_task handelt es sich um eine spezielle Unterroutine dieses Schedulers, die jeden ProzeĂ eine bestimmte Zeit arbeiten lĂ€Ăt und ihn danach deaktiviert.
Das Nachvollziehen der Bearbeitungsreihenfolge der beiden Simulationsprogramme sollte Ihnen (aufgrund der oben stehenden, theoretischen Ăberlegungen) keine Probleme bereiten. Trotzdem noch ein paar Worte zum Ablauf: Nach dem Starten der Programme wird zuerst eine Meldung auf dem Bildschirm ausgegeben, die u.a. anzeigt, um welche Art des Schedulings es sich handelt. Danach wird versucht, die notwendigen ProzeĂdaten vom aktuellen Laufwerk einzulesen; diese Daten mĂŒssen in einem File of task untergebracht sein, das den Namen tasks.in trĂ€gt. Die Komponenten task.pid und task.ankunft werden zur Bearbeitung der Prozesse eigentlich nicht benötigt; sie werden jedoch innerhalb von task_state als ProzeĂinformation mit ausgegeben. Das Multi-Level-Queue-Verfahren wurde von mir mit vier verschiedenen Warteschlangen realisiert. Aus diesem Grund sollten Sie darauf achten, als Task-PrioritĂ€ten nur die Ziffern von 1 bis 4 zu wĂ€hlen, da ansonsten ein EinfĂŒgen in die entsprechenden Warteschlangen nicht möglich ist. (Die Ziffer 1 steht dabei ĂŒbrigens fĂŒr hohe, die 4 fĂŒr niedrige PrioritĂ€t...)
AnschlieĂend können Sie einen beliebigen (ganzzahligen, positiven) Wert fĂŒr die Zeitscheibe(n) eingeben. Danach wird der Scheduler gestartet, der die Verarbeitung der Prozesse steuert. Nach dem Ende der Simulation kommen Sie mit einem âReturnâ zur aufrufenden Umgebung (z.B. Desktop) zurĂŒck.
AbschlieĂend möchte ich noch einige Hinweise zur Entwicklung eines âechtenâ Multitasking-Kerns geben: Zur Unterbrechung eines laufenden Programmes bietet sich im ST z.B. der Timer-A des MFP an. Diesen Timer könnten Sie mit einem geeigneten Wert fĂŒr die Zeitscheibe (z.B. 10ms) programmieren, so daĂ nach Ablauf dieser Frist eine eigene Interrupt-Routine angesprungen wird, die das Umschalten der Prozesse ĂŒbernimmt (die geretteten Parameter des gestoppten Programms in einen ProzeĂkontrollblock packen, einen neuen laden usw.) Um möglichst sauber zu programmieren, sollten Sie dabei zwei Dinge nicht vergessen; erstens sollte bei jedem Verbiegen eines Systemvektors eine XBRA-Struktur angelegt werden (es gibt leider immer noch Programmierer, die sich nicht an dieses Konzept halten!), und zweitens sollten Sie sich bei allen Stack-Manipulationen darĂŒber im klaren sein, welchen Prozessor Sie vor sich haben. Der MC-68030 im Atari-TT packt nĂ€mlich einiges mehr an Informationen auf den Stack, so daĂ man schnell auf falsche Parameter zugreift, wenn man nicht aufpaĂt. Der vorhandene Prozessor lĂ€Ăt sich z.B. durch den Cookie-Jar abfragen, und auch die Systemvariable $59E ist in diesem Zusammenhang recht nĂŒtzlich: sie enthĂ€lt den Wert NULL , wenn der Prozessor 6 Bytes bei Trap-Aufrufen auf dem Stack ablegt, anderenfalls werden dort 8 Bytes hinterlegt.
Anmerkungen
So, damit ist unsere kleine EinfĂŒhrung in die Scheduling-Strategien erst einmal beendet. Ich hoffe, dem einen oder anderen Leser die Grundtechniken dieser Verfahren zumindest ansatzweise verdeutlicht zu haben. NatĂŒrlich können im Rahmen eines solchen Artikels nicht alle Aspekte ausreichend angesprochen werden; insbesondere sei noch einmal darauf hingewiesen, daĂ die hier vorgestellten Algorithmen hauptsĂ€chlich in Ein-Prozessor-Maschinen Verwendung finden. Richtig interessant wird die Sache aber erst dann, wenn man es mit Mehrprozessor-Maschinen wie z.B. der Atari Transputer Workstation (ATW) zu tun hat, von der man in den letzten Monaten leider auch nichts mehr hört; sollten Sie jetzt Blut geleckt haben, kann ich Sie auf die unten angegebene Literatur verweisen...
Doch es mĂŒssen ja nicht gleich Transputer oder ein MultiGEM-Projekt sein, auf das Sie sich nun stĂŒrzen: Vielleicht implementiert der eine oder andere von Ihnen ja einen âkleinenâ Multitasking-Kern in eines seiner eigenen Programme. Es ist doch einfach toll, wenn man wie z.B. in Phoenix umfangreiche Sortierungen vornehmen kann und gleichzeitig neue Daten eingibt, wĂ€hrend im Hintergrund exportiert wird und, und, und...
Literatur:
[1] Oberschelp/Vossen, âRechneraufbau und Rechner Strukturenâ, Oldenbourg-Verlag, 3.Auflage 1989
[2] Andrew S. Tanenbaum, âOperating Systems: Design and Implementationâ, Prentice-Hall
[3] Franz-Josef Rammig, Skriptum zur âInformatik Bâ, UniversitĂ€t-GH Paderborn, SS 1990
[4] Rudolf Pichler, âMultitasking zum Selbermachenâ, c't Magazin 06/90, Seite 304 ff.
(*
Programm : Extended Standard-Library
Unit : queue.pas
Funktion : Implementation des abstrakten
Datentyps 'Schlange'
Edit : 09.04.91
Autor : Rolf Kotzian, Paderborn
Copyright (c) MAXON Computer 1991
*)
UNIT Queue;
(* Public-Symbols *)
Interface
Type task =
record
pid : integer; (* ProzeĂkennung *)
ankunft, (* Ankunftszeit *)
laenge : Longint; (* geschĂ€tzte AusfĂŒhrungszeit *)
prior : integer, (* PrioritÀt *)
end;
Squeue = ^Selem;
Schlange = record
anfang, ende : Squeue;
end;
Selem = record
value : task;
rest : Squeue;
end;
(* Prototyping *)
procedure create_queue (var schlange: Schlange);
function is_empty_queue (schlange: Schlange): boolean;
procedure front (schlange Schlange;
var t: task);
procedure deq_queue (var schlange: Schlange);
procedure enq_queue (var schlange: Schlange,
t: task);
(* Private Symbols *)
Implementation
(* 'create_queue': erzeugt leere Schlange *)
procedure create_queue (var schlange: Schlange);
begin
schlange.ende := Nil;
end; (* create_queue *)
(* 'is empty queue': prĂŒft, ob Schlange leer... *)
function is_empty_queue (schlange: Schlange): boolean;
begin
is_empty_queue = (schlange.ende = Nil);
end; (* is_empty_queue *)
(* 'front' liefert (vorderstes) Element der Schlange... *)
procedure front (schlange Schlange;
var t: task);
var dummy: ^Selem;
begin
if not is_empty_queue (schlange) then
begin
dummy := schlange.anfang;
t.pid := dummy^.value.pid;
t.ankunft := dummy^.value.ankunft;
t.laenge := dummy^.value.laenge;
t.prior := dummy^.value.prior;
end;
end; (* front *)
(* 'deq_queueâ: entfernt (vorderstes) Element
der Schlange... *)
procedure deq_queue (schlange: Schlange);
var dummy: ^Selem;
begin
if not is_empty_queue (schlange) then
begin
dummy := schlange.anfang;
schlange.anfang = schlange.anfang^.rest,
if schlange.anfang = Nil then
schlange.ende := Nil;
dispose (dummy);
end;
end; (* deq_queue *)
(* 'enq_queue': hÀngt neues Element (hinten) an
die Schlange *)
procedure enq_queue (var schlange: Schlange;
t: task);
var dummy : ^Selem;
began
new (dummy);
dummy^.value.pid := t.pid,
dummy^.value.ankunft := t.ankunft,
dummy^.value.laenge := t.laenge,
dummy^.value.prior := t.prior;
dummy^.rest := Nil;
if schlange.ende = Nil then
schlange.anfang := dummy
else schlange.ende^.rest := dummy;
schlange.ende := dummy;
end (* enq_queue *)
Begin
End (* End of Unit 'queue' *)
(*
Programm: Scheduling-Simulator
Unit : tasks.pas
Funktion: Hilfsroutinen zum Task-Handling
Edit : 13.04.91
Autor : Rolf Kotzian, Paderborn
Copyright (c) MAXON Computer 1991
*)
UNIT Tasks;
(* Public Symbols *)
Interface
Uses Queue:
(* Prototyping *)
procedure task_state (t: task);
procedure work_task (var s:Schlange;
var t:task;
time: integer);
procedure schedule_rr (var s:Schlange;
time_slice: integer);
(* Private Symbols *)
Implementation
(* 'task_state': wird bei jedem Aufruf der Task
't' aufgerufen und gibt deren
aktuellen Zustand wie folgt
aus:
- ProzeĂkennung (PID)
- Ankunftszeit der Task
- (geschÀtzte) (Rest-) Laufzeit
- PrioritÀt
*)
procedure task_state (t: task);
begin
Write (t.pid:2, ' ', t.ankunft:8);
Write (' ', t.laenge 8);
Writeln (t.prior:11);
end; (* task_state *)
(* 'work_task': lÀsst den Prozess 't' genau
'time' Zeiteinheiten arbeiten
und stoppt
diesen anschlieĂend. *)
procedure work_task (var s:Schlange, var t:task;
time: integer);
var i: integer;
begin
task_state (t);
for i := 1 to time do
(* Unsere Task darf jetzt genau 'time'
Zeiteinheiten schuften ! *)
(* Restlaufzeit ermitteln *)
t.laenge := t.laenge - time;
if t.laenge > 0 then
(* Task wieder einreihen *)
enq_queue(s, t);
(* Schlangenkopf entfernen *)
deq_queue (s);
end; (* work_task *)
(* 'schedule_rr': Scheduling-Algorithmus, b.d.
die Prozesse nach dem Round-
Robin Verfahren (RR)
ausgewÀhlt werden... *)
procedure schedule_rr (var s: Schlange;
time_slice: integer);
var t: task;
time: integer;
begin
(* Es wird so lange gearbeitet, bis alle
Prozesse komplett bedient wurden, die
Warteschlange also leer ist *)
while not is_empty_queue(s) do
begin
(* erste Taks aus der Warteschlange holen... *)
front (s,t);
(* reicht das Limit der Zeitscheibe
zur vollstÀndigen Bearbeitung der Task aus ? *)
if t.laenge > time_slice then
(* Nein ! *)
work_task (s, t, time_slice)
else (* Ja, Limit reicht aus ! *)
work_task (s, t, t.laenge);
end;
end; (* schedule_rr *)
Begin
End (* End of Unit 'Tasks' *)
(*
Programm : Scheduling-Simulator
Modul : Round_Robin.pas
Funktion : ProzeĂverarbeitung nach dem
Round-Robin Verfahren
Edit : 13.04.91
Autor : Rolf Kotzian, Paderborn
Copyright: (c) MAXON Computer 1991
*)
Program Round_Robin (input, output);
Uses Queue, Tasks;
(* Variablen-Vereinbarungen *)
Var dat : File of task;
t : task;
s : Schlange;
time_slice: integer;
Begin (* Main *)
ClrScr;
Writeln ('Scheduling-Simulator');
Write ('ProzeĂverarbeitung nach dem 1);
Writeln ('Round-Robin Verfahren');
Writeln ('by Rolf Kotzian, Paderborn');
Writeln;
Writeln ('(c) 1991 MAXON Computer GmbH');
Writeln; Writeln;
Reset (dat, 'tasks.in');
if IOresult = 0 then
begin
(* leere Warteschlange erzeugen *)
create_queue (s);
while not eof (dat) do
begin
(* ProzeĂdaten aus File
lesen und in eine Warte-
schlange einfĂŒgen *)
Read (dat, t);
enq_queue (s, t);
end;
end
else
begin
(* leider konnte das File 'tasks in' nicht gelesen werden *)
Writeln ('Kann Daten nicht lesen '');
Halt;
end;
Write ('Wie groĂ ist die Zeitscheibe? ');
Readln (time_slice); Writeln;
(* Nun wird der Scheduler aufgerufen, der die
Bearbeitung der Tasks steuert *)
Write ('PID: ANKUNFT LĂNGE: ');
Writeln (' PRIORITĂT: ');
Write (' ============================') ;
Writeln (' ==========');
Writeln;
schedule_rr (s, time_slice);
Readln;
End. (* Main *)
(*
Programm : Scheduling-Simulator
Modul : Multi_Level_Queue.pas
Funktion : ProzeĂverarbeitung nach dem MLQ-Verfahren
Edit : 14.04.91
Autor : Rolf Kotzian. Paderborn
Copyright (c) MAXON Computer 1991
*)
Program Multi_Level_Queue (input, output);
Uses Queue, Tasks;
(* Variablen-Vereinbarungen *)
Var dat : File of task;
t : task;
s : array[1..4] of Schlange;
time: array[1 .4] of integer;
Begin (* MAIN *)
ClrScr;
Writeln ('Scheduling-Simulator');
Write ('ProzeĂverarbeitung nach dem â);
Writeln ('Multi-Level-Queue Verfahren');
Writeln ('by Rolf Kotzian, Paderborn');
Writeln;
Writeln ('(c) 1991 MAXON Computer GmbH');
Writeln; Writeln;
Reset (dat, 'tasks.in');
if IOResult = 0 then
begin
(* leere Warteschlangen erzeugen *)
create_queue (s[1]);
create_queue (s[2]);
create_queue (s[3]);
create_queue (s[4]);
while not eof (dat) do
begin
(* die Prozessdaten werden aus
dem File gelesen, und in die
entsprechende Warteschlange
(abhÀngig von der PrioritÀt)
eingereiht... *)
Read (dat, t);
enq_queue (s[t.prior], t);
end;
end
else
begin
(* leider konnte das File 'tasks.in'
nicht gelesen werden... *)
Writeln ('Kann Daten nicht lesen !');
Halt;
end;
(* Nun werden die Zeitscheiben fĂŒr jede zu
bearbeitende Klasse festgelegt *)
Write ('Zeitscheibe der Klasse 1 ');
Readln (time[1]);
Write ('Zeitscheibe der Klasse 2 '),
Readln (time[2]);
Write ('Zeitscheibe der Klasse 3 ');
Readln (time[3]);
Write ('Zeitscheibe der Klasse 4 ');
Readln (time[4]);
(* Jede Klasse wird wiederum nach dem
Round-Robin Verfahren bearbeitet... *)
Write ('PID: ANKUNFT: LĂNGE: ');
Writeln (' PRIORITĂT ');
Write ('=============================');
Writeln ('===========');
Writeln;
schedule_rr (s[1], time[1]);
schedule_rr (s[2], time[2]);
schedule_rr (s[3], time[3]);
schedule_rr (s[4], time[4]);
Readln;
End. (* MAIN *)