Abwärtskompatibilität

Viele der in diesem Kapitel behandelten Teile der Gerätetreiber-API haben sich von Kernel-Release zu Kernel-Release geändert. Für diejenigen von Ihnen, die ihren Treiber auch unter Linux 2.0 oder 2.2 benötigen, folgt hier eine kurze Zusammenfassung der Unterschiede.

Warteschlangen in Linux 2.2 und 2.0

Ein relativ kleiner Anteil des in diesem Kapitel behandelten Materials hat sich während der 2.3-Entwicklung geändert. Die einzige maßgebliche Änderung erfolgte im Bereich der Warteschlangen. Im 2.2-Kernel gab es eine andere, einfachere Implementation von Warteschlangen, aber es fehlten einige wichtige Features wie exklusive Sleeps. Die neue Implementation wurde mit der Kernel-Version 2.3.1 eingeführt.

Die Wartenschlangen-Implementation in Linux 2.2 verwendete Variablen des Typs struct wait_queue * anstelle von wait_queue_head_t. Dieser Zeiger mußte vor der ersten Verwendung mit NULL initialisiert werden. Eine typische Deklaration und Initialisierung einer Warteschlange sah so aus:


 struct wait_queue *my_queue = NULL;

Die diversen Funktionen zum Schlafenlegen und Aufwecken sahen genauso aus, mit Ausnahme des Variablentyps für die Warteschlange selbst. Daher ist es mit ein wenig Code wie dem folgenden(der aus der Header-Datei sysdep.h stammt, die wir zum Kompilieren unseres Beispiel-Codes verwenden) einfach, Code zu schreiben, der mit allen 2.x-Versionen des Kernels funktioniert.


# define DECLARE_WAIT_QUEUE_HEAD(head) struct wait_queue *head = NULL
  typedef struct wait_queue *wait_queue_head_t;
# define init_waitqueue_head(head) (*(head)) = NULL

Die synchronen Versionen von wake_up wurden in 2.3.29 hinzugefügt; in sysdep.h gibt es Makros gleichen Namens, so daß Sie dieses Feature in Ihrem Code verwenden und trotzdem portabel bleiben können. Die Ersatz-Makros expandieren zu einem normalen wake_up, weil die zugrundeliegenden Mechanismen in früheren Kerneln nicht vorhanden sind. Die Timeout-Versionen von sleep_on wurden im Kernel 2.1.127 hinzugefügt. Der Rest der Warteschlangen-Schnittstelle ist relativ unverändert geblieben. Die Header-Datei sysdep.h definiert die Makros, die benötigt werden, um Ihre Module mit Linux 2.2 und Linux 2.0 kompilieren zu können, ohne den Code mit zu vielen #ifdefs zuzumüllen.

Das Makro wait_event gab es im Kernel 2.0 noch nicht. Für diejenigen, die es benötigen, haben wir eine Implementation in sysdep.h mitgeliefert.

Asynchrone Benachrichtigung

Einige kleine Änderungen an der asynchronen Benachrichtigung sind in den Versionen 2.2 und 2.4 vorgenommen worden.

In Linux 2.3.21 bekam kill_fasync ein drittes Argument. Vor dieser Version wurde kill_fasync als


 kill_fasync(struct fasync_struct *queue, int signal);

aufgerufen. Glücklicherweise kümmert sich sysdep.h darum.

In der Version 2.2 wurde der Typ des ersten Arguments der Methode fasync geändert. Im Kernel 2.0 wurde ein Zeiger auf die inode-Struktur des Geräts anstelle des Integer-Dateideskriptors übergeben:


 int (*fasync) (struct inode *inode, struct file *filp, int on);

Um diese Inkompatibilität zu umgehen, verwenden wir den gleichen Ansatz wie bei read und write: Wir benutzen eine Wrapper-Funktion, wenn das Modul mit 2.0-Header-Dateien kompiliert wird.

Das Argument inode der Methode fasync wurde auch übergeben, wenn die Methode von release aus aufgerufen wurde, anstelle von -1, wie es in neueren Kerneln der Fall ist.

Die Methode fsync

Das dritte Argument der fsync-Methode in file_operations (der Integer-Wert datasync) wurde in der 2.3-Serie hinzugefügt, was bedeutet, daß portabler Code grundsätzlich eine Wrapper-Funktion für ältere Kernel enthalten muß. Es gibt aber eine Falle, wenn man portable fsync-Methoden schreiben will: Mindestens ein Distributor, den wir hier lieber nicht erwähnen wollen, hat die 2.4-API von fsync in seinen 2.2-Kernel gepatcht. Die Kernel-Entwickler vermeiden normalerweise (normalerweise...) API-Änderungen in einer stabilen Serie, aber sie haben nur wenig Kontrolle darüber, was die Distributoren machen.

