Mit dem Kernel verbinden

Wir schauen uns die Struktur von Netzwerk-Treibern an, indem wir zunächst den Quellcode von snull untersuchen. Es wird Ihnen helfen, die folgende Beschreibung zu verstehen und zu sehen, wie echte Linux-Netzwerk-Treiber funktionieren, wenn Sie den Quellcode einiger Treiber parat haben. Wir würden die Dateien loopback.c, plip.c und 3c509.c vorschlagen, deren Komplexität in dieser Reihenfolge zunimmt. Auch skeleton.c könnte nützlich sein, wenn dieser Beispiel-Treiber auch nicht wirklich funktioniert. All diese Dateien stehen in drivers/net in den Kernel-Quellen.

Module laden

Wenn ein Treiber-Modul in den laufenden Kernel geladen wird, fordert es Ressourcen und bietet Fähigkeiten an. Soweit ist das nichts Neues. Ebenfalls nichts Neues ist die Art und Weise, wie Ressourcen geladen werden. Der Treiber sollte nach seinem Gerät und den Hardware-Adressen (I/O-Ports und IRQ-Leitungen) suchen — diese aber nicht registrieren —, wie es in “the Section called Einen Interrupt-Handler installieren in Kapitel 9” in Kapitel 9 beschrieben wurde. Ein Netzwerk-Treiber wird in der Modul-Initialisierungsfunktion anders registriert als Zeichen- und Block-Treiber. Weil es kein Gegenstück zu Major- und Minor-Nummern bei Netzwerk-Schnittstellen gibt, fordert ein Netzwerk-Treiber auch keine solche Nummer an. Statt dessen fügt der Treiber für jede erkannte Schnittstelle eine Datenstruktur in die globale Liste der Netzwerk-Geräte ein.

Jede Schnittstelle wird durch ein struct net_device-Datenelement beschrieben. Die beiden Strukturen für sn0 und sn1, die snull-Schnittstellen, sind folgendermaßen deklariert:


 
struct net_device snull_devs[2] = {
    { init: snull_init, },  /* init, weiter nichts */
    { init: snull_init, }
};

Die Initialisierung sieht ziemlich einfach aus, es wird nur ein Feld gesetzt. Tatsächlich ist die net_device-Struktur riesig, und wir werden in der Folge noch weitere Felder füllen. Aber es ergibt keinen Sinn, jetzt schon die gesamte Struktur zu behandeln; wir werden statt dessen jedes Feld bei Bedarf erklären. Die Definition der Struktur finden Sie in <linux/netdevice.h>, falls Sie sich eingehender dafür interessieren.

Das erste Feld in struct net_device, das wir uns anschauen werden, ist name, das den Namen der Schnittstelle (also den String, der die Schnittstelle identifiziert) enthält. Der Treiber kann einen Namen für die Schnittstelle hart codieren oder eine dynamische Zuweisung erlauben, die folgendermaßen funktioniert: Wenn der Name einen %d-Format-String enthält, wird der erste verfügbare Name verwendet, der nach dem Ersetzen des Strings durch eine kleine ganze Zahl gefunden wurde. eth%d wird also zum ersten verfügbaren ethn-Namen; die erste Ethernet-Schnittstelle heißt eth0, die weiteren folgen in numerischer Reihenfolge. Die snull-Schnittstellen werden defaultmäßig sn0 und sn1 genannt. Wenn aber beim Laden eth=1 angegeben wird (was dazu führt, daß die Integer-Variable snull_eth den Wert 1 enthält), dann verwendet snull_init eine dynamische Zuweisung, wie hier gezeigt wird:


 

if (!snull_eth) { /* "sn0" und "sn1" zuweisen */
    strcpy(snull_devs[0].name, "sn0");
    strcpy(snull_devs[1].name, "sn1");
} else { /* automatische Zuweisung verwenden */
    strcpy(snull_devs[0].name, "eth%d");
    strcpy(snull_devs[1].name, "eth%d");
}

Das andere initialisierte Feld heißt init und ist ein Funktionszeiger. Immer, wenn Sie ein Gerät registrieren, fordert der Kernel den Treiber auf, sich zu registrieren. Die Initialisierung besteht aus dem Ermitteln der physikalischen Schnittstelle und dem Ausfüllen der net_device-Struktur, wie im nächsten Abschnitt beschrieben wird. Wenn die Initialisierung fehlschlägt, wird die Struktur nicht in die globale Liste der Netzwerk-Geräte eingetragen. Dieses ungewöhnliche Verfahren ist besonders beim Hochfahren des Systems nützlich; jeder Treiber versucht, seine eigenen Geräte zu registrieren, aber nur Geräte, die wirklich existieren, werden in die Liste aufgenommen.

