Ismael Ripoll
Über den Autor:
Promotion an der Universidad Politecnica de Valencia 1996.
Lehrt über Betriebssysteme im Departement DISCA.
Forschungesinteressen: Echtzeitprobleme und Betriebssysteme. Benutzt Linux seit 1994. Hobbies:
Trecking in den Pyrenäen, Skifahren und Elektonik.
|
Anfang dieses Jahres (1998) wurde ein neues Echtzeitbetriebssystem basierend auf Linux vorgestellt. KURT ist ein weiches (soft versus firm) Echtzeitsystem, d. h. es versucht, die Ausführungszeiten einzuhalten, aber falls irgendein Arbeitsschritt ein bißchen zu spät beendet wird, tritt nicht gleich eine Katastrophe ein. Die Realtime-Tasks von KURT können von allen Features von Linux Gebrauch machen, im Gegensatz zu denjenigen von RT-Linux. Die Verbesserungen, die an dem Kernel vorgenommen wurden, sind:
Die Realtime-Tasks sind Module, die dynamisch geladen werden.
Eine der charakteristischen Eigenschaften von KURT ist die Planungsstrategie, die es anwendet. Es wurde ein zyklischer Scheduler eingebaut. Diese Art von Scheduler basiert auf dem Gebrauch einer Tabelle, des Planes, in der jeder Planungsvorgang festgehalten wird: Zeitpunkt der Aktivierung, auszuführende Aufgabe und deren Dauer, usw. Die Tabelle wird während der Entwurfsphase des Systems aufgestellt, später während der Ausführung besteht die Aufgabe des Schedulers darin, die Tabelle der Reihe nach auszulesen und den Anweisungen zu folgen. Wenn er am Ende der Tabelle angekommen ist, fängt er am Tabellenanfang erneut mit der Ausführung an, daher der Ausdruck zyklische Planungsstrategie. Diese Methode des Scheduling hat viele Vorteile:
Es ist sehr einfach, den Scheduler einzubauen.
Er ist sehr effizient.
Wenn der Plan einmal aufgestellt ist, läßt sich sofort bestimmen, ob das System korrekt ist. (Es gibt Wissenschaftler, welche behaupten, daß dies die einzige Methode ist, das hundertprozentige Funktionieren eines Echtzeitsystems zu garantieren.) [Xu93].
Der Hauptnachteil rührt von der Schwierigkeit her, den Plan aufzustellen. Und jedesmal, wenn man einen der Parameter eines Tasks ändert, muß der Plan komplett von Neuem aufgestellt werden. Außerdem ist der Plan und damit der Speicherbedarf gewöhnlich sehr groß.
Vielleicht glauben viele, daß die Echtzeittechniken nur bei der NASA Anwendung finden, in ferngelenkten Raketen oder ähnlichen Dingen. Wenn dies vielleicht vor einigen Jahren auch zutreffend war, so hat sich die Situation heute sehr verändert und wird sich noch mehr ändern dank der zunehmenden Eingliederung der Informatik und der Elektronik in das tägliche Leben. Echtzeitsysteme sind im Alltag vor allem im Bereich der Telekommunikation und Multimediaanwendungen präsent. Zum Beispiel, wenn wir eine Sounddatei, die wir auf einem Medium gespeichert haben, abspielen wollen, muß das Programm kontinuierlich (besser gesagt periodisch) die Töne lesen, dekomprimieren und an die Soundkarte schicken. Wenn wir zur gleichen Zeit, in der wir die Aufnahme anhören, mit einem andern Programm, z. B. einem Texteditor, arbeiten oder einen Linuxkernel kompilieren, dann ist sicher, daß sich Ruhepausen einstellen, da der Prozessor damit beschäftigt ist, andere Aufgaben zu erfüllen. Wenn wir anstelle von Ton ein Video abspielen, treten Sprünge oder Schnitte im Bild auf. Diese Systeme werden weiche Echtzeitsysteme genannt (die Nichteinhaltung einer Zeitvorgabe verursacht keine große Katastrophe, aber doch eine Leistungsminderung).
Die Anwendungsmöglichkeiten von RT-Linux gehen über reine Echtzeitanwendungen hinaus. Mit RT-Linux können wir totale Kontrolle auf den PC ausüben (ich sage PC und nicht Computer, denn bis jetzt gibt es RT-Linux noch nicht für andere Plattformen), wie man es mit MSDOS machen kann. In einem Realtime-Task können wir auf alle Schnittstellen des PC's zugreifen, Interrupttreiber installieren, Interrupts zeitweise stillegen, d. h. wir können die Maschine 'abschießen', wie wenn es Windows wäre. Diese Möglichkeit ist sehr attraktiv für alle, denen es gefällt, kleine elektronische Geräte an den Computer anzuschließen.
Um RT-Linux zu verstehen und anwenden zu können, muß man über die dynamisch ladbaren Module von Linux Bescheid wissen. Matt Welsh hat einen ganzen Artikel geschrieben, in dem er sehr ausführlich alle Aspekte, Module betreffend, beschreibt.
Was ist das?Module sind "Teile des Betriebssystems" die sich im laufenden Betrieb einfügen und entfernen lassen. Wenn ein Programm kompiliert wird, das sich aus mehreren Quelltextdateien zusammensetzt, wird zunächst jede Datei für sich kompiliert, was eine Objektdatei ".o" ergibt, und dann werden alle miteinander verbunden, indem die Links aufgelöst werden. Damit entsteht eine einzige ausführbare Datei. Nehmen wir an, wir könnten die Objektdatei, die die Funktion main enthält, ausführen, und das Betriebssystem wäre fähig, die übrigen Dateien nur dann in den Speicher zu laden und zu verlinken, wenn sie gebraucht werden würden. Linux kann dies mit dem Kernel selbst tun. Wenn Linux bootet, wird nur die ausführbare Datei vmlinuz, die die absolut notwendigen Teile enthält, in den Speicher geladen. Im laufenden Betrieb können die Module dann selektiv geladen oder entfernt werden, je nachdem ob sie im jeweiligen Moment benötigt werden.
Die Module sind ein optionales Feature von Linux, das man bei der Kompilierung des Kernels frei wählen kann. Die Kernels aller Distributionen, die ich kenne, wurden mit aktivierter Moduloption kompiliert.
Man kann sogar neue Module schreiben und sie laden, ohne den Kernel neu kompilieren oder den Computer neu starten zu müssen.
Wenn das Modul einmal geladen ist, wird es ein Teil des eigentlichen Betriebssystems, weshalb
es von allen Funktionen des Kernels Gebrauch machen und auf alle Variablen und Datenstrukturen zugreifen kann.
der Modulcode mit höchster Priorität vom Prozessor ausgeführt wird. In der i386-Architektur wird er auf Niveau 0 (ring level 0) ausgeführt, womit er jede Art von Zugriff auf Eingang/Ausgang hat und privilegierte Anweisungen ausführen kann.
die Speicherung des Programmes wie der Daten direkt im physischen Speicher geschieht, mit dem kein Paging gemacht wird, oder Swapping, wie oft fälschlicherweise gesagt wird. Auf diese Weise kann nie ein Pagingfehler während der Ausführung auftreten.
Wie man sieht, hat ein dynamisch geladenes Modul an sich schon einige Echtzeiteigenschaften: es vermeidet Verzögerungen durch Paging-Fehler und hat Zugang zu allen Hardwareressourcen.
Wie werden sie erzeugt und angewandt?
#define MODULE #include <linux/module.h> #include <linux/cons.h> static int output=1; int init_module(void) { printk("Output= %d\n",output); return 0; } void cleanup_module(void){ printk("Adiós, Bye, Chao, Ovuar, \n"); } |
Zur Kompilierung verwenden wir folgende Parameter:
Die Option "-c" zeigt dem gcc, daß er stoppen soll, sobald die Objektdatei erzeugt worden ist und den Link-Schritt nicht mehr auszuführen hat. Das Ergebnis ist die Objektdatei beispiel1.o.
Der Kernel verfügt nicht über eine Standardausgabe, weshalb man die Funktion printf()...nicht verwenden kann. Dagegen bietet der Kernel als Alternative printk(), welche fast genauso funktioniert, außer, daß das Resultat in einen Ringpuffer geschrieben wird (kernel ring buffer). In diesen Puffer werden alle Messages des Kernels geschrieben, tatsächlich sind es die Messages, die wir beim Booten von Linux sehen. Zu jedem Zeitpunkt können wir den Inhalt mit dem Befehl dmesg betrachten, oder direkt die Datei /proc/kmsg anschauen.
Wie man sieht ist die Funktion main() nicht vorhanden, statt dessen wird die Funktion init_module() aufgerufen, welcher kein Parameter übergeben wird. Bevor das Modul entfernt wird, ruft man cleanup_module() auf. Um das Modul zu laden, führt man den Befehl insmod aus.
Wir haben gerade beispiel1 installiert und seine Funktion init_module() ausgeführt. Wenn wir die Resultate sehen wollen, geben wir folgendes ein:
Der Befehl lsmod ermöglicht, die Module aufzulisten, die gerade installiert sind:
Und schließlich entfernt rmmod ein Modul wieder aus dem Kernel:
Die Ausgabe von dmesg zeigt, daß die Funktion cleanup_module() ausgeführt wurde.
Nun müssen wir nur noch wissen, wie man einem Modul Parameter übergeben kann. Dies geht aber überraschend einfach. Man kann den globalen Variablen Werte zuweisen, indem man die Zuweisung als Parameter von insmod schreibt. Zum Beispiel:
Wir wissen jetzt schon alles Notwendige über Module, kehren wir zu RT-Linux zurück.
Erinnern wir uns, daß wir, um RT-Linux benutzen zu können, zuerst den Linuxkernel so vorbereiten mußten, daß er die Echtzeitmodule unterstützt. Dieser Vorgang wurde in der ersten Ausgabe dieser Serie beschrieben.
Man kann RT-Linux auf zwei unterschiedliche Arten anwenden:
Wie ein klassisches Echtzeitsystem mit einem Scheduler basierend auf festen Prioritäten.
Wie ein nackter PC, so ähnlich, wie man es in DOS machen kann: die Interrupts festhalten und die totale Kontrolle über den Computer übernehmen.
In diesem Artikel werde ich nur beschreiben, wie man RT-Linux entsprechend einem System mit festen Prioritäten einsetzt. Das Beispiel, das wir sehen werden, macht nichts Nützliches, es setzt lediglich ein Echtzeittask in Betrieb, nämlich eine einfache Schleife:
Beispiel2
#define MODULE #include <linux/module.h> #include <linux/kernel.h> #include <linux/version.h> #include <linux/rt_sched.h> RT_TASK task; void fun(int computo) { int loop,x,limit; limit = 10; while(1){ for (loop=0; loop<computo; loop++) for (x=1; x<limit; x++); rt_task_wait(); } } int init_module(void) { RTIME now = rt_get_time(); rt_task_init(&task,fun, 50 , 3000, 1); rt_task_make_periodic(&task, now+(RTIME)(RT_TICKS_PER_SEC*4000)/1000000, (RTIME)(RT_TICKS_PER_SEC * 100)/1000000); return 0; } void cleanup_module(void){ rt_task_delete(&task); } |
Um es zu kompilieren, verwenden wir folgende Befehlszeile:
Da dieses Programm ein Modul ist, ist der Ausgangspunkt die Funktion init_module(). Als erstes liest es den aktuellen Zeitpunkt und speichert ihn in einer lokalen Variablen; die Funktion rt_get_time() gibt die Zahl der RT_TICKS_PER_SEC zurück, die abgelaufen sind, seit der Computer gestartet wurde (in der aktuellen Einstellung hat RT_TICKS_PER_SEC den Wert 1.193.180, was eine Auflösung von 0.838 Mikrosekunden ergibt). Mit rt_task_init() wird die Struktur task initialisiert, doch der Task startet noch nicht. Das Hauptprogramm des zuvor erzeugten Task ist fun(), der zweite Parameter. Der folgende Parameter ist der Wert, der an den neuen Task übergeben wird, wenn seine Ausführung beginnt. Man beachte, daß fun() einen Parameter vom Typ int erwartet. Der folgende Parameter ist die Größe des Taskstapels. Da jeder Task eine eigene Ausführungsablauf hat, braucht er einen eigenen Stapel. Der letzte Parameter ist die Priorität. In diesem Fall, mit nur einem Task im System, kann jeder beliebige Wert für die Priorität gesetzt werden.
rt_task_make_periodic() macht den Task zu einem periodischen Task. Die Funktion erwartet zwei Zeitwerte als Parameter. Die erste Zeitangabe ist der absolute Zeitpunkt, zu dem er aktiviert wird und der nächste Parameter gibt den Zeitabstand zwischen den aufeinanderfolgenden Aktivierungen an.
Der Echtzeittask (Funktion fun()) ist eine Endlosschleife, in der es nur zwei Aktionen gibt: eine Schleife, die nur der Zeitverwaltung dient, und der Aufruf von rt_task_wait(). rt_task_wait() ist eine Funktion, die die Ausführung des Task, der sie aufruft, bis zur nächten Aktivierung unterbricht. Dann wird die nächste Anweisung nach rt_task_wait() ausgeführt. Beachte, daß ein periodischer Task nicht bei jeder Aktivierung von Anfang an ausgeführt wird, sondern daß der eigentliche Task stehen bleiben (wenn er seine Arbeit erledigt hat) und auf die nächste Aktivierung warten muß. Auf diese Weise kann man einen Task programmieren, der nur bei der ersten Aktivierung bestimmte Initialisierungsaufgaben erledigt.
Um beispiel2 ausführen zu können, muß man zuerst das Modul rt_prio_sched installieren, danach braucht man die Funktionen rt_task_make_pericodic(), rt_task_delete() und rt_task_init(). Die Funktion rt_get_time() ist in keinem Modul enthalten, sondern ist in den Kernel kompiliert, weshalb man nichts installieren muß, um sie verwenden zu können.
Da rt_prio_sched ein Systemmodul ist - es wurde bei der Kompilierung des Linuxkernels erzeugt und ins Verzeichnis /var/modules/2.0.33/ kopiert - kann man den Befehl modprobe anwenden, der eine einfachere Möglichkeit darstellt, Module zu laden (modprobe kann das benötigte Modul in verschiedenen Verzeichnissen suchen; siehe modprobe(1)).
Wenn alles gut gegangen ist, kann man mit dem Befehl lsmod feststellen, daß die zwei Module richtig geladen worden sind.
Also, jetzt haben wir schon ein Echtzeittask zum Laufen gebracht. Fällt Dir etwas auf? Wenn der Prozessor ein bißchen langsam ist, bemerkst Du vielleicht schon, daß Linux etwas gemächlicher läuft als sonst. Man kann die Zahl der Iterationen der Schleife von fun() erhöhen, indem man den dritten Parameter von rt_task_init() verändert. Eine Möglichkeit zu sehen, daß Linux weniger Prozessorzeit hat, ist das Programm ico. Die Zeit, die von den Echtzeittasks gebraucht wird, bewirkt, daß der Prozessor langsamer zu laufen scheint. Linux glaubt, daß alle seine Prozesse mehr Zeit für diesselbe Arbeit benötigten. Wenn jetzt die Rechenzeit des Tasks (die Zeit, die für die Ausführung aller Iterationen der Schleife notwendig ist) länger ist als 100 ms, scheint Linux, sich "aufzuhängen", denn es ist der Task im Hintergrund und der Echtzeittask beansprucht 100 % der Rechenzeit. In Wirklichkeit hat sich Linux NICHT aufgehängt, es hat einfach keine Prozessorzeit zur Verfügung.
In RT-Linux gibt es nur eine Kommunikationsform: Real-Time FIFO. Die Arbeitsweise ist den Pipes von Unix sehr ähnlich, die Kommunikation geschieht über einen Datenfluß ohne Struktur. Ein FIFO ist ein Bytes-Puffer mit fester Größe, auf den durch Lese- und Schreiboperationen zugegriffen werden kann.
Mit den FIFOs können Echtzeittasks sowohl unter sich als auch mit normalen Linuxtasks kommunizieren.
Aus der Sicht eines normalen Linuxprozesses ist ein FIFO eine spezielle (zeichenorientierte) Datei. Normalerweise ist sie in /dev/rtf0, /dev/rtf1, usw. Diese Dateien existieren in Linux nicht, weshalb man sie anlegen muß. Das kann man folgendermaßen machen:
Wenn man mehr FIFOs braucht, legt man sie einfach an: rtf4, rtf5 usw. Diese speziellen Dateien sind die Schnittstellen für einen Treiber des Betriebssystems, aber wenn es keinen Treiber gibt, nützt die Datei nichts. Der Versuch, eine spezielle Datei zu öffnen, für die das Betriebssystem keinen Treiber hat, wird fehlschlagen.
Aus der Sicht eines Echtzeittasks werden die FIFOs mittels spezieller Funktionen angesprochen:
rt_create(unsigned int fifo, int size): erzeugt ein FIFO mit einem Puffer der Größe size. Von da an existiert das Device, zu dem man von /dev/rtf[fifo] aus Zugang hat, und man kann es benutzen bis man es löscht.
rt_destroy(unsigned int fifo): löscht das FIFO und macht den Speicher frei.
rt_fifo_put(fifo, char *buf, int count): versucht count Bytes aus dem Puffer buf zu schreiben. Wenn im FIFO nicht genügend Platz ist, wird -1 zurückgegeben.
rt_fifo_get(fifo, char *buf, count): versucht count Bytes aus dem FIFO in buff einzulesen. Wenn nicht genügend Daten zu Verfügung stehen, wird -1 zurückgegeben.
Sehen wir nun ein Beispiel eines Systems, das von diesen Funktionen Gebrauch macht. Dieses Beispiel ist eine kleine Abänderung eines der Beispiele des RT-Linux-Pakets (sound):
Beispiel3
#define MODULE #include <linux/module.h> #include <linux/rt_sched.h> #include <linux/rtf.h> #include <asm/io.h> RT_TASK task; static int filter(int x){ static int oldx; int ret; if (x & 0x80) { x = 382 - x; } ret = x > oldx; oldx = x; return ret; } void fun(int dummy) { char data; char temp; while (1) { if (rtf_get(0, &data, 1) > 0) { data = filter(data); temp = inb(0x61); temp &= 0xfd; temp |= (data & 1) << 1; outb(temp,0x61); } rt_task_wait(); } } int init_module(void){ rtf_create(0, 4000); /* enable counter 2 */ outb_p(inb_p(0x61)|3, 0x61); /* to ensure that the output of the counter is 1 */ outb_p(0xb0, 0x43); outb_p(3, 0x42); outb_p(00, 0x42); rt_task_init(&task, fun, 0 , 3000, 1); rt_task_make_periodic(&task, (RTIME)rt_get_time()+(RTIME)1000LL, (RTIME)(RT_TICKS_PER_SEC / 8192LL)); return 0; } void cleanup_module(void){ rt_task_delete(&task); rtf_destroy(0); } |
Genau wie im Beispiel2 brauchen wir das Modul rt_prio_sched, aber hier benötigen wir auch das Modul rt_fifo_new, um die FIFOs verwenden zu können.
Es wird ein periodischer Echtzeittask mit einer Frequenz von 8192 Hz erzeugt. Dieser liest Bytes vom FIFO 0, und wenn er etwas findet, schickt er es an die Schnittstelle für den PC-Lautsprecher. Wenn wir eine Sound Datei im ".au"-Format nach /dev/rtf0 kopieren , können wir sie anhören. Man braucht nicht zu sagen, daß die Tonqualität miserabel ist, denn die Hardware des PC läßt nur ein Bit zur Erzeugung eines Signals zu. Im Verzeichnis testing/sound des Pakets befindet sich die Datei linux.au zum Ausprobieren.
Zur Kompilierung und Ausführung:
Man beachte, wie man das Programm cat dazu benutzen kann, um in jede beliebige Datei zu schreiben, sogar in spezielle, wie etwa Gerätedateien (unter /dev/). Man könnte auch den Befehl cp verwenden.
Um sehen zu können, wie die Echtzeit die Wiedergabequalität beeinflußt, muß man einfach ein Programm schreiben, das dasselbe, aber in einem Userprozeß macht:
#include <unistd.h> #include <asm/io.h> #include <time.h> static int filter(int x){ static int oldx; int ret; if (x & 0x80) x = 382 - x; ret = x > oldx; oldx = x; return ret; } espera(int x){ int v; for (v=0; v<x; v++); } void fun() { char data; char temp; while (1) { if (read(0, &data, 1) > 0) { data = filter(data); temp = inb(0x61); temp &= 0xfd; temp |= (data & 1) << 1; outb(temp,0x61); } espera(3000); } } int main(void){ unsigned char dummy,x; ioperm(0x42, 0x3,1); ioperm(0x61, 0x1,1); dummy= inb(0x61);espera(10); outb(dummy|3, 0x61); outb(0xb0, 0x43);espera(10); outb(3, 0x42);espera(10); outb(00, 0x42); fun(); } |
Dieses Programm wird wie ein normales Programm kompiliert:
Und zur Ausführung:
Um von einem normalen Linuxprogramm aus auf die Hardwareschnittstellen des Computers zugreifen zu können, muß man sich vom Betriebssystem die Berechtigung verschaffen. Dies ist eine grundlegende und notwendige Schutzvorkehrung, um zu verhindern, daß ein Programm z. B. direkt auf die Festplatte zugreifen kann. Der Befehl ioperm() zeigt dem Betriebssystem an, daß man auf eine bestimmte Kategorie von E/A-Adressen zugreifen will. Der Zugriff wird nur den Programmen gewährt, die mit Superuserrechten ausgeführt werden. Als weiteres Detail ist hervorzuheben, wie die Frequenz von 8192 Hz erzeugt wird, mit der der Ton ertönt. Wenn auch einen Befehl nanodelay() existiert, so hat dieser nur eine Auflösung von einer Millisekunde, weshalb man den Zeitablauf durch eine Warteschleife steuern muß. Die Schleife wird so eingestellt, daß sie mehr oder weniger auf einem Pentium 100 MHz funktioniert.
Versuche nun, beispiel4 zusammen mit dem Programm ico auszuführen. Wie hört es sich an? Hast du den Eindruck, daß Echtzeit zu etwas zu gebrauchen ist?
This website is maintained by Miguel Angel Sepulveda © Ismael Ripoll 1998 LinuxFocus 1998 |