Kapitel 5. Fortgeschrittene Operationen in Zeichen-Treibern

Inhalt
ioctl
Blockierende I/O
poll und select
Asynchrone Benachrichtigung
Ein Gerät positionieren
Zugriffskontrolle auf Gerätedateien
Abwärtskompatibilität
Schnellreferenz

In Kapitel 3 haben wir einen vollständigen Gerätetreiber geschrieben, auf den der Benutzer schreiben und aus dem er lesen kann. Ein echtes Gerät bietet aber normalerweise viel mehr Möglichkeiten als synchrones Lesen und Schreiben. Jetzt, da wir wissen, wie wir auf die Fehlersuche gehen können, wenn etwas schiefgeht, können wir weitermachen und neue Operationen implementieren.

Eine der Funktionalitäten, die normalerweise zum Lesen und Schreiben hinzukommen, ist das Steuern der Hardware über den Gerätetreiber. Diese Kontrolloperationen werden normalerweise in der ioctl-Methode implementiert. Die Alternative wäre es, den auf das Gerät geschriebenen Datenfluß zu analysieren und spezielle Sequenzen als Steuerbefehle zu interpretieren. Diese Technik sollte vermieden werden, weil sie verlangt, daß einzelne Zeichen zu Steuerzwecken reserviert werden und deswegen nicht mehr im Datenfluß auftauchen dürfen. Außerdem ist diese Technik komplexer als ioctl. Trotzdem ist dieser Ansatz manchmal nützlich und wird von TTYs und anderen Geräten verwendet. Wir beschreiben ihn weiter hinten in diesem Kapitel in “the Section called Geräte ohne ioctl steuern”.

Wie wir im vorigen Kapitel schon angedeutet haben, stellt der Systemaufruf ioctl einen gerätespezifischen Einsprungpunkt für den Treiber dar, um “Steuerbefehle” zu handhaben. ioctl ist insofern gerätespezifisch, als es — im Gegensatz zu read und anderen Methoden — den Applikationen ermöglicht, auf spezielle Merkmale der angesprochenen Hardware zuzugreifen. Dazu können die Konfiguration des Gerätes und das Wechseln von Betriebsmodi gehören. Diese Steuerbefehle stehen normalerweise nicht über read oder write zur Verfügung. Beispielsweise wird alles, was Sie auf einen seriellen Port schreiben, als Kommunikationsdaten verwendet; Sie können nicht durch Schreibvorgänge die Baudrate des Gerätes ändern. Genau dafür, für die Steuerung des I/O-Kanals, ist ioctl gedacht.

Ein weiteres wichtiges Merkmal echter Geräte (im Gegensatz zu scull) ist es, daß die gelesenen oder geschriebenen Daten mit anderer Hardware ausgetauscht werden, was eine Synchronisation erfordert. Diese Lücke wird durch blockierende I/O und asynchrone Benachrichtigung gefüllt. Wir werden dies in diesem Kapitel anhand eines modifizierten scull-Gerätes erläutern. Der Treiber verwendet eine Interaktion zwischen verschiedenen Prozessen, um asynchrone Ereignisse zu erzeugen. Wie beim ursprünglichen scull brauchen wir auch hier keine Spezial-Hardware, um den Treiber auszuprobieren. Wir werden uns auch noch mit richtiger Hardware beschäftigen, aber erst in Kapitel 8.

ioctl

Die ioctl-Funktion im User-Space entspricht dem folgenden Prototyp:


int ioctl(int fd, int cmd, ...);

Dieser Prototyp unterscheidet sich von den anderen Unix-Systemaufrufen durch die Punkte, die normalerweise auf eine variable Anzahl von Argumenten hinweisen. In einem realen System kann ein Systemaufruf aber nicht eine variable Anzahl von Argumenten haben. Systemaufrufe müssen eine wohldefinierte Anzahl von Argumenten haben, denn Anwendungsprogramme können auf diese (wie in “the Section called User-Space und Kernel-Space in Kapitel 2” in Kapitel 2 beschrieben) nur durch Hardware-“Tore” zugreifen. Daher repräsentieren die Punkte im Prototyp keine variable Anzahl von Argumenten, sondern ein einziges optionales Argument, das traditionell als char *argp bezeichnet wird. Die Punkte sind einfach nur da, um während des Kompilierens die Typenüberprüfung abzuschalten. Was das dritte Argument bedeutet, hängt vom jeweiligen Steuerbefehl ab (dem zweiten Argument). Manche Befehle erwarten keine Argumente, andere einen Integer-Wert und wieder andere erwarten einen Zeiger auf andere Daten. Mit einem Zeiger können beliebige Daten an den ioctl-Aufruf übergeben werden; das Gerät kann eine beliebige Menge von Daten mit dem User-Space austauschen.

Die ioctl-Treibermethode verwendet ihre Argumente entsprechend der folgenden Deklaration:



int (*ioctl) (struct inode *inode, struct file *filp,
              unsigned int cmd, unsigned long arg);

Die Zeiger inode und filp sind die Werte, die dem von der Applikation übergebenen Dateideskriptor fd entsprechen, und werden genauso benutzt wie bei open. Das cmd-Argument wird unverändert übergeben, das optionale arg-Argument wird dagegen in Form eines unsigned long übergeben, egal ob es sich dabei ursprünglich um einen Integer-Wert oder einen Zeiger handelte. Wenn das aufrufende Programm das dritte Argument nicht übergibt, dann bekommt auch der Treiber kein sinnvolles arg übergeben.