Weil die eigentliche Initialisierung an anderer Stelle stattfindet, hat die Initialisierungsfunktion wenig zu tun, so daß eine einzige Anweisung ausreicht:


 
for (i=0; i<2;  i++)
    if ( (result = register_netdev(snull_devs + i)) )
        printk("snull: Fehler %i beim Registrieren von Gerät \"%s\"\n",
               result, snull_devs[i].name);
    else device_present++;


Jedes Gerät initialisieren

Das Suchen des Gerätes sollte in der init-Funktion der Schnittstelle vorgenommen werden, die oft auch die “Such”-Funktion genannt wird. Das Argument von init ist ein Zeiger auf das initialisierte Gerät. Der Rückgabewert der Funktion ist entweder 0 oder ein negativer Fehlercode, normalerweise -ENODEV.

Bei der snull-Schnittstelle ist natürlich kein echtes Suchen notwendig, weil es keine zugehörige Hardware gibt. Wenn Sie einen echten Treiber für echte Hardware schreiben, dann gelten die üblichen Regeln wie beim Suchen von Zeichen-Geräten: Überprüfen Sie die I/O-Ports, bevor Sie sie benutzen, und schreiben Sie während des Suchens nicht darauf. Außerdem sollten Sie es vermeiden, an dieser Stelle I/O-Ports und Interrupt-Leitungen zu registrieren. Die eigentliche Registrierung sollte zurückgehalten werden, bis das Gerät geöffnet wird. Das ist besonders wichtig, wenn Interrupt-Leitungen mit anderen Geräten gemeinsam genutzt werden. Sie wollen ja schließlich nicht, daß Ihre Schnittstelle jedesmal aufgerufen wird, wenn ein anderes Gerät eine IRQ-Leitung aktiviert, nur um dann zu antworten: “Nein, das ist nicht für mich.”

Die Hauptaufgabe der Initialisierungsroutine ist das Füllen der dev-Struktur für dieses Gerät. Beachten Sie, daß diese Struktur bei Netzwerk-Geräten immer zur Laufzeit zusammengesetzt wird. Aufgrund der Art und Weise, wie Netzwerk-Geräten gesucht werden, kann die dev-Struktur nicht wie die Strukturen file_operations oder block_device_operations zur Laufzeit eingerichtet werden. Beim Beenden von dev->init sollte die dev-Struktur daher mit korrekten Werten gefüllt sein. Glücklicherweise kümmert sich der Kernel durch die Funktion ether_setup, die mehrere Felder in struct net_device füllt, um Ethernet-weite Defaults.

Der Kern von snull_init sieht folgendermaßen aus:


 

ether_setup(dev); /* einige Felder zuweisen */

dev->open            = snull_open;
dev->stop            = snull_release;
dev->set_config      = snull_config;
dev->hard_start_xmit = snull_tx;
dev->do_ioctl        = snull_ioctl;
dev->get_stats       = snull_stats;
dev->rebuild_header  = snull_rebuild_header;
dev->hard_header     = snull_header;
#ifdef HAVE_TX_TIMEOUT
dev->tx_timeout     = snull_tx_timeout;
dev->watchdog_timeo = timeout;
#endif
/* Default-Flags beibehalten, lediglich NOARP hinzufuegen */
dev->flags           |= IFF_NOARP;
dev->hard_header_cache = NULL;      /* Caching deaktivieren */
SET_MODULE_OWNER(dev);

Das einzig Ungewöhnliche an diesem Code ist das Setzen des Flags IFF_NOARP. Damit wird festgelegt, daß diese Schnittstelle ARP, das “Address Resolution Protocol”, nicht benutzen soll. ARP ist ein Ethernet-Protokoll auf einer sehr niedrigen Ebene, das die Aufgabe hat, IP-Adressen in Ethernet Medium Access Control (MAC)-Adressen umzuwandeln. Weil die von snull simulierten “entfernten” Systeme nicht wirklich existieren, gibt es auch niemanden, der für sie ARP-Anfragen beantworten kann. Anstatt snull durch das Hinzufügen einer ARP-Implementation zu verkomplizieren, haben wir uns entschieden, die Schnittstelle als nicht-ARP-fähig zu markieren. Die Zuweisung von hard_header_cache erfolgt aus einem ähnlichen Grund: Damit wird das Caching (nicht-existenter) ARP-Antworten auf dieser Schnittstelle abgeschaltet. Dies wird detailliert im Abschnitt “the Section called MAC-Adreßauflösung” in diesem Kapitel erläutert.

Der Initialisierungscode belegt auch einige Felder (tx_timeout und watchdot_timeo), die mit dem Umgang mit Übertragungs-Timeouts zu tun haben. Wir behandeln diesen Punkt gründlich im Abschnitt “the Section called Übertragungs-Timeouts” in diesem Kapitel.

