Fspool: Spoolen (fast) ohne RAM-Verbrauch

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.

Nachladen

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.

Realisation

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.

Anwendung

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.

Anregungen

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

Joachim Dudel
Aus: ST-Computer 01 / 1992, Seite 92

Links

Copyright-Bestimmungen: siehe Über diese Seite