Zugriff auf den User-Space in Linux 2.0

Der Speicherzugriff wurde in den 2.0-Kerneln noch anders geregelt. Das virtuelle Speichersystem von Linux war zu dieser Zeit noch weniger weit entwickelt. Das neue System war die zentrale Änderung, die die Entwicklung von Version 2.1 einleitete, und brachte deutliche Performance-Verbesserungen mit sich - leider aber auch einen weiteren Satz von Inkompatibilitäten für Treiber-Entwickler.

In Linux 2.0 wurden folgende Funktionen für den Speicherzugriff verwendet:

verify_area(int mode, const void *ptr, unsigned long size);

Diese Funktion arbeitete ähnlich wie access_ok, führte aber eine genauere Überprüfung durch und war langsamer. Im Erfolgsfall gab sie 0, ansonsten -EFAULT zurück. Neuere Kernel-Header definieren diese Funktion noch, aber sie ist nur noch ein Wrapper um access_ok. Wenn Sie die Version 2.0 des Kernels verwenden, dann ist der Aufruf von verify_area nie optional, und kein sicherer Zugriff auf den User-Space ist ohne eine vorherige explizite Überprüfung möglich.

put_user(datum, ptr)

Das Makro put_user sieht seinem heutigen Äquivalent sehr ähnlich. Es unterscheidet sich aber von ihm dadurch, daß nichts überprüft wurde und es keinen Rückgabewert gab.

get_user(ptr)

Dieses Makro holte den Wert an der angegebenen Adresse und gab ihn als Rückgabewert zurück. Auch hier wurde keine Überprüfung ausgeführt.

verify_area mußte explizit aufgerufen werden, weil keine Zugriffsfunktion auf den User-Space eine Überprüfung durchführte. Die große Neuerung in Linux 2.1, die gleichzeitig eine inkompatible Änderung an den Funktionen get_user und put_user erzwang, bestand darin, daß die Überprüfung der User-Space-Adressen der Hardware überlassen wurde, weil der Kernel jetzt in der Lage war, Prozessor-Ausnahmen, die während des Kopierens von Daten in den User-Space ausgelöst wurden, abzufangen und zu bearbeiten.

Als Beispiel dafür, wie die älteren Aufrufe verwendet werden, werfen Sie bitte einen weiteren Blick auf scull. Eine scull-Version, die die 2.0-API verwendet, würde verify_area folgendermaßen aufrufen:


 int err = 0, tmp;

 /*
  * die Bitfelder für Typ und Nummer extrahieren und keine falschen Befehle
  * decodieren: ENOTTY vor verify_area() zurueckgeben
  */
 if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY;
 if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY;

 /*
  * die Richtung ist eine Bitmaske; VERIFY_WRITE faengt
  * R/W-Uebertragungen ab. Type ist User-orientiert, waehrend
  * verify_area Kernel-orientiert ist, so daß die Konzepte von "read"
  * und "write" umgedreht werden.
  */
 if (_IOC_DIR(cmd) & _IOC_READ)
   err = verify_area(VERIFY_WRITE, (void *)arg, _IOC_SIZE(cmd));
 else if (_IOC_DIR(cmd) & _IOC_WRITE)
   err = verify_area(VERIFY_READ, (void *)arg, _IOC_SIZE(cmd));
 if (err) return err;

get_user und put_user können dann folgendermaßen verwendet werden:


 case SCULL_IOCXQUANTUM: /* eXchange: arg als Zeiger verwenden */
  tmp = scull_quantum;
  scull_quantum = get_user((int *)arg);
  put_user(tmp, (int *)arg);
  break;

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

Nur ein kleiner Bestandteil des ioctl-switch-Codes wurde hier gezeigt, weil er sich etwas von dem für Version 2.2 und neuer unterscheidet.

Das Leben des kompatibilitätsbewußten Treiber-Entwicklers wäre relativ einfach, wenn put_user und get_user nicht in allen Linux-Versionen als Makros implementiert worden wären und sich deren Schnittstellen nicht geändert hätten. Deswegen ist ein schneller Fix mittels Makros nicht möglich.