Da beim dritten Argument die Typprüfung abgeschaltet ist, kann der Compiler Sie nicht warnen, wenn Sie ein ungültiges Argument an ioctl übergeben; Sie werden den Fehler also bis zur Laufzeit des Programms nicht bemerken. Diese fehlende Kontrolle kann als kleineres Problem der ioctl-Definition angesehen werden, ist aber ein Preis, den man für die allgemeine Funktionalität bezahlen muß.

Wie Sie sich vorstellen können, bestehen die meisten Implementationen von ioctl aus einer switch-Anweisung, die das korrekte Verhalten anhand des cmd-Arguments auswählt. Verschiedene Befehle haben verschiedene numerische Werte, die normalerweise in Form symbolischer Namen angegeben werden, um den Code zu vereinfachen. Ein symbolischer Name wird durch eine Präprozessor-Definition zugewiesen. Benutzerdefinierte Treiber deklarieren solche Symbole für gewöhnlich in ihren Header-Dateien; scull.h tut das für scull. Benutzerprogramme müssen natürlich diese Header-Datei einbinden, um auf diese Symbole zugreifen zu können.

Die ioctl-Befehle auswählen

Bevor Sie Code für ioctl schreiben, müssen Sie die Zahlen auswählen, die den Befehlen entsprechen sollen. Unglücklicherweise funktioniert die naheliegendste Möglichkeit, nämlich kleine Nummern, mit 1 beginnend, zu nehmen, nicht besonders gut.

Die Befehlsnummern sollten im ganzen System eindeutig sein, um Fehler zu vermeiden, bei denen der richtige Befehl an das falsche Gerät geschickt wird. So eine Verwechslung kann leicht passieren. So kann ein Programm beispielsweise versuchen, die Baudrate eines nicht-seriellen Eingabegerätes wie eines FIFOs oder eines Audiogeräts zu verändern. Wenn jede ioctl-Nummer nur einmal auftritt, dann bekommt die Applikation einen EINVAL-Fehler zurück, anstatt erfolgreich etwas Unbeabsichtigtes zu tun.

Um Programmierern dabei zu helfen, eine solche Eindeutigkeit zu erreichen, sind diese Codes in mehrere Bitfelder aufgeteilt worden. Die ersten Versionen von Linux verwendeten 16-Bit-Zahlen: die oberen acht Bits waren eine “magische” Nummer, die zum Gerät gehörte, und die unteren acht eine fortlaufende Nummer innerhalb dieses Gerätes. Der Grund dafür war, daß Linus “keinen Plan hatte” (um ihn zu zitieren), und man erst später auf eine bessere Aufteilung der Bitfelder kam. Unglücklicherweise verwenden eine ganze Reihe von Treibern weiterhin die alte Konvention. Das geht auch nicht anders: Ein Ändern der Befehlscodes würde Unmengen von Binärprogrammen nicht mehr funktionieren lassen. In unseren Quellen werden wir aber ausschließlich die neue Konvention für Befehlscodes verwenden.

Um ioctl-Nummern entsprechend der neuen Konvention für Ihren Treiber auszuwählen, sollten Sie zunächst include/asm/ioctl.h und Documentation/ioctl-number.txt lesen. Die Header-Datei definiert die Bitfelder, die Sie verwenden werden: Typ (magische Zahl), laufende Nummer, Übertragungsrichtung und Größe des Arguments. ioctl-number.txt führt die im Kernel verwendeten magischen Nummern auf, damit Sie Ihre eigenen Nummern ohne Überlappungsgefahr wählen können. Diese Textdatei nennt auch die Gründe, warum dieser Konvention gefolgt werden sollte.

Die alte und jetzt nicht mehr empfohlene Methode, eine ioctl-Nummer auszuwählen, war einfach: Wähle eine magische 8-Bit-Zahl wie “k” (Hex 0x6b), und addiere eine ganze Zahl, wie im folgenden Beispiel:



#define SCULL_IOCTL1 0x6b01
#define SCULL_IOCTL2 0x6b02
/* .... */

Wenn sich Applikation und Treiber über die Zahlen einig sind, müssen Sie nur die switch-Anweisung im Treiber implementieren. Diese Methode, ioctl-Nummern zu definieren, die aus alter Unix-Tradition stammt, sollte aber heute nicht mehr verwendet werden. Wir haben Ihnen das nur gezeigt, damit Sie einen Eindruck davon bekommen, wie ioctl-Nummern aussehen.

Die neue Methode verwendet vier Bitfelder, die die folgenden Bedeutungen haben. Alle neuen Symbole, die wir in dieser Liste einführen, sind in <linux/ioctl.h> definiert.

type

Die magische Zahl. Wählen Sie einfach eine Zahl (nachdem Sie ioctl-number.txt zu Rate gezogen haben), und benutzen Sie diese im gesamten Treiber. Dieses Feld ist 8 Bit breit (_IOC_TYPEBITS).

number

Die laufende Nummer. Sie ist 8 Bits breit (_IOC_NRBITS).

direction

