Kapitel 10. Überlegte Verwendung von Datentypen

Inhalt
Verwendung der Standard-C-Typen
Datenelementen eine explizite Größe zuweisen
Schnittstellenspezifische Typen
Andere Portabilitätsfragen
Verkettete Listen
Schnellreferenz

Bevor wir zu den fortgeschritteneren Themen kommen, müssen wir uns noch kurz mit einigen Portabilitätsfragen befassen. Moderne Versionen des Linux-Kernels sind hochportabel und laufen auf mehreren sehr verschiedenen Architekturen. In Anbetracht dieser Multiplattform-Eigenschaft von Linux sollten auch Treiber für den ernsthaften Einsatz portabel sein.

Ein Kern-Problem beim Schreiben von Kernel-Code besteht aber darin, daß man sowohl auf Datenelemente bekannter Länge zugreifen möchte (beispielsweise auf Dateisystem-Datenstrukturen oder auf Register auf Hardware-Karten), aber andererseits auch die Fähigkeiten der einzelnen Prozessoren (32-Bit- und 64-Bit-Architekturen und möglichst auch 16-Bit-Architekturen) ausnutzen möchte.

Mehrere Probleme, auf die die Kernel-Entwickler beim Portieren des x86-Codes auf neue Architekturen gestoßen sind, haben mit inkorrekten Datentypen zu tun. Die meisten Fehler können durch striktes Typisieren und Kompilieren mit den Schaltern –Wall –Wstrict-prototypes vermieden werden.

Die vom Kernel verwendeten Datentypen können in drei Klassen eingeteilt werden: Standard-C-Typen wie int, Typen mit expliziten Größen wie u32 und Kernel-Objekt-spezifische Typen wie pid_t. Wir werden noch sehen, wann und wie jede der drei Klassen verwendet werden sollte. Die letzten Abschnitte dieses Kapitels beschreiben dann einige weitere typische Probleme, auf die Sie stoßen könnten, wenn Sie einen Treiber von x86 auf andere Plattformen portieren möchten, und führen die verallgemeinerte Unterstützung für verkettete Listen ein, die von neueren Kernel-Header-Dateien bereitgestellt wird.

Wenn Sie den hier gegebenen Ratschlägen folgen, dann kompiliert und läuft Ihr Treiber auch auf Plattformen, auf denen Sie ihn gar nicht testen können.

Verwendung der Standard-C-Typen

Während die meisten Programmierer daran gewöhnt sind, beliebig Standardtypen wie int oder long zu verwenden, müssen beim Programmieren von Gerätetreibern einige Vorsichtsmaßnahmen beachtet werden, um Typisierungskonflikte und obskure Fehler zu vermeiden.

Die Standard-Datentypen können aber nicht verwendet werden, wenn Sie “zwei Füllbytes” oder “etwas, was einen 4-Byte-String repräsentiert” benötigen, weil die normalen C-Datentypen nicht auf allen Architekturen die gleiche Größe haben. Um Ihnen die Größe der diversen C-Typen zu zeigen, haben wir in die Beispielprogramme auf dem O'Reilly-FTP-Server das Programm datasize im Verzeichnis misc-progs mit aufgenommen. Hier ist eine Beispielausgabe von einem PC (die letzten vier Typen werden im nächsten Abschnitt eingeführt):

morgana% misc-progs/datasize
arch   Size:  char  shor   int  long   ptr long-long  u8 u16 u32 u64
i686            1     2     4     4     4     8        1   2   4   8

Das Programm zeigt unter anderem, daß long-Werte und Zeiger auf 64-Bit-Plattformen eine andere Größe haben, wenn man es auf verschiedenen Linux-Computern ausführt:

arch   Size:  char  shor   int  long   ptr long-long  u8 u16 u32 u64
i386            1     2     4     4     4     8        1   2   4   8
alpha           1     2     4     8     8     8        1   2   4   8
armv4l          1     2     4     4     4     8        1   2   4   8
ia64            1     2     4     8     8     8        1   2   4   8
m68k            1     2     4     4     4     8        1   2   4   8
mips            1     2     4     4     4     8        1   2   4   8
ppc             1     2     4     4     4     8        1   2   4   8
sparc           1     2     4     4     4     8        1   2   4   8
sparc64         1     2     4     4     4     8        1   2   4   8

Interessanterweise verwendet der User-Space der Linux-sparc64-Plattform 32-Bit-Code, weswegen Zeiger im User-Space 32 Bit breit sind, obwohl sie im Kernel ganze 64 Bit breit sind. Man kann dies durch Laden des Moduls kdatasize (auch dieses finden Sie bei den Beispieldateien im Verzeichnis misc-modules) verifizieren. Das Modul meldet die Größen mittels printk beim Laden und löst dann einen Fehler aus (damit man es nicht extra entladen muß):

kernel: arch   Size:  char short int long  ptr long-long u8 u16 u32 u64
kernel: sparc64         1    2    4    8    8     8       1   2   4   8

Sie müssen zwar beim Mischen von int und long vorsichtig vorgehen, aber es gibt dafür auch manchmal gute Gründe. Ein solches Beispiel sind Speicheradressen, die für den Kernel eine Sonderbedeutung haben. Obwohl Adressen konzeptionell Zeiger sind, funktioniert die Speicherverwaltung besser, wenn Integer-Datentypen verwendet werden. Der Kernel behandelt den physikalischen Speicher wie ein riesiges Array, und eine Speicheradresse ist lediglich ein Index in dieses Array. Außerdem kann ein Zeiger leicht dereferenziert werden; wenn Sie aber direkt mit Speicheradressen arbeiten, wollen Sie diese fast nie auf diese Weise dereferenzieren. Die Verwendung eines Integer-Typs verhindert diese Dereferenzierung und vermeidet damit Fehler. Deswegen sind Adressen im Kernel unsigned long. Damit wird die Tatsache ausgenutzt, daß Zeiger und lange Integer immer die gleiche Größe haben, zumindest aber auf allen Plattformen, die derzeit von Linux unterstützt werden.

Der C99-Standard definiert die Typen intptr_t und uintptr_t für Integer-Variablen, die einen Zeigerwert aufnehmen können. Diese Typen werden im 2.4-Kernel fast gar nicht verwendet, aber es würde uns nicht wundern, wenn sie im Rahmen der weiteren Entwicklung öfter auftauchen würden.