8.3. Netzwerk-Subsystem

Wie die Blockgerätetreiber kommuniziert auch ein Netzwerktreiber in den wenigsten Fällen direkt mit einer Applikation. Vielmehr wird der Treiber durch den Kernel – in diesem Fall durch das Netzwerk-Subsystem – angesteuert. Darüber hinaus implementiert das Netzwerk-Subsystem die unterschiedlichen Protokolle, so dass für den Treiberprogrammierer zumeist nur die Implementierung von Sende- und Empfangsfunktion anfällt.

Komplizierter wird der Code, falls hoch performante Treiber entwickelt werden sollen. Informationen hierüber enthält die Kerneldokumentation (siehe [Napi2002]). Für die Entwicklung eines Netzkartentreibers findet sich Beispielcode im Kernel: /usr/src/linux-2.6.5/drivers/net/isa-skeleton.c.

8.3.1. Datenaustausch zur Kommunikation

Abbildung 8-11. Einbettung von Netzwerk-Subsystem und Netzwerktreiber im Kernel

Ein Netzwerk-Subsytem liegt zwischen den Applikationen und den Netzwerkkartentreibern (kurz Netzwerktreibern). Daraus folgt, dass die Applikationen nicht direkt mit den Netzwerktreibern kommunizieren, sondern vielmehr mit den Kommunikations-Stacks des Netzwerk-Subsystems (siehe Abbildung Einbettung von Netzwerk-Subsystem und Netzwerktreiber im Kernel). Dieses wiederum leitet die Aufträge (in Form von Paketen) weiter an den entsprechenden Treiber. Der Treiber schließlich führt die eigentlichen Hardwarezugriffe durch, so dass das Paket verschickt wird. Im umgekehrten Fall – ein Paket kommt bei einem Netzwerktreiber an – übergibt der Treiber das Paket dem Netzwerk-Subsystem mit seinen Protokoll-Stacks. Das Subsystem sucht daraufhin die entsprechende Applikation, der das Paket zuzustellen ist.

Die Pakete, über die der Netzwerktreiber mit dem Netzwerk-Subsystem des Kernels kommuniziert, heißen Socket-Buffer (SKB). Zur Verwaltung dieser Speicherbereiche (beispielsweise zum Anlegen und Freigeben) stellt das Netzwerk-Subsystem Funktionen zur Verfügung.

Der prinzipielle Ablauf innerhalb eines Netzwerktreiber-Programms sieht als Erstes vor, dass sich der Treiber während seiner Initialisierung beim Netzwerk-Subsystem anmeldet. Sobald ein Gerät identifiziert wird (z.B. vom PCI-Subsystem), wird die Geräteinitialisierung vorgenommen. Die wichtigste Aufgabe dieser Funktion – neben der Initialisierung der Hardware – besteht in dem Aufsetzen der Datenstruktur struct net_device, die das Netzwerk-Interface repräsentiert, über welches später der Treiber angesprochen wird.

Funktionen, die den Funktionen driver_open und driver_close bei zeichenorientierten Treibern entsprechen, gibt es auch bei einem Netzwerktreiber. Sie heißen open und stop und dienen im Wesentlichen dazu, den Datenaustausch, der über die Socket-Buffer abgewickelt wird, zu aktivieren bzw. zu deaktivieren.

Neben den Initialisierungs- und Deinitialisierungsfunktionen muss der Netzwerktreiber-Entwickler zudem die Hardwareankopplung realisieren. Schließlich wird die übrige Funktionalität (das gesamte Kommunikationsprotokoll) bereits in den höheren Schichten abgearbeitet.