Die Richtung der Datenübertragung, falls bei diesem Befehl Daten übertragen werden. Die möglichen Werte sind _IOC_NONE (keine Datenübertragung), _IOC_READ, _IOC_WRITE und _IOC_READ; ⊢ _IOC_WRITE (Daten werden in beiden Richtungen übertragen). Datentransfers werden aus Sicht der Applikation bezeichnet; _IOC_READ bedeutet also, daß vom Gerät gelesen wird, der Treiber muß also in den User-Space schreiben. Beachten Sie, daß dieses Feld eine Bitmaske ist; _IOC_READ und _IOC_WRITE können also mit einer logischen UND-Operation extrahiert werden.

size

Die Menge der betroffenen Daten. Die Breite dieses Feldes ist architekturabhängig und variiert zur Zeit von 8 bis 14 Bit. Sie können den Wert für Ihre Architektur aus dem Makro _IOC_SIZEBITS ermitteln. Wenn Sie Ihren Treiber portabel halten wollen, sollten Sie sich jedoch nicht auf mehr als 255 verlassen. Sie müssen dieses Feld nicht verwenden. Wenn Sie größere Datentransfers benötigen, können Sie es einfach ignorieren. Wir werden bald sehen, wozu es gut ist.

Die Header-Datei <asm/ioctl.h>, die von <linux/ioctl.h> eingebunden wird, definiert Makros, mit denen Sie Befehlsnummern wie folgt erzeugen können: _IO(type,nr), _IOR(type,nr,dataitem), _IOW(type,nr,dataitem) und _IOWR(type,nr,dataitem). Jedes Makro entspricht einem der möglichen Werte für die Übertragungsrichtung. Die Felder type und number werden als Argumente übergeben, und das Feld size wird abgeleitet, indem Sie sizeof auf das Argument dataitem anwenden. Außerdem definiert die Header-Datei Makros, um die Nummern wieder zu decodieren: _IOC_DIR(nr), _IOC_TYPE(nr), _IOC_NR(nr) und _IOC_SIZE(nr). Wir werden hier nicht weiter auf diese Makros eingehen. Die Header-Datei ist leicht zu lesen, und weiter unten finden Sie auch Beispiel-Code.

Das folgende Listing zeigt, wie ioctl-Befehle in scull definiert werden. Konkret setzen diese Befehle die konfigurierbaren Parameter des Treibers bzw. fragen sie ab.


 
/* 'k' als Magic Number verwenden */
#define SCULL_IOC_MAGIC  'k'

#define SCULL_IOCRESET    _IO(SCULL_IOC_MAGIC, 0)

/*
 * S steht fuer "Setzen": über einen Zeiger
 * T steht fuer "Tell" (Sagen, Setzen): direkt über den Argumentwert
 * G steht fuer "Get" (Abholen): Antwort durch Setzen ueber den Zeiger
 * Q steht fuer "Query" (Abfrage): Rueckgabewert ist die Antwort
 * X steht fuer "eXchange": G und S atomar
 * H steht fuer "sHift": T und Q atomar
 */
#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1, scull_quantum)
#define SCULL_IOCSQSET  _IOW(SCULL_IOC_MAGIC, 2, scull_qset)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC,  3)
#define SCULL_IOCTQSET  _IO(SCULL_IOC_MAGIC,  4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5, scull_quantum)
#define SCULL_IOCGQSET  _IOR(SCULL_IOC_MAGIC, 6, scull_qset)
#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC,  7)
#define SCULL_IOCQQSET  _IO(SCULL_IOC_MAGIC,  8)
#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, scull_quantum)
#define SCULL_IOCXQSET  _IOWR(SCULL_IOC_MAGIC,10, scull_qset)
#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC, 11)
#define SCULL_IOCHQSET  _IO(SCULL_IOC_MAGIC, 12)
#define SCULL_IOCHARDRESET _IO(SCULL_IOC_MAGIC, 15) /* zum Debuggen */

#define SCULL_IOC_MAXNR 15

Der letzte Befehl, HARDRESET, wird benutzt, um den Verwendungszähler des Moduls auf Null zurückzusetzen, so daß das Modul entladen werden kann, wenn etwas mit dem Zähler schiefgeht. Die echte Quelldatei definiert auch die Befehle zwischen IOCHQSET und HARDRESET, die hier nicht zu sehen sind.

Wir haben uns entschieden, beide Möglichkeiten der Übergabe von Integer-Argumenten zu implementieren — über einen Zeiger und als expliziter Wert —, auch wenn es eine etablierte Konvention ist, daß ioctl Daten über Zeiger übergibt. Entsprechend sind auch beide Möglichkeiten, eine Integer-Zahl zurückzugeben, implementiert: über einen Zeiger oder über den Rückgabewert. Das funktioniert, solange der Rückgabewert eine positive ganze Zahl ist, denn positive Werte bleiben erhalten (wie wir es schon bei read und write gesehen haben), während negative Werte als Fehler-Code interpretiert werden und im User-Space in errno landen.

Die “Austausch”- (exchange) und “Verschiebe”- (shift) Operationen sind für scull nicht so besonders nützlich. Wir haben “exchange” implementiert, um zu zeigen, wie der Treiber mehrere Operationen zu einer einzigen atomaren Operation kombinieren kann, und haben “shift” implementiert, um “tell” und “query” zu ergänzen. Manchmal werden solche atomaren[1] test-and-set-Operationen benötigt, insbesondere dann, wenn Applikationen Sperren setzen und freigeben müssen.

