Глава 22. Драйверы карты сетевого устройства

Все мы знаем, что сетевая среда встроена в ядро Linux. Несколько лет назад Linux применялся только для его работы в сети, однако это изменилось теперь; Linux намного больше чем некий сервер и работает в миллионах встроенных устройств. За эти годы Linux получил репутацию наилучшей из сетевых операционных систем. Назло всему этому Linux не может делать всё. Получив гигантское разнообразие существующих контроллеров Ethernet Linux обнаружил, что нет иного пути кроме как выставить некий API разработчикам, которым нужно написать некий драйвер для их сетевого устройства, или для тех, которым требуется выполнить сетевую разработку ядра обычным образом. Такой API предлагает достаточно абстрактный уровень, предназначенное для гарантии великодушия имеющегося кода разработчика, а также его портирование в прочие архитектуры. Данная глава просто пройдётся по всем частям этого API, который имеет дело с разработкой драйвера NIC (Network Interface Card, Карты сетевого интерфейса) и обсудит его структуры данных и методы.

В этой главе мы обсудим следующие темы:

  • Структура данных драйвера NIC и обход его основной буферной структуры основного сокета

  • Архитектура драйвера NIC и описание методов, а также передача и приём пакетов

  • Разработка болванки драйвера NIC для целей тестирования

Структура данных драйвера

Когда вы имеете дело с устройствами NIC, имеются две структуры данных, с которыми вам придётся выступать:

  • Структура struct sk_buff, определяемая в include/linux/skbuff.h, который является основной структурой данных во всем сетевом коде Linux и который должен быть включён в ваш код:

    
    #include <linux/skbuff.h>
     	   

    Все отправляемые и принимаемые пакеты обрабатываются с применением этой структуры данных.

  • Структура struct net_device; это та структура, при помощи которой любое устройство NIC представляется самому ядру. Именно она является тем интерфейсом, посредством которого имеет место перемещение данных. Она определяется в include/linux/netdevice.h, который также должен быть включён в ваш код:

    
    #include <linux/netdevice.h>
     	   

Прочие файлы, которые следует включать в свой код это include/linux/etherdevice.h для относящихся к MAC и Ethernet функций (например, alloc_etherdev()) и include/linux/ethtool.h для поддержки ethtools:


#include <linux/ethtool.h>
#include <linux/etherdevice.h>
 	   

  Структура буфера сокета

Эта структура обёртывает все пакеты, которые проходят через некий NIC:


struct sk_buff {
  struct sk_buff    * next;
  struct sk_buff    * prev;
  ktime_t            tstamp;
  struct rb_node     rbnode; /* применяется в netem & стеке tcp */
  struct sock       * sk;
  struct net_device * dev;
  unsigned int       len;
  unsigned int       data_len;
  __u16              mac_len;
  __u16              hdr_len;
  unsigned int       len;
  unsigned int       data_len;
  __u16              mac_len;
  __u16              hdr_len;
  __u32              priority;
  dma_cookie_t       dma_cookie;
  sk_buff_data_t     tail;
  sk_buff_data_t     end;
  unsigned char     * head;
  unsigned char     * data;
  unsigned int       truesize;
  atomic_t           users;
};
 	   

Ниже приводятся описания значений всех элементов в данной структуре:

  • next и prev: представляет следующий и предыдущий буферы в данном списке.

  • sk: тот сокет, который ассоциирован с данным пакетом.

  • tstamp: то время, когда этот пакет ушёл/ принят

  • rbnode: альтернатива для next/ prev, представляющая в некоем Красно- чёрном дереве.

  • dev: представляет то устройство, из которого появился/ к которому направлен этот пакет. Это поле связано с прочими не перечисленными здесь полями. Ими являются input_dev и real_dev. Они отслеживает устройства, связанные с этим пакетом. Более того, input_dev всегда относится к некоторому устройству, из которого принят этот пакет.

  • len: общее число байт в данном пакете. Буферы сокета (SKBs, Socket Buffers) состоят из некого линейного буфера данных и, не обязательно, некоторого набора одной или более областей, именуемых помещениями (rooms). В случае, когда имеются такие помещения, data_len будет сохранять общее число байт такой области данных.

  • ac_len: содержит длину имеющегося заголовка MAC.

  • csum: содержит контрольную сумму (checksum) данного пакета.

  • Priority: представляет значение приоритета пакета в QoS.

  • truesize: сохраняет отслеживание того сколько байт системной памяти потребляется данным пакетом, включая общий объём памяти, занимаемый самой структурой struct sk_buff.

  • users: используется для подсчёта ссылок всех объектов SKB.

  • Head: Head, data, tail являются указателями на различные области (помещения) в данном буфере сокета.

  • end: указывает на самый конец всего буфера сокета.

Здесь были обсуждены лишь несколько полей данной структуры. Полное описание доступно в include/linux/skbuff.h, который является основным файлом заголовка, который вам следует включить чтобы иметь дело с буферами сокета.

 

Выделение буфера сокета

Размещение некоторого буфера сокета слегка хитроумно, так как требует по крайней мере трёх различных функций:

  • Прежде всего, всё выделение памяти должно быть выполнено с применением функции netdev_alloc_skb()

  • Увеличить и выровнять заголовок помещения с помощью функции skb_reserve()

  • Расширить всю применяемую область данных этого буфера (который будет содержать данный пакет) применив функцию skb_put()

Давайте взглянем на следующий рисунок:

 

Рисунок 22.1



