Передача пакетов

Предыдущая  Содержание  Следующая V*D*V

Наиболее важными задачами, выполняемыми сетевыми интерфейсами, является передача и приём данных. Мы начнём с передачи, потому что это немного легче для понимания.

 

Передача подразумевает процесс посылки пакета через сетевую связь. Всякий раз, когда ядро должно передать пакет, оно вызывает метод драйвера hard_start_xmit, чтобы поместить данные в исходящую очередь. Каждый пакет, обрабатываемым ядром, содержится в структуре буфера сокета (struct sk_buff), определение которой содержится в <linux/skbuff.h>. Структура получила свое название от абстракции Unix, используемой для представления сетевого соединения, сокет. Даже если интерфейс ничего с сокетами не делает, каждый сетевой пакет принадлежит сокету на более высоких сетевых уровнях и входные/выходные буферы любого сокета являются списками структур struct sk_buff. Та же структура sk_buff используется для размещения сетевых данных во всех сетевых подсистемах Linux, но что касается интерфейса, буфер сокета представляет собой просто пакет.

 

Указатель на sk_buff обычно называют skb и мы следуем этой практике и в коде примера и в тексте.

 

Буфер сокета представляет собой сложную структуру и для работы с ним ядро предлагает ряд функций. Функции описываются позже в разделе "Буферы сокетов"; сейчас же, чтобы написать рабочий драйвер, вполне достаточно нескольких основных фактов о sk_buff.

 

Буфер сокета, передаваемый в hard_start_xmit, содержит физический пакет так, как он должен выглядеть на носителе информации, укомплектованный заголовками этого уровня передачи. Интерфейсу нет необходимости изменять передаваемые данные. skb->data указывает на передаваемый пакет, а skb->len является его длиной в октетах. Эта ситуация становится немного более сложной, если ваш драйвер может обработать ввод/вывод с разборкой/сборкой; мы опишем это в разделе "Ввод/вывод с разборкой/сборкой".

 

Далее приведён код передачи пакетов в snull; физические механизмы передачи были выделены в другую функцию, потому что каждый драйвер интерфейс должен реализовать его в зависимости от того специфического оборудования, которым он управляет:

 

int snull_tx(struct sk_buff *skb, struct net_device *dev)

{

    int len;

    char *data, shortpkt[ETH_ZLEN];

    struct snull_priv *priv = netdev_priv(dev);

 

    data = skb->data;

    len = skb->len;

    if (len < ETH_ZLEN) {

        memset(shortpkt, 0, ETH_ZLEN);

        memcpy(shortpkt, skb->data, skb->len);

        len = ETH_ZLEN;

        data = shortpkt;

    }

    dev->trans_start = jiffies; /* сохраняем текущее время */

 

    /* Запоминаем skb, так что мы сможем освободить его вовремя прерывания */

    priv->skb = skb;

 

    /* настоящая доставка данных зависит от устройства и здесь не показана */

    snull_hw_tx(data, len, dev);

 

    return 0; /* Наше простое устройство не может дать ошибку */

}

 

Таким образом, функция передачи просто выполняет некоторые проверки пакета и передаёт данные через функцию, связанную с оборудованием. Обратите внимание, однако, на те действия, которые выполняются, когда пакет для передачи меньше, чем минимальная длина, поддерживаемая используемым носителем информации (который, для snull, наш виртуальный "Ethernet"). Во многих сетевых драйверах Linux (а также и других операционных систем) была обнаружена потеря данных в таких ситуациях. Вместо того, чтобы создать такого рода уязвимость безопасности, мы копируем короткие пакеты в отдельный массив, который мы можем явно дополнить нулями до полной длины, требуемой носителем информации. (Мы можем смело поместить данные в стек, поскольку минимальная длина, 60 байт, достаточно мала).

 

Возвращаемым значением из hard_start_xmit при успехе должен быть 0; в этот момент ваш драйвер взял на себя ответственность за пакет, следует приложить все усилия для обеспечения успешности передачи и в конце необходимо освободить skb. Ненулевое возвращаемое значение показывает, что данный пакет не может быть передан в это время; ядро повторит попытку позже. В этой ситуации ваш драйвер должен остановить очередь, пока любая причиной отказа не будет разрешена.

 