Die explizite laufende Nummer hat keine spezielle Bedeutung. Sie wird nur benutzt, um die einzelnen Befehle auseinanderzuhalten. Natürlich könnten Sie auch die gleiche laufende Nummer für einen lesenden und einen schreibenden Befehl verwenden, weil sich die eigentliche ioctl-Nummer in den “direction”-Bits unterscheidet, aber es gibt keinen Grund dafür, das zu tun. Wir haben uns entschieden, die laufende Nummer nur in der Deklaration zu verwenden, so daß wir ihr keinen symbolischen Wert zugewiesen haben. Daher stehen in der obigen Definition explizite Zahlen. Wir zeigen Ihnen hier eine Möglichkeit, die Befehlsnummern zu verwenden; Sie können das auch anders machen.

Der Wert des ioctl-cmd-Arguments wird derzeit nicht vom Kernel benutzt, und es ist unwahrscheinlich, daß das in der Zukunft passieren wird. Daher könnten Sie, wenn Sie faul sind, auch die komplexen, oben gezeigten Deklarationen vermeiden und explizit einen Satz von skalaren Nummern deklarieren. Auf der anderen Seite könnten Sie dann aber nicht von den Bitfeldern profitieren können. Die Header-Datei <linux/kd.h> ist ein Beispiel für diesen veralteten Ansatz, bei dem skalare 16-Bit-Werte zur Definition der ioctl-Befehle verwendet wurden. Diese Quelldatei verließ sich auf skalare Nummern, weil sie die damals verfügbare Technologie verwendete, und nicht etwa aus Faulheit. Wenn man die Datei jetzt noch ändern würde, würde das zu einer umfassenden Inkompatibilität führen.

Der Rückgabewert

Die Implementation von ioctl ist normalerweise eine switch-Anweisung auf Basis der Befehlsnummer. Aber was sollte im default-Fall gemacht werden, also wenn die Befehlsnummer nicht zu einer gültigen Operation gehört? Diese Frage wird kontrovers diskutiert. Die meisten Kernel-Funktionen geben -EINVAL (“invalid argument”, ungültiges Argument) zurück, was sinnvoll ist, denn der Befehlswert ist ja ungültig. Der POSIX-Standard schreibt allerdings vor, daß in diesem Fall -ENOTTY zurückgegeben werden sollte. Der dazugehörige Text ist in allen Bibliotheken bis einschließlich libc5 “Not a typewriter”. Erst in libc6 wurde das in “Inappropriate ioctl for device” geändert, was die Sache besser trifft. Weil die meisten neueren Linux-Systeme libc6-basiert sind, bleiben wir beim Standard und verwenden -ENOTTY. Es ist aber immer noch recht gebräuchlich, -EINVAL als Antwort auf einen ungültigen ioctl-Befehl zurückzugeben.

Die vordefinierten Befehle

Auch wenn der ioctl-Systemaufruf meistens auf Geräten arbeitet, erkennt der Kernel einige Befehle selbst. Beachten Sie, daß diese Befehle, wenn Sie auf Ihr Gerät angewendet werden, decodiert werden, bevor Ihre eigenen Datei-Operationen aufgerufen werden. Wenn Sie also die gleiche Nummer für einen Ihrer ioctl-Befehle verwenden, dann wird dieser Befehl nie aufgerufen werden, und die Applikation wird nach etwas Unerwartetem fragen, weil die ioctl-Nummer nicht eindeutig ist.

Die vordefinierten Befehle sind in drei Gruppen eingeteilt:

  • diejenigen Befehle, die auf jeder Datei (normale Datei, Gerätedatei, FIFO und Socket) ausgeführt werden können

  • diejenigen Befehle, die nur auf normalen Dateien ausgeführt werden können

  • dateisystemtyp-spezifische Befehle

Die Befehle in der letzten Gruppe werden von der Implementation des umgebenden Dateisystems ausgeführt (siehe den Befehl chattr). Autoren von Gerätetreibern interessieren sich nur für die erste Gruppe von Befehlen, deren magische Zahl “T” ist. Wie die anderen Gruppen funktionieren, können Sie sich bei Interesse selbst anschauen; ext2_ioctl ist eine äußerst interessante Funktion (wenn auch einfacher, als Sie vielleicht vermuten), weil sie die Flags für append-only und für die Unveränderlichkeit implementiert.

Die folgenden ioctl-Befehle sind auf jeder Datei vordefiniert:

FIOCLEX

Setzt den close-on-exec-Schalter (File IOctl CLose on EXec). Das Setzen dieses Flags sorgt dafür, daß der Dateideskriptor geschlossen wird, wenn der aufrufende Prozeß ein neues Programm ausführt.

FIONCLEX

Setzt den close-on-exec-Schalter zurück.

FIOASYNC

Schaltet die asynchrone Benachrichtigung für die Datei ein oder aus (siehe dazu den Abschnitt “Asynchrone Benachrichtigung” weiter hinten in diesem Kapitel). Beachten Sie, daß die Kernel-Versionen bis einschließlich 2.2.4 diesen Befehle fälschlicherweise dazu verwendeten, das O_SYNC-Flag zu ändern. Weil beide Aktionen auch anders erreicht werden können, verwendet niemand den Befehl FIOASYNC, der hier nur aus Gründen der Vollständigkeit aufgeführt ist.