Hier noch einmal die wesentlichen Unterschiede zwischen einem Netzwerktreiber und einem zeichenorientierten Gerätetreiber:

  1. An die Stelle der bekannten Struktur struct file_operations tritt bei den Netzwerktreibern die Struktur struct net_device (<linux/netdevice.h>). In dieser wird unter anderem die Adresse einer Funktion aufgenommen, die zum Versenden der Pakete zuständig ist.

  2. Anders als bei den Character- und Block-Devices wird ein Kommunikationsdevice nicht im Filesystem als Gerätedatei hinterlegt. Stattdessen wird als Referenz ein Name (z.B. eth0) fest im Netzwerk-Subsystem des Kernels vorgehalten.

  3. Die Funktionen des Netzwerktreibers werden durch die oberen Schichten des Kommunikationsprotokolls (z.B. durch den tcp/ip-Stack) aufgerufen und nicht primär durch eine Applikation im Userspace.

  4. Bei Character-Device-Treibern wird das Lesen von Daten durch die Applikation ausgelöst. Netzwerktreiber hingegen empfangen Pakete interruptgesteuert. Aus diesem Grund fehlt in der struct net_device ein Funktionspointer für das Lesen respektive Empfangen von Paketen. Stattdessen startet die ISR des Netzwerktreibers die Empfangsroutine, und diese ruft – sobald die Daten von der Hardware abgeholt wurden – eine Systemfunktion auf, die die weitere Bearbeitung der empfangenen Daten vornimmt. Diese sorgt dann später dafür, dass das Paket der zugehörigen Applikation übergeben wird.

Für einen einfachen Netzwerktreiber müssen neben den Funktionen zur Treiberinitialisierung (module_init) bzw. Treiberdeinitialisierung (module_exit) zunächst die folgenden Funktionen kodiert werden:

In der Funktion zur Treiberinitialisierung (module_init) meldet sich dieser beim Netzwerk-Subsystem unter Zuhilfenahme der Funktion register_netdev an. Hierbei übergibt er ein Objekt vom Typ struct net_device, welches bei Modultreibern zuvor durch Aufruf der Funktion alloc_netdev erzeugt werden muss. Die Struktur speichert – neben einem Formatstring, über den der Name des Netzwerkgerätes gebildet wird – auch noch hardwarespezifische Informationen. Wichtiger ist aber noch, dass hier auch die Adressen der Funktionen abgelegt werden, die aufgerufen werden sollen, wenn:

  1. das Netz-Device initialisiert werden soll (auf User-Ebene wird ifconfig up aufgerufen; das Netzwerk-Subsystem ruft daraufhin die Funktion open des Netzwerktreibers auf),

  2. das Netz-Device – durch Aufruf von ifconfig down – deinitialisiert werden soll (stop),

  3. ein Paket über das Gerät zu verschicken ist (transmit) und

  4. ein neues Netzwerkgerät erkannt wird (Probe-Funktion, init).

Die Adressen dieser Funktionen werden normalerweise in einer Setup-Funktion eingetragen (siehe Beispiel Setup-Funktion des Netzwerktreibers).

Beispiel 8-6. Setup-Funktion des Netzwerktreibers

static void __init my_net_setup( struct net_device *dev )
{
    dev->init = my_net_probe;
    dev->open = my_net_open;
    dev->stop = my_net_close;
    dev->hard_start_xmit = my_net_send;
    dev->destructor = free_netdevice;
}

Die Adresse dieser Setup-Funktion wiederum ist der 3. Parameter, der alloc_netdev beim Aufruf übergeben wird. Ansonsten wird noch der Name des Gerätes (2. Parameter, als Formatstring) und die Anzahl der zusätzlich in der Struktur zu reservierenden Bytes übergeben (1. Parameter):

static struct net_device *my_net;
...
static int __init net_init(void)
{
    if( (my_net=alloc_netdev(0,"net%d",my_net_setup))==NULL )
        return -ENOMEM;
    return register_netdev(my_net);
}