Процесс выделения буферов сокета

  1. Мы выделяем некий буфер, достаточно большой чтобы содержать некий пакет совместно с его заголовком Ethernet посредством функции netdev_alloc_skb():

    
    struct sk_buff *netdev_alloc_skb(struct net_device *dev,
                                       unsigned int length)
     	   

    Эта функция возвращает NULL в случае отказа. Таким образом, если она выделяет память, netdev_alloc_skb() может быть вызван из некоторого атомарного контекста.

    Так как заголовок Ethernet имеет длину 14 байт, он требует наличия некоторого выравнивания с тем, чтобы ЦПУ не сталкивался с проблемами производительности при обращении к этой части буфера. Соответствующее название данного параметра header_len должно быть header_alignment, так как этот параметр применяется для выравнивания. Обычным значением является 2, и именно это является той причиной, почему само ядро определяется неким выделенным макросом для данной цели, NET_IP_ALIGN, в include/linux/skbuff.h:

    
    #define NET_IP_ALIGN 2
     	   
  2. Второй шаг резервирует выделенную память для необходимого заголовка, уменьшая оставшееся хвостовое помещение. Функцией которая это выполняет является skb_reserve():

    
    void skb_reserve(struct sk_buff *skb, int len)
     	   
  3. Самый последний шаг состоит в расширении используемой области данных этого буфера настолько, чтобы вмещать размер пакета с помощью функции skb_put(). Эта функция возвращает некий указатель на самый первый байт данной области данных:

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

Размещённый буфер сокета должен быть переправлен в имеющийся сетевой уровень ядра. Именно это самый последний шаг жизненного цикла такого буфера сокета. Для этого следует применять функцию netif_rx_ni():


int netif_rx_ni(struct sk_buff *skb)
 	   

Мы обсудим как применять предыдущие шаги в том разделе данной главы, который имеет отношение с приёмом пакета.

  Структура сетевого интерфейса

Некий сетевой интерфейс представляется в имеющемся ядре как какой- то экземпляр структуры struct net_device, определяемой в include/linux/netdevice.h:


struct net_device {
  char name[IFNAMSIZ];
  char *ifalias;
  unsigned long mem_end;
  unsigned long mem_start;
  unsigned long base_addr;
  int irq;
  netdev_features_t features;
  netdev_features_t hw_features;
  netdev_features_t wanted_features;
  int ifindex;
  struct net_device_stats stats;
  atomic_long_t rx_dropped;
  atomic_long_t tx_dropped;
  const struct net_device_ops *netdev_ops;
  const struct ethtool_ops *ethtool_ops;
  unsigned int flags;
  unsigned int priv_flags;
  unsigned char link_mode;
  unsigned char if_port;
  unsigned char dma;
  unsigned int mtu;
  unsigned short type;
  /* Информация адреса интерфейса. */
  unsigned char perm_addr[MAX_ADDR_LEN];
  unsigned char addr_assign_type;
  unsigned char addr_len;
  unsigned short neigh_priv_len;
  unsigned short dev_id;
  unsigned short dev_port;
  unsigned long last_rx;
  /* Информация адреса интерфейса, применяемая в eth_type_trans() */
  unsigned char *dev_addr;
  struct device dev;
  struct phy_device *phydev;
};
 	   

Структура struct net_device относится к тем структурам данных ядра, которым требуется динамическое выделение, с применением их собственной функции размещения. Некий NIC размещается в самом ядре посредством имеющейся функцииalloc_etherdev().


struct net_device *alloc_etherdev(int sizeof_priv);
 	   

Эта функция возвращает NULL в случае отказа. Имеющийся параметр sizeof_priv представляет тот размер памяти, который должен быть выделен для некоторой частной структуры данных, подключаемой к данному NIC и которая может быть выделена имеющейся функцией netdev_priv():


void *netdev_priv(const struct net_device *dev)
 	   

Задавая определённую priv_struct, которая является нашей частной структурой, ниже приводится некая реализация того как вы выделяете какое- то сетевое устройство совместно с такой частной структурой данных:


struct net_device *net_dev;
struct priv_struct *priv_net_struct;
net_dev = alloc_etherdev(sizeof(struct priv_struct));
my_priv_struct = netdev_priv(dev);
 	   

Не используемые сетевые устройства должны быть освобождены с помощью имеющейся функции free_netdev(), которая к тому же освобождает память, выделенную под частные данные. Вам следует вызывать данный метод только после того, как данное устройство удалено из регистрации в данном ядре:


void free_netdev(struct net_device *dev)
 	   

После того, как структура net_device была завершена и заполнена, вам следует вызвать register_netdev() на неё. Данная функция объясняется позже в данной главе в разделе Методы драйвера. Просто запомните что эта функция регистрирует наше сетевое устройство в имеющемся ядре с тем, чтобы его можно было применять. При этом вы действительно должны убедиться, что данное устройство на самом деле может выполнять сетевые операции перед тем как вызывать такую функцию.


int register_netdev(struct net_device *dev)
 	   

Методы устройства

Сетевые устройства относятся к той категории устройств, которые не отображаются в каталоге /dev (в отличии от блочных устройств, устройств ввода или символьных устройств). Таким образом, как и все такие виды устройств, драйвер NIC выставляет набор функций для их выполнения. Само ядро выставляет операции, которые могут быть выполнены в данном сетевом интерфейсе посредством имеющейся структуры struct net_device_ops, которая является полем основной структуры struct net_device, представляющей данное сетевое устройство (dev->netdev_ops). Поля структуры struct net_device_ops определяются следующим образом:


struct net_device_ops {
   int (*ndo_init)(struct net_device *dev);
   void (*ndo_uninit)(struct net_device *dev);
   int (*ndo_open)(struct net_device *dev);
   int (*ndo_stop)(struct net_device *dev);
   netdev_tx_t (*ndo_start_xmit) (struct sk_buff *skb,
                              struct net_device *dev);
   void (*ndo_change_rx_flags)(struct net_device *dev, int flags);
   void (*ndo_set_rx_mode)(struct net_device *dev);
   int (*ndo_set_mac_address)(struct net_device *dev, void *addr);
   int (*ndo_validate_addr)(struct net_device *dev);
   int (*ndo_do_ioctl)(struct net_device *dev,
                              struct ifreq *ifr, int cmd);
   int (*ndo_set_config)(struct net_device *dev, struct ifmap *map);
   int (*ndo_change_mtu)(struct net_device *dev, int new_mtu);
   void (*ndo_tx_timeout) (struct net_device *dev);