FIONBIO

''File IOctl Nonblocking I/O'' (wird später in “the Section called Blockierende und nicht-blockierende Operationen” beschrieben). Dieser Aufruf modifiziert den O_NONBLOCK-Schalter in filp->f_flags. Das dritte Argument des Systemaufrufs wird benutzt, um zu bestimmen, ob der Schalter gesetzt oder zurückgesetzt werden soll. Wir werden später noch auf die Aufgabe dieses Schalters zurückkommen. Beachten Sie, daß dieser Schalter auch mit dem Systemaufruf fcntl unter Verwendung des F_SETFL-Befehls verändert werden kann.

Das letzte Element in der Liste führt einen neuen Systemaufruf namens fcntl ein, der ioctl ähnlich sieht. Die Ähnlichkeit ist sogar so groß, daß auch fcntl ein Befehlsargument und ein zusätzliches optionales Argument erwartet. fcntl und ioctl werden hauptsächlich aus historischen Gründen auseinandergehalten: Als die Entwickler von Unix auf das Problem stießen, daß I/O-Operationen irgendwie gesteuert werden mußten, entschieden sie, daß Dateien und Geräte unterschiedlich zu behandeln seien. Zu der Zeit waren die einzigen Geräte mit ioctl-Implementationen Terminals, weswegen -ENOTTY auch die Standardantwort bei einem ungültigen ioctl-Befehl ist. Das hat sich geändert, aber fcntl bleibt aus Kompatibilitätsgründen im Namen.

Das Argument von ioctl benutzen

Bevor wir uns den ioctl-Code von scull ansehen, müssen wir noch erklären, wie das zusätzliche Argument verwendet wird. Wenn es sich um einen Integer-Wert handelt, ist das einfach: Der Wert kann direkt verwendet werden. Handelt es sich aber um einen Zeiger, muß man vorsichtiger vorgehen.

Wenn ein Zeiger verwendet wird, um auf den User-Space zu verweisen, müssen wir sichergehen, daß die Adresse gültig und die entsprechende Seite gerade geladen ist. Wenn Kernel-Code versucht, auf eine Adresse außerhalb des gültigen Bereiches zuzugreifen, dann löst der Prozessor eine Ausnahme aus. In den Versionen des Kernels bis einschließlich 2.0.x werden diese Ausnahmen in Oops-Meldungen umgewandelt; Version 2.1 und spätere gehen mit diesem Problem weniger rabiat um. Auf jeden Fall liegt es in der Verantwortung des Treibers, alle verwendeten Adressen im User-Space zu überprüfen und im Fehlerfall auch einen Fehler zurückzumelden.

Die Überprüfung der Adressen ist ab Kernel 2.2.x in der Funktion access_ok implementiert, die in <asm/uaccess.h> deklariert ist:


int access_ok( int type, const void* addr, unsigned long size );

Das erste Argument sollte entweder VERIFY_READ oder VERIFY_WRITE sein, je nach dem, ob die auszuführende Aktion Schreiben oder Lesen ist. Das Argument addr enthält die Adresse im User-Space, und size gibt die Anzahl der Bytes an. Wenn beispielsweise ioctl einen Integer-Wert aus dem User-Space lesen muß, dann ist size gleich sizeof(int). Wenn Sie eine Adresse sowohl lesen als auch beschreiben müssen, dann verwenden Sie VERIFY_WRITE; eine Obermenge von VERIFY_READ.

Im Gegensatz zu den meisten anderen Funktionen gibt access_ok einen Booleschen Wert zurück: 1 im Erfolgsfall (Zugriff erlaubt) und 0 im Fehlerfall (Zugriff nicht erlaubt). Wenn ein Fehler zurückgegeben wird, geben Treiber normalerweise -EFAULT an den Aufrufer zurück.

An access_ok gibt es eine Reihe interessanter Dinge zu bemerken: Zunächst wird der Speicherzugriff nicht vollständig überprüft, sondern es wird nur kontrolliert, daß sich die Speicherreferenz in einem Speicherbereich befindet, auf den der Prozeß Zugriff haben könnte. Insbesondere kontrolliert access_ok, daß sich die Adresse nicht im Kernel-Space befindet. Weiterhin muß Treiber-Code nur selten access_ok aufrufen. Die weiter hinten beschriebenen Speicherzugriffsroutinen erledigen das für Sie. Trotzdem zeigen wir deren Verwendung, damit Sie sehen können, wie man das macht - und aus Gründen der Abwärtskompatibilität, auf die wir am Ende des Kapitels eingehen werden.

Der Quellcode von scull nutzt die Bitfelder in der ioctl-Nummer aus, um die Argumente vor der switch-Anweisung überprüfen zu können:


 int err = 0, tmp;
 int ret = 0;

 /*
  * Die type- und number-Bitfelder extrahieren und falsche Befehle
  * nicht decodieren: ENOTTY (falsches ioctl) vor access_ok()
  * zurueckgeben.
  */
 if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY;
 if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY;

 /*
  * Die Richtung ist eine Bitmaske, und VERIFY_WRITE faengt
  * R/W-Uebertragungen ab. 'Type' ist User-orientiert, waehrend
  * access_ok Kernel-orientiert ist, so daß das Konzept von "read" und
  * "write" umgedreht wird.
  */
 if (_IOC_DIR(cmd) & _IOC_READ)
   err = !access_ok(VERIFY_WRITE, (void *)arg, _IOC_SIZE(cmd));
 else if (_IOC_DIR(cmd) & _IOC_WRITE)
   err = !access_ok(VERIFY_READ, (void *)arg, _IOC_SIZE(cmd));
 if (err) return -EFAULT;

