Paket-Übertragung

Die wichtigsten Aufgaben einer Netzwerk-Schnittstelle sind die Übertragung und der Empfang von Daten. Wir fangen hier mit der Übertragung an, weil sie etwas einfacher zu verstehen ist.

Immer wenn der Kernel ein Datenpaket übertragen will, ruft er die Methode hard_start_transmit auf, um die Daten in die ausgehende Warteschlange zu stellen. Jedes Paket, das vom Kernel verarbeitet wird, ist in einer Socket-Buffer-Struktur (struct sk_buff) enthalten. Die Definition dieser Struktur steht in <linux/skbuff.h>. Der Name dieser Struktur stammt aus der Unix-Abstraktion, mit der eine Netzwerk-Verbindung repräsentiert wird, dem Socket. Selbst, wenn die Schnittstelle nichts mit Sockets zu tun hat, gehört doch jedes Netzwerk-Paket zu einem Socket in den höheren Netzwerk-Schichten, und die Eingabe-/Ausgabe-Puffer jedes Sockets sind Listen mit struct sk_buff-Strukturen. Die gleiche sk_buff-Struktur wird auch verwendet, um Netzwerkdaten überall in den Netzwerk-Subsystemen von Linux zu speichern. Für die Schnittstelle ist ein Socket-Buffer aber nichts weiter als ein Paket.

Ein Zeiger auf sk_buff wird normalerweise skb genannt. Wir folgen dieser Konvention sowohl im Beispiel-Code als auch im Text.

Der Socket-Buffer ist eine komplexe Struktur, weswegen der Kernel eine Reihe von Funktionen bereitstellt, die darauf operieren. Diese werden später in “the Section called Die Socket-Buffer” beschrieben; im Moment reichen uns einige Grundlagen über sk_buff, um einen funktionierenden Treiber schreiben zu können.

Der Socket-Buffer, der an hard_start_xmit übergeben wird, enthält das physikalische Paket, wie es auf dem Medium erscheinen soll, mit den Headern für die Übertragung. Die Schnittstelle muß die zu übertragenden Daten nicht modifizieren. skb->data zeigt auf das zu übertragende Paket, und skb->len ist dessen Länge in Oktetten.

Der Paket-Übertragungscode von snull ist unten angeführt, wobei der Mechanismus zur physikalischen Übertragung in einer anderen Funktion isoliert worden ist, weil jeder Treiber das passend für die angesteuerte Hardware implementieren muß.


 
int snull_tx(struct sk_buff *skb, struct net_device *dev)
{
    int len;
    char *data;
    struct snull_priv *priv = (struct snull_priv *) dev->priv;
    len = skb->len < ETH_ZLEN ? ETH_ZLEN : skb->len;
    data = skb->data;
    dev->trans_start = jiffies; /* Zeitstempel speichern */

    /* skb merken, damit wir ihn zur Interrupt-Zeit freigeben koennen */
    priv->skb = skb;

    /* die eigentliche Datenuebertragung ist geraetespezifisch und wird
       hier nicht gezeigt */
    snull_hw_tx(data, len, dev);

    return 0; /* Unser Geraet gelingt immer und klebt nicht */
}

Die Übertragungsfunktion führt also nur einige Konsistenzüberprüfungen auf dem Paket durch und überträgt die Daten dann mittels der Hardware-abhängigen Funktion. Diese Funktion (snull_hw_tx) wird hier nicht gezeigt, weil sie sich ausschließlich mit der Implementation der Tricks im snull-Gerät (einschließlich der Manipulation der Quell- und Ziel-Adressen) beschäftigt und für Autoren echter Netzwerk-Treiber wenig interessant ist. Natürlich finden Sie den Code aber in den Beispiel-Programmen, wenn Sie daran interessiert sind.

Steuerung der Nebenläufigkeit von Übertragungen

Die Funktion hard_start_xmit wird vor nebenläufigen Aufrufen durch ein Spinlock (xmit_lock) in der Struktur net_device geschützt. Sobald die Funktion zurückkehrt, kann sie aber wieder aufgerufen werden. Die Funktion kehrt zurück, wenn die Software damit fertig ist, der Hardware Anweisungen zur Paket-Übertragung zu geben; die Hardware wird aber wahrscheinlich noch nicht fertig sein. Bei snull, wo alle Arbeit in der CPU geschieht, ist das kein Problem, so daß die Paket-Übertragung beendet ist, bevor die Übertragungsfunktion zurückkehrt.

Echte Hardware-Schnittstellen übertragen ihre Pakete dagegen asynchron und haben nur eine begrenzte Menge von Speicher zur Verfügung, in dem sie ausgehende Pakete speichern können. Wenn dieser Speicher voll ist (was bei mancher Hardware schon bei einem einzigen zur Übertragung ausstehenden Paket der Fall sein kann), muß der Treiber das Netzwerk-System anweisen, keine weiteren Übertragungen zu starten, bis die Hardware bereit ist, neue Daten zu schicken.