"Зависящая от оборудования" функция передачи (snull_hw_tx) здесь пропущена, так как она полностью занята реализацией хитростей устройства snull, включая манипулирование адресами источника и назначения, и представляет небольшой интерес для авторов настоящих сетевых драйверов. Конечно, она присутствует в исходном тексте примера для тех, кто хочет пойти и посмотреть, как она работает.

Управление конкуренцией при передаче

Функция hard_start_xmit защищена от одновременных вызовов спин-блокировкой (xmit_lock) в структуре net_device. Однако, как только функция возвращается, она может быть вызвана снова. Функция возвращается когда программа закончила инструктаж оборудования о передаче пакетов, но аппаратная передача к этому моменту будет, вероятно, не завершена. Это не проблема для snull, который делает всю свою работу используя процессор, поэтому передача пакетов завершена до того, как функция передачи возвращается.

 

С другой стороны, настоящие аппаратные интерфейсы передают пакеты асинхронно и имеют ограниченный объём памяти, доступной для хранения исходящих пакетов. Когда это память исчерпана (что на некотором оборудовании происходит и с одним подготовленным для передачи пакетом), драйвер должен сообщить сетевой системе не начинать любую другую передачу, пока оборудование не готово к приёму новых данных.

 

Такое уведомление осуществляется вызовом netif_stop_queue, эта функция приводилась ранее для остановки очереди. После того, как ваш драйвер остановил свою очередь, он должен организовать перезапуск очереди в какой-то момент в будущем, когда он снова сможет принимать пакеты для передачи. Чтобы сделать это, следует вызвать:

 

void netif_wake_queue(struct net_device *dev);

 

Эта функция выглядит похожей на netif_start_queue, за исключением того, что она также толкает сетевую систему, чтобы она снова начала передачу пакетов.

 

Большинство современного сетевого оборудования поддерживает внутреннюю очередь для передачи нескольких пакетов; таким образом, оно может получить от сети лучшую производительность. Сетевые драйверы для таких устройств должны поддерживать в любой момент времени наличие нескольких исходящих передач, но память устройства можно заполнить независимо от того, поддерживает или нет оборудование несколько исходящих передач. Всякий раз, когда память устройства заполняется до той точки, где нет места для максимально большого пакета, драйвер должен остановить очередь, пока пространство снова не станет доступным.

 

Если вам необходимо отключить передачу пакетов из любой места, отличного от вашей функции hard_start_xmit (возможно, в ответ на запрос переконфигурации), функцией, которую вы захотите использовать является:

 

void netif_tx_disable(struct net_device *dev);

 

Эта функция ведёт себя так же, как netif_stop_queue, но также гарантирует, что когда она возвращается, ваш метод hard_start_xmit не работает на другом процессоре. Как обычно, эта очередь может быть перезапущена с помощью netif_wake_queue.

Таймауты при передаче

Большинство драйверов, которые имеют дело с реальным оборудованием, должны быть готовы к тому, что изредка оборудование не реагируют на запросы. Интерфейсы могут забыть, что они делают, или система может потерять прерывание. Такой вид проблемы часто возникает при использовании некоторых устройств, разработанных для работы на персональных компьютерах.

 

Многие драйверы решают эту проблему установкой таймеров; если операция ещё не завершена к тому времени, как таймер истечёт, что-то не так. Сетевая система, как это случается, по существу является сложным набором конечных автоматов, управляемых массой таймеров. Таким образом, сетевой код находится в хорошем положении, чтобы обнаружить таймауты передачи, как часть своей обычной работы.

 

Таким образом, сетевым драйверам не требуется самостоятельно беспокоиться о выявлении таких проблем. Вместо этого, они должны только установить период ожидания, который ведёт в поле watchdog_timeo структуры net_device. Этот период, который выражен в тиках, должен быть достаточно продолжительным, чтобы учитывать нормальные задержки при передачи (например, коллизии, происходящие из-за перегруженности сетевой среды).

 