Nach dem Aufruf von access_ok kann der Treiber gefahrlos die eigentliche Übertragung durchführen. Außer copy_from_user und copy_to_user kann der Treiber noch zwei weitere Funktionen verwenden, die für die am häufigsten verwendeten Datengrößen (1, 2 und 4 Bytes, auf 64-Bit-Plattformen auch 8 Bytes) optimiert sind. Diese Funktionen sind in der folgenden Liste beschrieben und in <asm/uaccess.h> definiert.

put_user(datum, ptr), __put_user(datum, ptr)

Diese Makros schreiben das Datum in den User-Space; sie sind relativ schnell und sollten anstelle von copy_to_user verwendet werden, wenn nur einzelne Werte übertragen werden. Da bei der Expansion von Makros keine Typenüberprüfung stattfindet, können Sie beliebige Zeigertypen an put_user übergeben, die Adressen im User-Space enthalten sollten. Die Größe der übertragenen Daten hängt vom Typ des ptr-Arguments ab und wird während des Kompilierens mit einer speziellen gcc-Pseudofunktion bestimmt, die hier zu zeigen sich nicht lohnt. Wenn ptr also ein Zeiger auf char ist, wird ein Byte übertragen, ansonsten entsprechend 2, 4 und möglicherweise 8 Bytes.

put_user versucht sicherzustellen, daß der Prozeß an die angegebene Speicheradresse schreiben darf. Im Erfolgsfall wird 0, ansonsten -EFAULT zurückgegeben. __put_user führt weniger Überprüfungen durch (ruft nicht access_ok auf), kann aber trotzdem bei manchen Arten von fehlerhaften Adressen fehlschlagen. __put_user sollte also nur dann verwendet werden, wenn der Speicherbereich bereits mit access_ok überprüft wurde.

Als generelle Regel werden Sie __put_user aufrufen, um ein paar Taktzyklen zu sparen, wenn Sie eine read-Methode implementieren oder mehrere Elemente kopieren und deswegen access_ok nur einmal direkt vor der ersten Datenübertragung aufrufen.

get_user(ptr), __get_user(local,ptr)

Diese Makros werden dazu verwendet, ein einziges Datum aus dem User-Space zu holen. Sie verhalten sich genauso wie put_user und __put_user, übertragen aber die Daten in die entgegengesetzte Richtung. Der abgeholte Wert wird in der lokalen Variablen local gespeichert; der Rückgabewert gibt an, ob die Operation erfolgreich war oder nicht. Auch hier sollte __get_user nur verwendet werden, wenn die Adresse bereits mit access_ok überprüft worden ist.

Wenn mit einer der angegebenen Funktionen versucht wird, einen nicht passenden Wert zu übertragen, dann ist das Ergebnis meistens eine merkwürdige Compiler-Fehlermeldung wie “conversion to non-scalar type requested”. In diesen Fällen müssen copy_to_user und copy_from_user verwendet werden.

Capabilities und eingeschränkte Operationen

Der Zugriff auf ein Gerät wird durch die Zugriffsrechte auf die Gerätedatei(en) gesteuert; der Treiber ist an der Überprüfung der Zugriffsrechte normalerweise nicht beteiligt. Es gibt aber Situationen, in denen beliebigen Benutzern Lese-/Schreibrechte auf das Gerät gewährt, einige andere Operationen aber verweigert werden sollen. Beispielsweise sollten nicht alle Benutzer eines Bandlaufwerks die Default-Blockgröße verändern dürfen, und die Berechtigung, auf eine Festplatte zuzugreifen, bedeutet noch lange nicht, daß der Benutzer auch das Laufwerk formatieren darf. In diesen Fällen muß der Treiber zusätzliche Überprüfungen durchführen, um sicherzustellen, daß der Benutzer die angegebene Operation durchführen darf.

Unix-Systeme haben traditionell privilegierte Operationen auf den Superuser beschränkt. Das ist ein Alles-oder-nichts-Konzept: Der Superuser darf absolut alles machen, alle anderen Benutzer sind dagegen stark eingeschränkt. Im Linux-Kernel gibt es seit der Version 2.2 ein flexibleres System namens Capabilities. Ein Capabilities-basiertes System ist kein Alles-oder-nichts-System mehr, sondern teilt die privilegierten Operationen in mehrere Untergruppen auf. Damit kann ein bestimmter Benutzer (oder ein Programm) das Recht bekommen, eine privilegierte Operation auszuführen, ohne damit auch das Recht zu bekommen, andere, damit nicht verwandte Operationen ausführen zu dürfen. Capabilities werden derzeit kaum im User-Space verwendet, im Kernel-Space dagegen fast überall.