Die Datenstruktur struct net_device enthält noch weitere zu initialisierende Elemente. Viele dieser Strukturelemente hängen vom Kommunikationssystem ab, sind also beispielsweise ethernetspezifisch. Das Netzwerk-Subsystem hat für einige Standardkommunikationssysteme Hilfsfunktionen definiert, die die Initialisierung vereinfachen. Hilfsfunktionen gibt es für

Ethernetether_setup
FDDI fddi_setup
HIPPI hippi_setup
Token Ring tr_setup
Fibre Channel fc_setup
Local Talk ltalk_setup

Zumeist wird diese Initialisierung während der Probe-Funktion des Netzwerktreibers durchgeführt:

static int my_net_probe(struct net_device *dev)
{
    ether_setup(dev);
    return 0;
}

Das Abmelden des Treibers beim Netzwerk-Subsystem erfolgt durch Aufruf der Funktion unregister_netdev.

static void __exit net_exit(void)
{
    unregister_netdev(my_net);
}

8.3.2. Geräteinitialisierung

Anders als bei den bisher vorgestellten Treibern sind im Netzwerk-Subsystem drei Fälle der Geräteinitialisierung zu unterscheiden:

  1. Geräteinitialisierung im Fall eines Built-in-Treibers.

  2. Geräteinitialisierung im Fall eines Modultreibers, wobei ein sonstiges Subsystem des Kernels (z.B. PCI, USB oder PNP) die Hardware-Erkennung durchführt.

  3. Geräteinitialisierung im Fall eines Modultreibers, der selbst für die Hardware-Erkennung zuständig ist (typischerweise für Kommunikations-Hardware, die in einer PC-Architektur über den ISA-Bus angeschlossen wird).

Netzwerktreiber als Built-in-Treiber

Hierbei wesentliches Kennzeichen ist die statische Definition der Struktur struct net_device in der Datei drivers/net/Space.c. Dabei wird der Formatstring (Element name) initialisiert, über den später die Namensreferenz für das Interface gebildet wird.

In die Datei drivers/net/Space.c wird darüber hinaus auch die Adresse der Probe-Funktion eingetragen (Element init).

static struct net_device eth0_dev = {
    .name       = "eth%d",
    .next       = &eth1_dev,
    .init       = ethif_probe,
};

Aufgrund der statischen Natur des Built-in-Treibers ist eine netzwerkspezifische Deinitialisierung nicht notwendig.

Soll ein Treiber sowohl als Built-in-Treiber als auch als Modultreiber kompilierbar sein, muss der Entwickler auf bedingte Kompilierung zurückgreifen:

#ifdef MODULE
...
#else
...
#endif

Netzwerktreiber als Modultreiber mit automatischer Hardware-Erkennung

Modultreiber, deren Hardware über ein anderes Subsystem des Betriebssystemkerns (z.B. PCI , USB oder PNP) erkannt wird, verwenden die Geräteinitialisierung des Subsystems, um die Interface-Datenstruktur struct net_device

Das XXXX ist hierbei durch die Kennung für das gewünschte Kommunikationssystem (z.B. Ethernet) zu ersetzen.

Anstatt beide Funktionen getrennt aufzurufen, kann der Entwickler die Funktion alloc_XXXXdev gebrauchen. Sie nimmt sowohl die Allozierung als auch die erste Initialisierung vor.

Ist der erste Parameter der Funktionen größer Null, wird nicht nur die Struktur als solche, sondern zugleich Speicher für private Daten angelegt. Das Element priv innerhalb der Datenstruktur zeigt dann auf diesen Datenbereich.

Dazu ein Beispiel: Die Funktion alloc_netdev wird mit »56« als erstem Parameter aufgerufen:

    if( (my_net=alloc_netdev(56,"net%d",my_net_setup))==NULL )
        return -ENOMEM;
Durch den Aufruf werden 56 Bytes für private Daten alloziert. Der Entwickler kann auf diesen Speicherbereich über my_net->priv zugreifen.

Ein derart allozierter, privater Datenbereich wird später durch die korrespondierende Freigabefunktion (free_netdev) mit freigegeben.