Если текущее системное время превышает время устройства trans_start по крайней мере на время периода ожидания, сетевой уровень в конечном счёте вызывает метод драйвера tx_timeout. Работой этого метода является сделать все необходимое, чтобы выяснить проблему и обеспечить надлежащее завершение любых передач, которые уже находятся в процессе выполнения. Важно, в частности, чтобы драйвер не потерял любой из буферов сокетов, которые были доверены ему сетевым кодом.

 

snull обладает способностью имитировать блокировку передатчика, которая управляется двумя параметрами во время загрузки:

 

static int lockup = 0;

module_param(lockup, int, 0);

 

static int timeout = SNULL_TIMEOUT;

module_param(timeout, int, 0);

 

Если драйвер загружен с параметром lockup=n, блокировка моделируется только для каждого n-го переданного пакета и поле watchdog_timeo установлено в заданное значение ожидания. При моделировании блокировок для предотвращения возникновения других попыток передачи snull также вызывает netif_stop_queue.

 

Обработчик таймаута передачи в snull выглядит следующим образом:

 

void snull_tx_timeout (struct net_device *dev)

{

    struct snull_priv *priv = netdev_priv(dev);

    PDEBUG("Transmit timeout at %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;

}

 

Когда при передаче происходит таймаут, драйвер должен отметить ошибку в статистике интерфейса и принять меры, чтобы сбросить устройств в нормальное состояние, чтобы могли быть переданы новые пакеты. Когда таймаут происходит в snull, драйвер вызывает snull_interrupt для заполнения "пропавшего" прерывания и перезапускает очередь передачи с помощью netif_wake_queue.

Ввод/вывод с разборкой/сборкой

Процесс создания пакета для передачи по сети включает в себя сборку нескольких штук. Пакетные данные часто должны быть скопированы из пользовательского пространства и также должны быть добавлены заголовки, используемые разными уровнями сетевого стека. Эта сборка может требовать копирования немалого количества данных. Однако, если сетевой интерфейс, который назначен для передачи пакета, может выполнять ввод/вывод с разборкой/сборкой, пакет не требуется собирать в единый кусок и большей части такого копирования можно избежать. Ввод/вывод с разборкой/сборкой позволяет также передачу с "нулевым копированием" сетевых данных непосредственно из буферов пользовательского пространства.

 

Ядро не пропускает разобранные пакеты в ваш метод hard_start_xmit, пока не установлен бит NETIF_F_SG в поле features структуры устройства. Если вы установили этот флаг, вам необходимо посмотреть на специальное поле "информация для общего использования" внутри skb, чтобы увидеть, состоит ли пакет из одного или нескольких фрагментов и найти разбросанные фрагменты, если это необходимо. Для доступа к этой информации существует специальный макрос; он называется skb_shinfo. Первый шаг при передаче потенциально фрагментированных пакетов обычно выглядит примерно так:

 

if (skb_shinfo(skb)->nr_frags == 0) {

    /* Просто как обычно используем skb->data и skb->len */

}

 

Поле nr_frags сообщает, как много фрагментов было использовано для построения пакета. Если оно равно 0, пакет состоит из одного куска и может быть доступен как обычно через поле data. Однако, если оно отлично от нуля, ваш драйвер должен перебрать и упорядочить для передачи каждый отдельный фрагмент. Поле data структуры skb указывает просто на первым фрагмент (по сравнению с полным пакетом, как в нефрагментированном случае). Длина фрагмент должна рассчитываться вычитанием skb->data_len из skb->len (которое по-прежнему содержит длину полного пакета). Остальные фрагменты должны быть найдены в массиве, названном frags, в общей информационной структур; каждая запись в frags является структурой skb_frag_struct:

 

struct skb_frag_struct {

    struct page *page;

    __u16 page_offset;

    __u16 size;

};

 

Как вы можете видеть, мы снова имеем дело со структурами page, а не виртуальными адресами ядра. Ваш драйвер должен перебрать все фрагменты, отображая каждый для DMA передачи и не забывая при этом первый фрагмент, который указан напрямую через skb. Ваше оборудование, разумеется, должно собрать фрагменты и передать их как единый пакет. Обратите внимание, что если у вас установлен флаг функции NETIF_F_HIGHDMA, некоторые или все эти фрагменты могут быть расположены в верхней памяти.

Предыдущая  Содержание  Следующая