Die vollständige Menge der Capabilities finden Sie in <linux/capability.h>. Zu denjenigen Capabilities, die für Gerätetreiber-Autoren interessant sein könnten, gehören folgende:

CAP_DAC_OVERRIDE

Die Fähigkeit, Zugriffsbeschränkungen auf Dateien und Verzeichnisse zu überschreiben.

CAP_NET_ADMIN

Die Fähigkeit, Netzwerk-Administrationsaufgaben durchzuführen, darunter auch diejenigen, die Netzwerkschnittstellen betreffen.

CAP_SYS_MODULE

Die Fähigkeit, Kernel-Module zu laden oder zu entladen.

CAP_SYS_RAWIO

Die Fähigkeit, “rohe” I/O-Operationen durchzuführen. Dazu gehören der Zugriff auf Geräteschnittstellen oder die direkte Kommunikation mit USB-Geräten.

CAP_SYS_ADMIN

Eine zusammenfassende Capability, die den Zugriff auf viele Operationen der Systemadministration erlaubt.

CAP_SYS_TTY_CONFIG

Die Fähigkeit, TTYs zu konfigurieren.

Bevor eine privilegierte Operation ausgeführt wird, sollte ein Gerätetreiber mit der Funktion capable (definiert in <sys/sched.h>) überprüfen, daß der aufrufende Prozeß die entsprechenden Rechte hat:

 int capable(int capability);

Im scull-Beispiel-Treiber darf jeder Benutzer die Quantum- und Quantum-Mengen-Größen abfragen, aber nur privilegierte Benutzer dürfen diese Werte verändern, weil unpassende Werte die System-Performance negativ beeinflussen könnten. Wenn nötig, überprüft die scull-Implementation von ioctl die Rechte eines Benutzers folgendermaßen:


 if (! capable (CAP_SYS_ADMIN))
   return -EPERM;

Mangels einer spezifischeren Capability wurde hier CAP_SYS_ADMIN verwendet.

Die Implementation der ioctl-Befehle

Die Implementation von ioctl in scull überträgt nur die konfigurierbaren Parameter des Geräts und erweist sich als sehr einfach:


 

 switch(cmd) {

#ifdef SCULL_DEBUG
   case SCULL_IOCHARDRESET:
        /*
         * Zaehler auf 1 zuruecksetzen, um bei Problemen das Entladen
         * zu ermoeglichen. Wir verwenden hier 1 und nicht 0, weil die
         * Geraetedatei im aufrufenden Prozess noch geschlossen werden muß.
         */
     while (MOD_IN_USE)
       MOD_DEC_USE_COUNT;
     MOD_INC_USE_COUNT;
        /* kein break: der naechste Block soll ebenfalls ausgefuehrt werden */
#endif /* SCULL_DEBUG */

   case SCULL_IOCRESET:
    scull_quantum = SCULL_QUANTUM;
    scull_qset = SCULL_QSET;
    break;

   case SCULL_IOCSQUANTUM: /* Set: arg zeigt auf den Wert */
    if (! capable (CAP_SYS_ADMIN))
      return -EPERM;
    ret = _ _get_user(scull_quantum, (int *)arg);
    break;

   case SCULL_IOCTQUANTUM: /* Tell: arg ist der Wert */
    if (! capable (CAP_SYS_ADMIN))
      return -EPERM;
    scull_quantum = arg;
    break;

   case SCULL_IOCGQUANTUM: /* Get: arg ist ein Zeiger auf das Ergebnis */
    ret = _ _put_user(scull_quantum, (int *)arg);
    break;

   case SCULL_IOCQQUANTUM: /* Query: zurueckgeben (Wert ist positiv) */
    return scull_quantum;

   case SCULL_IOCXQUANTUM: /* eXchange: arg als Zeiger verwenden */
    if (! capable (CAP_SYS_ADMIN))
      return -EPERM;
    tmp = scull_quantum;
    ret = _ _get_user(scull_quantum, (int *)arg);
    if (ret == 0)
      ret = _ _put_user(tmp, (int *)arg);
    break;

   case SCULL_IOCHQUANTUM: /* sHift: wie Tell + Query */
    if (! capable (CAP_SYS_ADMIN))
      return -EPERM;
    tmp = scull_quantum;
    scull_quantum = arg;
    return tmp;

   default: /* redundant, cmd wurde schon mit MAXNR verglichen */
    return -ENOTTY;
 }
 return ret;

Zusätzlich gibt es in scull sechs weitere Befehle, die auf scull_qset arbeiten. Diese entsprechen denen von scull_quantum und sind im obigen Beispiel nicht enthalten, um Platz zu sparen.

Die sechs Möglichkeiten, Argumente zu übergeben und zurückzubekommen, sehen aus der Sicht des Aufrufers (also aus dem User-Space betrachtet) so aus:


int quantum;

ioctl(fd,SCULL_IOCSQUANTUM, &quantum);
ioctl(fd,SCULL_IOCTQUANTUM, quantum);

ioctl(fd,SCULL_IOCGQUANTUM, &quantum);
quantum = ioctl(fd,SCULL_IOCQQUANTUM);

ioctl(fd,SCULL_IOCXQUANTUM, &quantum);
quantum = ioctl(fd,SCULL_IOCHQUANTUM, quantum);

