Die Socket-Buffer

Wir haben jetzt den größten Teil der Themen aus dem Bereich der Netzwerk-Schnittstellen erläutert. In den nächsten Abschnitten folgt noch eine detaillierte Erklärung, wie die sk_buff-Struktur aufgebaut ist. Diese Struktur befindet sich im Kern des Netzwerk-Subsystems des Linux-Kernels, und wir führen hier sowohl die wichtigsten Felder der Struktur ala auch die darauf operierenden Funktionen ein.

Obwohl es streng genommen nicht nötig ist, die Interna von sk_buff zu verstehen, kann es bei der Fehlersuche und der Code-Optimierung hilfreich sein, wenn man in der Lage ist, den Inhalt zu analysieren. Wenn Sie beispielsweise einen Blick in loopback.c werfen, werden Sie eine Optimierung finden, die auf Wissen über die Interna von sk_buff basiert. Auch hier gilt die übliche Warnung: Wenn Sie Code schreiben, der sich interne Details der Struktur sk_buff zunutze macht, dann sollten Sie darauf vorbereitet sein, daß der Code mit zukünftigen Kernel-Versionen nicht mehr funktioniert. Manchmal rechtfertigen die Performance-Vorteile aber den zusätzlichen Pflegeaufwand.

Wir werden hier nicht die gesamte Struktur beschreiben, sondern uns auf die Felder beschränken, die von einem Treiber verwendet werden können. Wenn Sie alle Felder sehen möchten, sollten Sie sich <linux/skbuff.h> anschauen, wo die Struktur definiert ist und die Prototypen der Funktionen stehen.

Die wichtigen Felder

Mit “Die wichtigen Felder” sind hier diejenigen Felder in der Struktur gemeint, die für einen Treiberprogrammierer nützlich sein können. Die folgende Liste hat keine bestimmte Reihenfolge:

struct net_device *rx_dev;, struct net_device *dev;

Die Geräte, die diesen Puffer senden oder empfangen.

union { /* ... */ } h;, union { /* ... */ } nh;, union { /*... */} mac;

Zeiger auf die verschiedenen Ebenen von Headern im Paket. Jedes Feld der Unions ist ein Zeiger auf einen anderen Typ von Datenstruktur. h enthält Zeiger auf Header der Transport-Schicht (beispielsweise struct tcphdr *th), nh enthält Header der Netzwerk-Schicht (wie struct iphdr *iph), und mac enthält Zeiger auf Header der Verbindungsschicht (wie struct ethdr *ethernet).

Wenn Ihr Treiber die Quell- und Ziel-Adresse eines TCP-Pakets anschauen muß, dann kann er diese Adressen in skb->h.th finden. Sehen Sie sich dazu die Header-Datei an, in der alle Header-Typen stehen, auf die auf diese Weise zugegriffen werden kann.

Beachten Sie, daß Netzwerk-Treiber für das Setzen des mac-Zeigers bei eingehenden Paketen verantwortlich sind. Diese Aufgabe wird normalerweise von ether_type_trans erledigt, aber Nicht-Ethernet-Treiber müssen skb->mac.raw direkt setzen, wie in “the Section called Nicht-Ethernet-Header” noch gezeigt wird.

unsigned char *head;, unsigned char *data;, unsigned char *tail;, unsigned char *end;

Mit diesen Zeigern werden die Daten im Paket adressiert. head zeigt auf den Anfang des allozierten Bereichs, data ist der Anfang der gültigen Oktetts (und damit meistens etwas größer als head), tail ist das Ende der gültigen Oktetts, und end zeigt auf die maximal für tail zulässige Adresse. Man kann es sich auch so vorstellen, daß der verfügbare Platz eine Größe von skb->end - skb->head und der aktuell belegte Platz eine Größe von skb->tail - skb->data hat.

unsigned long len;

Die Länge der Daten selbst (skb->tail - skb->head).

unsigned char ip_summed;

Dieses Feld wird vom Treiber bei eingehenden Paketen gefüllt und wird bei der Bildung der TCP/UDP-Prüfsummen verwendet. Es wurde bereits weiter vorn in “the Section called Paket-Empfang” beschrieben.

unsigned char pkt_type;

Für die Auslieferung verwendete Paket-Klassifizierung. Dieses Feld wird intern für die Auslieferung eingehender Pakete verwendet. Der Treiber muß dieses Feld auf PACKET_HOST ("dieses Paket ist für mich"), PACKET_BROADCAST, PACKET_MULTICAST oder PACKET_OTHERHOST ("nein, dieses Paket ist nicht für mich")setzen. Ethernet-Treiber modifizieren pkt_type nicht explizit, weil eth_type_trans das schon für sie macht.

Die verbleibenden Felder der Struktur sind nicht besonders interessant. Sie werden hauptsächlich dazu benutzt, Puffer-Listen und den Speicher, der zum Socket gehört, zu verwalten usw.

Funktionen, die auf Socket-Buffern operieren