   struct net_device_stats* (*ndo_get_stats)(
                              struct net_device *dev);
};
 	   

Давайте рассмотрим что означает каждый элемент в данной структуре:

  • int (*ndo_init)(struct net_device *dev) и void(*ndo_uninit)(struct net_device *dev): Это дополнительные функции инициализации/ деинициализации выполняемые соответственно когда драйвер вызывает register_netdev()/ unregister_netdev() чтобы регистрировать/ удалять из зарегистрированных это сетевое устройство в имеющемся ядре. Большинство драйверов не предоставляют эти функции, так как данное реальное задание выполняется функциями ndo_open() и ndo_stop().

  • int (*ndo_open)(struct net_device *dev): Подготавливает и открывает данный интерфейс. Данный интерфейс открывается всякий раз, когда утилиты ip или ifconfig активируют его. В данном методе рассматриваемый драйвер должен запросить/ пометить/ зарегистрировать любой необходимый ему сетевой ресурс (порты ввода/ вывода, IRQ, DMA и тому подобное), включить это оборудование и осуществить все прочие установки, запрашиваемые таки устройством.

  • int (*ndo_stop)(struct net_device *dev: Само ядро исполняет данную функцию когда данный интерфейс выходит из строя (Например, ifconfig <name> и тому подобное). Данная функция должна выполнять операции в обратном порядке тому, который осуществлялся в ndo_open().

  • int (*ndo_start_xmit) (struct sk_buff *skb, struct net_device *dev): Этот метод вызывается всякий раз, когда данное ядро желает отправить некий пакет через данный интерфейс.

  • void (*ndo_set_rx_mode)(struct net_device *dev): Этот метод вызывается для изменения режима фильтрации списка адресов интерфейса, групповой передачи или неупорядоченности. Рекомендуется выполнять эту функцию.

  • void (*ndo_tx_timeout)(struct net_device *dev): само ядро вызывает этот метод при отказе в завершении в рамках разумного периода передачи некоторого пакета , обычно для тактов dev->watchdog. Рассматриваемый драйвер должен проверить что случилось, обработать эту проблему и возобновить передачу пакета.

  • struct net_device_stats *(*get_stats)(struct net_device *dev): Данный метод возвращает статистику указанного устройства. именно она может просматриваться через исполнение netstat -i или ifconfig.

Предыдущее описание опустило многие поля. Описание полной структуры доступно в файле include/linux/netdevice.h. В действительности обязателен только ndo_start_xmit, однако хорошим практическим приёмом является предоставлять все те перехваты вспомогательных механизмов (helper), которыми снабжено ваше устройство.

  Открытие и закрытие

Функция ndo_open() вызывается самим ядром всякий раз когда данный сетевой интерфейс настраивается авторизованными пользователями (например, администратором), которые могут применять любые утилиты пользовательского пространства, например, ifconfig или ip.

Как и прочие операции сетевых устройств, данная функция ndo_open() получает некий объект struct net_device в качестве своего параметра, из которой данный драйвер должен получить тот специфичный для устройства объект, который хранится в поле priv на момент времени размещения самого объекта net_device.

Данный сетевой контроллер обычно выставляет некоторое прерывание при каждом получении пакета или завершении его обработки при передаче. Рассматриваемый драйвер должен зарегистрировать некий обработчик прерываний, который будет вызываться всякий раз, когда этот контроллер выставляет некое прерывание. Данный драйвер может зарегистрировать такой обработчик прерывания либо в процедуре init()/ probe(), либо в функции open. Некоторые устройства требуют того, чтобы такое прерывание было разрешено установками в каком- то специальном регистре в данном оборудовании. В таком случае следует запросить данное прерывание в определённой функции probe и просто установить/ очистить соответствующий бит разрешения в применяемом методе открытия/ закрытия.

Давайте просуммируем что должна делать наша функция open:

  1. Обновить MAC адрес интерфейса (в случае если пользователь изменил его и если ваше устройство допускает это).

  2. При необходимости осуществить сброс оборудования и вывести его из режима низкого энергопотребления.

  3. Запросить все ресурсы (память ввода/ вывода, каналы DMA, IRQ).

  4. Установить соответствие IRQ и зарегистрированных обработчиков прерывания.

  5. Проверить состояние соединения интерфейса.

  6. Вызвать в данном устройстве net_if_start_queue() чтобы позволить имеющемуся ядру знать, что ваше устройство готово передавать пакеты.

Ниже приводится некий пример функции open:


/*
 * Эта процедура устанавливать всё при каждом открытии, даже
 * регистрироваться, что необходимо только один раз при загрузке, так что
 * существует способ восстановления без перезагрузки если что- то пойдёт не так.
 */
static int enc28j60_net_open(struct net_device *dev)
{
  struct priv_net_struct *priv = netdev_priv(dev);

  if (!is_valid_ether_addr(dev->dev_addr)) {
        [...] /* Может быть выдать отладочное сообщение ? */
        return -EADDRNOTAVAIL;
  }
  /*
   * Выполнить сброс оборудования здесь и вывести его из 
   * режима низкого энергопотребления
   */
  my_netdev_lowpower(priv, false);

  if (!my_netdev_hw_init(priv)) {
        [...] /* обработка отказа в аппаратном сбросе */
        return -EINVAL;
  }
  /* Обновление текущего MAC адреса (в случае если пользователь изменил его)
   * Этот новый адрес сохраняется в поле netdev->dev_addr
   */
  set_hw_macaddr_registers(netdev, MAC_REGADDR_START,
  netdev->addr_len, netdev->dev_addr);

  /* Разрешить прерывания */
  my_netdev_hw_enable(priv);

  /* Теперь мы готовы принимать передаваемые запросы от 
   * построенного в очередь уровня имеющейся сетевой среды.
   */
  netif_start_queue(dev);

  return 0;
}
 	   

netif_start_queue() просто позволяет стоящим выше пользователям вызывать процедуру ndo_start_xmit данного устройства. Другими словами, она информирует имеющееся ядро что данное устройство готово обрабатывать передаваемые запросы.

Закрывающий метод с другой стороны просто должен выполнить все действия в обратном порядке относительно того, который осуществлялся при открытии:


/* Обратная net_open() процедура. */
static int enc28j60_net_close(struct net_device *dev)
{
  struct priv_net_struct *priv = netdev_priv(dev);

  my_netdev_hw_disable(priv);
  my_netdev_lowpower(priv, true);

  /**
   * netif_stop_queue - остановить передачу пакетов
   *
   * Прекратить вызовы данного устройства с верхнего уровня процедурой ndo_start_xmit.
   * Применяется для управления потоком когда ресурсы передачи не доступны.
   */
  netif_stop_queue(dev);

  return 0;
}
 	   

netif_stop_queue() просто выполняет все обратные netif_start_queue() действия, сообщая имеющемуся ядру о необходимости прекращения вызовов процедуры ndo_start_xmit данного устройства. Мы не можем более обрабатывать запросы передачи.

  Обработка пакета

Обработка пакетов состоит из передачи и приёма пакетов. Это самая основная задача любого драйвера сетевого интерфейса. Передача относится только к отправке исходящих кадров, в то время как приём соответствует входящим кадрам.

Существует два способа для проведения сетевого обмена данными: путём опроса или по прерыванию. Опрос, который является неким видом движимого таймеров прерывания, заключается в непрерывной проверке ядром с заданными интервалами времени любых, прослушивая некую линию IRQ и ожидая, что данное устройство уведомит о каком- то изменении посредством определённого IRQ. Движимые прерыванием обмены могут увеличивать накладные расходы системы на протяжении времён интенсивного обмена. Именно по этой причине некоторые драйверы смешивают такие два метода. Такая часть имеющегося ядра, которая позволяет смешивать такие два метода имеет название New API (NAPI), причём она состоит из применения опроса на протяжении времён высокого обмена и использования движимого IRQ управления когда обмен становится обычным. Новым драйверам следует применять NAPI если имеющееся оборудование может его поддерживать. Однако, NAPI не обсуждается в данной главе, которая сосредотачивается на конкретике метода управляемого прерываниями.

 

Приём пакета

Когда некий пакет возникает в имеющейся карте сетевого интерфейса, рассматриваемый драйвер должен построить некий новый буфер сокета вокруг него и скопировать такой пакет в своё поле sk_ff->data. Применяемый вид копирования не имеет значения и при этом может использоваться DMA. Имеющийся драйвер обычно осведомляется о новых возникших данных посредством прерываний. Когда этот NIC получает некий пакет, он возбуждает прерывание, и оно будет обработано самим драйвером, который должен проверить имеющийся регистр состояния прерывания данного устройства и выяснить реальную причину, вызвавшую распространение данного прерывания (это может быть RX, хорошо, ошибка RX и так далее). Бит (биты), которые отвечают за установленное событие, вызвавшее данное прерывание будут отправлены в имеющийся регистр состояния.

Самая хитроумная часть будет состоять в выделении и построении необходимого буфера сокета. Однако формально мы уже обсуждали это в самом первом разделе данной главы. Поэтому давайте теперь не будем тратить время и сразу перепрыгнем к примеру обработчика RX. Наш драйвер должен выполнить столько выделений sk_buff, сколько пакетов он принял:


/*
 * Обработчик RX
 * Эта функция вызывается в той работе обработчика, которая отвечает за  
 * принятие пакета (bottom half). Мы используем We раьоту, так как доступ к 
 * нашему устройству (которое располагается на шине SPI) может быть в состоянии сна
 */
static int my_rx_interrupt(struct net_device *ndev)
{
  struct priv_net_struct *priv = netdev_priv(ndev);
  int pk_counter, ret;

  /* Давайте получим общее число принятых нашим устройством пакетов */
  pk_counter = my_device_reg_read(priv, REG_PKT_CNT);
  if (pk_counter > priv->max_pk_counter) {
      /* обновляем статистики */
      priv->max_pk_counter = pk_counter;
  }
  ret = pk_counter;

  /* устанавливаем начало буфера приёма */
  priv->next_pk_ptr = KNOWN_START_REGISTER;
  while (pk_counter-- > 0)
        /*
         * Вызывая данную внутреннюю вспомогательную функцию в некотором цикле 
         * "while", пакеты начинают выделяться один за другим из данного устройства
         * и направляться в имеющийся сетевой уровень.
         */
         my_hw_rx(ndev);

  return ret;
}
 	   

Ниже приводится помощник, который отвечает за получение одного пакета их данного устройства, продвижения его в имеющуюся сетевую среду ядра и определения числа пакетов:


/*
 * Функция аппаратного приёма.
 * Считывает имеющийся буфер памяти, обновляет текущий указатель FIFO 
 * для освобождения этого буфера.
 * Данная функция уменьшает имеющийся счётчик пакетов.
 */
static void my_hw_rx(struct net_device *ndev)
{
   struct priv_net_struct *priv = netdev_priv(ndev);
   struct sk_buff *skb = NULL;
   u16 erxrdpt, next_packet, rxstat;
   u8 rsv[RSV_SIZE];
   int packet_len;

   packet_len = my_device_read_current_packet_size();
   /* Не можем пересекать границы */
   if ((priv->next_pk_ptr > RXEND_INIT)) {
          /* адрес пакета разрушен: сбрасываем логику RX */
          [...]
          /* Обновляем состояния ошибок RX */
          ndev->stats.rx_errors++;
          return;
   }
   /* Считываем следующий указатель и вектор состояния rx 
    * Это зависит от устройства
    */
   my_device_reg_read(priv, priv->next_pk_ptr, sizeof(rsv), rsv);

   /* Проверяем наличие ошибок в регистре состояния RX данного устройства,
    * и обновляем соответствующим образом состояния ошибок
    */
   if(an_error_is_detected_in_device_status_registers())
         /* В зависимости от данной ошибки,
          * stats.rx_errors++;
          * ndev->stats.rx_crc_errors++;
          * ndev->stats.rx_frame_errors++;
          * ndev->stats.rx_over_errors++;
          */
   } else {
          skb = netdev_alloc_skb(ndev, len + NET_IP_ALIGN);
          if (!skb) {
                ndev->stats.rx_dropped++;
          } else {
                skb_reserve(skb, NET_IP_ALIGN);
                /*
                 * копируем данный пакет из буфера приёма своего устройства
                 * в данные памяти имеющегося буфера сокета.
                 * Помните, что skb_put() возвращает указатель на самое 
                 * начало области данных.
                 */
                my_netdev_mem_read(priv,
                      rx_packet_start(priv->next_pk_ptr),
                      len, skb_put(skb, len));

                /* Устанавливаем ID протокола данного пакета */
                skb->protocol = eth_type_trans(skb, ndev);
                /* обновляем статистики  RX */
                ndev->stats.rx_packets++;
                ndev->stats.rx_bytes += len;

                /* Представляем буфер сокета своему сетевому уровню */
                netif_rx_ni(skb);
          }
   }
   /* Перемещаем текущий указатель RX чтения на самое начало следующего
    * имеющегося принимаемого пакета.
    */
   priv->next_pk_ptr = my_netdev_update_reg_next_pkt();
}
 	   