Natürlich würde ein normaler Treiber nicht so eine Mischung von Aufrufmodi implementieren. Wir haben das hier nur gemacht, um die verschiedenen Möglichkeiten zu demonstrieren. Normalerweise würde der Datenaustausch konsistent erfolgen, entweder (normalerweise) über Zeiger oder (seltener) über Werte, und man würde es vermeiden, die beiden Techniken zu mischen.

Geräte ohne ioctl steuern

Manchmal ist es besser, wenn der Treiber durch das Schreiben von Steuersequenzen auf das Gerät gesteuert wird. Diese Technik wird beispielsweise im Konsolentreiber verwendet, wo sogenannte Fluchtsequenzen (escape sequences) dazu dienen, den Cursor zu bewegen, die Default-Farbe zu verändern oder andere Konfigurationsaufgaben durchzuführen. Der Vorteil dieser Technik besteht darin, daß der Benutzer das Gerät steuern kann, indem er einfach Daten auf das Gerät schreibt; er braucht also keine speziellen Programme (die er möglicherweise auch noch selbst schreiben müßte), nur um das Gerät zu konfigurieren.

Beispielsweise beeinflußt das Programm setterm die Konfiguration der Konsole (oder eines anderen Terminals) durch die Ausgabe von Fluchtsequenzen. Man gewinnt damit zusätzlich die Möglichkeit, Geräte aus der Ferne zu steuern. Das steuernde Programm kann auf einem anderen Computer laufen als das kontrollierte Gerät, denn für die Konfiguration ist nur eine einfache Umleitung des Datenstroms notwendig. Sie sind es bereits gewöhnt, das mit Terminals zu machen, aber die Technik kann viel allgemeiner verwendet werden.

Der Nachteil dieses “Steuerns durch Ausgaben” besteht darin, daß Policy-Beschränkungen für das Gerät eingeführt werden. Beispielsweise ist es nur möglich, diese Technik zu verwenden, wenn Sie sicher sind, daß die Steuersequenz nicht in den Daten auftauchen kann, die normalerweise auf das Gerät geschrieben werden. Bei Terminals ist das nur teilweise der Fall. Während ein Textterminal eigentlich nur dazu da ist, ASCII-Zeichen anzuzeigen, schlüpfen manchmal Steuerzeichen im Datenstrom durch und beeinflussen die Konfiguration der Konsole. Das kann beispielsweise passieren, wenn Sie den Befehl grep auf einer Binärdatei ausführen, denn die extrahierten Zeilen können alles enthalten. Oft haben Sie dann am Ende den falschen Zeichensatz auf Ihrer Konsole.[2]

Dieses “Controlling-by-Write” ist immer dann die beste Möglichkeit, wenn es sich um Geräte handelt, die keine Daten übertragen, sondern nur auf Befehle antworten — wie beispielsweise Roboter.

Einer der Treiber, die einer der Autoren aus Spaß geschrieben hat, bewegt beispielsweise eine Kamera entlang zweier Achsen. In diesem Treiber ist das “Gerät” einfach nur ein Paar aus alten Schrittmotoren, auf die man nicht richtig schreiben und von denen man nicht lesen kann. Bei Schrittmotoren ergibt das Konzept, “einen Datenstrom zu schicken”, keinen Sinn. In diesem Fall interpretiert der Treiber ASCII-Befehle und konvertiert diese in eine Folge von Impulsen, die auf die Schrittmotoren wirken. Die Idee ähnelt etwas den AT-Befehlen, die man an ein Modem schickt, um die Kommunikation zu konfigurieren; der Hauptunterschied besteht darin, daß der serielle Port, über den mit dem Modem kommuniziert wird, auch reale Daten übertragen muß. Der Vorteil der direkten Steuerung bei diesem Gerät besteht darin, daß Sie cat verwenden können, um die Kamera zu bewegen, und keinen speziellen Code schreiben und kompilieren müssen, der ioctl-Aufrufe ausführen würde.

Wenn man befehlsorientierte Treiber schreibt, gibt es keinen Grund, die Methode ioctl zu implementieren. Ein zusätzlicher Befehl im Interpreter ist einfacher zu implementieren und zu benutzen.

Manchmal entscheiden Sie sich aber vielleicht für die andere Richtung: Anstatt write zu einem Interpreter zu machen und ioctl zu vermeiden, könnten Sie auch write ganz vermeiden und ausschließlich ioctl-Befehle verwenden und zum Treiber ein spezielles Kommandozeilenwerkzeug mitliefern, mit dem Befehle zum Treiber geschickt werden können. Dieser Ansatz verschiebt die Komplexität vom Kernel Space in den User-Space, wo man damit einfacher umgehen kann. Das hält den Treiber klein, verhindert aber die Verwendung von einfachen Befehlen wie cat oder echo.

Fußnoten

[1]

Ein Stückchen Programmcode heißt atomar, wenn es immer wie eine einzige Anweisung ausgeführt wird, ohne daß zwischendrin der Prozessor unterbrochen werden und etwas anderes (wie die Ausführung anderen Codes) passieren kann.

[2]

Strg-N wählt den alternativen Zeichensatz aus, der aus grafischen Symbolen besteht und daher nicht besonders gut geeignet ist, um Eingaben in der Shell zu machen; wenn Sie dieses Problem haben, geben Sie mit echo ein Strg-O ein, um den primären Zeichensatz wieder zu aktivieren.