> > > > > > Eine mögliche Lösung besteht darin, einen neuen Satz versionsunabhängiger Makros zu definieren. sysdep.h verwendet dabei Makros in Großbuchstaben: GET_USER, _ _GET_USER und so weiter. Die Argumente sind die gleichen wie bei den Kernel-Makros von Linux 2.4, aber der Aufrufer muß sicherstellen, daß zunächst verify_area aufgerufen worden ist (weil das in 2.0 notwendig ist).

Capabilities in 2.0

Im 2.0-Kernel gab es überhaupt keine Capabilities-Abstraktion. Alle Überprüfungen von Zugriffsrechten testeten einfach, ob der aktuelle Prozeß als Superuser ausgeführt wurde; falls das der Fall ist, war die Operation zulässig. Zu diesem Zweck wurde die Funktion suser() verwendet; sie erwartet keine Argumente und gibt einen von Null verschiedenen Wert zurück, wenn der Prozeß Superuser-Rechte hat.

suser existiert auch noch in neueren Kerneln, sollte aber nicht mehr verwendet werden. Es ist besser, eine Version von capable in 2.0 zu definieren, wie das in sysdep.h gemacht wird:


# define capable(anything) suser()

Auf diese Weise kann man Code schreiben, der portabel ist, aber auch auf modernen, Capability-orientierten Systemen funktioniert.

Die select-Methode in Linux 2.0

Der 2.0-Kernel unterstützte den Systemaufruf poll nicht; es wurde nur der BSD-artige select-Aufruf unterstützt. Die zugehörige Gerätetreiber-Methode wurde daher select genannt und funktionierte etwas anders, auch wenn die durchgeführten Aktionen fast identisch sind.

Die select-Methode erwartet einen Zeiger auf eine select_table und muß diesen Zeiger nur dann an select_wait weitergeben, wenn der aufrufende Prozeß auf die angeforderte Bedingung (SEL_IN, SEL_OUT oder SEL_EX) warten soll.

Der scull-Treiber deckt diese Inkompatibilität durch die Deklaration einer speziellen select-Methode ab, die verwendet wird, wenn für einen 2.0-Kernel kompiliert wird:


#ifdef _ _USE_OLD_SELECT_ _
int scull_p_poll(struct inode *inode, struct file *filp,
         int mode, select_table *table)
{
  Scull_Pipe *dev = filp->private_data;

  if (mode == SEL_IN) {
    if (dev->rp != dev->wp) return 1; /* lesbar */
    PDEBUG("Warte auf Lesen\n");
    select_wait(&dev->inq, table); /* auf Daten warten */
    return 0;
  }
  if (mode == SEL_OUT) {
    /*
     * Der Puffer ist ein Ring-Puffer; er ist voll, wenn "wp" direkt
     * hinter "rp" steht. "left" ist 0, wenn der Puffer leer ist, und
     * "1", wenn er vollstaendig gefuellt ist.
     */
    int left = (dev->rp + dev->buffersize - dev->wp) % dev->buffersize;
    if (left != 1) return 1; /* schreibbar */
    PDEBUG("Warte auf Schreiben\n");
    select_wait(&dev->outq, table); /* warten auf freien Platz */
    return 0;
  }
  return 0; /* kann nie von einer Ausnahme unterbrochen werden */
}
#else /* statt dessen Poll verwenden, wurde bereits gezeigt */

Das hier verwendetete Präprozessorsymbol _ _USE_OLD_SELECT_ _ wird von sysdep.h entsprechend der Kernel-Version definiert.

Positionieren in Linux 2.0

Vor Linux 2.1 hieß die Methode llseek lseek und erwartete auch andere Parameter. Aus diesem Grund konnte man unter Linux 2.0 nicht über die 2 GByte-Grenze hinaus in einer Datei oder einem Gerät positionieren, auch wenn der Systemaufruf llseek damals schon unterstützt wurde.

Der Prototyp der Datei-Operation im 2.0-Kernel sah folgendermaßen aus:


 int (*lseek) (struct inode *inode, struct file *filp , off_t off,
 int whence);

Diejenigen, die mit 2.0 und 2.2 kompatible Gerätetreiber schreiben wollen, definieren meist gesonderte Implementationen der seek-Methode für die beiden Schnittstellen.

2.0 und SMP

Weil Linux 2.0 SMP-Systeme nur minimal unterstützte, konnten Race Conditions der in diesem Kapitel erwähnten Art normalerweise nicht vorkommen. Der 2.0-Kernel hatte durchaus eine Spinlock-Implementation, aber weil nur jeweils ein Prozessor Kernel-Code ausführen konnte, war es seltener notwendig zu sperren.