Конечно, единственная причина почему мы вызываем данный обработчик RX из отсроченной работы обусловлено тем, что мы располагаемся в некоторой шине SPI. Все приведённые предыдущие операции могут быть выполнены из hwriq в случае некоторого устройства MMIO. Рассмотрите имеющийся драйвер NXP FEC, приводимый в drivers/net/ethernet/freescale/fec.c чтобы понять как это достигается.

 

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

Когда имеющемуся ядру необходимо отправлять пакеты из определённого интерфейса, оно вызывает метод ndo_start_xmit драйвера, который должен вернуть NETDEV_TX_OK в случае успеха или NETDEV_TX при неудаче и в этом случае вы не можете ничего поделать с буфером сокета, так как он всё ещё находится во владении имеющегося уровня сетевой очереди при возвращении данной ошибки. Это означает, что вы не можете изменять какие- либо поля SKB или освобождать данный SKB и так далее. Данная функция защищена от наличия одновременных вызовов взаимной блокировкой.

Передача пакета в большинстве случаев делается асинхронной. Имеющийся sk_buff такого передаваемого пакета заполняется своими верхними уровнями. Его поля data содержат подлежащие отправке пакеты. Драйверы должны выделять пакет из sk_buff->data и записывать его в имеющийся в аппаратном устройстве FIFO до достижения порогового значения (обычно определённое в самом драйвере, или предоставляемое в некотором описании устройства) в некий специальный регистр данного устройства. При этом драйверу необходимо сообщить своему ядру чтобы оно не запускало никакие передачи пока данное оборудование не станет готовым принимать новые данные. Такое уведомление осуществляется посредством имеющейся функции netif_stop_queue().