Schließlich ruft dieser Code SET_MODULE_OWNER auf, um das owner-Feld der net_device-Struktur mit einem Zeiger auf das Modul selbst zu füllen. Der Kernel verwendet diese Information genau wie das owner-Feld der file_operations-Struktur, um den Verwendungszähler des Moduls zu pflegen.

Wir werden jetzt ein weiteres Feld aus struct net_device einführen, und zwar priv. Es hat eine ähnliche Aufgabe wie der Zeiger private_data, den wir bei Zeichen-Geräten benutzt haben. Im Gegensatz zu fops->private_data wird dieser Zeiger bei der Initialisierung und nicht beim Öffnen alloziert, weil das Datenelement, auf das priv verweist, die statistischen Daten über die Schnittstellen-Aktivität enthält. Es ist wichtig, daß diese statistische Information immer zur Verfügung steht — auch dann, wenn die Schnittstelle deaktiviert ist, weil der Benutzer diese Statistik jederzeit mit ifconfig abrufen kann. Die Speicherverwendung durch das Allozieren von priv bei der Initialisierung statt beim Öffnen ist nicht weiter relevant, weil die meisten vorhandenen Schnittstellen im System fast immer aktiviert sind. Das Modul snull deklariert eine Daten-Struktur namens snull_priv, die für priv benutzt werden soll:


struct snull_priv {
    struct net_device_stats stats;
    int status;
    int rx_packetlen;
    u8 *rx_packetdata;
    int tx_packetlen;
    u8 *tx_packetdata;
    struct sk_buff *skb;
    spinlock_t lock;
};

Die Struktur enthält eine Instanz von struct net_device_stats, dem normalen Aufbewahrungsort für Schnittstellen-Statistiken. Die folgenden Zeilen in snull_init allozieren dev->priv:


 
dev->priv = kmalloc(sizeof(struct snull_priv), GFP_KERNEL);
if (dev->priv == NULL)
    return -ENOMEM;
memset(dev->priv, 0, sizeof(struct snull_priv));
spin_lock_init(& ((struct snull_priv *) dev->priv)->lock);

Entladen von Modulen

Beim Entladen des Moduls passiert nichts Besonderes. Die Aufräumfunktion deregistriert einfach die Schnittstelle aus der Liste, nachdem sie den Speicher aus der privaten Struktur freigegeben hat:


 
void snull_cleanup(void)
{
    int i;

    for (i=0; i<2;  i++) {
        kfree(snull_devs[i].priv);
        unregister_netdev(snull_devs + i);
    }
    return;
}


Modularisierte und nicht-modularisierte Treiber

Bei Block- und Zeichen-Treibern gibt es keinen großen Unterschied zwischen modularisierten und nicht-modularisierten Treibern, aber bei Netzwerk-Treibern ist die Lage anders.

Wenn ein Treiber als Teil des Standard-Linux-Kernels ausgeliefert wird, deklariert er keine eigenen net_device-Strukturen, sondern verwendet statt dessen die in drivers/net/Space.c deklarierten. Space.c deklariert eine verkettete Liste aller Netzwerk-Geräte, sowohl treiberspezifische Strukturen wie plip1 als auch allgemein verwendete Strukturen. Diese eth-Gerätestrukturen deklarieren ethif_probe als ihre init-Funktion. Ein Programmierer, der dem Standard-Kernel eine neue Ethernet-Schnittstelle hinzufügt, muß lediglich in ethif_probe einen Aufruf seiner Treiber-Initialisierungsfunktion hinzufügen. Programmierer von Nicht-eth-Treibern fügen ihre Strukturen dagegen in Space.c ein. In beiden Fällen muß nur die Quelldatei Space.c modifiziert werden, wenn der Treiber in den Kernel selbst gelinkt werden soll.

Beim Hochfahren des Systems durchläuft der Code zur Initialisierung des Netzwerk-Subsystems alle net_device-Strukturen und ruft ihre Such-Funktionen (dev->init) auf, indem er diesen einen Zeiger auf das Gerät selbst übergibt. Wenn ein Netzwerk-Gerät gefunden wurde, initialisiert der Kernel die nächste verfügbare net_device-Struktur zur Verwendung mit dieser Schnittstelle. Auf diese Art und Weise können den Geräten inkrementell Namen wie eth0, eth1 usw. zugewiesen werden, ohne jedesmal das name-Feld ändern zu müssen.

Wenn ein modularisierter Treiber geladen wird, dann muß er dagegen (wie wir in diesem Kapitel schon gesehen haben) seine eigenen net_device-Strukturen deklarieren, selbst wenn die angesteuerte Schnittstelle eine Ethernet-Schnittstelle ist.

Neugierige Leser können durch das Studium von Space.c und net_init.c mehr über die Initialisierung von Schnittstellen erfahren.