In letzter Zeit hatte ich öfters die Situation, längere Texte Ausdrucken zu wollen, ohne meine Arbeit zu unterbrechen. Kein Problem, nehme ich halt einen Drucker-Spooler. Dummerweise brauchen die soviel RAM, wie der Text lang ist. Mein Hauptprogramm (z.B.TeX) benötigt aber fast den gesamten knappen Speicher (1MB) - Was tun?
Unter UNIX ist das alles gar kein Problem - man „druckt“ in ein File und läßt dieses von einem Hintergrundprogramm Stück für Stück ausdrucken. Das müßte doch auch unter TOS gehen! Wie „normale“ Drucker-Spooler unter TOS arbeiten, ist schon oft beschrieben worden (z.B. [1]): Man gibt ein Zeichen über den Centronics-Port aus und wartet, bis der Drucker wieder empfangsbereit ist, dann gibt man das nächste Zeichen aus usw. Woher weiß man, ob der Drucker gerade bereit ist? Ganz einfach, das teilt er uns über den busy(Beschäftigt)-Anschluß mit.
Der springende Punkt ist das Warten. TOS löst das so, daß es in einer Schleife den busy-Anschluß solange abfragt, bis er low (unwahr) ist. Damit ist der Prozessor die ganze Zeit (unter-)beschäftigt. Die bessere Lösung ist es, nach Ausgabe des Zeichens seine Routine abzubrechen (und somit den Prozessor für andere Aufgaben frei -zugeben). Damit man seine Routine jemals wieder fortsetzen kann, aktiviert man einen Interrupt.
Das sind Bedingungen, die das gerade laufende Programm unterbrechen und es an einer der Bedingung zugeordneten Stelle fortsetzen. Meistens wird dort auf die Bedingung (z.B. eine Taste wurde gedrückt) geeignet reagiert (z.B. das Zeichen auf dem Bildschirm darstellen).
Danach wird das unterbrochene Programm wieder fortgesetzt. Glücklicherweise gibt es auch einen Interrupt für das busy-Signal, er wird allerdings bis jetzt nicht verwendet.
Diese Methode kann man jetzt einfach erweitern: Man lädt ein Stück des Files in einen Zwischenpuffer (z.B. 4KB), druckt die Zeichen in der gerade beschriebenen Weise aus, lädt das nächste Stück usw.
Leider ist es nicht ganz so einfach (dann hätte ich mein Programm wohl nicht selber schreiben müssen). Es könnte ja sein, daß das Hauptprogramm gerade auf die Festplatte zugreift, wenn ich es unterbreche, um selber auf die Festplatte zuzugreifen (um das nächste Stück nachzuladen). Das TOS, das die Zugriffe „organisiert“, ist dann verständlicherweise etwas verwirrt und bleibt hängen. Dummerweise tritt diese Situation erstaunlich häufig ein und ist in keiner Weise zu tolerieren.
Wie löst z.B. UNIX dieses Problem? Unter UNIX darf immer nur ein Programm auf die Festplatte, den Drucker oder andere „Geräte“ zugreifen. Sobald eine Routine z.B. die Platte benutzt, setzt sie eine sogenannte Semaphore (Schalter). Solange die Semaphore für die Platte gesetzt ist, darf kein anderes Programm auf sie zugreifen, es muß warten, bis sie wieder frei ist. Sobald die Routine die Platte nicht mehr braucht, gibt sie die Semaphore wieder frei - das nächstbeste Programm setzt sie wieder, um die Platte zu verwenden (in [2] wird die Verwaltung von „Geräten“ un-terUNIX ausführlich beschrieben).
Wir bräuchten also unter TOS eine Semaphore für die Festplatte. Da TOS Multitasking nicht unterstützt, hat man kaum Hoffnung, daß es so etwas gibt. Aber es existiert die Systemvariable flock (floppy-lock, „Disketten-Schloß“). Sie regelt den Zugriff auf die DMA-Einheit und den Disketten-Controller (siehe auch [3]).
Leider wird sie etwas zu spät gesetzt - Die „Organisation“ durch das TOS erfolgt direkt davor und kann also immer noch gestört werden. Da bleibt dann nur noch eine etwas ungewöhnliche „Semaphore“, der Supervisor-Modus. In diesen geht der Prozessor, wenn ein Interrupt oder ein Trap ausgeführt wird - und Betriebssystemaufrufe sind Traps. Der Supervisor-Modus schaltet also öfter als eigentlich nötig, mir fiel aber nichts besseres ein. Jetzt bräuchte man nur noch einen Interrupt auf den Supervisor-Modus - und den gibt es nicht (das wäre ja auch eine Endlosschleife). Es gibt aber den Timer-Interrupt, der alle 5ms auftritt, und da kann man dann feststellen, ob der Prozessor gerade im Supervisor-Modus war (im Timer-Interrupt ist er natürlich drin). Wenn wir auf die Festplatte zugreifen wollen, gehen wir also so vor: Wir aktivieren den Timer-Interrupt und übergeben die Kontrolle wieder an das Hauptprogramm. Alle 5ms wird unser Timer-Interrupt aufgerufen. Da testen wir, ob gerade der Supervisor-Modus gesetzt war. Dann tun wir lieber gar nichts (bzw. führen den „normalen“ Timer-Interrupt durch), weil es dann möglich ist, daß gerade eine Betriebssystemroutine auch auf die Platte zugreifen will. War der Supervisor-Modus nicht gesetzt, können wir bedenkenlos mit der Platte „rumspielen“.
Ein Nachteil dieser Methode ist es, daß uns jede TOS-Routine von der Festplatte fernhält, und die sind manchmal recht lang (die Zeicheneinleseroutine cconrs kann Stunden dauern, wenn keiner auf die Idee kommt, mal die Return-Taste zu drücken). Dafür stören wir mit Sicherheit keine kritischen Routinen, da diese immer im Supervisor-Modus arbeiten.
Nach der theoretischen Abhandlung nun zum realen Programm (Listing 1). Da viel im System gebastelt wird und es kurz und schnell sein muß, ist es natürlich in Assembler geschrieben.
Am Anfang wird die Spooler-Routine auf den trap #0 gelegt. Falls dieser aus irgend einem Grund nicht mehr frei sein sollte, kann man natürlich einen anderen nehmen. Nummer 1 (GEMDOS), 2 (GEM), 9 (SIGNUM!), 13 (BIOS) und 14 (XBIOS) sollte man nicht belegen. Über diesen Trap wird später vom Benutzer der Spooler ähnlich einer Betriebssystemroutine aufgerufen.
Jetzt ist das Programm auch schon zu Ende, es beendet sich, löscht sich aber nicht im Speicher, durch ptermres. Die Spooler-Routine „schläft“ jetzt, bis sie durch trap #0 „aufgeweckt“ wird. Es erscheint eigentlich logischer, daß man den Spooler erst dann installiert, wenn man ihn braucht. Das hat aber Nachteile:
Dann kommt die trap #0-Routine. Sie testet zuerst, ob schon ein Spool-Prozeß im Gange ist (Variable flag) - dann ignoriert sie die Aufforderung zum Drucken. Sonst installiert sie den busy-Interrupt, läßt ihn aber erstmal inaktiv. Danach setzt sie den Zeiger, der die Position in der Datei angibt (count), auf Null (Dateianfang). Bevor sie den Timer-Interrupt „umbiegt“, um den Supervisor-Modus feststellen zu können, rettet sie ihre Register in einen festen Speicherbereich. Nach dem „Umbiegen“ hört die Routine scheinbar auf (rte), sie lauert aber nur auf den Timer-Interrupt!
Ist dieser erfolgt, testet sie auf Supervisor-Modus: Bei dem Interrupt wird das Prozessor-Status-Wort auf dem Stack abgelegt, Bit # 13 von diesem zeigt den Supervisor-Modus an. Ist dieser gesetzt, springen wir in die alte Timer-Interrupt-Routine - der nächste Interrupt ohne Supervisor-Modus kommt bestimmt.
Falls Sie den Sprung im Listing nicht finden, die Befehlsfolge
move.l adr,-(sp)
rts
entspricht einem indirekten Sprung über adr [Bei anderen Prozessoren schreibt man einfach jmp(adr)].
Befand sich der Prozessor im User-Modus (Gegenteil von Supervisor-Modus), wird der Timer-Interrupt wieder „zurückgebogen“ und wieder frei gegeben (siehe Listing). Davor wurden die Register ausgetauscht: Die Register des Hauptprogramms werden gerettet und die alten der Spooler-Routine wiederhergestellt.
Jetzt folgt zur Abwechslung mal ein recht konventionelles Programmstück: Die Datei SPOOL.TSK auf Laufwerk C: wird geöffnet, auf die richtige Stelle positioniert, und ein 4KB langer Block wird eingelesen. Laufwerk C: deshalb, weil das jeder hat, der eine Festplatte besitzt (eine RAM-Disk wäre etwas pervers, dann kann man ja gleich einen normalen Spooler nehmen). Das Wurzel Verzeichnis ist am schnellsten zu finden, und wir suchen unsere Datei immerhin alle paar Sekunden ...
Spätestens jetzt sollte klar sein, daß ein Betrieb mit Diskette recht nervenaufreibend wäre. Rein technisch wäre es kein Problem. Das ständige Surren des Laufwerks und die Einschränkung, daß man die Diskette nicht wechseln darf, verbreitet aber schnell nur noch Frust.
Im Falle eines Fehlers wird der Spool-Prozess ordnungsgemäß beendet: busy-Interrupt abstellen usw. So kann man einen Druckabbruch z.B. durch Löschen der Datei SPOOL. TSK problemlos provozieren.
Jetzt wird endlich der busy-Interrupt aktiviert und das erste Zeichen an den Drucker geschickt. Zur Technik des Centronics-Ports und seiner Bedienung verweise ich mal wieder auf [3]. Danach wird auf die selbe Weise, mit der wir vorher auf den Timer-Interrupt gelauert haben, auf den busy-Interrupt gewartet.
Der Rest des Programms schließt die beiden Schleifen -Zeichen an den Drucker senden und File-Blöcke nachladen.
Die Programme wurden mit dem DEVPAC-Assembler entwickelt, sie können aber nach wenigen Änderungen mit jedem beliebigen Assembler verarbeitet werden. Wer zu faul zum Abtippen ist, kann sich die Leserdiskette besorgen.
Den Drucker-Spooler setzt man am besten in den Auto-Ordner, so ist sichergestellt, daß er den Speicher nicht unnötig zerstückelt. Die Daten, die ausgedruckt werden sollen, schreibt man unkodiert in die Datei SPOOL.TSK im Wurzelverzeichnis von C:.
Gute Programme haben die Möglichkeit, den Ausdruck in eine Datei umzuleiten. Falls dies nicht möglich ist, schaut man meistens ziemlich in die Röhre - diese Programme greifen fast immer direkt auf den Centronics-Port zu und lassen einem so keine Chance, die Daten abzufangen und umzuleiten.
Den Ausdruck starte man entweder mit dem Programm FSPOOL.PRG (Listing 2) oder in eigenen Programmen durch trap #0. Will man den Ausdruck stoppen, entferne man die Datei SPOOL.TSK, nenne sie um oder löse einen Reset aus.
Zum Abschluß möchte ich noch ein paar Anregungen geben, wie man meinen Spooler komfortabler als durch mein „Minimal-Programm“ aufrufen kann:
Der Schlüssel zu diesen Erweiterungen ist immer der trap #0. Durch diesen wird der Ausdruck gestartet.
Literatur:
[1] Markus Kraft, HC-FIX, ST-Computer 12/89
[2] Andrew S. Tanenbaum, Operating Systems, Design and Implementation, Prentice-Hail
[3] Jankowski, Reschke, Rabich, ATARI ST-Profibuch, Sybex-Verlag, 1988
* fspool.prg
* Minimal-Programm zum Starten des Spoolers
trap #0 ; Spooler starten
clr -(sp) ; ptermO
trap #1 ; und das war’s!
* fspooler.prg DEVPAC-Ass.
* Druckerspooler über File
* (c)'91 by MAXON Computer GmbH
* written by Joachim Dudel
* text-segment
resanf pea initsp(pc) ; Routine initsp..
move #$20,-(sp) ; auf TrapO legen
move #5,-(sp) ; setexc
trap #13 ; bios
addq.l #8,sp
move.l d0,oldtrap ; TrapO retten
pea text(pc) ; (c) ausgeben
move #9,-(sp) ; cconws
trap #1 ; gemdos
addq.l #6,sp
clr -(sp) ; Fehlercode = 0
move.l #(resend-resanf+$100),-(sp)
move #49,-(sp) ; ptermres
trap #1 ; gemdos
* Das Programm ist beendet, bleibt aber im
* Speicher und wartet auf TrapO...
dc.b "XBRADSpF" ; XBRA-Kennung
oldtrap ds.1 1 ; alter TrapO
* Hier springt TrapO hin
initsp tst flag ; schon in Betrieb?
beq.s cont
rte ; ja -> zurück!
cont st ; flag jetzt in Betrieb
move.l $100.w,oldbusy ; busy-int. retten
bclr.b #0,$fa03.w ; aktive-edge
move.l #busy,$100.w ; busy-int. inst,
bset.b #0,$fa15.w ; entmaskieren
clr.l count ; File-Zeiger = 0
* Schleife zum 4kByte Blöcke einiesen & ausgeben
loop bclr.b #0,$fa09.w ; busy deaktivieren
* Register austauschen
* (d0-d2 und a0-a2 werden von Traps zerstört)
movem.l d0-d2/a0-a2,savesp
movem.l save(pc),d0-d2/a0-a2
move.l $114.w,old200 ; 200Hz-Int. retten
move.l #load,$114.w ; eigene Routine
rte ; warten auf Userm.
* jetzt kommt das Hauptprogramm wieder dran...
dc.b "XBRADSpF" ; XBRA-Kennung
old200 ds.l 1 ; alter 200Hz-Int.
* Hier landet der umgeleitete 200Hz-Int.
load btst.b #5,(a7) ; Supervisormodus?
beq.s lcont ; nein -> lcont
move.l old200(pc),-(sp) ; sonst.,
rts ; ..alter 200HZ-I.
lcont movem.l d0-d2/a0-a2,save ; Register-
movem.l savesp(pc),d0-d2/a0-a2 ; Wechsel
* 200Hz wiederherstellen
move.l old200(pc),$114.w
* in Service(200Hz) löschen
move.b #%11011111,$fall.w
move sr,d0 ; Status-Register
* Interrupt-Priorität auf 5 setzen
and #%1111100011111111,d0
or #%0000010100000000,d0
move d0,sr ; Status-Register
* so können wieder 200Hz-Int. auftreten -
* die braucht GEMDOS beim laden
clr -(sp) ; nur lesen
pea fname(pc) ; 'C:\spool.tsk'
move #61,-(sp) ; fopen
trap #1 ; gemdos
addq.l #8,sp
move d0,handle ; file-handle
bmi ende ; Fehler -> ende
clr -(sp) ; ab Datei-Anfang
move handle(pc),-(sp)
move.l count (pc),-(sp) ; Position
move #66,-(sp) ; fseek
trap #1 ; gemdos
lea $a(sp),sp
tst.l d0 ; alles o.k.?
bmi ende ; nein -> ende
pea puffer(pc) ; 4kB-Puffer
move.l #$1000,-(sp) ; nächste 4kByte
move handle(pc),-(sp)
move #63,-(sp) ; fread
trap #1 ; gemdos
lea $c(sp),sp
move.l d0,length ; eingelesene Bytes
move handle(pc),-(sp)
move #62,-(sp) ; fclose
trap #1 ; gemdos
addq.l #4,sp
move.l length(pc),d0
subq.l #1,d0 ; d0: Puffer-Zähler
bmi ende ; Datei schon leer
add.l #$1000,count ; 4kB weiter merken
lea puffer(pc),a0 ; a0: Puffer-Zeiger
lea $8800.w,a1 ; a1: PSG-Adressen
lea $8802.w,a2 ; a2: PSG-Daten
bset.b #0,$fa09.w ; busy-int. aktiv
* Schleife für einzelne Zeichen Ausgeben
cent move.b #15,(a1) ; portB wählen
move.b (a0)+,(a2) ; Byte auf Centr.
move.b #14,(a1) ; portA wählen
move.b (a1),d1 ; d1: portA
bclr.b #5,d1 ; strobe low
move.b d1,(a2) ; senden
bset.b #5,d1 ; strobe high
move.b d1,(a2) ; senden
movem.l d0-d2/a0-a2,savesp ; Register-
movem.l save(pc),d0-d2/a0-a2 ; Wechsel
* in-service(busy) löschen & auf busy-int. warten
move.b #%11111110,$fall.w
rte
* solange kann das 'Hauptprogramm1 weiterlaufen.
dc.b "XBRADSpF" ; XBRA-Kennung
oldbusy ds.l 1 ; alter busy-Vektor
* Einsprung für busy-int.
busy movem.l d0-d2/a0-a2,save ; Register-
movem.l savesp(pc),d0-d2/a0-a2 ; Wechsel
dbra d0,cent ; nächstes Byte
* Puffer ist leer, also wieder nachladen
bra loop
* Routine sauber beenden
ende bclr.b #0,$fa09.w ; busy-int. deakt.
move.l oldbusy(pc),$100.w
clr ; flag nicht in Betrieb
movem.l save(pc),d0-d2/a0-a2 Register
rte ; warten auf TrapO
* data-segment
text dc.b 13,10,"Drucker-Spooler "
dc.b "installiert",13,10
dc.b "(c)'91 by MAXON Computer GmbH"
dc.b 13,10
dc.b "written by Joachim Dudel"
dc.b 13,10,0
even
fname dc.b "C:\SPOOL.TSK",0 ; Dateiname
even
* bss-segment
handle dsbss 1 ; File-Handle
count dsbss.l 1 ; Datei-Position
length dsbss.l 1 ; Datei-Länge
flag dsbss 1 ; 'in Betrieb'
* Register-Puffer (aO-a2/dO-d2) ; für
save dsbss.l 6 ; Hauptprogramm
savesp dsbss.l 6 ; Interrupt
puffer dsbss.b $1000 ; Spool-Puffer
resend dsbss 0 ; Programmende