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
© Copyright 2024 ExpyDoc