Verwendet eine Applikation einen Systemcall, der eine Interaktion mit einem Gerät zur Folge hat, wird durch den Betriebssystemkern im Treiber eine korrespondierende Funktion aufgerufen (die so genannten applikationsgetriggerten Treiberfunktionen). Die Namen dieser Treiber-Einsprungspunkte sind zwar frei wählbar, wir verwenden aber folgendes Schema: Der Name bildet sich aus dem Namensvorsatz »driver« und dem Namen des zugehörigen Systemcalls (zum Beispiel »read« für read). Dies ergibt die folgenden Funktionsnamen:
driver_open
driver_close
driver_read
driver_write
driver_ioctl
driver_poll
Beispiel 5-6. Die Struktur struct file_operations
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*aio_write) (struct kiocb *, const char __user *, 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 *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*aio_fsync) (struct kiocb *, 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 *); ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); }; |
Die Abbildung Einbindung des Gerätetreibers in das System verdeutlicht noch einmal die dargestellten Komponenten und Abläufe.
Der Treiber kann nur verwendet werden, wenn im Dateisystem eine Zuordnung zwischen dem symbolischen Gerätenamen und der Majornumber, die innerhalb des Kernels den Treiber identifiziert, angelegt wurde. Hierzu dient das mknod-Kommando. Außerdem muss der Treiber überhaupt geladen sein. Dies geschieht über insmod. Beim Laden des Treibers 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 beim open-Call übergebenen Dateinamens erkennt das Betriebssystem, über welchen Treiber die Applikation auf ein Gerät zugreifen möchte. Dieser Dateiname stellt im Filesystem die Zuordnung zwischen dem symbolischen Gerätenamen und der zugehörigen Majornumber her. Der Aufruf des Systemcalls open durch die Applikation führt dazu, dass der Kernel
Speicher für eine Datenstruktur vom Typ struct file anlegt, in der unter anderem auch die Flags (z.B. blockierender/nicht blockierender Zugriff oder lesender/schreibender Zugriff) der Applikation abgelegt werden, und
eine open-Funktion des Treibers (hier driver_open genannt) aufruft, der er unter anderem die zuvor initialisierte Datenstruktur struct file übergibt.
Die Instanz der Datenstruktur struct file repräsentiert die zugreifende Applikation und wird im Folgenden mit Treiberinstanz bezeichnet (vergleiche Treiberinstanzen). Eine Treiberinstanz kennzeichnet einen Rechenprozess. Falls der Rechenprozess mehrfach das Gerät öffnet, umfasst er mehrere Treiberinstanzen.
Die Aufgabe der driver_open-Funktion besteht darin, zu überprüfen, ob die Applikation auf das Gerät zugreifen darf oder nicht. Darf die Applikation zugreifen, gibt die Treiber-Funktion »0«, ansonsten einen sinnvollen Fehlercode zurück. Der Rückgabewert wird der Applikation durch den Betriebssystemkern als Ergebnis des open-Systemcalls übergeben.
Konnte das Gerät »geöffnet« werden, greift die Applikation über den beim Öffnen zurückgegebenen Dateideskriptor über die Funktionen read und write zu.
Schließt die Applikation das Gerät beispielsweise durch Aufruf des Systemcalls close wieder, wird wiederum eine korrespondierende Funktion im Treiber aufgerufen (driver_close). Die Adresse dieser Funktion muss in der Struktur struct file_operations in der Variablen mit Namen release abgelegt werden. Auch wenn ansonsten die Namen der Variablen in dieser Struktur mit den Namen der Funktionen am Applikationsinterface übereinstimmen – in diesem Fall wurde eine Ausnahme gemacht.
Ruft die Applikation ein open auf, überprüft der Kernel zunächst, ob durch den Aufruf dieser Funktion irgendwelche Zugriffsrechte, die mit der Gerätedatei assoziiert sind, verletzt werden. Ist dies der Fall, teilt der Betriebssystemkern dies der Applikation in Form eines negativen Fehlercodes mit. Ansonsten ruft der Kernel die zugehörige driver_open-Funktion im Treiber auf. Dieser Funktion werden zwei Parameter übergeben:
struct file *instanz
struct inode *geraetedatei
Die Struktur struct file *instanz enthält sämtliche Elemente, die die aufrufende Treiberinstanz spezifizieren. Innerhalb des Treibers werden Sie auf diese Struktur zugreifen:
um festzustellen, in welchem Modus der Zugriff erfolgt, ob lesend, schreibend oder lesend und schreibend und
um auszuwerten, ob die Treiberinstanz, also der Rechenprozess, einen blockierenden oder einen nicht blockierenden Zugriff durchführt.
Die Struktur struct inode *geraetedatei enthält sämtliche Elemente, die die zugehörige Gerätedatei spezifizieren. Dazu gehören die Zugriffsrechte ebenso wie Informationen über den Besitzer (Owner) der Datei. Der häufigste Grund, auf die Elemente dieser Datenstruktur zuzugreifen, besteht darin, die Major-, aber vor allem auch die Minor-Nummer auszulesen. Hiermit können Sie feststellen, über welche Gerätedatei die Applikation zugreift.
Der Funktion driver_open fällt die wichtige Aufgabe zu, zu überprüfen, ob die zugreifende Treiberinstanz zum Zugriff berechtigt ist. Die Regeln, ob eine Treiberinstanz legitimiert ist oder nicht, legt der Treiberentwickler fest. Darüber hinaus führt die Funktion noch Initialisierungen durch. In Kapitel Treiberinstanzen wird beispielsweise gezeigt, wie die Struktur struct file innerhalb der driver_open-Funktion um instanzenspezifische Parameter erweitert wird.
Nicht jeder Treiber muss driver_open kodieren. Ist keine solche Funktion vorhanden, geht der Kernel davon aus, dass der Zugriff auf den Treiber erlaubt ist. Dies entspricht der Implementierung einer Funktion, bei der bei jedem Aufruf grundsätzlich der Wert »0« zurückgegeben wird:
static int driver_open( struct inode *geraetedatei, struct file *instanz ) { return 0; } |
Ein Beispiel soll die Implementierung dedizierter Zugriffsrechte verdeutlichen. So soll ein Treiber realisiert werden, der genau einer Instanz den schreibenden Zugriff, beliebig vielen Treiberinstanzen aber den lesenden Zugriff gestattet. Für die Realisierung muss der Treiberentwickler wissen, dass im Feld f_flags der Struktur struct file die Zugriffsflags abgelegt sind. Die Zugriffsarten selbst sind im Kernel genauso kodiert wie in der Applikation. Mit diesem Wissen ergibt sich die in Beispiel Dedizierter Zugriffsschutz in driver_open vorgestellte Realisierung der Funktion driver_open.
Beispiel 5-7. Dedizierter Zugriffsschutz in driver_open
static int driver_open( struct inode *geraetedatei, struct file *instanz ) { if( instanz->f_flags&O_RDWR || instanz->f_flags&O_WRONLY ) { if( write_count > 0 ) { return -EBUSY; } write_count++; } return 0; } |
In einem zweiten Beispiel soll ein Codefragment vorgestellt werden, welches den Zugriff auf den Treiber über unterschiedliche Gerätedateien veranschaulicht. Die unterschiedlichen Gerätedateien sind anhand der Minornumber erkennbar. Die Minornumber wiederum ist in der vom Kernel der Funktion driver_open übergebenen Struktur struct inode zu finden. Hier ist es jedoch nicht notwendig, den genauen Feldnamen zu kennen. Vielmehr stellt der Kernel mit iminor eine einfach verwendbare Zugriffsfunktion zur Verfügung:
static int driver_open( struct inode *geraetedatei, struct file *instanz ) { if( iminor(geraetedatei)==0 ) { ... // Zugriff über die Gerätedatei mit der Minor-Nummer 0 } else { ... // Zugriff über alle anderen Gerätedateien } ... |
Für den Zugriff auf die Majornumber existiert ebenfalls eine eigene Funktion: imajor.
Ruft die Applikation ein close auf, wird treiberseitig die korrespondierende Funktion driver_close angestoßen, deren Adresse innerhalb der struct file_operations unter dem Namen release abgelegt ist.
Aufgabe von driver_close alias release ist es, die mit der Treiberinstanz assoziierten Ressourcen freizugeben. Fallen jedoch beim Schließen der Gerätedatei keinerlei Aufgaben für den Treiber an – würde die Funktion driver_close also nur »0« zurückgeben – muss die Funktion nicht kodiert werden.
Im Folgenden soll die zum Beispiel Dedizierter Zugriffsschutz in driver_open passende driver_close-Funktion vorgestellt werden. Da beim driver_open die Anzahl zugreifender Instanzen aufaddiert wird, muss diese bei Aufruf von driver_close wieder nach unten korrigiert werden:
static int driver_close( struct inode *geraetedatei, struct file *instanz ) { if( instanz->f_flags&O_RDWR || instanz->f_flags&O_WRONLY ) { write_count--; } return 0; } |
Wird in der Applikation der Systemcall read aufgerufen, führt dies dazu, dass im zugehörigen Treiber ebenfalls eine read-Funktion (driver_read) aktiviert wird. Die in der Applikation übergebenen Parameter, nämlich die Adresse eines Speicherbereiches (Buffer) im User-Space und die maximal zu lesende Anzahl Bytes, werden direkt an den Treiber weitergereicht.
Da das Memory-Management die Speicherbereiche der Applikation und des Kernels voreinander schützt, darf der Treiber auf den von der Applikation übergebenen Buffer nicht direkt zugreifen. Der Zugriff erfolgt vielmehr über Funktionen, die im Kapitel Daten zwischen Kernel- und User-Space transferieren ausführlich vorgestellt werden.
Der Prototyp der read-Funktion sieht folgendermaßen aus:
static ssize_t driver_read( struct file *instanz, char __user *userbuffer, size_t count, loff_t *offset ) |
Im Treiber lauten die Parameter, die die Applikation bereitgestellt hat, »userbuffer« (Adresse des Speicherbereichs im User-Space) und »count« (maximale Anzahl zu kopierender Bytes).
Der Parameter »instanz« kennzeichnet die zugehörige Treiberinstanz. Über diesen Parameter kann beispielsweise festgestellt werden, auf welches logische Gerät (erkennbar an der Minornumber) zugegriffen wird und ob der Zugriff blockierend oder nicht blockierend erfolgt.
Der Parameter »offset« ist ein Zeiger auf den Offset, auf den innerhalb des Gerätes zugegriffen werden soll. Die Applikation beeinflusst diesen Parameter durch den Systemcall lseek. In vielen Fällen spielt dieser Parameter keine Rolle.
Die driver_read-Funktion hat zur Aufgabe, die vom Gerät angeforderten Daten in den Speicherbereich der Applikation zu kopieren. Dabei ist darauf zu achten, dass nicht mehr Bytes in den übergebenen Buffer kopiert werden, als dieser überhaupt zur Verfügung stellt. Das eigentliche Kopieren wird über eine Funktion (copy_to_user) angestoßen. Diese Funktion kopiert die Daten nur, falls die Adresse, die die Applikation übergeben hat, auf einen gültigen (existenten) Speicherbereich zeigt.
Der Rückgabewert der Funktion copy_to_user ist die Anzahl Bytes, die nicht kopiert werden konnte.
Fordert eine Applikation Daten an, obwohl zum gegenwärtigen Zeitpunkt keine solchen vorhanden sind, reagiert driver_read abhängig vom Zugriffsmodus der Treiberinstanz. Greift die Treiberinstanz nicht blockierend zu, wird der Fehlercode »-EAGAIN« (definiert in der Datei <asm/errno.h>) zurückgegeben. Befindet sich dagegen die Treiberinstanz im blockierenden Modus, muss die driver_read-Funktion den zugehörigen Rechenprozess (Task oder Thread) in den Zustand schlafend versetzen. Dies wird im Kapitel Zugriffsmodi im Treiber realisieren erläutert.
Konnten Daten in den Applikationsbereich kopiert werden, wird die Anzahl der kopierten Daten zurückgegeben. Sind keine Daten vorhanden, die kopiert werden können, wird »0« zurückgegeben, was soviel wie End Of File (EOF) bedeutet.
Mit den bisherigen Kenntnissen lassen sich bereits einfache Treiber erstellen. Im Beispiel Hello-World-Treiber stellt der Treiber ein virtuelles Gerät zur Verfügung, das bei einem lesenden Aufruf den String »Hello World« zurück gibt.
Beispiel 5-8. Hello-World-Treiber
Um den Treiber testen zu können, muss dieser zunächst generiert werden. Hat der Entwickler ein geeignetes Makefile (siehe Beispiel Makefile zum Hello-World-Treiber) erstellt, reicht der Aufruf von make aus.
Beispiel 5-9. Makefile zum Hello-World-Treiber
ifneq ($(KERNELRELEASE),) obj-m := hello.o else KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules endif |
Heißt die Quelldatei des Treibers »hello.c«, so heißt das zu ladende Modul »hello.ko«. Der Superuser hat das Recht, das Kommando insmod auszuführen.
root# insmod hello.ko root# |
Mit Hilfe des Kommandos lsmod lässt sich verifizieren, ob das Laden erfolgreich war oder nicht. Konnte das Modul nicht geladen werden, liegt das vielleicht daran, dass bereits ein Treiber geladen ist, der die gleiche Major-Nummer verwendet. Entweder ist dieses bereits geladene Modul zu entladen oder eine andere, freie Majornumber zu verwenden.
Ist der Treiber erfolgreich geladen, kann er ausgetestet werden. Dazu muss der Treiberentwickler, mit Superuser-Rechten ausgestattet, zunächst die Gerätedatei anlegen:
root# mknod hellodevice c 240 0 root# |
Die Majornumber muss natürlich mit der Majornumber übereinstimmen, unter der sich der Treiber beim Kernel angemeldet hat. Da bei dem Hello-World-Treiber die Minornumber nicht ausgewertet wird, kann hier jede beliebige Minornumber verwendet werden.
Damit die Treiberfunktionen getriggert werden können, muss eine Applikation auf den Treiber zugreifen. Es ist jedoch nicht zwangsläufig notwendig, eine eigene Applikation zu schreiben, die die Systemcalls open und read aufruft. Vielmehr kann auf Applikationen zurückgegriffen werden, die auf jedem Unix-System enthalten sind, wie beispielsweise cat
$ cat hellodevice |
Das Programm cat liest so lange aus der angegebenen Datei, bis es End Of File (0 Bytes gelesen) zurückbekommt. Da der Hello-World-Treiber jedoch ständig die Länge des Strings »Hello World« zurückgibt, bricht cat nicht von alleine ab. Dies muss der Anwender mit der Tastenkombination »CTRL C« selbst erledigen.
Auch innerhalb der Funktion driver_read kann die Minornumber ausgelesen werden. Der Übergabeparameter struct file enthält eine Referenz auf die zugehörige Gerätedatei. Der Zugriff sieht damit folgendermaßen aus:
static ssize_t minor_read( struct file *instanz, char __user *userbuffer, size_t count, loff_t *offs ) { int minor_number = iminor(instanz->f_dentry->d_inode); ... |
Eine Applikation, die Daten ausgeben möchte, ruft den Systemcall write auf. Dieser Systemcall triggert die write-Funktion im Treiber, die hier als driver_write bezeichnet wird.
Der Prototyp dieser Funktion sieht folgendermaßen aus:
static ssize_t driver_write( struct file *instanz, const char __user *userbuffer, size_t count, loff_t *offs) |
Die Parameter haben die gleiche Bedeutung wie bei der driver_read-Funktion. Der Parameter »instanz« repräsentiert sowohl die zugreifende Treiberinstanz mit dem logischen Gerät, welches über die Minornumber identifiziert wird, als auch den Zugriffsmodus (blockierend oder nicht blockierend). Der Parameter »userbuffer« enthält die Adresse des Speicherbereichs im User-Space, aus dem die Daten, die geschrieben werden sollen, stammen. Diese Adresse entspricht exakt der Adresse, die in der Applikation beim Aufruf des Systemcalls write angegeben wurde. Beim Parameter »count« handelt es sich um die Anzahl der Bytes, die geschrieben werden sollen. Der Parameter stammt ebenfalls direkt aus der zugreifenden Applikation.
Der Parameter »offs« gibt an, ab welchem Offset innerhalb des Gerätes (falls ein Gerät intern mehrere Adressen zur Verfügung stellt) die Daten abgelegt werden sollen. Dieser Parameter wird von den meisten zeichenorientierten Gerätetreibern nicht ausgewertet.
Um die Daten aus der Applikation in den Treiber zu bekommen, kann der Treiberentwickler auf die Funktion copy_from_user zurückgreifen. Die Funktion überprüft, ob die von der Applikation übergebene Adresse gültig ist. Daraufhin kopiert sie die Daten in einen Speicherbereich, den der Treiber angeben kann. Die Funktion copy_from_user gibt die Anzahl der Bytes zurück, die nicht kopiert werden konnten. Ist alles gut gegangen, gibt die Funktion also »0« zurück.
Die Funktion driver_write selbst gibt – ähnlich wie driver_read – die Anzahl der geschriebenen Bytes zurück. Falls keine Daten geschrieben werden konnten, returniert sie »0«.
Mit Hilfe dieser Kenntnisse ist das Schreiben eines Codes, der das logische Gerät /dev/null implementiert, trivial. /dev/null hat die Eigenschaft, alle übergebenen Daten ins »Nichts« zu schreiben. Für eine solche Funktionalität muss driver_write allein die Anzahl der zu schreibenden Bytes zurückgeben (siehe Beispiel Implementierung des logischen Gerätes /dev/null).
Der Aufruf der Funktion IO-Control bzw. der daraus resultierende Aufruf des zugehörigen Systemcalls triggert die IO-Control-Funktion im Treiber, die von uns driver_ioctl genannt wird.
Die typische Struktur einer driver_ioctl-Funktion ist durch eine Switch-Case-Anweisung gekennzeichnet. Abhängig vom Kommando, welches der Funktion ioctl durch die Applikation mit übergeben wurde, werden die zusätzlichen Parameter ausgewertet und die zugehörige Aktion im Treiber durchgeführt. Auch wenn es gemäß Prototyp möglich ist, zu einem IO-Control-Kommando mehrere Parameter zu spezifizieren, ist dies in der Praxis unüblich. Vielmehr wird eine zum Kommando gehörige Datenstruktur definiert und der Zeiger auf diese Datenstruktur als Parameter beim Aufruf mit übergeben.
Der zum Kommando gehörige Parameter ist per definitionem vom Typ unsigned long, so dass innerhalb der Funktion driver_ioctl in vielen Fällen eine Typwandlung (z.B. per Typecasting) durchgeführt werden muss. Beispiel Implementierung einer driver_ioctl-Funkion zeigt die Implementierung im Treiber für ein IO-Control IOCTL_BAUDRATE, Beispiel Komplettbeispiel IO-Control zeigt einen vollständigen, funktionstüchtigen Treiber.
Beispiel 5-11. Implementierung einer driver_ioctl-Funkion
Im fehlerfreien Fall – das Kommando war gültig und konnte erfolgreich ausgeführt werden – gibt die Funktion driver_ioctl »0« zurück. Ein negativer Wert bedeutet, dass ein Fehler aufgetreten ist.
Die beiden häufigsten Fehlercodes für IO-Control lauten
-EFAULT: das Argument zeigt auf einen ungültigen Speicherbereich und
-EINVAL: das Kommando oder der bzw. die zugehörigen Parameter sind falsch.
Beispiel 5-12. Komplettbeispiel IO-Control
#include <linux/fs.h> #include <linux/module.h> #include <linux/version.h> #include <linux/init.h> #include <asm/uaccess.h> MODULE_LICENSE("GPL"); #define IOCTL_MAJOR 240 #define IOCTL_GETVALUE 0x0001 static int driver_ioctl( struct inode *inode, struct file *instanz, unsigned int cmd, unsigned long arg) { printk("ioctl called 0x%4.4x %p\n", cmd, (void *)arg ); switch( cmd ) { case IOCTL_GETVALUE: copy_to_user( (void *)arg, "Hollahop\n", 10 ); break; default: printk("unknown IOCTL 0x%x\n", cmd); return -EINVAL; } return 0; } static struct file_operations ioctl_fops = { .owner= THIS_MODULE, .ioctl= driver_ioctl, }; static int __init ioctl_init(void) { if(register_chrdev(IOCTL_MAJOR, "ioctl-Driver", &ioctl_fops) == 0) { return 0; }; printk("ioctl: unable to get major %d\n",IOCTL_MAJOR); return -EIO; } static void __exit ioctl_exit(void) { printk("cleanup_module called\n"); unregister_chrdev(IOCTL_MAJOR,"ioctl-Driver"); } module_init( ioctl_init ); module_exit( ioctl_exit ); |
Die Kernelentwickler haben ein Schema festgelegt, wie die IO-Control Kommandos kodiert werden sollen. Eine kurze Beschreibung dazu findet sich im Abschnitt Mit Stil programmieren.
Über den poll- bzw. select-Systemcall erhält eine Applikation die Möglichkeit, gleich mehrere Ein- bzw. Ausgabequellen (Dateideskriptoren) auf Veränderung zu überwachen. Damit kann ein Anwenderprogramm mit einem Aufruf überprüfen, ob Daten an einer oder an mehreren Schnittstellen bzw. Geräten anliegen, so z.B., ob Daten von der Tastatur oder der seriellen Schnittstelle zum Lesen bereitliegen oder ob Daten an die parallele Schnittstelle ausgegeben werden können. Diese Überwachung findet zudem zeitüberwacht – mit einem im Mikrosekundenbereich einstellbaren Timeout-Wert – statt.
Die vom Treiber zur Verfügung zu stellende Poll-Funktion hat die Aufgabe, festzustellen, ob Daten mit dem nächsten Aufruf (read) gelesen und ob Daten mit dem nächsten Aufruf (write) geschrieben werden können. Da die Applikation, die den Poll-Systemcall aufruft, durch das Betriebssystem eventuell schlafen gelegt wird (dann, wenn keines der spezifizierten Geräte Daten zum Lesen bereithält bzw. Daten zum Schreiben entgegennehmen kann), muss das Betriebssystem auch mitbekommen, wenn der Treiber eventuell wieder Daten zur Verfügung hat. Da das »Schlafenlegen« der Applikation im Regelfall über eine Warteschlange erfolgt, werden dem Betriebssystem alle Warteschlangen bekannt gegeben, über die eine auf Daten von dem zugehörigen Gerät wartende Applikation aufgeweckt würde.
Beispiel 5-13. Die Funktion driver_poll
Tabelle 5-2. Die wichtigsten Flags der driver_poll-Funktion
Innerhalb der driver_poll-Funktion werden die einzelnen Flags miteinander verodert. Wenn Daten ohne zu blockieren gelesen werden können, werden die Flags POLLIN und POLLRDNORM gesetzt; wenn Daten geschrieben werden können, die Flags POLLOUT und POLLWRNORM (siehe Die Funktion driver_poll).
Die Applikationsfunktionen select (siehe Aufruf der Funktion select aus der Applikation) bzw. poll sind derart definiert, dass der dem select bzw. poll folgende read- bzw. write-Aufruf in jedem Fall erfolgreich sein muss, falls der entsprechende Dateideskriptor gesetzt wurde (FD_ISSET == TRUE).
Beispiel 5-14. Aufruf der Funktion select aus der Applikation
select( fdlast+1, &fdsetread, NULL, NULL, NULL ); |
Wartet beispielsweise ein Rechenprozess mittels select darauf, dass er Daten lesen kann und signalisiert select schließlich, dass die Daten wirklich gelesen werden können, dann muss der folgende Aufruf von read ohne Unterbrechung durch den Treiber durchgeführt werden. Und wenn genau nach dem select und vor dem read ein zweiter Rechenprozess lesend auf den Treiber zugreift? Dann wird dieser zweite Rechenprozess so lange schlafen gelegt, bis für ihn weitere Daten angekommen sind. Der erste Rechenprozess wird in jedem Fall bedient, sobald er einen read-Systemcall absetzt. Im Treiber muss also in der Poll-Funktion zwischengespeichert werden, dass eine Treiberinstanz Daten lesen darf und der Zugriff für andere Rechenprozesse in der Zwischenzeit nicht möglich ist.
Eine mögliche Lösung für das Problem ist im Beispielprogramm Realisierung einer Poll-Funktion im Treiber vorgestellt. Aus Gründen der Übersichtlichkeit ist der Code für den schreibenden Zugriff weggelassen und nur der Code für den lesenden Zugriff dargestellt. Auch ist nur der Kontrollfluss innerhalb des Treibers auskodiert, ein eigentlicher Datenaustausch (z.B. über die Funktion copy_from_user) wird nicht gezeigt. Die driver_write-Funktion wird im Beispiel verwendet, um dem Treiber das Vorhandensein von Daten zu signalisieren. In einem realen Beispiel würde diese Funktionalität wohl eher in einer Interrupt-Service-Routine zu finden sein.
Beispiel 5-15. Realisierung einer Poll-Funktion im Treiber
/******************************************************************/ /* Das Programm demonstriert eine Möglichkeit sicherzustellen, */ /* dass nur die Treiberinstanz Daten vom Treiber liest */ /* die auch beim select (poll) die Information bekommen hat, dass */ /* Daten zum Lesen vorhanden sind. */ /******************************************************************/ #include <linux/module.h> #include <linux/version.h> #include <linux/init.h> #include <linux/poll.h> #include <asm/io.h> #include <asm/uaccess.h> /* copy_to_user() */ MODULE_LICENSE("GPL"); #define DRIVER_MAJOR 240 static wait_queue_head_t read_queue; static unsigned long data_avail_for_read = 0; |
Ist das Flag »NULL«, bedeutet das nicht, dass generell keine Daten zum Lesen vorhanden sind. Es bedeutet vielmehr, dass nicht jeder x-beliebige Prozess Daten lesen kann.
Zurück | Zum Anfang | Weiter |
Den Kernel erweitern | Nach oben | Daten zwischen Kernel- und User-Space transferieren |