void netif_stop_queue(struct net_device *dev)
 	   

После отправки текущего пакета данная карта сетевого интерфейса возбуждает некое прерывание. Его помощник прерывания должен проверить почему возникло данное прерывание. В случае прерывания передачи он обязан обновить свои статистики (net_device->stats.tx_errors и net_device->stats.tx_packets), а также уведомить своё ядро что данное устройство освободилось для отправки новых пакетов. Такое уведомление осуществляется посредством netif_wake_queue():


void netif_wake_queue(struct net_device *dev)
 	   

Чтобы суммировать, передача пакета расщепляется на две части:

  • Операцию ndo_start_xmit, которая уведомляет своё ядро что это устройство занято, выполняет все установки и начинает необходимую передачу.

  • Обработчик прерывания TX, оторый обновляет статистики TX, а также уведомляет своё ядро что данное устройство доступно вновь

Указанная функция ndo_start_xmit должна в точности придерживаться следующих шагов:

  1. Вызвать netif_stop_queue() в своём сетевом устройстве чтобы проинформировать имеющееся ядро что данное устройство будет занято передачей данных.

  2. Записать содержимое buff->data в имеющийся FIFO устройства.

  3. Переключиться на саму передачу (указать устройству начать осуществление передачи).

    [Замечание]Замечание

    Операции (2) и (3) могут повлечь за собой засыпание устройств, располагающихся на медленных шинах (например, SPI) и может нуждаться в изменении структуры своей работы. Именно это наблюдается в нашем случае.

    Когда пакет передан, имеющийся обработчик прерывания TX должен выполнит следующие шаги:

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

    1. Проверить является ли прерывание прерыванием передачи.

    2. Считать регистр состояния дескриптора передачи и посмотреть каким является состояние данного пакета.

    3. Инкрементально увеличить статистики ошибок если имелись проблемы при данной передаче.

    4. Инкрементально увеличить статистики успешно переданных пакетов.

  5. Запустить очередь данной передачи, разрешив своему ядру вызывать имеющийся метод ndo_start_xmit снова посредством функции netif_wake_queue().