Diese Benachrichtigung geschieht durch das Aufrufen von netif_stop_queue, der Funktion, die wir schon oben zum Anhalten der Warteschlange eingeführt hatten. Wenn Ihr Treiber seine Warteschlange angehalten hat, muß er das Neustarten der Warteschlange irgendwann in der Zukunft organisieren, nämlich dann, wenn wieder Pakete zur Übertragung angenommen werden können. Dazu wird folgende Funktion aufgerufen:


void netif_wake_queue(struct net_device *dev);

Diese Funktion macht das gleiche wie netif_start_queue, stubst aber auch das Netzwerk-System an, damit dieses wieder Pakete überträgt.

Die meisten modernen Netzwerk-Schnittstellen unterhalten eine interne Warteschlange mit mehreren zu übertragenden Paketen; auf diese Art und Weise können sie die beste Performance aus dem Netzwerk herausholen. Netzwerk-Treiber für diese Geräte unterstützen mehrere ausstehende Übertragungen zu einem beliebigen Zeitpunkt, aber der Geräte-Speicher kann voll werden, ob die Hardware nun mehrere ausstehende Übertragungen unterstützt oder nicht. Immer wenn der Gerätespeicher so weit gefüllt ist, daß das größtmögliche Paket nicht mehr hineinpaßt, sollte der Treiber die Warteschlange anhalten, bis wieder Platz ist.

Übertragungs-Timeouts

Die meisten Treiber, die es mit richtiger Hardware zu tun haben, müssen darauf vorbereitet sein, daß die Hardware gelegentlich versagt. Schnittstelle können vergessen, was sie gerade machen, oder das System kann einen Interrupt verlieren. Diese Art von Problemen tritt bei manchen Geräten für Personal Computer besonders häufig auf.

Viele Treiber behandeln dieses Problem durch Timer. Wenn die Operation in einem gewissen Zeitraum nicht abgeschlossen worden ist, ist etwas falsch. Das Netzwerk-System ist übrigens im wesentlichen eine komplizierte Ansammlung von Zustandsmaschinen, die von einer Unmenge von Timern kontrolliert wird. Als solche kann der Netzwerk-Code gut Übertragungs-Timeouts automatisch entdecken.

Netzwerk-Treiber müssen sich daher keine Gedanken über das Entdecken solcher Probleme machen. Statt dessen legen sie lediglich einen Timeout-Zeitraum fest, der in das Feld watchdog_timeo der Struktur net_device geschrieben wird. Dieser Zeitraum, der in Jiffies angegeben wird, sollte lang genug sein, um normale Verzögerungen bei der Übertragung (wie Kollisionen, die durch Verstopfungen des Netzwerk-Mediums hervorgerufen werden) zu berücksichtigen.

Wenn die aktuelle Systemzeit die trans_start-Zeit des Geräts um wenigstens den Timeout-Zeitraum überschreitet, wird die Netzwerk-Schicht irgendwann die Methode tx_timeout des Treibers aufrufen. Die Aufgabe dieser Methode ist es, alles zu tun, was notwendig ist, um das Problem zu beheben und die ordnungsgemäße Beendigung noch ausstehender Übertragungen zu gewährleisten. Es ist besonders wichtig, daß der Treiber keine Socket-Buffer verliert, die ihm vom Netzwerk-Code anvertraut worden sind.

snull kann Übertragungsblockaden simulieren; dies geschieht durch zwei Lade-Parameter:


static int lockup = 0;
MODULE_PARM(lockup, "i");

#ifdef HAVE_TX_TIMEOUT
static int timeout = SNULL_TIMEOUT;
MODULE_PARM(timeout, "i");
#endif

Wenn der Treiber mit dem Parameter lockup=n geladen wird, dann wird alle n übertragenen Pakete eine Blockade simuliert, und das Feld watchdog_timeo wird auf den timeout-Wert gesetzt. Beim Simulieren von Blockaden ruft snull auch netif_stop_queue auf, um andere Übertragungsversuche zu verhindern.

Der Timeout-Handler von snull sieht folgendermaßen aus:


void snull_tx_timeout (struct net_device *dev)
{
    struct snull_priv *priv = (struct snull_priv *) dev->priv;

    PDEBUG("Übertragung-Timeout bei %ld, latency %ld\n", jiffies,
                    jiffies - dev->trans_start);
    priv->status = SNULL_TX_INTR;
    snull_interrupt(0, dev, NULL);
    priv->stats.tx_errors++;
    netif_wake_queue(dev);
    return;
}

Wenn ein Übertragungs-Timeout geschieht, muß der Treiber den Fehler in der Schnittstellenstatistik markieren und dafür sorgen, daß das Gerät auf einen sinnvollen Zustand zurückgesetzt wird, in dem neue Pakete übertragen werden können. Wenn ein Timeout in snull auftritt, ruft der Treiber snull_interrupt auf, um den “fehlenden” Interrupt zu ergänzen und die Übertragungs-Warteschlange mit netif_wake_queue neu zu starten.