Ein Computerterminal ist kein klobiger alter Fernseher, vor dem eine Schreibmaschinentastatur liegt. Es ist eine Schnittstelle, an der sich Körper und Geist mit dem Universum zusammenschalten und Teile davon durch die Gegend bewegen können. Douglas AdamsDie verfügbare Hardware für LINUX-Rechner ist vielfältig. So wird auch vielfältige Software verlangt, um diese Hardware anzusprechen. Gerätetreiber sind also das Salz in der Suppe eines jeden Betriebssystems.
In UNIX-Systemen soll zudem die Hardware des Rechners vor dem Anwender versteckt werden, ohne daß ihre Funktionalität eingeschränkt wird. Dies geschieht, indem physische Geräte durch Dateien repräsentiert werden. So kann man portable Programme entwickeln, die sowohl auf verschiedene Geräte als auch auf Dateien mit denselben Systemrufen, wie etwa read() und write(), zugreifen können. Zu diesem Zweck sind in den LINUX-Kern Gerätetreiber integriert, die die alleinige Kontrolle über die Hardware ausüben.
Ist demnach ein Gerätetreiber fehlerfrei implementiert, kann das entsprechende Gerät durch den Anwender nie falsch benutzt werden. Die Schutzfunktion der Gerätetreiber ist also nicht zu unterschätzen.
In diesem Kapitel soll es uns deshalb darum gehen, die Funktionsweise und die richtige Implementation von Gerätetreibern zu zeigen. Als Beispiel sei der PC- Speaker-Treiber gewählt, der die Ausgabe von Sound-Samples auf dem internen Lautsprecher oder einem Digital-Analog-Wandler an der parallelen Schnittstelle unterstützt. Zusätzlich soll er noch zum Soundkartentreiber kompatibel sein.
Da im LINUX-Kern mehrere Gerätetreiber nebeneinander existieren müssen, werden sie anhand der Major-Nummer eindeutig identifiziert. Ein Gerätetreiber kann mehrere physische und virtuelle Geräte, zum Beispiel mehrere Festplatten und Partitionen, verwalten. Deshalb wird das einzelne Gerät durch die Minor-Nummer, eine Zahl zwischen 0 und 255, angesprochen.
Die Ausnahme dieser Regel ist hierbei der Gerätetreiber für Terminals und serielle Schnittstellen. Er nutzt die zwei Major-Nummern 4 und 5. Die Geräte mit der Major-Nummer 4 sind die virtuellen Konsolen, die einfachen seriellen Schnittstellen (Call-In-Geräte) und die Pseudoterininals[1]. Dabei besitzen die virtuellen Konsolen die Minor-Nummern 0 für tty0 bis 63[2]. Das spezielle Gerät /dev/tty0 bzw. /dev/console entspricht dabei der jeweils aktuellen virtuellen Konsole.
Für jede serielle Schnittstelle gibt es zwei logische Geräte, die Dial-In-Geräte ttySn und die Call-Out-Geräte cuan. Ein Prozeß, zum Beispiel getty, wird beim Öffnen des Dial-In-Geräts solange blockiert, bis die DTR-Leitung der Schnittstelle aktiv wird. Ein das Call-Out-Gerät öffnender Prozeß, meist ein herauswählendes Programm, erhält sofort Zugriff auf die serielle Schnittstelle, wenn sie von keinem anderen Prozeß genutzt wird. Damit wird ein Prozeß, der das Dial-In-Gerät öffnen wollte, auch weiterhin blockiert. Die seriellen Dial-In-Geräte erhalten die Minor-Nummern 64 für ttyS0 bis 127.
Die verbleibenden Minor-Nummern von 128 bis 255 werden für Pseudoterminals genutzt. Das Master-Pseudoterminal ptyn hat dabei die Minor-Nummer 128+n, während das zugehörige Slave-Terminal ttypn die Minor-Nummer 192+n besitzt.
Die Major-Nummer 5 ist für das aktuelle Terminal sowie für die Call-Out-Geräte reserviert. Das Gerät /dev/tty mit der Minor-Nummer 0 entspricht immer dem dem Prozeß zugehörigen Terminal. Die Call-Out-Geräte cuan haben die entsprechenden Minor-Nummem 64+n und unterscheiden sich somit nur durch die Major-Nummer von ihren "Zwillingen".
Die zur Zeit fest vergebenen Major-Nummern können Tabelle 7.1 entnommen werden. Bei der Entwicklung eigener Treiber sollte man zunächst eine noch nicht benutzte Major-Nummer ermitteln. Bei einer späteren Veröffentlichung des eigenen Treibers sollte er beim LlNUX Device Registrar[3] registriert werden. Dieser vergibt dann eine offizielle Major-Nummer, die garantiert von keinem anderen Gerätetreiber verwendet wird. So erhielt der PC-Speaker-Treiber die Major-Nummer 30.
Major Gerätetyp 0 unnamed für das proc-FS, NFS, usw. 1 b/c Speichergeräte (rd* / mem, null,...) 2 b Disketten(fd*) 3 b IDE-, MFM- und RLE-Festplatten (hd*) 4 c Terminals (pty*, tty?*) 5 c Terminals(tty, cua*) 6 c Parallele Schnittstellen (lp *) 7 b Bandgeräte (nicht SCSI) ungenutzt 8 b SCSI-Festplatten (sd*) 9 c SCSI-Tapes (<n>st*) 10 c Busmäuse (bin, psaux) 11 b SCSI-CD-ROM (scd*) 12 b QIC02-Tape (rmt*, tape*) 13 b Festplatten an XT-8-Bit-Controler (xd*) 14 c Soundkarten (audio, dsp,...) 15 b/c Cdu31a CD-ROM / Joystick (js*) 16,17,18 c Netzwerk, AF_UNIX, AF_NET 21 b SCSI Generic 22 b 3. und 4. IDE-Festplatte 23 b Mitsumi CD-ROM (mcd*) 24 b Sony535 CD-ROM 25 b Matsushita CD-ROM (sbpcd*) 27 c QIC117-Tape 30 c PC-Speaker-Treiber (pcaudio,pcsp,...) 31 c Link-Interface Tabelle 7.1: Die LINUX-Major-Liste[1]Pseudoterminals sind Gerätepaare von Master- und Slaveterminals, die zusammen wie eine Terminaleinheit agieren. Das Slave-Terminal ist das Interface, das sich wie eine Terminaleinheit gegenüber einem Nutzerprogramm verhält, während der Master die Gegenseite (beim Terminal den Nutzer) repräsentiert (siehe [Ste92b]).
Als Blockgeräte werden Geräte bezeichnet, auf die man wahlfreien Zugriff hat, d.h.von denen beliebige Blöcke gelesen und geschrieben werden können. Unter LINUX werden diese Lese- und Schreibzugriffe transparent vom Cache abgewickelt. Für Dateisysteme ist der wahlfreie Zugriff unbedingt erforderlich. Sie können deshalb nur auf Blockgeräte untergebracht werden.
Zeichenorientierte Geräte[4] wiederum sind Geräte, die meist nur sequentiell arbeiten und auf die deshalb ungepuffert zugegriffen wird. In diese Klasse fällt die gebräuchlichste Hardware, wie etwa Soundkarten, Scanner, Drucker usw., auch wenn sie intern mit Blöcken[5] arbeiten. Diese Blöcke besitzen jedoch sequentielle Natur. Auf sie kann auch nicht wahlfrei zugegriffen werden.
Ansonsten weicht LINUX hier etwas von der allgemeinen UNIX-Philosophie ab, da hier keine so strikte Trennung von Block- und Zeichengeräten vorgenommen wird. So existieren in anderen UNIX-Systemen zu den Blockgeräten korrespondierende Zeichengeräte, d.h. zeichenorientierte Schnittstellen zu Blockgeräten, die hauptsächlich zur Steuerung[6] des eigentlichen Geräts benutzt werden. In LINUX ist die Schnittstelle (VFS) der Block- und Zeichengeräte gleich, weshalb keine zusätzlichen Zeichengeräte benötigt werden.
Ein einzelnes Gerät ist demnach eindeutig durch den Gerätetyp (Block- oder Zeichengerät), die Major-Nummer des Gerätetreibers und seine Minor-Nummer identifiziert. Das Anlegen eines Geräts geschieht deshalb einfach durch:
# mknod /dev/name type major minormit dem Gerätetyp type b oder c.
Will man zusätzliche Hardware unter LINUX ansprechen, so wird man demnach in der Regel einen Zeichengerätetreiber entwickeln, da die zeichenorientierte Hardware in der Überzahl ist.
[4]im folgenden einfach als Zeichengeräte bezeichnet
[5]Sollen größere Datenmengen übertragen werden,
ist ein Blocktransfer günstiger, wie z.B. beim DMA-Betrieb.
[6]Kontrollprogramme von Blockgeräten bei anderen UNIX-Systemen,
wie etwa mkfs oder fsck, operieren auf dem entsprechenden
zeichenorientierten Raw-Device.
Der Gerätetreiber für die parallele Schnittstelle arbeitet standardmäßig im Pollingbetrieb (siehe Abschnitt 2.3). So fragt er die Schnittstelle (in diesem Fall den Statusport der Schnittstelle) solange ab, bis sie ein weiteres Zeichen lpchar entgegennehmen will und übergibt dann erst das Zeichen an die Schnittstelle. Dieses Vorgehen hat in den Quellen folgendes Aussehen:
#define LP_B(minor) lp_table[(minor)].base /* IO address */ #define LP_S(minor) inbp(LP_B((minor)) + 1) /* status port */ #define LP_CHAR(minor) lp_tab1e[(minor)].chars /* busy timeout */ static int lp_char_polled(char lpchar, int minor){ int status=0; unsigned long count=0; do{ status=LP_S(minor); count++; if (need_resched) schedule(); }while(!(status & LP_PBUSY) && count < LP_CHAR(minor)); ... outb_p(lpchar, LP_B(minor)); ... return 1; }Das Mitzählen der Abfragen gilt dem Erkennen eines Fehlers des Datenendgeräts (das ist in den meisten Fällen ein Drucker). Es entspricht dem Timeout und bedeutet, daß das letzte Zeichen nicht gesendet wurde. Die weitere Behandlung dieses Timeouts führt dann zu den Meldungen: "lpn off-line", "lpn out of paper" oder "lpn on fire". Die Anzahl LP_CHAR(minor) ist standardmäßig auf LP_INIT_CHAR (1000) gesetzt und kann mittels ioctl() geändert werden.
So wird ein Prozeß, der auf die parallele Schnittstelle im Interruptbetrieb schreiben will, vom Gerätetreiber im Interruptbetrieb nach dem Schreiben eines Zeichens mit der Funktion
interruptible_sleep_on(&lp->lp_wait_q);angehalten. Kann die parallele Schnittstelle weitere Zeichen entgegennehmen, löst sie einen IRQ aus. Die behandelnde ISR weckt den Prozeß daraufhin wieder und der Vorgang wiederholt sich. Die ISR ist dabei sehr einfach gehalten.
static void lp_interrupt(int irq){ struct lp_struct *lp &lp_table[0]; struct lp_struct *lp_end = &lp_table[LP_NO]; while (irq != lp->irq){ if (++lp >= lp_end) return; } wake_up(&lp->lp_wait_q); }Zuerst wird die Schnittstelle ermittelt, die den Interrupt auslöste, und danach der wartende Prozeß mit wake_up () "wachgeküßt".
Ein weiteres Beispiel ist die serielle Maus, die bei jeder Bewegung Daten an den seriellen Port überträgt, der einen IRQ auslöst. Erst die behandelnde ISR liest die Daten aus dem seriellen Port aus und stellt sie dem Anwendungsprogramm zur Verfügung.
Wie in Abschnitt 3.1.10 erwähnt, gibt es unter LINUX zwei Möglichkeiten der IRQ-Bearbeitung. Langsame IRQs werden mit Hilfe der Funktion
int request_irq(unsigned int irq, void (*handler) (int))installiert. Sie liefert 0 zurück, falls der IRQ frei war und belegt werden konnte.
Im Gegensatz zu dieser Definition wird der ISR als Argument jedoch ein Zeiger auf die Struktur pt_regs als Argument übergeben. Diese Struktur enthält alle Register des Prozesses, der durch den IRQ unterbrochen wurde. Auf diese Weise stellt z.B. der Timerinterrupt fest, ob ein Prozeß im Kern- oder im Nutzermodus unterbrochen wurde und zählt die jeweilige Zeit für die Abrechnung hoch. Die Behandlungsroutine eines langsamen IRQs hat also folgendes Aussehen.
void do_irq(struct pt_regs *regs);Langsame Interrupts laufen mit zugeschaltetem Interruptflag, d.h. sie können wiederum durch andere Interrupts unterbrochen werden. Am Ende eines langsamen Interrupts läuft derselbe Algorithmus wie beim Beenden eines Systemrufs ab.
Die zweite Art von IRQs sind die schnellen IRQs. Um diese Klasse von ISRs zu installieren, wird die Funktion
int irqaction(unsigned int irq, struct sigaction *new)benutzt. Sie ist eine allgemeinere Variante[7] der Funktion request_irq() und hat ihren Ursprung in der Überlegung, daß IRQs und Signale ähnlich zu behandeln sind. Aus der Struktur
typedef void (*__sighandler_t) (int); struct sigaction{ __sighandler_t sa_handler; sigset_t sa_mask; int sa_flags; void (*sa_restorer) (void); };werden nur die beiden Komponenten sa_handler und sa_mask benutzt. Ist in der Maske sa_mask das Flag SA_INTERRUPT gesetzt, wird die ISR als schneller Interrupt installiert, sonst wie oben beschrieben als langsamer Interrupt.
Einem schnellen Interrupt wird als Integer-Argument beim Aufruf der Funktion sa_handler jeweils die Nummer des IRQs mitgegeben. Man kann also eine ISR für mehrere IRQs benutzten. Schnelle Interrupts laufen außerdem mit abgeschaltetem Interruptflag. Will man in seiner Interruptroutine also auch andere Interrupts zulassen, muß ein Aufruf des Makros
sti();erfolgen.
Ein Beispiel soll die Installation eines schnellen Interrupts, d.h. des oben beschrieben lp_interrupt, zeigen.
sa.sa_handler = lp_interrupt; sa.sa_flags = SA_INTERRUPT; sa.sa_mask = 0; sa.sa_restorer = NULL; ret = irqaction(irq, &sa); if (ret){ printk("unable to use interrupt %d, error %d\n", irq, ret); return ret; }Normalerweise wird man also für die Kommunikation mit der Hardware schnelle Interrupts verwenden.
[7]Genaugenommen ruft request_irq() die Funktion irqaction() auf.
Für die Installation eines Bottom Halfs existiert keine Funktion im Kern; man muß sie selbst in die Tabelle bh_base eintragen.
struct bh_struct{ void (*routine) (void *); void *data; }; struct bh_struct bh_base[32]; enum TTMER_BH = 0, CONSCLE_BH, SERIAL_BH TTY_BH, INET_BH, KEYBOARD_BH };Wie aus der Definition der Struktur bh_struct folgt, kann man außerdem dem Bottom Half als Argument einen Zeiger auf beliebige Daten übergeben (data). Standardmäßig sind alle Bottom Halfs zugelassen, sie können aber auch mit den Funktionen
void disable_bh(int nr); void enable_bh(int nr);ab- und wieder zugeschaltet werden. Die Funktion
void markbh(int nr);dient zum Markieren eines Bottom Halfs, d.h. dieser Bottom Half wird zum nächstmöglichen Zeitpunkt abgearbeitet.
Betrachten wir nun die Verwendung eines Bottom Halfs.
Als Beispiel sei hier der Keyboard-
Installiert werden Bottom Halfs durch ein schlichtes
Außerdem muß man beim DMA-Betrieb von Geräten mit Problemen ganz
anderer Art kämpfen, die zum Teil aus der Kompatibilität zum Ur-PC stammen.
Seit den Urzeiten des PCs schon vorhanden, ist der PC-Speaker aufgrund seines
Designs nicht gerade gut zur Ausgabe von Samples geeignet.
Wie Abbildung 7.1 zeigt, ist sowohl der Aufbau als auch die Programmierung
des Speakers sehr einfach.
Um ein analoges Signal über den Speaker übertragen zu können, wird die
Puls-
Als das eigentliche Problem bei der Verwendung der Puls-Längen-
Der Timerbaustein besitzt 4 I/O-Ports. Port 0x43 ist das Mode Control Register.
Die Daten-Ports 0x40 bis 0x42 sind den Timern 0 bis 2 zugeordnet.
Um einen Timer zu programmieren, muß also ein Kommando nach 0x43 und die
Timerkonstante in den entsprechen Daten-Port geschrieben werden.
Ein Kommando ist sehr einfach aufgebaut.
Die Bits 7 und 6 enthalten die Nummer des zu programmierenden Timers,
5 und 4 eine der in Tabelle 7.2 aufgezeigten Zugriffsarten und die Bits
3 bis 1 den Timer-Modus.
Leider ist im Standard-PC nur der Timer 0 interruptfähig, so daß die
beschriebene zweite Möglichkeit nicht ganz ungefährlich ist,
wird doch der für LlNUX so wichtige Timerinterrupt IRQ 0 verändert.
Die neue Interrupt-Routine muß dafür sorgen, daß die ursprüngliche
Prozedur in genau denselben Zeitintervallen wieder aufgerufen wird.
Außerdem benötigt die Interruptbehandlung im Protected Mode weitaus mehr Zeit
als im Real Mode, so daß durch die größere Anzahl ausgelöster Interrupts
die Rechenzeit merklich verbraucht wird.
Je höher die reale Samplerate, desto besser, da diese als Eigenfrequenz
(Pfeifen) zu hören ist[10].
Diese Frequenzen ergeben bei Benutzung des Timers 2 mögliche Timerkonstanten
zwischen 1 und 74 (eine 0 würde 65536 bedeuten und ist deshalb unzulässig).
Da die Konstanten direkt mit den Samples zusammenhängen, kann man also
nur 6 Bit (1-65) ausgeben.
Als Maximalwert für die reale Samplerate sind also 18357Hz möglich
(dies entspricht 1,193MHz/65).
Dieser Wert ist allerdings nicht sehr gebräuchlich, deshalb werden
mit Hilfe zusätzlich generierter Samples (Oversampling) auch andere
Sampleraten unterstützt.
Aus Zeitgründen sorgt ein einfacher Algorithmus dafür, daß durch die
Wiederholung[11] einzelner Samples die Daten "auseinandergezogen" werden.
Soll die Ausgabe z.B. mit 10000Hz erfolgen, muß jedes Sample
im Durchschnitt 1,8mal abgespielt werden.
Die Ausgabe über Digital-Analog-Wandler (DAC) hingegen ist sehr einfach.
Diese werden einfach an einen Parallelport angeflanscht und wandeln die
eingehenden 8 Bit in ein Analogsignal um.
Da der Parallelport die eingehenden Werte zwischenpuffert,
kann der Aufbau eines solchen DACs sehr einfach sein,
im genügsamsten Falle handelt es sich einfach um ein Widerstandsnetz.
Außerdem kann der Parallelport die Daten in fast beliebiger
Geschwindigkeit ausgeben, Timer 0 kann also mit der wahren Samplerate
programmiert werden.
Ebenso entfällt die Transformation der Samples in eine 6-Bit-Darstellung;
die Ausgabe über DACs benötigt deshalb weniger Prozessorzeit als die
über den internen Lautsprecher.
Letzter Pluspunkt für diese Lösung: Fehlende Interrupts äußern sich nur
durch eine Verlangsamung des abgespielten Sounds und sind
innerhalb gewisser Grenzen fast unhörbar.
[9]Das Nichtbehandeln eines einzigen lnterrupts läßt die Dynamik
des Lautsprechers zusammenbrechen.
Dies ist der Grund für die Nebengeräusche bei Diskettenzugriffen
oder sogar bei Mausbewegungen.
Zudem hat die Marktentwicklung dazu geführt, das verschiedenste Hardware
die gleichen I/O-Adreßräume belegt.
Meist kann man noch mittels Jumpern verschiedene Basisadressen auswählen.
Dies ist zwar oft nötig, verwirrt aber unbedarfte Nutzer,
da sich in Dokumentationen meist nur der Hinweis befindet,
man sollte "die Standardbelegung beibehalten und im Falle eines
Nichtfunktionierens Jumper XX auf Stellung YY" setzen.
Bei der Entwicklung eines Treibers hat man also zunächst die Möglichkeit
des "sicheren" Weges.
Sämtliche Parameter werden vor dem Compilieren fest eingestellt.
Das ist zwar sehr sicher, aber nicht sehr komfortabel.
Wer will schon jedesmal den Kern neu übersetzen, wenn er einen Jumper
umgesteckt hat?
Es sind also Algorithmen gesucht, die Hardware "erkennen".
Im Idealfall müßte eine solche Erkennung allein durch Auslesen von
I/O-Ports möglich sein, aber leider ist das bei der Entwicklung neuer
Hardware keine Option.
Man ist also gezwungen, ins Blaue hinein Werte zu schreiben,
I/O-Ports auszulesen und davon abhängig seine Entscheidung zu treffen.
Meist werden dabei gewisse Besonderheiten einzelner Chips ausgenutzt
(sprich Bugs bzw. "unbenutzte Features"), die dann dazu führen können,
daß die kompatible Hardware eines anderen Herstellers nicht erkannt wird.
Das bei weitem unangenehmste Problem ist aber, daß das "Probeschreiben"
die Funktionsweise anderer Hardware hemmen bzw. das System zum Absturz
bringen kann.
Der zweite Fall tritt häufig bei der Entwicklung eines Treibers auf,
denn meist bemerkt man das Nichtfunktionieren eines anderen Geräts erst
viel später...
Unter LINUX lassen sich deshalb I/O-Adreßbereiche sperren.
Dazu muß dem Kern beim Start ein Bootparameter übergeben werden,
der alle gesperrten Bereiche enthält.
Startet das System nach dem Einbau einer neuen Karte also nicht mehr,
sollte man zunächst versuchen, den Adreßraum dieser Karte auszublenden.
Ein fiktives Beispiel soll dies verdeutlichen.
Eine Scanner-Karte belege die Adressen 0x300-0x30F (dort könnte sich auch
eine Netzwerkkarte befinden).
Mit Hilfe des Bootparameters
Will ein Gerätetreiber also I/O-Ports (schreibend) testen, sollte zunächst
mit Hilfe der Funktion check_region() die Erlaubnis dazu eingeholt werden.
Dazu wollen wir ein Fragment des Skeleton für Netzwerktreiber betrachten.
Leider bleibt noch das leidige Problem der Erkennung von IRQ- und DMA-Kanälen.
Allerdings ist es prinzipiell einfach zu lösen.
Hat man die Karte eindeutig identifiziert, werden einfach alle möglichen
Kanäle alloziert und ein Interrupt ausgelöst.
Man muß sich lediglich merken, welcher ausgelöst wurde und hoffen,
das keine andere Hardware aktiv ist.
Zuletzt sei noch erwähnt, auf welche Weise der PC-Speaker-Treiber Stereo-on-
One's erkennt.
Da bereits beim Design darauf Wert gelegt wurde (und die drei möglichen
Parallelports glücklicherweise auf festen Adressen liegen), ist dies sehr
einfach.
Das Datenbit 7 wurde mit dem Steuereingang BUSY verbunden.
Da dieses Steuersignal invertiert gelesen wird, ergibt sich folgende
Funktion.
Zur Erzeugung von "Geräuschen" könnte auch ein Programm auplay[13]
geschrieben werden, welches mit Hilfe des Systemrufes ioperm() die
entsprechenden Ports freigibt
Ein Gerätetreiber wäre also doch ratsam.
Die eigentliche Implementation des PC-Speaker-Treibers läuft dabei auf das
Ausfüllen der im vorigen Kapitel beschriebenen Struktur
file_operations hinaus.
Der Programmierer muß dabei je nach Art des Geräts nicht alle Funktionen
belegen.
Zusätzlich muß er eine weitere Prozedur zur Initialisierung des Treibers
bereitstellen.
Die Namen dieser C-Funktionen sollten alle nach demselben Schema
gebildet sein, um Konflikte mit existierenden Funktionen zu vermeiden.
Die sicherste Variante ist, einen kurzen Namen des Treibers den
eigentlichen Funktionsnamen voranzustellen.
So werden für den PC-Speaker-Treiber, oder kurz "pcsp", die Funktionen
pcsp_init(), pcsp_read() usw. im folgenden genauer erklärt.
Das gleiche Vorgehen sollte auch für externe und statische C-Variablen
Anwendung finden.
[13]Das Programm auplay von RICK MILLER war der Anstoß zur Implementation
des PC-Speaker-Treiber.
Diese Kommandozeile wird von der Funktion parse_options(), die sich in
init/main.c befindet, in ihre einzelnen Bestandteile zerlegt.
Für jeden dieser Parameter wird die Funktion checksetup() aufgerufen.
Diese Funktion vergleicht den Anfang des Parameters mit den im Feld
bootsetups[] gespeicherten Strings und ruft bei Übereinstimmung
die jeweilige setup-Funktion auf.
Ein Parameter sollte dabei den folgenden Aufbau haben:
Der Aufruf der init-Funktion muß in einer der folgenden Funktionen[15],
je nach Art des Gerätetreibers, stattfinden:
Dies gilt besonders für Geräte, die nicht während des Betriebs gewechselt
bzw. angeschlossen werden können, wie etwa Festplatten.
Kann kein Gerät gefunden werden, sollte jetzt der Treiber eine Meldung
ausgeben (das Nichterkennen des Geräts könnte ja auch ein Hardwarefehler
sein) und sicherstellen, daß das Gerät später auch nicht angesprochen wird.
Findet z.B. ein CD-ROM-Treiber kein CD-Laufwerk, hat es keinen Sinn, daß
der Treiber Speicher für Puffer belegt - das Laufwerk kann nicht während
des Betriebes hinzukommen.
Anders ist dies bei Geräten, die später zugeschaltet werden können.
Wenn der PC-Speaker-Treiber keinen Stereo-on-One[18] erkennt,
läßt der Treiber diesen auch später noch zu.
Wurden ein oder mehrere Geräte erkannt, sollten diese innerhalb der
init-Funktion initialisiert werden, wenn dies notwendig ist.
Außerdem kann hier bequem Speicher für Puffer alloziert werden.
Dazu wird der init-Funktion beim Aufruf das Argument kmem_start mitgegeben,
das zwar vom Typ long ist, bei dem es sich aber um die höchste vom Kern
bis jetzt benutzte Adresse handelt (siehe Abschnitt 4.4).
Diese muß man sich einfach nur merken und um die benötigte Anzahl
von Bytes erhöhen (siehe pc speaker_init()).
Als Rückgabewert muß init() dann die neue Endadresse zurückliefern.
[15]Die Funktionen werden in start_kernel (Datei init/main.c) in der
angegebenen Reihenfolge aufgerufen.
Kann ein Datenbyte für eine gewisse Zeitspanne nicht übertragen werden,
sollte der Treiber den Versuch aufgeben (Timeout) bzw. es nach einer
weiteren Wartezeit noch einmal versuchen.
Dafür kann man den folgenden Mechanismus verwenden.
Betrachten wir nun die write-Funktion des PC-Speaker-Treibers als Beispiel
für den Interruptbetrieb.
Sind alle Puffer voll, muß der Prozeß angehalten werden, bis zumindest ein
Puffer wieder frei ist.
Dazu wird die Funktion interruptible_sleep_on() verwendet
(siehe Abschnitt 3.1.5).
Wurde der Prozeß durch ein Signal aufgeweckt, so endet write(),
sonst geht der Transfer weiterer Daten in den freigewordenen Puffer weiter.
Betrachten wir nun den prinzipiellen Aufbau der ISR.
Wie man sieht, paßt diese ISR nicht in das zuvor erklärte Schema von
langsamen und schnellen Interrupts.
Das liegt daran, daß der Timerinterrupt in LINUX ein langsamer Interrupt ist,
aber für den PC-Speaker-Treiber aus Geschwindigkeitsgründen ein schneller
Interrupt benötigt wird.
Darum enthält der PC-Speaker-Treiber eine "dritte" Art, die gewissermaßen
schnelle und langsame Interrupts beinhaltet.
Die Routine pcsp_do_timer() wird wie ein schneller Interrupt aufgerufen
(allerdings mit gesetztem Interruptflag, d.h. unterbrechbar); gibt sie 0
zurück, wird der Interrupt beendet.
Anderenfalls wird der ursprüngliche Timerinterrupt als langsamer
Interrupt gestartet.
Da der ursprüngliche Timerinterrupt viel seltener aufgerufen werden muß,
bringt dieses Vorgehen einen großen Geschwindigkeitsvorteil.
Die ioctl-Funktion erhält als Parameter ein Kommando sowie ein Argument.
Da unter LINUX
Betrachten wir ein Fragment der ioctl-Funktion des PC-Speaker-Treibers.
Somit läßt sich die ioctl-Funktion auch dazu verwenden, andere Funktionen
innerhalb des Treibers, die nicht vom Virtuellen Dateisystem erfaßt werden,
auszuführen.
Ein weiteres Beispiel für dieses Verhalten ist im Treiber für die serielle
Schnittstelle enthalten.
Das Kommando TIOCSERCONFIG startet die automatische Erkennung des
UART-Bausteins sowie der benutzten IRQs für die Schnittstellen.
Falls das Argument wait gleich NULL ist, soll das Gerät nur abgefragt werden.
Ist es bereit für die abgefragte Funktion, soll select() eine 1 zurückliefern,
sonst eine 0.
Für wait ungleich NULL muß der Prozeß bis zur Verfügbarkeit des
Geräts angehalten werden.
Dazu wird jedoch nicht sleep_on() verwendet; diese Aufgabe erledigt die
Funktion
Falls das Gerät verfügbar wird (im allgemeinen durch einen Interrupt
angezeigt), weckt ein wake_up_interruptible(wait_address) den Prozeß
wieder auf.
Dies zeigt der Maus-Interrupt des Treibers.
lseek() ist für Zeichengeräte nur bedingt sinnvoll, da sie nicht
positionieren können.
Da jedoch die Standardfunktion lseek() im Virtuellen Dateisystem keine
Fehlermeldung zurückgibt, muß man explizit eine lseek-Funktion definieren,
falls man will, daß der Treiber auf lseek() mit Fehler reagieren soll.
Die mmap-Funktion dient zum Einblenden des Gerätes in den
Nutzeradreßbereich.
Diese Funktion ist für Zeichengeräte nicht sinnvoll, da sie eine
"Adressierung" von Daten innerhalb des Geräts voraussetzt.
Somit ist mmap() nur von Dateisystemen und höchstens von Blockgeräten
zu verwenden.
Eine Ausnahme von dieser Regel ist des Gerät /dev/mem, da dieses
(natürlich) keinen unendlichen Datenstrom repräsentiert,
sondern den endlichen und adressierbaren Speicher.
[17]Blockgeräte können zwar eine Fsync-Funktion haben, verwenden dann aber die Funktion
block_fsync () des Caches.
static void keyboard_interrupt(int int_t_regs){
...
mark_bh(KEYBOARD_BH); /* kbd_bh() wird markiert */
...
}
static void khd_bh(void * unused){
...
if (got_break){ /* Test auf Break */
if (tty && !I_IGNBRK(tty)){
if (I_BRKINT(tty)){
flush_input(tty);
...
}
unsigned long kbd_init(unsigned long kmem_start){
...
/* der Keyboard Bottom Half wird initialisiert */
bh_base[KEYBOARD_BH].routine = kbd_bh;
request_irq(KEYBOARD_IRQ,keyboard_interrupt);
...
}
Die Init-Funktion des Keyboard-Treibers installiert kbd_bh() als Bottom Half
und keyboard_interrupt() als langsamen Interrupt.
Bei jedem Aufruf des Keyboard-Interrupts wird mark_bh(KEYBOARD_BH) aufgerufen,
d.h. der Bottom Half läuft zum ersten Zeitpunkt nach Beendigung des
Keyboard-
bh_base[KRYBOARD_BH].routine = kbd_bh;
[8]Das kann durchaus geschehen, wenn z.B. ein langsamer Interrupt von einem anderen unterbrochen
wird.
7.2.4 DMA-Betrieb
Sollen besonders viele Daten kontinuierlich von bzw. zu einem Gerät
transportiert werden, bietet sich der DMA-Betrieb an.
In dieser Betriebsart transferiert der DMA-Controller Daten ohne Mithilfe
des Prozessors direkt aus dem Speicher zu einem Gerät.
Normalerweise löst das Gerät danach einen IRQ aus, so daß in der
behandelnden ISR der nächste DMA-Transfer vorbereitet werden kann.
Leider gibt es auch Beispiele für DMA-fähige Geräte, die keinen IRQ
unterstützen.
Viele Handscanner fallen in diese Kategorie.
Will man einen Gerätetreiber für diese Klasse schreiben, muß man den
DMA-Controller pollen, um das Ende eines Transfers festzustellen.
Um diese Probleme zu lösen, existiert die Funktion sound_mem_init(),
die Puffer für den DMA-Transfer zur Soundkarte alloziert und dabei auf
64-K-Grenzen achtet.
Leider wird im Protected Mode das DMA-Konzept durch die notwendigen
physischen Adressen sehr gestört, so daß der Soundtreiber die Daten
erst mit Hilfe des Prozessors in die DMA-Puffer kopiert und dann mittels
DMA zur Soundkarte transferiert.
Dieses Vorgehen widerspricht eigentlich der Idee, Daten ohne Hilfe
des Prozessors zu übertragen, ist aber trotzdem sinnvoll, da man sich
nicht um ein Timing bei der Datenübertragung zur Soundkarte kümmern muß.7.3 Die Hardware
Wollen wir nun einen Gerätetreiber für den internen Lautsprecher schreiben,
kommen wir nicht umhin, uns genauer mit dieser Hardware und ihrer Steuerung
zu beschäftigen.
System Control Latch
+---------------+
| | | | | | |1|0|--
+---------------+ |
|\ | |
| \- +-+ | |
| | | |&|------------------ |
| | |---| |-- |
| | | +-+ | +---------------+ |
| /- --|Timerkonstante |-- Restart Gate
|/ +---------------+
Abbildung 7.1: Schematischer Anschluß des PC-Speakers
Der Timerbaustein 8253 besitzt drei interne Timer. Timer 2 ist zur Verwen-
dung mit dem PC-Speaker bestimmt.
Dazu ist der Ausgang des Timers 2 über ein AND-Gatter mit dem Bit 1 des
System Control Latches auf I/O-Adresse 0x61 verbunden.
Bit 0 dient zum Start bzw. Neustart des Timers 2.
Der Speaker kann also nur entweder voll ein- oder ausgeschaltet sein.
Normalerweise wird der Timer 2 als Frequenzteiler programmiert
(d.h. beide Bits sind gesetzt).
Dadurch werden Rechteckwellen erzeugt, die den "typischen" Klang des
internen Lautsprechers ausmachen.
Die Frequenz entsteht durch Teilung der Timer-Grundfrequenz von
1,193 MHz (=4,77MHz/4) durch die eingestellte Timerkonstante.
Die zweite Möglichkeit besteht darin, den Timer 2 als Retriggerable Oneshot
zu programmieren.
Durch Anlegen einer 1 am Restart Gate wird der Timer gestartet
und gibt eine 0 aus.
Nach dem Herunterzählen der Timerkonstante wird wieder eine 1 ausgegeben.
Nach einer gewissen Zeit, die dem maximalen Samplewert entspricht, wird eine
neue Konstante in den Timer 2 übertragen und dieser wieder gestartet.
Diese konstante Zeit kann wiederum mit Hilfe einer Zeitschleife oder mit
dem Timer 0 erzeugt werden.
Timer 0 läuft normalerweise im Teilermodus und generiert nach jedem
Herunterzähien der Timerkonstante den IRQ0.
Diese vom Timer 0 erzeugte Frequenz ist gleichzeitig die Samplerate,
mit der die Samples abgespielt werden können.
Sie wird im folgenden Reale Samplerate genannt.
In der Interruptbehandlungsroutine muß Timer 2 dann neu initialisiert werden.
Dieses Vorgehen zeigt Abbildung 7.2.
Timer- 1|-- -- -- --
Ausgang | _____ _____ _____ ___
0|__________________________
*****
Länge des Intervalls, wird durch Timerkonstante 2 festgelegt
Reset 1| --- --- --- --
Gate |__ ____ ____ ____ _
0|__________________________
*******
Konstantes Intervall, wird durch Tirner 0 erzeugt
Intervallänge = Timerkonstante/1193180 sec
Abbildung 7.2: Puls-Längen-Modulation mit Hilfe der Timer 0 und 2
Um z.B. einen Ton mit 10000Hz zu erzeugen, sind folgende Schritte notwendig:
Das "Stillegen" erfolgt einfach durch outb_p(inb_p(0x61)&0xFC,0x61).
Dadurch wird sowohl der Lautsprecher abgeschaltet als auch der Timer
gestoppt.
Bits Mode Erklärung
54
00 Latch der Zähier wird in ein internes Register
übertragen und kann danach ausgelesen
werden
01 LSB only nur die unteren 8 Bit des Zähiers werden
übertragen
10 MSB only nur die oberen 8 Bit des Zählers werden
übertragen
11 LSB/MSB zunächst werden die unteren, danach die
oberen 8 Bit übertragen
Tabelle 7.2: Bits 4 und 5 des timer-kommandos
Kommen wir zurück zur Puls-Längen-Modulation.
Wie bereits erwähnt, ist die Wahl der Zeitintervalle sehr wichtig.
Versuche haben gezeigt, daß für eine reale Samplerate zwischen
16000Hz und 18000Hz die besten Resultate erzielt werden.
[10]Ab welcher Eigenfrequenz dieses Pfeifen hörbar ist,
hängt von der jeweiligen Anatomie ab.
Ich höre erst ab 14500Hz etwas, andere hören auch 17000Hz noch.
[11]Normalerweise würden die neuen Samples durch Interpolation berechnet.
Bei der Ausgabe durch den internen Lautsprecher ist damit jedoch
keine Qualitätsverbesserung zu erzielen.
7.3.1 Hardwareerkennung
Obwohl der Lautsprecher des PCs sich stets auf denselben Portadressen befindet,
muß dies nicht auch bei jeder Zusatzhardware so sein.
Da leider durch das Design des ISA-Busses für die möglichen Portadressen
Grenzen gesetzt sind[12], kann es zu Adreßüberschneidungen kommen.
Das wohl geläufigste Beispiel ist die Belegung der I/O-Adresse der
COM4-Schnittstelle durch ISA-Karten mit S3-Chip.
reserve=0x300, 0x10
wird dieser Bereich ausgeschlossen.
#include <LINUX/ioport.h>
netcard_probe(struct device *dev){
...
for (port = &ports[0]; *port; port++){
int ioaddr = *port;
if (check_region(ioaddr, ETHERCARD_TOTAL_SIZE)) continue;
if (inb(ioaddr) != 0x57) continue;
dev->base_addr = ioaddr;
if (netcard_probe1(dev,ioaddr) == 0) return 0;
}
dev->base_addr = base_addr;
return ENODEV;
}
Liefert check_region() also einen Wert ungleich 0, darf auf mindestens einen
Port in diesem Bereich nicht zugegriffen werden, und ein Test ist zu
unterlassen.
/* testet, ob sich ein Stereo-on-One an lp(port) befindet */
inline static int stereo1_detect(unsigned port){
outb(0,LP_B(port));
if(LP_S(port) & 0x80){
outb(0xFF,LP_B(port));
return (LP_S(port) & 0x80) ? 0 : 1;
}
return 0;
}
[12]Nur die ersten 10 Bit einer Portadresse liegen auf dem Bus.
Das bedeutet, daß alle 65536 möglichen Portadressen auf den Bereich
0-0x3FF abgebildet werden.
7.4 Die Implementation eines Treibers
Nach detailierter Erklärung der Hardware des internen Lautsprechers drängt
sich nun die Frage auf, warum extra ein Gerätetreiber gebraucht wird,
um das Schreiben und Lesen einiger I/O-Ports zu erledigen.
if (ioperm(0x61,1,1) || ioperm(0x42,l,1) || ioperm(0x43,1,1) {
printf("can't get I/O permissions for internal speaker\n");
exit(-1)
}
und die Ausgabe der Samples danach selbst übernimmt.
Dies würde aber zu mehreren Nachteilen führen:
Das Schreiben eines "Audio-Dämons" der aus einer Named Pipe die Sampledaten
liest und beim Hochfahren des Systems über die Datei rc.local gestartet wird,
hilft dabei nur bedingt.
Das Problem der Zeitabstimmung bleibt auf jeden Fall erhalten.
Ein Gerätetreiber dagegen arbeitet mit Kern-Berechtigung und kann somit über
alle Ressourcen frei verfügen.
Dies sollte aber bei der Implementation eines Treibers nicht vergessen werden,
da Fehler im Treiber sich möglicherweise schlimmer auswirken können
als Fehler in einem Programm.[14]
for ( j = 1; j < DELAY; j++)
Dieses Busy Waiting ist nicht akzeptabel, da eine genaue Abstimmung der
Samplerate nicht möglich ist.
Die Nutzung des Timer-Interrupts ist dafür eine wesentlich elegantere
Variante, kann aber nur im Kern geschehen.
Die Zugangsbeschränkung für Geräte ist demgegenüber relativ einfach und
wird im folgenden noch erklärt.
[14]Dies stimmt nur bedingt, da man beim Programm auplay über die falsche
Benutzung des Mode Control Registers auf I/O-Adresse 0x43 den Timerinterrupt
durcheinander und den Rechner zum Absturz bringen kann.
7.4.1 Die Setup-Funktion
Manchmal möchte man einem Gerätetreiber oder allgemein dem LINUX-Kern
Parameter übergeben.
Dies kann notwendig sein, wenn eine automatische Erkennung von Hardware
nicht möglich ist oder zum Konflikt mit anderer Hardware führt.
Dazu bietet sich die Nutzung der LlNUX-Bootparameter an, die dem Kernel
während des Bootvorgangs übergeben werden können.
Im allgemeinen werden diese Parameter in Form einer Kommandozeile
vom LINUX-Lader LILO kommen (siehe Abschnitt D.2.5).
name=param1,...,paramn
Die ersten zehn Parameter versucht checksetup () noch in Integer-Zahlen um-
zuwandeln.
Gelingt dies, werden sie in einem Feld abgelegt.
Index 0 dieses Feldes enthält die Anzahl umgewandelter Parameter.
Der Rest der Zeile wird einfach als String weitergereicht.
Als Beispiel soll hier die setup-Funktion des PC-Speaker-Treibers dienen.
void pcsp_setup(char *s, int *p){
if (!strcmp(s,"off")){
pcsp_enabled = 0;
return;
}
if (p[0] > 0) pcsp.maxrate = p[1];
pcsp_enabled = 1;
}
Wie zu sehen ist, testet diese Funktion zunächst das Vorhandensein des Wortes
"off".
Der Boot-Parameter "pcsp=off" schaltet den PC-Speaker-Treiber also ab.
Ist sonst die Anzahl numerischer Parameter nicht 0, wird der erste Parameter
p[1] zur Initialisierung einer globalen Variablen des PC-Speaker-Treibers
benutzt.
Diese Funktion muß jetzt noch registriert werden.
Dazu trägt man sie in das Feld bootsetups[] ein,
wie die folgenden Zeilen zeigen.
struct{
char *str;
void (*setup_func) (char *, int *);
}bootsetups[]={
...
#ifdef CONFIG_PCSP
{"pcsp=", pcsp_setup},
#endif
{0,0}
};
Bei der Verwendung einer Setup-Funktion sollte man beachten, daß sie vor der
Initialisierung der Gerätetreiber durch ihre init()-Funktion aufgerufen wird.
Man sollte in der Setup-Funktion also nur globale Variablen setzen, die dann
von der init-Funktion ausgewertet werden können.7.4.2 Init
init() wird nur während der Kern-Initialisierung aufgerufen, erfüllt jedoch
wichtige Aufgaben.
Diese Funktion dient der Überprüfung des Vorhandenseins eines Geräts,
dem Aufbau interner Strukturen des Gerätetreibers sowie der Anmeldung
des Geräts.
Die Funktion pcsp_init() wird demnach in unserem Fall von der Funktion
chr_dev_init() aufgerufen.
long pcsp_init(long kmem_start){
Damit LINUX mit dem Treiber überhaupt etwas anfangen kann, muß dieser regi-
striert werden.
Dazu dient die Funktion register_chrdrv(), die
Eine zurückgelieferte Null signalisiert, daß der neue Treiber registriert
ist.
if (register_chrdev(PCSP_MAJOR,"pcsp",&pcsp_fops))
printk("unable tu get major 30 for pcsp devices\n");
else
printk("PCSP-device 0.6 init: \n");
...
init() ist auch der richtige Platz, um zu testen, ob überhaupt ein Gerät,
welches vom Treiber unterstützt wird, vorhanden ist.
pcsp.buf[0] = (unsigned char *)kmem_start;
pcsp.buf[1] = (unsigned char *) (kmem_start + ABLK_STZE);
return (kmem_start + 2*ABLK_SIZE)
}
Der hier allozierte Speicher wird allerdings für immer belegt und auch
nicht ausgelagert.
Er läßt sich also für Interrupt-Puffer benutzen, ist aber ansonsten für
Prozesse unbenutzbar.
Man sollte sich also überlegen, wieviel Speicher auf diese Weise
alloziert wird.
[16]Ein Stereo-on-One ist ein von MARK J. Cox entworfener einfacher
Stereo-Digital-Analog-Wandler, der aber nur einen Parallelport belegt
und softwaremäßig erkannt werden kann.
7.4.3 Open und Release
Die open-Funktion ist verantwortlich für die Verwaltung aller Geräte.
open() wird aufgerufen, sobald ein Prozeß eine Gerätedatei öffnet.
Falls, wie in unserem Fall, nur ein Prozeß mit einem Gerät arbeiten kann,
muß open() als Rückgabewert -EBUSY zurückgeben.
Kann ein Gerät von mehreren Prozessen gleichzeitig benutzt werden,
sollte open() die dafür notwendigen Warteschlangen einrichten,
falls diese nicht in read() oder write() eingerichtet werden können.
Falls kein Gerät existiert (z.B. wenn ein Treiber mehrere Geräte
unterstützt, aber nur eines vorhanden ist), sollte er -ENODEV zurückgeben.
Ansonsten ist open() der richtige Platz, um für den Treiber notwendige
Standardeinstellungen zu initialisieren.
Der PC-Speaker-Treiber setzt in der open-Funktion beide Puffer auf die
Länge 0 und verriegelt den Treiber gegen einen späteren Zugriff durch
einen anderen Prozeß.
Ein gelungenes Öffnen ist durch eine 0 als Rückgabewert anzuzeigen.
static int pcsp_open(struct inode *inode, struct file *file){
if (pcsp_active) return -EBUSY;
pcsp.buffer = pcsp.end = pcsp.buf[0];
pcsp.in[0] = pcsp.in[1] = 0;
pcsp.timer_on = 0;
pcsp_active = 1;
return 0;
Die release-Funktion wird im Gegensatz zu open() erst aufgerufen, wenn der
Dateideskriptor auf das Gerät freigegeben wird (siehe Abschnitt 6.2.6).
Ihre Aufgabe sind Aufräumaktionen globaler Natur, u.a. das Leeren von
Warteschlangen.
Bei bestimmten Geräten kann es auch sinnvoll sein, zunächst alle Daten,
die sich noch in Puffern befinden, an das Gerät weiterzuleiten.
Im Falle des PC-Speaker-Treibers bedeutet das, daß die Gerätedatei
schon geschlossen werden kann, bevor alle Daten in den Ausgabepuffern
abgespielt sind. Die Funktion pcsp_sync() wartet deshalb darauf,
daß beide Puffer geleert wurden.
static void pcsp_release(struct inode *inode, struct file *file){
pcsp_sync();
pcsp_stop_timer();
outb_p(0xb6,0x43); /* binary, mode 2, LSB/MSB, ch 2 */
pcsp_active = 0;
}
Die release-Funktion ist optional; allerdings ist eine solche Konstellation
schwerlich vorstellbar.7.4.4 Read und Write
read() und write() sind vom Prinzip her zueinander symmetrische Funktionen.
Da man vom internen Lautsprecher keine Daten lesen kann, ist im
PC-Speaker-Treiber nur write() implementiert.
Der Einfachheit halber soll jedoch zunächst der Aufbau einer
write-Funktion für Treiber im Pollingbetrieb betrachtet werden.
Hier wird wieder der Druckertreiber bemüht.
static int lp_write_polled(struct inode *inode, struct file *file,
char *buf, int count){
int retval;
unsigned int minor = MINOR(inode->i_rdev);
char c, *temp = buf;
temp = buf;
while (count > 0){
c = get_fs_byte(temp);
retval = lp_char_polled(c, minor);
...
if (!retval){
/* Fehlerbehandlung */
}
}
return temp-buf;
}
Zu beachten ist, daß sich der Puffer buf im Nutzeradreßraum befindet und
deshalb Bytes mit Hilfe von get_fs_byte() gelesen werden müssen.
if (current->signal & ~current->blocked){
if (temp != buf) return temp-buf;
else return -EINTR;
}
current->state = TASR_INTRRRUPTIBLE;
current->timeout = jiffies + LP_TIME(minor);
schedule();
Zunächst prüft man, ob der aktuelle Prozeß Signale empfangen hat.
Wenn ja, endet die Funktion und gibt die Anzahl der übertragenen
Bytes zurück.
Danach wird der Prozeß durch die erste Zuweisung in den Zustand
TASK_INTERRUPTIBLE versetzt und der Zeitpunkt des "Aufwachens" festgelegt.
Dazu muß zum aktuellen Wert von jiffies die minimale Wartezeit in Ticks
hinzugefügt werden.
Der Aufruf von schedule() hält den Prozeß bis nach dem Verstreichen der Zeit
oder bis zum Eintreffen eines Signals an.
Danach erst kehrt der Programmablauf aus schedule() zurück und
current->timeaut ist 0, wenn ein Timeout aufgetreten ist.
static int pcsp_write(struct inode *inode, struat file *file,
char *buffer, int count){
unsigned long copy_size;
unsigned long total_bytes_written = 0;
unsigned bytes_written;
int i;
do{
bytes_written = 0;
copy_sIze = (count <= ABLK_SIZE) ? count : ABLK_SIZE;
i = pcsp.in[0] ? 1 : 0;
if (copy_size && !pcsp.in[i]) {
memcpy_fromfs(pcsp.buf[i],buffer,copy_size);
pcsp.in[i] = copy_size;
if (pcsp.timer_on) pcsp_start_timer();
bytes_written += copy_size;
buffer += copy_size;
}
if (pcsp.in[0] && pcsp.in[i]){
interruptible_sleep_an (&pcsp_sleep);
if (current->signal & ~current->blocked){
if (total_bytes_written + bytes_written)
return tota1_bytes_written + bytes_written;
else
return -EINTR;
}
}
total_bytes_written += bytes_written;
count -= bytes_written;
}while (count > 0);
return total_bytes_written;
In den ersten freien Puffer werden zunächst mittels memcpy_fromfs() Daten aus
dem Nutzerbereich übertragen.
Dies ist unbedingt notwendig, da der Interrupt unabhängig vom aktuellen
Prozeß auftreten kann und man somit die Daten nicht während des
Interrupts aus dem Nutzerbereich holen kann.
Der Zeiger buffer würde dann ja in den Nutzeradreßraum des jeweils
aktuellen Prozesses zeigen.
Sollte der entsprechende Interrupt noch nicht initialisiert sein,
wird er jetzt eingeschaltet (pcsp_start_timer()).
Da die Übertragung der Daten zum Gerät in der ISR erfolgt,
kann write() den nächsten Puffer füllen.
int pcsp_do_timer(void){
if (pcsp.index < pcsp.in[pcsp.actual]){
/* Ausgabe eines Bytes */
...
}
if (pcsp.index >= pcsp.in[pcsp.actual]){
pcsp.xfer = pcsp.index = 0;
pcsp.in[pcsp.actual] = 0;
pcsp.actual = 1;
pcsp.buffer = pcsp.buf[pcsp.actual]
if (pcsp_sleep) wake_up_interruptible(&pcsp_sleep)
}
...
}
Solange sich im aktuellen Puffer noch Daten befinden, werden diese ausgegeben.
Ist der Puffer leer, wird auf den zweiten Puffer umgeschaltet und mittels
wake_up_interruptible() der Prozeß wieder aufgeweckt.
Das if vor dem Aufruf der Funktion ist eigentlich unnötig,
da wake_up_interruptible() diesen Test selbst vornimmt.
Er geschieht an dieser Stelle lediglich aus Zeitgründen.7.4.5 IOCTL
Obwohi ein Gerätetreiber versucht, die Bedienung von Geräten nach außen hin
möglichst zu abstrahieren, hat doch jedes Gerät seine speziellen
Eigenschaften.
Dazu können verschiedene Operationsmodi ebenso wie gewisse
Grundeinstellungen gehören.
Auch eine Einstellung von Geräteparametern zur Laufzeit, wie IRQs,
I/O-Adresse usw., ist denkbar.
sizeof(unsigned long) == sizeof(void *)
gilt, kann als Argument auch ein Zeiger auf Daten im Nutzeradreßraum
übergeben werden.
Normalerweise besteht die ioctl-Funktion deshalb aus einer großen
switch-Anweisung, in der für das Argument eine entsprechende
Typumwandlung stattfindet.
ioctl-Aufrufe verändern zumeist nur Treiber-globale Variablen
oder globale Geräteeinstellungen.
static int pcsp_ioctl(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg){
unsigned long ret;
unsigned long *ptr = (unsigned long *)arg;
int i, error;
switch (cmd){
case SNDCTL_DSP_SPEED:
error = verify_area(VERIFY_READ,ptr,4);
if (error) return error;
arg = get_fs_long(ptr)
if (arg<4000 || arg>44100 || ARG>pcsp.maxrate)
return -EINVAL;
else /* reset default speed */
pcsp_calc_srate (arg);
return 0;
...
case SNDCTL_DSP_SYNC:
pcsp_sync ();
pcsp_stop_timer();
return 0;
...
}
}
Das Kommando SNDCTL_DSP_SPEED formt das Argument arg in einen Zeiger
um und liest mit seiner Hilfe die neue Samplerate.
Danach berechnet die Funktion pcsp_calc_srate() lediglich einige
Zeitkonstanten in Abhängigkeit von der neuen Samplerate.
SNDCTL_DSP_SYNC hingegen ignoriert das Argument völlig und ruft die
Funktion pcsp_sync () auf.
Diese Funktion hält den Prozeß solange an, bis alle Daten, die sich noch
in Puffern befinden, abgespielt wurden.
Diese Synchronisation ist z.B. nötig, wenn während des Abspielens von
Audiodaten die Samplerate oder der Abspielmodus (Mono oder Stereo)
geändert wird oder die Ausgabe von Audiodaten mit anderen Ereignissen
im Prozeß synchronisiert werden soll.7.4.6 Select
Obwohl select() nicht implementiert ist, soll doch hier die Funktionsweise
beschrieben werden, da diese Funktion insbesondere für Zeichengeräte
sinnvoll ist.
Als Beispiel sei die select-Implementierung des ATI-Busmaus-Treibers
betrachtet:
static int mouse_select(struct inode *inode, struct file *file,
int sel_type, select_table *wait){
if (sel_type != SEL_IN) return 0;
if (mouse.ready) return 1;
select_wait(&mouse.wait,wait);
return 0;
}
Die Aufgabe der select-Funktion besteht in einer Überprüfung, ob vom Gerät
gelesen (sel_type == SEL_IN) oder Daten an das Gerät geschrieben werden
können (SEL_OUT).
Mit SEL_EX kann man zudem noch auf das Eintreffen einer Ausnahmebedingung
warten.
Da fast die gesamte Komplexität dieser Aufgabe vom Virtuellen Dateisystem
übernommen wird, ist die Aufgabe der select-Funktion einfach zu
beschreiben.
void select_wait(struct wait_queue **wait_address, select_table *p)
Als Argumente erwartet sie eine Warteschlage sowie das letzte der
select-Funktion übergebene Argument.
Da select_wait() sofort zuruckkehrt, wenn dieses Argument NULL ist,
kann man sich die Abfrage sparen und bekommt einen Funktionsaufbau,
wie in der oben gezeigten Beispielfunktion.
void mouse_interrupt(int unused){
if (dx != 0 || dy != 0 || buttons != mouse.latch_buttons){
mouse.latch_buttons |= buttons;
mouse.dx += dx;
mouse.dy += dy;
mouse.ready = 1;
wake_up_interruptible(&mouse.wait);
}
...
}
7.4.7 Lseek, Readdir, MMap und Fsync
Diese Funktionen sind im PC-Speaker-Treiber nicht implementiert.
Da die Struktur file_operations nicht nur für Geräte, im besonderen
Zeichengeräte, verwendet wird, enthält sie von Gerätetreibern nicht
genutzte Funktionen.
So ist readdir() und fsync()[17] für Geräte sinnlos.