Netzwerktreiber als Modultreiber mit eigener Hardware-Erkennung

Ist der Treiber selbst – und nicht irgendein Subsystem des Kernels – für die Hardware-Erkennung zuständig, wird zumeist die Geräteinitialisierung durch das Netzwerk-Subsystem angestoßen. Zunächst wird die Datenstruktur struct net_device definiert. Anschließend wird das Element init mit der Adresse der Geräteinitialisierungsfunktion initialisiert. Während der Geräteinitialisierung werden die übrigen Parameter der Datenstruktur durch Aufruf einer der Funktionen XXXXsetup eingesetzt.

static int my_net_probe(struct net_device *dev)
{
    ether_setup(dev);
    ...
    return 0;
}

Ein Rückgabewert von »0« zeigt an, dass die Netzwerkkarte vorgefunden wurde und das Interface konfiguriert ist.

8.3.3. Netzwerktreiber deinitialisieren

Das Deinitialisieren des Netzwerktreibers ist vergleichsweise einfach. Es kann – bei einem Treiber mit automatischer Hardware-Erkennung – entweder in der Subsystem üblichen Gerätedeinitialisierungsfunktion stattfinden, oder aber auch in einer Deinitialisierungsfunktion, die das Netzwerk-Subsystem aufruft, sobald jegliche Zugriffe auf die Elemente des Treibers von Seiten des Netzwerks abgeschlossen sind.

Wesentliche Aufgabe des Treiberentwicklers ist es, die während der Initialisierung angelegte Datenstruktur struct net_device wieder freizugeben (sofern die Funktion dynamisch mit Hilfe einer Funktion wie alloc_netdev angelegt wurde). Im einfachsten Fall erfolgt dies durch Aufruf der Funktion free_netdev. Falls wirklich nur diese eine Funktion aufgerufen werden soll, kann sie auch direkt als Deinitialisierungsfunktion Verwendung finden. Dazu muss free_netdev während des Aufsetzens der Struktur (siehe Beispiel Setup-Funktion des Netzwerktreibers) nur in die struct net_device eingetragen werden:

    dev->destructor = free_netdevice;

8.3.4. Start und Stopp des Treibers

Setzt der Anwender auf User-Ebene das Kommando ifconfig <netdevicename> up mit dem Namen, der den Treiber kennzeichnet, ab, wird die open-Funktion aufgerufen.

Die wichtigste programmtechnische Aufgabe dieser Funktion besteht darin, dem Netzwerk-Subsystem die Bereitschaft mitzuteilen, Pakete in Empfang nehmen und vor allem auch verschicken zu können. Sobald dies der Fall ist, wird die Funktion netif_start_queue aufgerufen.

static int my_net_open(struct net_device *dev)
{
    netif_start_queue(dev);
    return 0;
}

Ein Returnwert von »0« signalisiert, dass das Interface aktiviert wurde. Jeder andere Wert wird als Fehlercode interpretiert.

Das Kommando ifconfig <netdevicename> down bewirkt das Deaktivieren des Interfaces. Es wird eine Funktion aktiviert wird, deren Adresse im Element stop der Struktur struct net_device eingetragen ist. Innerhalb dieser Funktion wird netif_stop_queue aufgerufen. Damit weiß der Kernel, dass er keine Pakete mehr an den Treiber zum Versand weitergeben darf.

static int my_net_close(struct net_device *dev)
{
    netif_stop_queue(dev);
    return 0;
}

8.3.5. Senden und Empfangen

Der eigentliche Datenaustausch zwischen Kernel und Treiber findet über Pakete statt, die unter Linux als Socket-Buffer (SKB) bezeichnet werden. Der Zugriff auf solche Socket-Buffer wird vom Betriebssystem durch eine Reihe von Funktionen unterstützt.