Давайте просуммируем всё коротко в примере кода:


/* Где- то в нашем коде */
INIT_WORK(&priv->tx_work, my_netdev_hw_tx);

static netdev_tx_t my_netdev_start_xmit(struct sk_buff *skb,
                           struct net_device *dev)
{
   struct priv_net_struct *priv = netdev_priv(dev);

   /* Уведомляем своё ядро что наше устройство будет занято */
   netif_stop_queue(dev);

   /* Запоминаем этот skb для отложенной обработки */
   priv->tx_skb = skb;

   /* Данная работа будет копировать данные из sk_buffer->data в
    * имеющийся аппаратный FIFO и начнёт передачу
    */
   schedule_work(&priv->tx_work);

   /* Всё OK */
   return NETDEV_TX_OK;
}
 	   

Данная работа описывается далее:


/*
 * Функция аппаратной передачи.
 * Заполняет имеющийся буфер памяти и отправляет всё содержимое в
 * соответствующий буфер передачи в соответствующую сетевую среду
 */
static void my_netdev_hw_tx(struct priv_net_struct *priv)
{
   /* Записываем пакет в аппаратный буфер памяти TX устройства */
   my_netdev_packet_write(priv, priv->tx_skb->len,
priv->tx_skb->data);

/*
 * поддерживает ли данное сетевое устройство write-verify?
 * Выполняем его
 */
[...];

/* устанавливаем флаг запроса TX,
 * с тем, чтобы данное оборудование могло выполнять передачу.
 * Определяется устройством
 */
   my_netdev_reg_bitset(priv, ECON1, ECON1_TXRTS);
}
 	   

Управление прерыванием TX будет обсуждено в нашем следующем разделе.

  Пример драйвера

Мы можем суммировать все обсуждавшиеся концепции в следующем поддельном драйвере Ethernet:


#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/ethtool.h>
#include <linux/skbuff.h>
#include <linux/slab.h>
#include <linux/of.h>                 /* Для DT*/
#include <linux/platform_device.h>    /* Для устройств платформы */

struct eth_struct {
    int bar;
    int foo;
    struct net_device *dummy_ndev;
};

static int fake_eth_open(struct net_device *dev) {
    printk("fake_eth_open called\n");
    /* Теперь мы готовы принимать передаваемые запросы из
     * выстроенного в очередь уровня своей сетевой среды.
     */
    netif_start_queue(dev);
    return 0;
}

static int fake_eth_release(struct net_device *dev) {
    pr_info("fake_eth_release called\n");
    netif_stop_queue(dev);
    return 0;
}

static int fake_eth_xmit(struct sk_buff *skb, struct net_device *ndev) {
    pr_info("dummy xmit called...\n");
    ndev->stats.tx_bytes += skb->len;
    ndev->stats.tx_packets++;
    skb_tx_timestamp(skb);
    dev_kfree_skb(skb);
    return NETDEV_TX_OK;
}

static int fake_eth_init(struct net_device *dev)
{
    pr_info("fake eth device initialized\n");
    return 0;
};

static const struct net_device_ops my_netdev_ops = {
    .ndo_init = fake_eth_init,
    .ndo_open = fake_eth_open,
    .ndo_stop = fake_eth_release,
    .ndo_start_xmit = fake_eth_xmit,
    .ndo_validate_addr = eth_validate_addr,
    .ndo_validate_addr = eth_validate_addr,
}

static const struct of_device_id fake_eth_dt_ids[] = {
    { .compatible = "packt,fake-eth", },
    { /* sentinel */ }
};

static int fake_eth_probe(struct platform_device *pdev)
{
    int ret;
    struct eth_struct *priv;
    struct net_device *dummy_ndev;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    dummy_ndev = alloc_etherdev(sizeof(struct eth_struct));
    dummy_ndev->if_port = IF_PORT_10BASET;
    dummy_ndev->netdev_ops = &my_netdev_ops;

    /* Если требуется, dev->ethtool_ops = &fake_ethtool_ops; */

    ret = register_netdev(dummy_ndev);
    if(ret) {
        pr_info("dummy net dev: Error %d initalizing card ...", ret);
        return ret;
    }
    priv->dummy_ndev = dummy_ndev;
    platform_set_drvdata(pdev, priv);
    return 0;
}

static int fake_eth_remove(struct platform_device *pdev)
{
    struct eth_struct *priv;
    priv = platform_get_drvdata(pdev);
    pr_info("Cleaning Up the Module\n");
    unregister_netdev(priv->dummy_ndev);
    free_netdev(priv->dummy_ndev);

    return 0;
}

static struct platform_driver mypdrv = {
    .probe = fake_eth_probe,
    .remove = fake_eth_remove,
    .driver = {
        .name = "fake-eth",
        .of_match_table = of_match_ptr(fake_eth_dt_ids),
        .owner = THIS_MODULE,
    },
};

module_platform_driver(mypdrv);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_DESCRIPTION("Fake Ethernet driver");
 	   

Когда этот модуль загружен и установлено соответствие устройства, в вашей системе будет создан некий интерфейс Ethernet. Вначале давайте рассмотрим что отображает нам команда dmesg:


# dmesg
[...]
[146698.060074] fake eth device initialized
[146698.087297] IPv6: ADDRCONF(NETDEV_UP): eth0: link is not ready
		

Если выполнить команду ifconfig -a, этот интерфейс будет выведен на ваш экран:


# ifconfig -a
[...]
eth0 Link encap:Ethernet HWaddr 00:00:00:00:00:00
BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
		

Можно наконец окончательно завершить настройку этого интерфейса назначив некий IP адрес с тем, чтобы он мог отображаться ifconfig:


# ifconfig eth0 192.168.1.45
# ifconfig
[...]
eth0 Link encap:Ethernet HWaddr 00:00:00:00:00:00
inet addr:192.168.1.45 Bcast:192.168.1.255 Mask:255.255.255.0
BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
		

  Состояние и управление

Управлением устройством определяется некая ситуация, при которой имеющемуся ядру необходимо изменить свойства данного интерфейса по его собственной инициативе или в ответ на некое действие пользователя. Оно может затем воспользоваться либо операциями, выставляемыми через структуру struct net_device_ops, как это обсуждалось, или применить другой инструмент управления, ethtool, который требует от драйвера введения некоторого нового набора перехватов, которые мы обсудим в следующем разделе. И наоборот, состояние заключается в отчёте о текущем состоянии данного интерфейса.

 

Обработчик прерывания

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

Вот как наш обработчик hwrirq выглядит:


static irqreturn_t my_netdev_irq(int irq, void *dev_id)
{
   struct priv_net_struct *priv = dev_id;
   /*
    * Не можем ничего делать в контексте прерывания, так как мы обязаны
    * выполнить блокировку (spi_sync() является блокировкой) поэтому стреляйте определённым прерыванием
    * обрабатывающим рабочую последовательность.
    * Помните, мы выполняем доступ к регистрам своего netdev через шину SPI 
    * посредством вызова spi_sync().
    */
   schedule_work(&priv->irq_work);

   return IRQ_HANDLED;
}
 	   

Поскольку наше устройство сидит на шине SPI, всё откладывается в некую work_struct, которая определяется следующим образом:


static void my_netdev_irq_work_handler(struct work_struct *work)
{
   struct priv_net_struct *priv =
         container_of(work, struct priv_net_struct, irq_work);
   struct net_device *ndev = priv->netdev;
   int intflags, loop;

   /* запрещаем последующие прерывания */
   my_netdev_reg_bitclear(priv, EIE, EIE_INTIE);

   do {
         loop = 0;
         intflags = my_netdev_regb_read(priv, EIR);
         /* Обработчик прерываний DMA (не используется в настоящее время) */
         if ((intflags & EIR_DMAIF) != 0) {
               loop++;
               handle_dma_complete();
               clear_dma_interrupt_flag();
         }
         /* Обработчик изменённого LINK */
         if ((intflags & EIR_LINKIF) != 0) {
               loop++;
               my_netdev_check_link_status(ndev);
               clear_link_interrupt_flag();
         }
         /* Обработчик завершения TX */
         if ((intflags & EIR_TXIF) != 0) {
               bool err = false;
               loop++;
               priv->tx_retry_count = 0;
               if (locked_regb_read(priv, ESTAT) & ESTAT_TXABRT)
                     clear_tx_interrupt_flag();
         }

         /* Обработчик ошибки TX */
         if ((intflags & EIR_TXERIF) != 0) {
               loop++;
               /*
                * Сброс логики TX через установку/очистку соответствующего
                * бита в имеющемся правильном регистре
                */
               [...]

               /* Проверка Запоздалого столкновения передачи для повторной передачи */
               if (my_netdev_cpllision_bit_set())
                     /* Обработка столкновения */
                     [...]
         }
         /* Обработчик ошибки RX */
         if ((intflags & EIR_RXERIF) != 0) {
               loop++;
               /* Проверка свободности пространства FIFO для сигнализации переполнения RX */
               [...]
         }
         /* Обработчик RX */
         if (my_rx_interrupt(ndev))
               loop++;
   } while (loop);

   /* повторно разрешаем прерывания */
   my_netdev_reg_bitset(priv, EIE, EIE_INTIE);
}
 	   
 

Поддержка Ethtool

Ethtool является небольшой утилитой для исследования и тонкой настройки установок сетевых интерфейсов на основе Ethernet. С помощью ethtool имеется возможность управлять различными параметрами, такими как:

  • Скорость

  • Тип среды

  • Дуплексные операции

  • Получение/ установка содержимого регистра eeprom

  • Суммирование проверок оборудования

  • Wake-on-LAN и тому подобное

Те драйверы, которым необходима поддержка со стороны ethtool должны включать <linux/ethtool.h>. Он основывается на структуре struct ethtool_ops, которая является основным ядром его функциональности и содержит некий набор методов для сопровождения действий ethtool. Большинство методов достаточно однозначны; для подробностей обратитесь к include/linux/ethtool.h.

Для поддержки того, чтобы ethtool был целиком частью вашего драйвера, сам драйвер должен заполнить некую структуру ethtool_ops в имеющемся поле .ethtool_ops общей структуры structnet_device.


my_netdev->ethtool_ops = &my_ethtool_ops;
 	   

Макрос SET_ETHTOOL_OPS также может быть применён для этой цели. Имейте в виду, что ваши методы ethtool могут вызываться даже когда сам интерфейс остановлен.

Например, следующие драйверы реализуют поддержку ethtool:

  • drivers/net/ethernet/microchip/enc28j60.c

  • drivers/net/ethernet/freescale/fec.c

  • drivers/net/usb/rtl8150.c

Методы драйвера

Методами драйвера являются функции probe() и remove(). Они отвечают за регистрацию и удаление из зарегистрированных данного сетевого устройства в самом ядре. Ваш драйвер обязан предоставлять свою функциональность своему ядру через эти методы посредством структуры structnet_device. Именно эти операции являются тем, что может выполняться в данном сетевом интерфейсе;


static const struct net_device_ops my_netdev_ops = {
  .ndo_open = my_netdev_open,
  .ndo_stop = my_netdev_close,
  .ndo_start_xmit = my_netdev_start_xmit,
  .ndo_set_rx_mode = my_netdev_set_multicast_list,
  .ndo_set_mac_address = my_netdev_set_mac_address,
  .ndo_tx_timeout = my_netdev_tx_timeout,
  .ndo_change_mtu = eth_change_mtu,
  .ndo_validate_addr = eth_validate_addr,
};
 	   

