Ein-/Ausgabe

Ein-/Ausgabe
Dieser Teil beschreibt die Verwaltung von Ein-/Ausgabegeräten. Wir werden E/A am Beispiel von
Linux diskutieren und sehen, wie Gerätetreiber programmiert werden.
Inhalt
1.Grundlagen..................................................................................................................3
1.1.Grundstruktur von Linux...........................................................................................3
1.2.Gerätetreiber.........................................................................................................14
1.3.Systemprogrammierung..........................................................................................16
1.3.1.Kontrollfluss.....................................................................................................16
1.3.2.Anwendungsschnittstelle zu Gerätetreibern..........................................................22
1.3.3.Programmbeispiele............................................................................................29
2.Module.......................................................................................................................33
3.Treibergrundlagen.......................................................................................................39
3.1.Aufbau eines Treibers.............................................................................................39
3.2.Zuordnung Applikationsschnittstelle auf Treiber..........................................................43
3.2.1.Logische Geräte................................................................................................43
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
1/55
3.2.2.Major- und Minornumber...................................................................................44
3.3.Treiberfunktionen...................................................................................................48
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
2/55
1.Grundlagen
Eine der Hauptaufgaben eines Betriebssystems ist die Überwachung und Steuerung von
Ein-/Ausgabegeräten (I/O Devices).
Kommandos für die Gerätesteuerung müssen an Geräte gesendet werden können; Unterbrechungen, die von Geräten ausgelöst werden, müssen behandelt werden. Weiterhin sind
Fehler, die von einem Gerät verursacht werden, zu verwalten.
Ein gutes Betriebssystem stellt dazu eine einfache und leicht zu benutzende Schnittstelle für
die Programmierung zur Verfügung. Dadurch können unterschiedliche Geräte auf die selbe
Art und Weise angesprochen werden.
Im Folgenden wird auf diese Schnittstelle in Linux eingegangen und gezeigt, wie man Gerätetreiber programmiert, um Geräte an ein Linux System anzuschließen.
1.1.Grundstruktur von Linux
Die Architektur von Linux ist nachfolgend abgebildet:
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
3/55
Applications
Services
Libraries
Systemcall Interface
IO
Subsystem
Process
Management
User
Kernel
Memory
Management
Device Driver Layer
Hardware
Systemcall Interface (Dienstzugangsschnittstelle)
Über die Systemcall-Schnittstelle lassen sich aus der Applikation heraus die Dienste
des Betriebssystems nutzen. Diese Schnittstelle ist absolut unabhängig von jeglicher Programmiersprache. In den seltensten Fällen greift eine Applikation direkt auf das Systemcall-Interface zu, sondern nutzt zu diesem Zweck Bibliotheksfunktionen. So lautet
beispielsweise der Systemcall für die Ausgabe von Daten in eine Datei oder in ein Gerät
write.
Systemcalls erwarten ihre Argumente entweder in Registern oder auf dem Stack. Ein
Systemcall wird dann über den Assemblerbefehl INT bzw. TRAP mit einer Exceptionnummer (bei Linux beispielsweise 0x80) aufgerufen (Softwareinterrupt). Dieser Befehl führt zu
einer Exception, wobei die Exceptionnummer innerhalb des Betriebssystems die
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
4/55
Dienstaufrufsschnittstelle aktiviert. Hier wird anhand der Registerinhalte (Systemcallcode) in die entsprechenden Funktionen verzweigt.
Das folgende Assemblerprogramm verdeutlicht einen Systemcall:
$ cat write_hello_world.as
.text
.globl write_hello_world
write_hello_world:
movl $4,%eax
; //code fuer write systemcall (4)
movl $1,%ebx
; //file descriptor fd (1=stdout)
movl $message,%ecx ; //Adresse des Textes (buffer)
movl $12,%edx
; //Laenge des auszugebenden Textes
int $0x80
; //SW-Interrupt, Auftrag an das BS
ret
.data
message:
.ascii "Hallo World\n"
$
Der Code für den Systemcall wird in das Register eax geschrieben. Die Register ebx,
ecx und edx werden mit den Parametern des Funktionsaufrufes belegt (in diesem Fall
der Dateideskriptor 1 für stdout, die Adresse der auszugebenden Daten "Hello World"
und die Länge 12 des auszugebenden Strings). Danach wird der Softwareinterrupt für die
Systemcall-Schnittstelle (0x80) aufgerufen.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
5/55
Der angegebene Assemblercode realisiert einen Unterprogrammaufruf (ohne Rückgabewert,
also vom Typ void), der in einer Objektdatei abgelegt wird (normalerweise wird er in eine
Bibliothek geschrieben).
Objektdatei, die
$ as write_hello_world.as -o write_hello_world.o
Assembler erzeugt
$
Dann kann er in einem Anwendungsprogramm (hier C-Programm) verwendet werden:
$ cat hello_world.c
main( int argc, char **argv ) {
write_hello_world();
// rufe Systemcall wie Funktion auf
}
$
$ cc hello_world.c write_hello_world.o -o hello_world
$ hello_world
C-Compiler
Hallo World
verwendet
$
Objektdatei mit
Systemcall
Process Management
Das Prozess-Management ist für das Scheduling, d.h. für die quasi-parallele Bearbeitung von Rechenprozessen und den Context-Switch zuständig. Das eigentliche Aktivieren
des Prozesses durch den Scheduler wird mit Context-Switch bezeichnet. Hierbei werden die
bei der letzten Unterbrechung des Rechenprozesses in den Task-Kontroll-Block geretteten
Register zurück in die CPU geschrieben und der Program-Counter der CPU mit dem nächsten Befehl des neuen Rechenprozesses geladen.
Rechenprozesse bzw. Tasks im System befinden sich immer in einem bestimmten Zuständen. Der Taskzustand als auch diverse andere Informationen (Prozessor-RegisterinhalBetriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
6/55
te, Task-Priorität, bereits verbrauchte Rechnerressourcen) befinden sich im Task-Kontrollblock. Bisher haben wir vier Task-Zustände unterschieden. In der Praxis jedoch werden
die Zustände feiner unterteilt, so dass auch durchaus zwischen mehr Zuständen unterschieden wird (z.B. der theoretische Zustand warten kann in einem realen Betriebssystem
durch zwei Zustände, nämlich warten_auf_ein_Zeitereignis und warten_auf_Ein/Ausgabe abgebildet sein).
Linux kennt insgesamt 6 Taskzustände:
1.TASK_RUNNING
Unter Linux werden alle lauffähigen Tasks in einer runqueue genannten Liste eingehängt. Aus dieser Liste wählt der Scheduler den nächsten aktiven Rechenprozess aus
(damit entspricht der Zustand TASK_RUNNING dem oben erwähnten Zustand lauffähig).
2.TASK_INTERRUPTIBLE
In diesem Wartezustand kann der Task durch Signale oder durch das Ablaufen eines Timers unterbrochen und wieder in den Zustand TASK_RUNNING überführt werden.
3.TASK_UNINTERRUPTIBLE
Dies ist ebenfalls ein Wartezustand. Im Unterschied zu TASK_INTERRUPTIBLE kann
der Task jedoch nur durch ein wake_up wieder in den Zustand TASK_RUNNING überführt werden.
4.TASK_ZOMBIE
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
7/55
Der Task mit diesem Zustand gilt zwar als beendet, aber der zugehörige Elternprozess
hat bisher noch nicht den Prozessstatus abgeholt.
5.TASK_STOPPED
Der Task wurde angehalten, entweder durch ein entsprechendes Signal oder durch
ptrace. Der Systemcall ptrace ermöglicht einem Elternprozess das Debugging des
Kindprozesses.
6.SK_EXCLUSIVE
Dies ist kein eigener Zustand, vielmehr parametriert er die Wartezustände TASK_INTERRUPTIBLE und TASK_UNINTERRUPTIBLE. Schlafen mehrere Tasks auf mehrere Waitqueues, wird - falls dieses Flag gesetzt ist - nur dieser eine Task, und nicht alle Tasks geweckt.
Unter Linux existiert der Zustand activ nicht explizit. Der Zustand lauffähig ist unter Linux
der Zustand RUNNING, der Zustand wartend wird über INTERRUPTIBLE und UNINTERRUPTIBLE abgebildet. Der Zustand ruhend schließlich ist unter Linux entweder durch ZOMBIE
oder STOPPED realisiert.
Folgende Prozessübergänge sind möglich:
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
8/55
fork/clone
stopped
wait
exit
running
wakeup
zombie
wakeup
signal
interruptible
exit
wakeup
uninterruptible
sleep
current
sleep
Memory Management
Memory-Management umfasst
1.Adressumsetzung
2.Speicherschutz
3.Realisierung virtuellen Speichers.
Adressumsetzung ist notwendig, damit Rechenprozesse einen einheitlichen Adressraum
haben. Bei der Adressumsetzung wird die logische Adresse, die im Programm verwendet
wird, auf eine reale physikalische Adresse umgesetzt.
Mittels Speicherschutz wird verhindert, dass Applikationen auf den Speicher anderer Applikationen zugreifen können. Damit kann eine fehlerhafte Applikation den Speicher der anderen Applikationen nicht verändern und den Ablauf nicht stören. Insbesondere ist es auch
wichtig, dass Applikationen nicht auf den Speicherbereich zugreifen können, den das Betriebssystem selbst verwendet. Ansonsten könnten diese ja das Betriebssystem zum Absturz bringen.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
9/55
Für die Treiberentwicklung ergibt sich das Problem, dass man zwischen dem so genannten User-Space und dem Kernel-Space unterscheiden muss. Ein Treiber hat - als Teil
des Betriebssystems - nur direkten Zugriff auf den Kernel-Space. Da der Treiber aber
Daten zwischen der Applikation und beispielsweise der Hardware austauschen möchte,
muss er auch auf den User-Space zugreifen können. Dies wird durch Funktionen, die vom
Betriebssystem zur Verfügung gestellt werden, erreicht (copy_from_user und
copy_to_user).
Die dritte Aufgabe der Speicherverwaltungseinheit ist es, virtuellen Speicher zur Verfügung
zu stellen. Der prinzipiell innerhalb eines Programms adressierbare Adressraum ergibt sich
durch die Breite der Adressregister. Hat ein Adressregister beispielsweise eine Breite von 16
Bit, können damit 216=64KByte adressiert werden. Bei 232 ergeben sich bereits 4 TByte.
Damit lässt sich im Programm mehr Speicher ansprechen, als physikalisch tatsächlich vorhanden ist. Der physikalische Speicher kann dennoch dadurch vergrössert werden, dass
man Hintergrundspeicher (Festplattenplatz) mit verwendet. Dabei werden Teile des Hauptspeichers auf die Festplatte ausgelagert, wenn diese nicht benötigt werden.
Linux unterstützt virtuellen Speicher. Die Konsequenz für den Treiberprogrammierer: er kann nicht damit rechnen, dass zu jedem Zeitpunkt, an dem möglicherweise eine
Treiberfunktion aufgerufen wird, sich auch der zugehörige Userprozess im Speicher
befindet. Unter Linux ist dies nur der Fall, wenn die entsprechende Treiberfunktion
durch die Applikation getriggert wird (z.B. durch open, close, read oder write). In
diesen Fällen läuft der Treiber im so genannten user context. Wird hingegen eine Interrupt-Service-Routine des Treibers aufgerufen, spricht man vom interrupt context, aus dem
ein Zugriff auf Userprozess-Ressourcen nicht möglich ist.
IO-Subsystem
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
10/55
Den einheitlichen Zugriff auf Peripherie (IO) ermöglicht das IO-Subsystem. Dabei hat
sich heute im Wesentlichen das Konzept durchgesetzt, Peripherie bzw. Geräte wie Dateien
zu betrachten. Nicht die Applikation, aber das Betriebssystem unterscheidet damit mehrere
Arten von Dateien:
•
Normale Dateien (ordinary files)
•
Pipes (FIFO-Dateien)
•
Directories und
•
Gerätedateien.
Neben dieser Abstraktion bietet das IO-Subsystem auch noch Mechanismen an, um auf Hintergrundspeicher (Festplatte) eine Reihe von Dateien hierarchisch abzulegen.
Neben der Schnittstelle zur Applikation für den einheitlichen Hardwarezugriff und dem Filesystem bietet das IO-Subsystem noch die betriebssysteminterne Schnittstelle zur systemkonformen Integration von Hardware an: die so genannte Treiberschnittstelle.
Libraries
Zum Betriebssystem gehörige Bibliotheken (Libraries) abstrahieren den Zugriff auf die Systemcalls bzw. auf die Dienste. Libraries werden sowohl zu den eigenen Applikationen als
auch zu den Dienstprogrammen hinzugebunden. Man unterscheidet statische von dynamischen Bibliotheken. Während statische Libraries zu dem eigentlichen Programm beim
Linken hinzugebunden werden, werden dynamische Bibliotheken (auch shared libs genannt)
erst dann an das Programm gebunden, wenn dieses ausgeführt werden soll. Das bringt folgende Vorteile mit sich:
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
11/55
•
•
Das eigentliche Programm wird kleiner (der Code für die Libraries muss ja nicht abgelegt
werden).
Programme können sich eine „shared lib“ teilen, wodurch Hauptspeicherplatz gespart
wird. Dass mehrere Programme Code verwenden, der nur einmal im Speicher ist, ist wegen der Trennung von Code- und Datensegment möglich.
Nachteilig bei diesem Verfahren ist es, dass zum Ausführen einer Applikation nicht nur selbige, sondern zusätzlich auch alle zugehörigen Bibliotheken in der richtigen Version notwendig sind. Dies führt gerade bei komplexen Applikationen zuweilen zu Problemen. Nicht immer ist die erforderliche Bibliotheksversion auf dem Rechner verfügbar.
Linux unterstüzt das Laden dynamischer Libraries unter Programmkontrolle.
Dynamisch ladbare Bibliotheken werden im Regelfall vom System zur Applikation dazu gebunden (gelinkt), wenn die Applikation gestartet wird. Daneben gibt es auch die Variante,
dass eine Applikation selbst eine Bibliothek lädt.
Während früher beim Linken statischer Libraries die komplette Bibliothek hinzugebunden wurde, werden heute aus einer Bibliothek nur die Funktionen extrahiert, die
ein Programm auch wirklich einsetzt. Damit wird ebenfalls verhindert, dass der Programmcode auf der einen Seite zu stark anwächst oder auf der anderen Seite Funktionen
auf mehrere Libraries verteilt werden.
Beispiel:
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
12/55
$ cat dynamic_load.c
#include <stdio.h>
#include <dlfcn.h>
int main(int argc, char **argv) {
void *handle;
double (*cosine)(double);
// pointer to function with double argument
// returning double
char *error;
handle = dlopen ("libm.so", RTLD_LAZY);
if (!handle) {
fprintf (stderr, "%s\n", dlerror());
exit(1);
}
cosine = dlsym(handle, "cos"); // cosine becomes cos-function
// out of libm.so
if ((error = dlerror()) != NULL) {
fprintf (stderr, "%s\n", error);
exit(1);
}
printf ("%f\n", (*cosine)(2.0));
dlclose(handle);
return 0;
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
13/55
}
$
$ gcc dynamic_load.c -ldl -o dynamic_load
$
Device Driver Layer
In diesem Layer sind Gerätetreiber angesiedelt. Über Gerätetreiber werden die Zugriffe auf
die Hardware durchgeführt.
1.2.Gerätetreiber
Über Gerätetreiber werden unterschiedlichste Arten von Hardware in ein Betriebssystem, wie
z.B. Drucker, Kameras, Tastaturen, Bildschirme, Netzwerkkarten, Scanner integriert.
Da diese Geräte über diverse Bussysteme (z.B. PCI, SCSI, USB) angeschlossen werden können,
gibt es in Linux unterschiedliche Treiber-Subsysteme.
Während traditionell zwischen Character-Devices und Block-Devices unterschieden wird, findet
man in Linux die folgenden Subsysteme (unvollständige Liste):
Character-Devices
•
Block-Devices
•
Netzwerk
•
SCSI
•
USB
•
Irda
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
14/55
•
Cardbus und PCMCIA
•
Parallelport
Für diese Vielfalt ist die Applikationsschnittstelle erweitert worden. Zwischenzeitlich kann man
folgende Interfaces differenzieren:
•
Standard API (mit Funktionen wie open, close, read, write und ioctl).
•
Kommunikations-API.
•
Card-Services.
•
Multimedia-Interfaces (z.B. Video4Linux).
•
Durch diese Funktionsaufteilung kommt man zu den so genannten „stacked driver“,
den geschichteten Treibern.
Ein low-level Treiber ist für die Ansteuerung der eigentlichen Hardwareschnittstelle,
also beispielsweise eines ganz spezifischen USB-Controllers, zuständig. Da die Anzahl bzw. Auswahl der Komponenten für den direkten Hardwarezugriff gering ist, kommt man hier mit einer
geringen Anzahl von Treibern aus. Der high-level Treiber dagegen ist für einen Gerätetyp,
z.B. eine Webcam zuständig. Zwischen low-level Treiber und high-level Treiber liegt eine CoreTreiber Schicht. Diese erweitert die interne Treiberschnittstelle um gerätetypspezifische Funktionen.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
15/55
Applications
high level driver
Gerätefunktion
core driver
low level driver
Zugriff auf Hardware
Hardware
SCSI-, PCI- und auch Parallelport-Treiber stellen Untergruppen der Character- und Blockgerätetreiber dar. Irda, USB und Netzwerktreiber bilden eine eigene Gruppe von Treibern.
1.3.Systemprogrammierung
Hier wird kurz auf Systemprogrammierung eingegangen – aber nur soviel, wie man zum Schreiben von Gerätetreibern braucht.
1.3.1.Kontrollfluss
Für den Zugriff auf ein Gerät gibt es zwei Zugriffsmodi:
•
blocking und
•
non-blocking.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
16/55
Diese Zugriffsarten legen fest, wie der Kernel reagieren soll, falls eine Applikation Daten lesen möchte, diese aber zum Zeitpunkt der Anfrage nicht verfügbar sind, bzw. falls eine Applikation Daten schreiben möchte, und diese zum Zeitpunkt der Anfrage nicht schreibbar sind.
Die Zugriffsart wird entweder bereits beim Öffnen eines Gerätes spezifiziert oder wird nachträglich mit dem fcntl-Aufruf eingestellt.
•
Beim blocking-Mode wird die aufrufende Applikation (genauer der aufrufende Thread)
blockiert (in den Zustand „warten“ versetzt), solange der Treiber nicht in der Lage ist,
angeforderte Daten zu liefern (lesen) oder übergebene Daten zu schreiben.
Sobald Daten zum Lesen vorhanden sind bzw. geschrieben werden können, wird der Rechenprozess wieder aufgeweckt. Allerdings kann der Prozess auch dann wieder aufgeweckt werden, falls die Applikation ein Signal geschickt bekommt. Der Systemcall ist damit nicht erfolgreich abgeschlossen und liefert einen entsprechenden Fehlercode zurück .
Er muss ein zweites mal gestartet werden.
Beispiel für blockierenden Zugriff auf stdin:
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
17/55
$ cat blocking_access.c
#include <stdio.h>
int main( int argc, char **argv ) {
int fd=0; // stdin, standardmaessig im blocking mode
unsigned char buffer[512];
printf("Das Programm zeigt den Blocking mode. Es geht erst"
"\nweiter, wenn eine Eingabe (mit Return abgeschlossen)"
"\neingegeben wird.\n");
printf("WARTEN AUF EINGABE ...\n");
read( fd, buffer, sizeof(buffer) ); // read wartet
printf("%s\n", buffer);
printf("Und tschuesss ...\n");
}
$
•
Im non-blocking Mode wird der aufrufende Rechenprozess nicht schlafen gelegt, auch
wenn keine Daten bereit liegen bzw. geschrieben werden können, sondern der Fehlercode EAGAIN zurückgeliefert.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
18/55
$ cat nonblocking_access.c
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
int main( int argc, char **argv )
{
int fd=0; // stdin, standardmaessig im blocking mode
int retvalue;
char buffer[512];
printf("Das Programm zeigt den Non-Blocking mode."
"\nSTDIN wird jetzt auf non-blocking umgeschaltet und danach"
"\nwird davon gelesen.\n");
printf("WARTEN AUF EINGABE ist hier überflüssig\n");
fcntl( fd, F_SETFL, O_NONBLOCK); // stdin in nonblocking mode setzen
retvalue = read( fd, buffer, sizeof(buffer) );
if (retvalue < 0) {
// keine Zeichen in Eingabe
perror( "nach dem read" );
printf("retvalue (%d) errno=%d\n", retvalue, errno);
} else
printf("%s\n", buffer );
printf("Und tschuesss ...\n");
}
$
Wie muss das Programm gestertet werden, dass es Daten liest?
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
19/55
$ nonblocking_acess < nonblocking_acess.c
Das Programm zeigt den Non-Blocking mode.
STDIN wird jetzt auf non-blocking umgeschaltet und danach
wird davon gelesen.
WARTEN AUF EINGABE ist hier überflüssig
#include <stdio.h>
#include <fcntl.h>
...
Und tschuesss ...
$
$ nonblocking_acess
Das Programm zeigt den Non-Blocking mode.
STDIN wird jetzt auf non-blocking umgeschaltet und danach
wird davon gelesen.
WARTEN AUF EINGABE ist hier überflüssig
nach dem read: Resource temporarily unavailable
retvalue (-1) errno=29
Und tschuesss ...
$
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
20/55
Aufgabe
Welche Ausgabe produziert folgender Aufruf des Programms nonblocking_acess.
$ echo abc | nonblocking_acess
???????
$
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
21/55
1.3.2.Anwendungsschnittstelle zu Gerätetreibern
Das IO-Subsystem stellt eine einheitliche Schnittstelle für den Zugriff auf Geräte zur Verfügung.
Dabei werden Geräte und Dateien aus Sicht der Applikation gleich behandelt. Diese Schnittstelle
besteht im Wesentlichen aus den folgenden Funktionen:
•
open
•
close
•
read
•
write
•
ioctl
•
select
•
fcntl
•
lseek
Die meisten der o.g. Funktionen haben wir bereits gesehen.
open
Über diese Funktion meldet sich die Applikation beim Betriebssystem und insbesondere
beim Treiber an. Dabei wird über den zu übergebenden symbolischen Namen der zugehörige Treiber ausgewählt. Erst das Betriebssystem, danach der Treiber haben Gelegenheit zu
überprüfen, ob die Applikation überhaupt Zugriff auf den Treiber bekommen darf oder nicht.
Zugriff wird im Wesentlichen aufgrund zweier Kriterien erteilt beziehungsweise verweigert:
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
22/55
1.Art des gewünschten Zugriffes:
•
Will die Applikation (nur) lesen und ist dies erlaubt?
•
Will die Applikation (nur) schreiben und ist dies erlaubt?
•
Will die Applikation lesen und schreiben und ist dies erlaubt?
2.Belegung des Treibers durch einen anderen Task.
Öffnet die Applikation das Gerät (open), wird überprüft, ob nicht bereits ein anderer Rechenprozess auf das Gerät zugreift. Abhängig vom Treiber bzw. von den Fähigkeiten der
angesteuerten Hardware darf z.T. nur ein Rechenprozess zugreifen. Die Zugriffsberechtigungsüberwachung kann auch sehr dediziert erfolgen. Beispielsweise können auf manche
Geräte mehrere Tasks lesend zugreifen, aber nur ein Task schreibend.
Der open-Call liefert bei erfolgreichem Öffnen des Gerätes einen so genannten Filedeskriptor zurück. Dieser Filedeskriptor ist ein Index auf eine im Betriebssystem angelegte Datenstruktur (Speicherbereich), die die für die weiteren (produktiven) Zugriffe notwendige Zuordnung zwischen Rechenprozess und Treiber speichert.
close
Die close Funktion gibt die im Betriebssystem alloziierte Ressource (file-descriptor) wieder
frei. Bei Aufruf der Funktion wird auch der Treiber informiert. Dieser kann Aufräumarbeiten
durchführen. Er merkt sich, dass das Gerät nicht mehr (durch diesen Rechenprozess) belegt
ist und kann auf Treiberebene alloziierte Ressourcen freigeben.
read
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
23/55
Über die read Funktion werden Daten zwischen dem Treiber und dem Rechenprozess (Applikation) ausgetauscht. Die Applikation spezifiziert über den Filedeskriptor (erstes Argument) den Treiber, übergibt desweiteren (zweites Argument) eine Speicheradresse, in die
die vom Gerät zu lesenden Daten zu schreiben sind und übergibt schließlich als drittes Argument die Anzahl der maximal zu lesenden Bytes.
Die Funktion read ist so spezifiziert, dass diese im Default-Zugriffsmode BLOCKING, mindestens 1 Byte zurückgibt oder den entsprechenden Rechenprozess (hier genauer Thread)
blockiert (sprich in den Zustand „warten“ versetzt).
Oftmals wird fälschlicherweise angenommen, dass die Funktion Daten blockweise übergibt.
Das ist aber nicht der Fall. Es ist durchaus legal, dass die Funktion - trotz eines Leseauftrages von 32 Bytes - nur 1 Byte in den Buffer kopiert. Daher muss man ein read grundsätzlich in einer Leseschleife betreiben:
for( ByesLeft=BytesToRead, BufPtr=&Buffer; BytesLeft<0; ) {
BytesRead=read( fd, BufPtr, BytesLeft );
if( BytesRead≤0 ) {
// Fehlerbehandlung
...
}
BufPtr+=BytesRead;
BytesLeft-=BytesRead;
}
Die Funktion kann aber auch aufgrund einer Fehlerbedingung abgebrochen werden. Bekommt beispielsweise der aufrufende Rechenprozess ein Signal gesendet, wird der gerade
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
24/55
aktive Systemcall - und read ist ein Systemcall - abgebrochen und liefert einen entsprechenden Fehlercode.
write
Das Gegenstück zu read ist write. Mittels dieser Funktion schreibt eine Applikation Daten
auf ein Gerät. Das Betriebssystem prüft dabei, ob der Rechenprozess überhaupt eine
Schreibberechtigung hat. Ist dies der Fall, bekommt der Treiber den Schreibauftrag durchgereicht. Abhängig vom Zugriffsmodus wird der Treiber den Schreibauftrag starten und den
zugehörigen Rechenprozess schlafen legen (Blocking-Mode). Ist der Auftrag erledigt, wird
der Prozess aufgeweckt und bekommt die Anzahl der geschriebenen Bytes zurückübergeben. Die Applikation stellt die zu schreibenden Daten in einem Buffer zur Verfügung. Der
Buffer liegt im Adreßbereich der Applikation und muss daher im Treiber mittels einer Funktion (z.B. copy_from_user) in den Adreßbereich des Kernels (Kernel-Space) kopiert werden.
Mittels dieses allgemeinen Systemcalls lassen sich beliebige Funktionalitäten bzw. Daten
zwischen Applikation und Treiber austauschen. Ein Gerät kann dabei mehrere IO-Control
Kommandos unterstützen, die anhand einer Nummer applikations- und treiberseitig unterschieden werden. So flexibel diese Schnittstelle auch ist, so sehr sollte die Verwendung
bzw. Unterstützung im Treiber vermieden werden: IO-Controls stellen letztendlich nicht portierbare Eigenschaften eines Treibers dar.
ioctl
Mittels ioctl lassen sich bei der seriellen Schnittstelle Übertragungsgeschwindigkeit oder
auch Parity gerade/ungerade einstellen.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
25/55
Die beim ioctl verwendeten Parameter unterscheiden sich bei Windows und Unix. Windows
geht neben dem Kommando davon aus, dass zwei Buffer übergeben werden: ein Buffer für
Daten an den Treiber und ein Buffer, in den der Treiber Daten für die Applikation ablegen
kann. Dieses wird deshalb so gemacht, weil es unter Windows einen so genannten IO-Manager gibt, der für den Austausch von Daten zwischen Kernel- und User-Space zuständig
ist. Durch die angegebene Festlegung kann der IO-Manager effizient beim Einsprung in den
ioctl die Daten aus dem User-Space in den Kernel-Space kopieren, bzw. beim Aussprung
die Daten aus dem Kernel-Space in den User-Space.
Unter Linux wird - neben dem ioctl Kommando - nur ein weiteres Datum, im Regelfall ein
Zeiger auf eine Datenstruktur, übergeben. Die Datenstruktur selbst wird durch denjenigen,
der den Treiber konzipiert, festgelegt. Da vom Treiber aus der Zugriff auf den User-Space
problemlos (wenn auch nur über eine Funktion) möglich ist, kann diese Datenstruktur Zeiger enthalten, die in unterschiedliche Speicherbereiche der Applikation zeigen.
select/poll
Diese Funktionen ermöglichen das Überprüfen, ob ein oder mehrere Geräte Daten zum
Lesen bereit haben oder in der Lage sind, Daten zu schreiben. Insbesondere die Fähigkeit,
gleich mehrere Geräte oder Dateien gleichzeitig auf diese Eigenschaften hin zu überprüfen,
machen die Funktionen zu einer wichtigen Funktionalität komplexerer Applikationen.
Da die Funktion select gleichzeitig noch eine Zeitüberwachung ermöglicht, wird die
Funktion oft zum Warten auf Ablauf einer Zeitspanne, die unterhalb der mit der Funktion
sleep erreichbaren Sekunde liegt, verwendet.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
26/55
Beispiel (Test ob innerhalb von 5 Sekunden Daten auf stdin anstehen):
$ cat select_stdin.c
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
27/55
int main(void){
fd_set rfds;
struct timeval tv;
int retval;
/* Watch stdin (fd 0) to see when it has input. */
FD_ZERO(&rfds);
// remove descriptor from rfds
FD_SET(0, &rfds);
// add stdin to rfds
/* Wait up to five seconds. */
tv.tv_sec = 5;
// Micorsec
tv.tv_usec = 0;
// Nanosec
retval = select(1, &rfds, NULL, NULL, &tv); // Don't rely on the
//value of tv now!
if (retval)
printf("Data is available now.\n");
/* FD_ISSET(0, &rfds) will be true. */
else
printf("No data within five seconds.\n");
exit(0);
}
$
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
28/55
fcntl
Die wichtigsten Zugriffsparameter, wie beispielsweise der Zugriffsmode, werden beim Öffnen eines Gerätes mit angegeben. Einige Parameter, z.B. die Zugriffsart, lassen sich aber
mittels fcntl auch nachträglich ändern.
lseek
Mit dieser Funktion kann in einer Datei (bzw. bei einem Block-Device) gezielt ein bestimmtes Byte gelesen bzw. geschrieben werden. Die mit lseek mögliche Positionierung kann relativ zum Dateianfang, zum Dateiende oder der gerade aktuellen Position erfolgen.
1.3.3.Programmbeispiele
Zwei einfache Beispiel sollen den Zugriff auf Geräte verdeutlichen. Im ersten Beispiel wird auf
die Partitionstabelle auf einem PC zugegriffen. Diese befindet sich auf dem ersten Block der
jeweiligen Festplatte. Eine Festplatte wird auf das logische Gerät /dev/hda (bzw. /dev/sda
bei einer SCSI-Platte) abgebildet.
$ cat read_partitionstabelle.c
#include <stdio.h>
#include <fcntl.h> // für O_RDONLY
#define DISKFILE "/dev/hda"
// Programm zum Auslesen der Partitionstabelle auf einem PC.
// Nur der Superuser darf auf das Device /dev/hda zugreifen.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
29/55
main() {
int i, j, fd;
unsigned char buffer[512];
if( (fd=open(DISKFILE,O_RDONLY))<0 ) {
perror( DISKFILE );
exit( -1 );
}
// open disk
// read first block: partitiontable
if( read( fd, buffer, sizeof(buffer) )!=sizeof(buffer) ) {
perror( "read" );
exit( -2 );
}
// print out partition table
for( i=0; i<(sizeof(buffer)/16); i++ ) {
for( j=0; j<16; j++ ) {
printf("%2.2x ", buffer[i*16+j] );
}
printf( "\n");
}
close( fd );
}
$
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
30/55
Beim zweiten Beispiel werden Daten auf die parallele Schnittstelle ausgegeben. Die erste parallele Schnittstelle heißt /dev/lp0.
$ cat write_lp0.c
#include <stdio.h>
#include <fcntl.h> // für O_RDONLY
#define DISKFILE "/dev/lp0"
// Programm zur Ausgabe von Zeichen auf die parallele Schnittstelle.
main() {
int fd;
char *ptr="aaaaaa";
if( (fd=open(DISKFILE,O_WRONLY))<0 ) {
perror( DISKFILE );
exit( -1 );
}
if( write( fd,ptr,strlen(ptr))<strlen(ptr) ) {
perror( "write" );
}
close( fd );
}
$
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
31/55
Weitere interessante Themen der Systemprogrammierung unter Linux wären:
•
•
Prozesse
•
fork
•
Threads
Interprozesskommunikation
•
Messages
•
Shared Memory
•
Sockets
•
Signals
•
Semaphore
Diese Themen werden in der Wahlpflichtveranstaltung „Unix für Entwickler“ und „Systemprogrammierung in Linux“ oder wenn Zeit ist im Punkt „Ausgewählte Themen“ behandelt.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
32/55
2.Module
Linux bietet die Möglichkeit, einzelne Funktionalitäten in Form von Modulen auch nach Systemstart nachzuladen bzw. wieder aus dem System zu entfernen.
Vorteil:
•
•
Ressourcen werden nur dann belegt, wenn sie auch tatsächlich benötigt werden.
Die Entwicklungszeit wird verkürzt, denn es muss nicht jedes mal ein kompletter Betriebssystemkern hoch- und wieder runter gefahren werden, nur weil sich eine Zeile Code
geändert hat.
Module stellen also Programmcode dar, welcher ein Stück Funktionalität realisiert, die nicht zu
jedem Zeitpunkt benötigt wird.
Ein Modul stellt aber nicht zwangsweise eine abgeschlossene Funktionalität dar. Verschiedene
Technologien existieren, um eine Aufgabe auf mehrere Module aufzuteilen bzw. Module flexibel
mit anderen Modulen interagieren zu lassen.
Um Module zu laden, die Liste der geladenen Module anzuzeigen und Module wieder zu entfernen stehen Systemprogramme zur Verfügung:
•
insmod,
•
lsmod und
•
rmmod.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
33/55
Außerdem können Module Metainformationen, wie den Author, eine Beschreibung, falls es sich
bei einem Modul um einen Treiber handelt die unterstützten Geräte und die Modul-Lizenz enthalten. Unter Linux läßt sich diese Metainformation mit dem Programm modinfo anzeigen.
Die Übergabe von (Modul-) Parametern ist beim Laden des Moduls möglich.
Da ein Modul kein abgeschlossenes Programm, sondern Teil des Programms „Betriebssystemkern“ ist, gibt es keine main-Funktion. Stattdessen ruft der Loader (insmod), sobald sich der
Modulcode im Speicher befindet, die Funktion init_module auf, bzw. vor dem Entladen wird die
Funtkion cleanup_module aufgerufen. Es reicht aus, diese beiden Funktionen zu codieren, um
unter Linux ein Modul erstellt zu haben. Dabei gilt es zu beachten, dass die zu programmierende
init_module-Funktion den Wert 0 zurück liefert, falls das Laden erfolgreich war. Liefert die
Funktion einen anderen Wert zurück, entfernt der Betriebssystemkern den Modulcode wieder
aus dem Hauptspeicher unddas Laden war nicht erfolgreich.
Das folgende Beispiel zeigt ein einfaches Modul, das in die Logdatei /var/log/messages beim
Laden und Entfernen des Moduls einen String schreibt:
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
34/55
$ #include <linux/fs.h>
#include <linux/version.h>
#include <linux/module.h>
static int init_module(void) {
printk("init_module called\n");
return 0;
}
// write into log message file
static void cleanup_module(void) {
printk("cleanup_module called\n");
}
$
Das Übersetzten eines Moduls erfolgt durch:
$ cc -w -O -DMODULE -D__KERNEL__ -I/usr/src/linux-2.4/include -c erstesModul.c
$
Entstanden ist eine Objektdatei:
$ ls -l
-rw-r--r-1 as
users
-rw-r--r-1 as
users
$
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
268
1036
1. Okt 15:43 erstesModul.c
1. Okt 16:06 erstesModul.o
35/55
Der Superuser darf das Modul laden:
# insmod -f erstesModul.o
Warning: kernel-module version mismatch
erstesModul.o was compiled for kernel version 2.4.20
while this kernel is version 2.4.20-18.9
Module erstesModul loaded, with warnings
#
Auflisten aller geladenen Module und Check der Logdatei zeigen, dass das Modul geladen wurde
und der Code ausgeführt wurde:
# lsmod | grep erstesModul
erstesModul
824
0 (unused)
# tail -3 /var/log/messages
Oct
Okt
Okt
Oct
1
1
1
1
15:24:36
15:24:42
16:10:29
16:10:33
ibm
ibm
ibm
ibm
kernel: PCI: Sharing IRQ 11 with 02:00.0
gdm(pam_unix)[1391]: session opened for user as by (uid=0)
su(pam_unix)[3639]: session opened for user root by as(uid=500)
kernel: init_module called
#
Entfernt wird das Modul durch:
# rmmod erstesModul
#
Soll einem Modul beim Laden ein Parameter mitgegeben werden können, müssen sie in Form
von Name-Wert-Paaren angegeben werden:
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
36/55
# insmod -f modulMitParametern.o ioport=0x3f8
#
Im Modul selbst sind die Namen der Name-Wert-Paare als Variablen definiert und über ein Makro (MODULE_PARM) dem System bekanntgegeben. Das Makro MODULE_PARM_DESC dient wiederum zur Ablage von Metainformationen.
Dem Makro MODULE_PARM wird als zweiter Parameter der Variablentyp mitgegeben. Neben den
Standardvariablen ist auch die Übergabe von Feldern möglich, wobei in diesem Fall die Anzahl
der Feldelemente spezifiziert werden muß: [min[-max]]{b,h,i,l,s} Der Parameter min gibt
die minimale Anzahl Elemente an, max die maximale. Wird max weggelassen, wird für max der
Wert von min angenommen. Folgende Datentypen werden unterstützt:
Datentyp Kurzform
byte
b
short
h
int
i
long
l
string
s
Das folgende Beispiel zeigt, wie ein Modul mit Aufrufparametern zu kodieren ist:
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
37/55
$ cat modulMitParameter.c
#include <linux/fs.h>
#include <linux/version.h>
#include <linux/module.h>
int ioport=0x3e0;
MODULE_PARM(ioport,"i");
MODULE_PARM_DESC(ioport, "default is 0x3e0");
static int init_module(void) {
printk("init_module called with parameter %d\n, ioport");
return 0;
}
static void cleanup_module(void) {
printk("cleanup_module called\n");
}
$
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
38/55
3.Treibergrundlagen
Eine Reihe von Eigenschaften macht die Treiberentwicklung unter Linux einfach:
•
•
•
•
Linux-Treiber basieren auf dem bewährten Unix-Konzept zur systemkonformen Einbindung
von Geräten in das Betriebssystem.
Dank des Modulkonzeptes lassen sich neue Eigenschaften eines Treibers schnell und leicht
austesten, ohne dass das Betriebssystem jedes mal neu gebootet werden muss.
Zur Erstellung eines Linuxtreibers sind keinerlei zusätzliche Werkzeuge oder Dateien notwendig. Ein Editor, der Compiler und die Headerdateien reichen für die Entwicklung aus.
Dank offen gelegtem Quellcode lassen sich auch kompliziertere Probleme schnell lösen.
Darüber hinaus gibt es hervorragende Literatur und genügend Hilfestellung über das Internet.
3.1.Aufbau eines Treibers
Ein Gerätetreiber ist eine Menge von Funktionen, die vom Treiberprogrammierer zu erstellen
sind. Greift eine Applikation über die definierte Applikationsschnittstelle auf den Treiber zu, wird
im Treiber eine entsprechende Funktion getriggert. So mündet z.B. der Aufruf von open in der
Applikation in einer open-Funktion im Treiber.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
39/55
Applikationsfunktion
open()
User
triggert
Treiberfunktion
open()
Kernel
So wie die Aufrufparameter und die Rückgabewerte der Applikationsaufrufe festgelegt sind, so
sind sie auch bei den Treiberfunktionen definiert. Die Rückgabewerte der Treiber-Systemfunktionen (z.B. open, close, read, write) sind identisch mit den Rückgabewerten der entsprechenden
Applikationsfunktionen. Das gilt auch für die Fehlercodes, die sich sowohl für die Applikation als
auch für die Treiber in der Datei /usr/include/asm/errno.h nachsehen lassen.
Die Namen der Funktionen sind im Treiber im Gegensatz zur Applikationsschnittstelle (API) frei
wählbar. Damit der Betriebssystemkern weiß, welche Funktionalität eine Funktion realisiert, werden die Adressen der Funktionen vom Treiber in einer Datenstruktur abgelegt (struct
file_operations) und bei der Initialisierung dem Kernel übergeben.
Die für die Treibererstellung wichtigsten Funktionen sind open, close, read und write.
open
Diese Funktion wird aufgerufen, sobald eine Applikation ein open auf den Treiber durchführt
und der Kernel die Zugriffsberechtigung geprüft hat. Der Treiber überprüft seinerseits evenBetriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
40/55
tuelle Zugriffskonflikte, alloziiert notwendige Ressourcen und initialisiert die Hardware. Geht
alles gut, signalisiert er der Applikation den Erfolg, andernfalls wird der Zugriff auf das Gerät verweigert.
Auf einen Treiber kann prinzipiell mehrfach zugegriffen werden. Der zugreifende Prozess
wird im Treiber über die Struktur struct file identifiziert.
close
Getriggert durch die close-Funktion in der Applikation hat der Treiber die Möglichkeit, für
den spezifischen Task alloziierte Ressourcen wieder freizugeben.
read und write
Im Treiber gibt es jeweils eine Funktion für das Lesen und eine für das Schreiben von Daten. Diese Treiberfunktionen werden ausgeführt, wenn auf den Treiber ein read bzw. write
Systemcall ausgeführt wird. Die beim Aufruf an der Applikationsschnittstelle übergebenen
Parameter (Adresse und Länge des Buffers) werden direkt an die Treiberfunktionen durchgereicht. Der Treiber muss die Daten in diesen Userbuffer kopieren bzw. von diesem Userbuffer lesen. Für die Verarbeitung der Daten sind möglicherweise Hardwarezugriffe notwendig. Rückgabewert der Funktionen ist die Anzahl der gelesenen bzw. geschriebenen Bytes.
Auf welchen Treiber (Treiberauswahl) eine Applikation zugreift, bestimmt sie beim Aufruf
der Funktion open anhand des dabei zu übergebenden Dateinamens. Beispiel: lesender und
schreibender Zugriff auf ein Diskettenlaufwerk durch:
open( ''/dev/floppy'', O_RDWR );
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
41/55
Der Bezug zwischen dem beim Aufruf verwendeten Dateinamen (im Beispiel /dev/floppy)
und dem Treiber geschieht über eine systemweit eindeutige Nummer, der so genannten Majornumber. Wird eine Gerätedatei im Filesystem angelegt, muß die zugehörige Majornummer mit angegeben werden. Die Majornumber wird entweder im Treiber festgelegt oder
vom Kernel vergeben.
Der Treiber selbst meldet sich ebenfalls unter dieser Nummer bei seiner Initialisierung beim
Betriebssystem an (beispielsweise mit Hilfe der Funktion register_chrdev). Bei dieser Anmeldung wird auch die Liste der Funktionsadressen (struct file_operations) mit übergeben.
struct file_operations ist ein Array von Zeigern auf die Treiberfunktionen.
struct file_operations
open()
close
read()
Neben den Treiberfunktionen, die durch die Applikation selbst getriggert werden, kann ein Treiber weitere Funktionen enthalten, die entweder durch die Hardware oder durch das Betriebssys-
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
42/55
tem aufgerufen werden. Im ersten Fall handelt es sich um Interrupt-Service-Routinen, im zweiten Fall können das beispielsweise zeitgesteuerte Funktionen sein.
Zur systemkonformen Einbindung des Treibers in das Betriebssystem stehen eine Reihe von
Funktionen zur Verfügung. Mit Hilfe dieser Funktionen lassen sich Ressourcen allokieren, OS- interne Dienste nutzen oder auch nur Taskzustände verändern (z.B. Schlafen legen der Task).
3.2.Zuordnung Applikationsschnittstelle auf Treiber
3.2.1.Logische Geräte
Ein Gerätetreiber steuert eine bestimmte Art von Geräten an. So ist beispielsweise für die seriellen Schnittstellen ein Treiber - und nicht für jede einzelne serielle Schnittstelle ein eigener - zuständig.
Der Gerätetreiber schafft eine Sicht des physikalischen Gerätes für die Applikation. Man
spricht hier von logischen Geräten, weil diese Sicht nicht viel mit der physikalischen Ausprägung (Hardwareschnittstelle) zu tun haben muss.
Das physikalische Gerät wird auf eine oder mehrere so genannte (logische) Gerätedateien
abgebildet. Werden zur Abbildung mehrere logische Gerätedateien verwendet, verbirgt sich im
Regelfall hinter jeder Gerätedatei eine bestimmte Funktionalität. So wird beispielsweise im PC
unter Linux die erste SCSI-Festplatte als /dev/sda abgebildet. Über diese Datei kann eine entsprechend berechtigte Applikation jeden einzelnen Block der Platte lesen. Daneben werden aber
auch noch die Dateien /dev/sda1 bis /dev/sda4 (und noch mehr) zur Verfügung gestellt. Diese
repräsentieren jeweils eine Partition auf der Festplatte. Greift eine Applikation auf /dev/sda2 zu,
hat sie Zugriff auf die zweite Partition. Die Daten der 1. oder 3. Partition sind damit aber nicht
zu lesen.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
43/55
Für die serielle Schnittstelle gilt ähnliches. Je nachdem, welches logische Gerät angesprochen
wird (z.B. /dev/ttyS0 oder /dev/cua0), werden beim nachfolgenden Zugriff auf die Schnittstelle unterschiedliche Handshake-Signale verwendet.
3.2.2.Major- und Minornumber
Treiber werden über den Mechanismus der ladbaren Module Teil des Betriebssystems. Werden
sie durch das Laden aktiviert, reservieren sie die für die Ansteuerung der Hardware notwendigen Ressourcen. Danach melden sie sich als Treiber beim System an.
Damit eine Applikation einen Treiber identifizieren kann, ist dem symbolischen Geräte-namen (Treibernamen) eine so genannte Majornumber zugeordnet. Da der Treiber bei der Initialisierung (in der Funktion init_module beim Aufruf der Funktion register_chardev) sich beim
Betriebssystem ebenfalls unter einer Majornumber anmeldet, kann das Betriebssystem die Zuordnung zum Treiber durchführen.
Treiber Code:
register_chardev(MAJORNO, “Mini-Driver”, ...)
BS:
$ mknode NameLogGeraet MajorNo
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
44/55
Das beschriebene Verfahren hat das Problem, dass die Majornumber eine Zahl im Bereich
zwischen 0 und 255 ist. Es ist leicht vorstellbar, dass in der Praxis jedoch mehr als 255 Treiber
existieren.
Dieses Problem wird durch eine dynamische Zuteilung der Majornumber gelöst. Die Applikation interessiert sich nicht für die Majornumber, sondern greift über den symbolischen Namen zu. Der Treiber selbst meldet sich beim System zudem nicht nur mit einer Majornumber,
sondern ausserdem mit einem symbolischen Namen (der jedoch nicht identisch sein muß mit
dem symbolischen Namen, den die Applikation später verwendet) an.
Unter Linux ist daher festgelegt worden, dass das Betriebssystem die nächste freie Majornumber vergibt, wenn ein Treiber beim Anmelden (Registrieren) die Majornumber 0 angibt.
Auf der User-Ebene wird nach dem Laden des Treibers ein Programm (Skript) gestartet, welches über den dem Skript bekannten Treibernamen aus /proc/devices die vom System vergebene Majornumber ausliest und ein Gerätefile mit dieser Majornumber anlegt.
Ein solches Skript könnte folgendermassen aussehen:
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
45/55
$ cat createDevice.ksh
#!/bin/sh
Module="access"
ModuleName="BufDevice-Driver"
DeviceName="Driver"
MinorNumber=0
# Insert Module
/sbin/rmmod $Module
/sbin/insmod -f $Module.o || exit 1
# Identify allocated major number
MajorNumber=`cat /proc/devices | grep $ModuleName | cut -c1-3`
if [ "${MajorNumber}y" == "y" ]
then
echo "Module with name \"$ModuleName\" does not exist."
exit 1
fi
# create device
rm -f $DeviceName
mknod $DeviceName c $MajorNumber $MinorNumber
$
Neben der Majornumber gibt es auch noch eine Minornumber. Diese ist genauso wie die Majornumber mit dem symbolischen Geräteeintrag im Filesystem (Gerätenamen) verbunden. Dadurch lassen sich mehrere Gerätenamen (also die logischen Geräte), die mit einem Treiber
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
46/55
assoziiert sind, anlegen und im Treiber unterscheiden. Somit stellen die Minor-Nummern einen
Parameter dar, der dem Treiber bei den Aufrufen mit übergeben wird.
Über die Minornumber können mehrere Informationen codiert werden:
•
Funktionalitäten (z.B. die Art des Handshaking bei der seriellen Schnittstelle)
•
Zugriffsbereiche (z.B. die einzelnen Partitionen einer Festplatte)
•
eine spezifische Hardware, z.B. ob die 1. oder die 2. serielle Schnittstelle verwendet werden
soll.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
47/55
3.3.Treiberfunktionen
Über die Majornumber wird die Beziehung zwischen einem Open-Call auf der Applikationsseite und dem eigentlichen Gerätetreiber hergestellt. Prinzipiell existiert für jeden der erwähnten Systemcalls eine korrespondierende Funktion im Treiber. Damit muss der Treiberprogrammierer diesen Satz von Funktionen erstellen, um einen Treiber geschrieben zu haben.
Ein Treiber besteht damit aus den folgenden Funktionen:
•
init_module
•
cleanup_module
•
driver_open
•
driver_close
•
driver_read
•
driver_write
•
driver_select
•
driver_poll
•
driver_ioctl
•
Hinzu kommen noch Treiber-interne Funktionen, beispielsweise Interrupt-Service-Routinen.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
48/55
Ruft die Applikation also ein open auf, reicht das Betriebssystem genau diesen Aufruf an die
open Funktion des ausgewählten Treibers weiter. Ruft die Applikation ein read auf, um vom Gerät Daten zu lesen, wird dieser Aufruf durch das Betriebssystem an das driver-read weitergeleitet.
Nicht jeder Treiber implementiert alle aufgeführten Funktionen. Darüber hinaus existieren noch
weitere Funktionen, die bisher nicht erwähnt wurden, insbesondere für die Ankopplung von
Block-Devices an ein Filesystem.
Die Namen der Funktionen init_module und cleanup_module sind fix und können nicht geändert werden. Demgegenüber sind die Namen der übrigen Funktionen frei wählbar. Damit kann
im Treiber die Funktion driver_open problemlos oeffne_geraet genannt werden. Das Betriebssystem bekommt bei der Initialisierung (Registrierung des Treibers) eine Liste der Funktionsnamen mitgeteilt, wobei die Position auf dieser Liste über die Bedeutung einer Funktion entscheidet. Führt die Applikation beispielsweise einen read Aufruf durch, wird die Funktion aufgerufen,
die auf dieser Liste an dritter Stelle steht.
Die Liste ist unter Linux folgendermaßen strukturiert:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
49/55
};
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
static struct file_operations DriverFops = {
THIS_MODULE,
/* ab Kernel 2.4, define in module.h */
NULL,
/* llseek */
DriverRead,
/* read */
DriverWrite,
/* write */
NULL,
/* readdir */
NULL,
/* poll */
DriverIoctl,
/* ioctl */
NULL,
/* mmap */
DriverOpen,
/* open */
NULL,
/* flush */
DriverClose,
/* release */
NULL,
/* fsync */
NULL,
/* fasync */
NULL,
/* check_media_change */
NULL,
/* revalidate */
NULL,
/* lock */
NULL,
/* readv */
NULL,
/* writev */
};
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
50/55
Wird der Treiber geladen, wird zunächst die Treiber-Funktion init_module aufgerufen. Diese
Funktion hat die Aufgabe, das zugehörige Gerät zu finden und schließlich den Treiber beim System anzumelden (register_chardev).
Anhand des Parameters beim open-Call erkennt das Betriebssystem den von der Applikation gewünschten Treiber, da im Filesystem ja die Zuordnung zwischen dem symbolischen Geräte-namen und der zugehörigen Majornumber stattfindet. Die Treiber-Funktion open wird durch das
System angestoßen und überprüft, ob die Applikation auf das Gerät zugreifen darf oder nicht.
Ist das der Fall, gibt die Treiberfunktion 0 zurück, ansonsten einen sinnvollen Fehlercode.
Konnte das Gerät ``geöffnet'' werden, greift die Applikation über den beim Öffnen zurückgegebenen Filedeskriptor zu. Die an der Applikation übergebenen Argumente werden der zugehörigen Treiberfunktion übermittelt. Der Treiber muss darauf achten, dass von der Applikation übergebene Zeiger (z.B. der Buffer) nicht direkt verwendet werden dürfen! Im Betriebssystemkern,
also damit auch im Treiber (als Teil des Betriebssystemkerns), kann nicht direkt auf den Speicher zugegriffen werden, da das Memory-Management die Speicherbereiche der Applikationen
und des Kernels voreinander schützt.
Schließt die Applikation das Gerät wieder, wird wiederum eine korrespondierende Funktion im
Treiber aufgerufen: release.
Der folgende Programmcode zeigt einen einfachen Linux-Treiber (nur die open-Funktion ist realisiert):
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
51/55
$ cat FirstOpen.c
/* Open Aufruf */
#include <linux/module.h>
#include <linux/init.h>
#include <asm/io.h>
MODULE_LICENSE("GPL");
#define MINI_MAJOR 240
static int DriverOpen( struct inode *inode, struct file *file ){
printk("DriverOpen called\n");
return( 0 );
}
static struct file_operations MiniFops = {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
THIS_MODULE,
#endif
NULL, /* llseek */
NULL, /* read */
NULL, /* write */
NULL, /* readdir */
NULL, /* poll */
NULL, /* ioctl */
NULL, /* mmap */
DriverOpen, /* open */
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
52/55
NULL,
NULL,
NULL,
NULL,
NULL,
/*
/*
/*
/*
/*
flush */
release */
fsync */
fasync */
lock */
};
static int __init MiniInit(void)
{
if(register_chrdev(MINI_MAJOR, "Mini-Driver", &MiniFops) == 0) {
printk("%s\n", Version );
return 0;
};
printk("mini: unable to get major %d\n",MINI_MAJOR);
return( -EIO );
}
static void __exit MiniExit(void)
{
unregister_chrdev(MINI_MAJOR,"Mini-Driver");
}
module_init( MiniInit );
module_exit( MiniExit );
$
Das folgende Makefile beinhaltet die erforderlichen Übersetzungsschritte und das Anlegen der
Gerätedatei für den Treiber:
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
53/55
$ cat makefile
CFLAGS=-O -w -DMODULE -D__KERNEL__
CC=gcc
-I/usr/src/linux-2.4/include
driver:
$(CC) $(CFLAGS) -c FirstOpen.c
clean:
rm -f *~ FirstOpen.o
device:
mknod TestDevice c 240 0
$
Um den Treiber zu testen, könnte man jetzt eine Funktion schreiben, die ein open auf das Device mit der Majornumber 240 durchführt. Dazu muss das Device zunächst angelegt werden. Dies
geschieht über den Aufruf von make: make devices. Die Applikation kann nur den symbolischen
Namen des Devices verwenden.
Treiber mit gutem Design benötigen zum Austesten keine selbstgeschriebenen Programme. Es
existieren im System genügend Standardprogramme, um den Treiber in diesem Stadium zu testen. So kann man beinahe alle Kommandos verwenden und die Eingabe oder die Ausgabe auf
das neue Gerät umlenken. Dadurch wird automatisch ein open auf das Gerät durchgeführt. Der
syslogd muß die printk-Nachricht in der Open-Funktion anzeigen. Die Nachricht selbst erscheint allerdings nicht auf der Konsole, sondern wird in eine Datei geschrieben. Der Name der
Datei, in die der Syslogdaemon protokolliert, kann über das Konfigurationsfile /etc/syslogd.Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
54/55
conf eingestellt werden. Auf den meisten Systemen wird in die Datei /var/log/messages protokolliert. Günstig ist es bei der Treiberentwicklung, sich diese Datei bzw. Änderungen an dieser
Datei ständig anzeigen zu lassen. Dazu dient das Kommando tail mit der Option -f.
tail -f /var/log/messages zeigt ständig die im System protokollierten Zustände an.
Der Treiber läßt sich also folgendermaßen austesten:
$ make driver
$ make device
$ insmod FirstOpen.o
$ cat /etc/passwd > TestDevice
cat: Schreibfehler: Das Argument ist ungültig
$ tail /var/log/messages
...
Das Kommando cat ruft als erstes open auf und liest dann per read von dem zurückgegebenen
Filedeskriptor. Da in diesem Fall keine read-Funktion im Treiber implementiert ist, bricht cat
mit einer Fehlermeldung ab. Da aber die open-Funktion im Treiber aufgerufen werden konnte, erscheint dies auch in der Datei /var/log/messages.
Weiter müssten jetzt alle Treiberfunktionen diskutiert werden, damit man letztendlich auf neue
Hardware zugreifen kann. Dies wird Bestandteil einer Wahlpflichtveranstaltung „Linux Systemprogrammierung“ sein.
Betriebssysteme: EingabeAusgabe.sxw
Prof. Dr. Alois Schütte
55/55