Soll über den Netzwerktreiber ein Paket verschickt werden, ruft der Kernel die Funktion auf, die im Funktionspointer hard_start_xmit der Struktur struct net_device eingetragen wurde.

Abbildung 8-12. Struktur eines Netzwerktreibers

Um ein Paket zu empfangen, registriert der Treiber eine ISR (z.B. in der Funktion probe). Ist die Hardware nicht interruptfähig, muss ein entsprechendes Timer-Tasklet zum Pollen registriert werden. Löst die Hardware des Netzadapters einen Interrupt aus, wird (in der ISR) das Paket von der Hardware entgegengenommen und in einen Socket-Buffer verpackt. Die Weiterverarbeitung wird durch Aufruf der Funktion netif_rx angestossen. Die Bearbeitung des Pakets findet nicht mehr direkt in der ISR (mit gesperrten Interrupts) statt, sondern die Funktion netif_rx startet den Netzwerk-Empfangs-Softirq (NET_RX_SOFTIRQ).

8.3.5.1. Socket-Buffer

Socket-Buffer, die den Datenfluss zwischen Netzwerk-Subsystem und Kernel abwickeln, bestehen aus fünf Teilen:

  • dem Header (für die Betriebssystem-interne Listenverwaltung),

  • den Informationen bezüglich des Transport-Layers (z.B. tcphdr oder udphdr),

  • den Informationen bezüglich der Netzwerk-Schicht (z.B. iphdr oder ipv6hdr),

  • den Informationen bezüglich der Übertragungsschicht (z.B. ethernet) und

  • einem Tailor, der eine Reihe von notwendigen Verwaltungsinformationen (z.B. die Paketlänge und der Pointer auf die Daten »data«) enthält.

Abbildung 8-13. Datenmanagement im Socket-Buffer

Die Verwaltung der eigentlichen Nutzdaten ist in Abbildung Datenmanagement im Socket-Buffer dargestellt. Die eigentlichen Daten liegen an der Adresse, auf die der Zeiger »data« zeigt. In vielen Fällen ist es jedoch notwendig, dass zur Übertragung Header-Informationen (z.B. Ethernet-Header) direkt vor den eigentlichen Nutzdaten liegen. Daher bieten die Socket-Buffer vor und nach den eigentlichen Nutzdaten noch Platz, um solche Informationen abzulegen. Der gesamte zur Verfügung stehende Speicherbereich des Socket-Buffers ist also durch sock->end - sock->head gegeben. Die eigentlichen Nutzdaten liegen im Bereich sock->data bis sock->tail.

Sollen Pakete übertragen werden, wird die treiberspezifische send-Funktion aufgerufen. Diese bekommt als Parameter einen Socket-Buffer übergeben. Die Daten aus dem Socket-Buffer werden übertragen, die Statistik aktualisiert und der übergebene Buffer schließlich wieder freigegeben (dev_kfree_skb ).

Darüber hinaus sollte noch die Sendestatistik aktualisiert werden.

    /* Transmit Data */
    ...
    hardware_send_packet(ioaddr, skb->data, length);
    np->stats.tx_bytes += skb->len;

    dev->trans_start = jiffies;

    /* You might need to clean up and record Tx statistics here. */
    if( inw(ioaddr) == /*RU*/81 )
        np->stats.tx_aborted_errors++;
    dev_kfree_skb( skb );

    return 0;

Im Beispiel Netzwerk Null-Device ist der Code für ein Netzwerk Null-Device dargestellt, welches sämtliche Pakete, die über das Device verschickt werden, schluckt. Die Funktion my_net_send gibt das übergebene Paket (den SKB) ohne weitere Bearbeitung wieder frei.

Beispiel 8-7. Netzwerk Null-Device

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");

static struct net_device *my_net;
static int my_net_probe(struct net_device *dev);

static int my_net_receive()
{
}

static int my_net_send(struct sk_buff *skb, struct net_device *dev)
{
    kfree_skb( skb );
    return 0;
}