Netzwerk-Geräte, die einen sock_buff verwenden, greifen darauf mittels der offiziellen Funktionen zu. Es gibt viele Funktionen, die auf Socket-Buffern operieren, aber die folgenden sind die interessantesten:

struct sk_buff *alloc_skb(unsigned int len, int priority);, struct sk_buff *dev_alloc_skb(unsigned int len);

Einen Puffer allozieren. Die Funktion alloc_skb alloziert einen Puffer und initialisiert skb->data und skb->tail mit skb->head. Die Funktion dev_alloc_skb ist eine Bequemlichkeitsfunktion, die alloc_skb mit der Priorität GFP_ATOMIC aufruft und etwas Platz zwischen skb->head und skb->data reserviert. Dieser Datenbereich wird für Optimierungen in der Netzwerk-Schicht verwendet und sollte vom Treiber nicht angefaßt werden.

void kfree_skb(struct sk_buff *skb);, void dev_kfree_skb(struct sk_buff *skb);

Einen Puffer freigeben. kfree_skb wird intern vom Kernel verwendet. Treiber sollten dev_kfree_skb verwenden, was im Treiber-Kontext sicher aufgerufen werden kann.

unsigned char *skb_put(struct sk_buff *skb, int len);, unsigned char *__skb_put(struct sk_buff *skb, int len);

Diese Funktionen aktualisieren die Felder tail und len der sk_buff-Struktur und werden dazu benutzt, Daten am Ende des Puffers anzuhängen. Der Rückgabewert jeder Funktion ist der vorherige Wert von skb->tail (zeigt also auf den neu erzeugten Datenbereich). Manche Treiber verwenden den Rückgabewert, indem Sie ins(ioaddr, skb_put(...)) oder memcpy(skb_put(...),data,len) aufrufen. Der Unterschied zwischen den beiden Funktionen besteht darin, daß skb_put sicherstellt, daß die Daten in den Puffer passen, während __skb_put diese Kontrolle wegläßt.

unsigned char *skb_push(struct sk_buff *skb, int len);, unsigned char *__skb_push(struct sk_buff *skb, int len);

Diese Funktionen dekrementieren skb->data und inkrementieren skb->len. Sie ähneln damit skb_put, außer daß die Daten am Anfang des Pakets anstatt am Ende angefügt werden. Der Rückgabewert zeigt auf den gerade neu erzeugten Datenbereich. Die Funktionen werden verwendet, um einen Hardware-Header vor der Übertragung eines Pakets hinzuzufügen. Auch hier unterscheidet sich __skb_push dadurch von der anderen Funktion, daß sie nicht prüft, ob genug Platz zur Verfügung steht.

int skb_tailroom(struct sk_buff *skb);

Diese Funktion gibt die Menge des Speichers zurück, die für die Ablage von Daten im Puffer noch zur Verfügung steht. Wenn ein Treiber mehr Daten in einen Puffer schreibt, als hineinpassen, dann kommt es zu einer System-Panik. Sie werden jetzt vielleicht einwenden, daß ein printk schon ausreichend wäre, um auf den Fehler hinzuweisen, aber die Beschädigung des Speichers ist so schwerwiegend für das gesamte System, daß die Entwickler sich entschlossen haben, hier konsequent zu sein. In der Praxis sollte es nicht notwendig sein, den verfügbaren Platz zu überprüfen, wenn der Puffer korrekt alloziert worden ist. Weil die Treiber normalerweise die Paketgröße wissen, bevor sie den Puffer allozieren, wird nur ein sehr fehlerhafter Treiber zu viele Daten in den Puffer schreiben, und dann ist eine Panik auch eine angemessene Bestrafung.

int skb_headroom(struct sk_buff *skb);

Gibt die Menge des Speichers zurück, der vor data noch zur Verfügung steht, gibt also an, wie viele Oktette noch mit skb_push in den Puffer gestellt werden können.

void skb_reserve(struct sk_buff *skb, int len);

Diese Funktion inkrementiert sowohl data als auch tail. Sie kann verwendet werden, um Platz zu reservieren, bevor der Puffer gefüllt wird. Die meisten Ethernet-Schnittstellen reservieren vor dem Paket zwei Bytes, so daß der IP-Header nach dem 14 Byte großen Ethernet-Header an einer 16-Byte-Grenze steht. snull macht das auch so, auch wenn der Aufruf dieser Funktion in “the Section called Paket-Empfang” nicht zu sehen war, weil wir das Einführen zu vieler Konzepte zu diesem Zeitpunkt vermeiden wollten.

unsigned char * skb_pull(struct sk_buff *skb, int len);

Daten vom Anfang des Pakets entfernen. Ein Treiber benötigt diese Funktion nicht, aber wir haben sie der Vollständigkeit halber hier aufgeführt. Die Funktion dekrementiert skb->len und inkrementiert skb->data. Auf diese Art und Weise wird der Ethernet-Header am Anfang eingehender Pakete abgeschnitten.

Der Kernel definiert noch eine Reihe weiterer Funktionen, die auf Socket-Buffern operieren, aber diese sind für die oberen Schichten des Netzwerk-Codes gedacht; Treiber benötigen sie nicht.