Выше перечислены те операции, которые реализует большинство драйверов.

  Функция probe

Функция probe является достаточно базовой и требует исполнения только раннего init устройства, а затем регистрирует наше сетевое устройство в самом ядре.

Другими словами, функция probe должна:

  1. Выделите это сетевое устройство совместно с его частными данными применив функцию alloc_etherdev() (снабжённую помощником netdev_priv()).

  2. Выполните инициализацию полей частных данных (mutexes, spinlock, work_queue и так далее). Следует применять рабочие очереди (и семафоры) в случае когда ваше устройство расположено в шине, чьи функции могут находиться в состоянии сна (например, SPI). В этом случае ваш hwirq должен подтвердить сам код ядра и поставить данную работу, которая выполнит действия для данного устройства, в расписание. Альтернативным решением является применение потокового IRQ. Если данное устройство является MMIO, следует применять взаимную блокировку для защиты критических разделов и начала избавления рабочих очередей.

  3. Проинициализируйте зависящие от шины параметры и функции (SPI, USB, PCI и тому подобное).

  4. Опросите устройства и создайте карту ресурсов (память ввода/ вывода, каналы DMA, IRQ).

  5. Если это необходимо, создайте некий случайный адрес MAC и назначьте его данному устройству.

  6. Заполните обязательные (или полезные) свойства netdev: if_port, irq, netdev_ops, ethtool_ops и таму подобные.

  7. Поместите своё устройство в состояние с низким состояние энергопотреблением (имеющаяся функция open() удаляет его из данного режима).

  8. Наконец, вызовите для этого устройства register_netdev().

Для некоторого сетевого устройства SPI такая Функция probe может выглядеть примерно так:


static int my_netdev_probe(struct spi_device *spi)
{
   struct net_device *dev;
   struct priv_net_struct *priv;
   int ret = 0;

   /* Выделение сетевого интерфейса */
   dev = alloc_etherdev(sizeof(struct priv_net_struct));
   if (!dev)
   [...] /* обработка ошибки -ENOMEM */

   /* Частные данные */
   priv = netdev_priv(dev);
   /* установка частных данных и зависящих от шины параметров */
   [...]

   /* Инициализация некоторых работ */
   INIT_WORK(&priv->tx_work, data_tx_work_handler);
   [...]

   /* На самом деле устройство инициализирует только несколько моментов */
   if (!my_netdev_chipset_init(dev))
   [...] /* обработка ошибки -EIO */

   /* Создание и назначение некоторого случайного MAC адреса данному устройству */
   eth_hw_addr_random(dev);
   my_netdev_set_hw_macaddr(dev);

   /* Установка платы должна настроить соответствующие типы переключений фронтами;
    * в настоящее время переключения уровнем не работают.
    */
   ret = request_irq(spi->irq, my_netdev_irq, 0, DRV_NAME, priv);
   if (ret < 0)
   [...]; /* Обработка отказа запроса irq */

   /* Заполняем некоторые обязательные или полезные свойства netdev */
   dev->if_port = IF_PORT_10BASET;
   dev->irq = spi->irq;
   dev->netdev_ops = &my_netdev_ops;
   dev->ethtool_ops = &my_ethtool_ops;

   /* Помещаем устройство в режим сна */
   My_netdev_lowpower(priv, true);

   /* Регистрируем наше устройство в своём ядре */
   if (register_netdev(dev))
   [...]; /* Обрабатываем ошибку отказа в регистрации */

   dev_info(&dev->dev, DRV_NAME " driver registered\n");

   return 0;
}
 	   
[Замечание]Замечание

Вся эта глава в основном инспирирована устройством enc28j60 производимым Microchip. Вы можете взглянуть на его код в drivers/net/ethernet/microchip/enc28j60.c.

Функция register_netdev() берёт заполненный объект struct net_device и добавляет его в имеющиеся интерфейсы ядра; в случае успеха возвращается 0, а в случае неудачи возвращаются отрицательные коды ошибки. Сам объект struct net_device должен сохраняться в структуре шины вашего устройства с тем, чтобы к нему можно было бы позже осуществлять доступ. При этом, если ваше устройство является частью некоторой глобальной частной структуры, это именно та структура, которую вам следует регистрировать.

[Замечание]Замечание

Заметим, что дублирование названия устройства может приводить к отказу в регистрации.

  Выгрузка модуля

Это функция очистки, которая основывается на двух функциях. Наша функция освобождения драйвера может выглядеть так:


static int my_netdev_remove(struct spi_device *spi)
{
  struct priv_net_struct *priv = spi_get_drvdata(spi);

  unregister_netdev(priv->netdev);
  free_irq(spi->irq, priv);
  free_netdev(priv->netdev);

  return 0;
}
 	   

Функция unregister_netdev() удаляет данный интерфейс из имеющейся системы и наше ядро не может больше вызывать его методы; free_netdev() освобождает память применявшуюся самой структурой struct net_device в области памяти размещавшей частные данные, а также во внутренних выделениях, связанных с данным сетевым устройством. Отметим, что вам никогда не следует освобождать netdev->priv самостоятельно.

Выводы

Данная глава описала всё необходимое для написания некоторого драйвера устройства NIC. Даже не смотря на то, что эта глава полагается на некий сетевой интерфейс, располагающийся на какой- то шине SPI, все принципы остаются теми же самыми для сетевых интерфейсов USB или PCI. Вы также можете применять данный драйвер пустышки для целей тестирования. После этой главы очевидно, что драйверы NIC больше не будут мистикой для вас.