static int my_net_open(struct net_device *dev)
{
    netif_start_queue(dev);
    return 0;
}

static int my_net_close(struct net_device *dev)
{
    netif_stop_queue(dev);
    return 0;
}

static int my_net_probe(struct net_device *dev)
{
    ether_setup(dev);
    return 0;
}

static void __init my_net_setup( struct net_device *dev )
{
    dev->init = my_net_probe;
    dev->open= my_net_open;
    dev->stop= my_net_close;
    dev->hard_start_xmit= my_net_send;
    dev->destructor = free_netdev;
}

static int __init net_init(void)
{
    if( (my_net=alloc_netdev(0,"net%d",my_net_setup))==NULL )
            return -ENOMEM;
    return register_netdev(my_net);
}

static void __exit net_exit(void)
{
    unregister_netdev(my_net);
}

module_init( net_init );
module_exit( net_exit );

Werden von der Hardware Daten empfangen, muss der Treiber zunächst einen Socket-Buffer allozieren (Funktion dev_alloc_skb). Während die Funktion alloc_skb einen Buffer der Länge »len« alloziert, reserviert die Funktion dev_alloc_skb noch zusätzlich, gleich zu Beginn des Buffers 16 Byte für den Ethernet-Header (14 Byte + 2 Byte Alignment).

Existiert der Buffer, kann der Treiber die Daten von der Hardware abholen und in den Buffer ablegen. Das notwendige Anpassen der Pointer (»head«, »data«, »tail« und »end«) im Socket-Buffer wird durch das Betriebssystem durch Funktionen wie skb_put unterstützt: skb_put passt die Zeiger an und gibt den Anfang des Datenbereichs zurück.

Das Weiterverarbeiten der soeben empfangenen Daten durch den Kernel wird durch die Funktion netif_rx getriggert.

    ...
        skb = dev_alloc_skb(pkt_len);
        if (skb == NULL) {
            printk(KERN_NOTICE "%s: Memory squeeze, dropping packet.\n",
                dev->name);
            lp->stats.rx_dropped++;
            break;
        }
        skb->dev = dev;

        /* 'skb->data' points to the start of sk_buff data area. */
        memcpy(skb_put(skb,pkt_len), (void*)dev->rmem_start, pkt_len);
        /* or */
        insw(ioaddr, skb->data, (pkt_len + 1) >> 1);

        netif_rx(skb);
        lp->stats.rx_packets++;

Die Funktion skb_put gibt einen Zeiger auf den Bereich im Socket-Buffer, der als Nächstes beschrieben werden darf (dieser beginnt bei skb->tail), zurück. Danach wird der Tail-Zeiger auf das neue Ende des Socket-Buffers verschoben. Sollen mehr Daten in den Buffer untergebracht werden, als Platz vorhanden ist, wird ein Kernel-Panic ausgegeben.

8.3.5.2. Netzwerk-Statistik

Statistische Informationen werden in einer Struktur vom Typ struct net_device_stats gespeichert. Da diese Struktur unter anderem Fehlerzähler enthält, die durch die Hardware verwaltet werden könnten, greift das Betriebssystem über eine durch den Treiber zur Verfügung zu stellende Funktion (get_stats) auf diese Information zu.

    ...
    dev->get_stats = my_net_get_stats;
    ...
static struct net_device_stats *my_net_get_stats(struct net_device *dev)
{
    struct net_local *lp = (struct net_local *)dev->priv;
    short ioaddr = dev->base_addr;
    unsigned long flags;

    if( netif_running(dev) ) {
        spin_lock_irqsave( &lp->lock, flags );
        /* Update the statistics from the device registers. */
        lp->stats.rx_missed_errors = inw(ioaddr+1);
        spin_unlock_irqrestore( &lp->lock, flags );

        return &lp->stats;
    }  
}  


Lizenz