Создание драйвера сетевого устройства - Часть 1

Автор: Bhaskaran
Перевод: Андрей Киселев

Введение

Эта статья призвана помочь читателю в понимании принципов создания драйвера сетевой ethernet-платы для ОС Linux. В качестве примечания -- разработка модуля драйвера выполнена на языке программирования C, а потому я полагаю, что читатель достаточно хорошо знаком с языком C и ОС Linux. В этом документе будут показаны лишь самые основные моменты, которые необходимо знать, чтобы написать драйвер сетевой платы (лучшие и более профессиональные примеры драйверов вы наверняка сможете найти в исходных текстах ядра).

Сетевая подсистема в Linux и PCI-платы

Вполне очевидно, что сетевая подсистема является неотъемлемой частью ядра Linux. Кроме того, можно утверждать, что Linux является одной из "самых безопасных и надежных" Сетевых Операционных Систем, из представленных в настоящее время на рынке. Внутренняя реализация сетевой подсистемы в ядре делится на две большие части. Первая -- это собственно реализация стека протоколов TCP/IP (каталог /usr/linux/net/ipv4). Вторая -- это реализация драйверов различных сетевых устройств (/usr/src/linux/drivers/net ).

Код, реализующий стек TCP/IP, разработан таким образом, что он очень просто интегрируется с драйверами самых разнообразных сетевых устройств (как реальных, так и виртуальных), освобождая разработчика от необходимости задумываться о том как работает код сетевого или транспортного уровня. Единственное требование состоит в том, что реализуемый модуль должен обеспечивать стандартный программный интерфейс для доступа к аппаратной части, которая может быть представлена сетевой Ethernet-картой, для случая подключения к локальной сети, или модемом -- в случае подключения к Интернет по коммутируемым каналам.

В настоящее время рынок сетевых устройств может предложить вам огромное разнообразие сетевых плат, среди которых я хочу отметить RTL8139. Это "plug and play" Ethernet плата, подключаемая через разъём шины PCI. PCI -- это Peripheral Component Interconnect (Взаимодействие с Периферийными Устройствами), полный набор спецификаций, определяющих порядок взаимодействия между различными частями компьютера. Архитектура PCI была разработана в качестве замены более раннего стандарта ISA, выгодно отличаясь от последней такими характеристиками, как скорость передачи данных, простота добавления и удаления дополнительных устройств, независимость от процессора и пр.

Основы работы с сетью

Выполнить настройку сетевой подсистемы на PC можно с помощью утилиты netconfig. С помощью этой утилиты можно задать IP-адрес, состоящий из 4-х октетов, маску подсети, адрес шлюза, первичный сервер имен и пр. После успешного завершения конфигурирования компьютер под управлением Linux может принимать сообщения переданные на его IP-адрес.

Еще одна немаловажная утилита, предназначенная для диагностики и ручной настройки сетевых интерфейсов, это ifconfig. Типичный пример вывода утилиты ifconfig, запущенной без аргументов, представлен ниже (у вас он может выглядеть несколько иначе, в зависимости от конкретных настроек и конфигурации системы).

eth0      Link encap:Ethernet  HWaddr 00:80:48:12:FE:B2
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:100
          RX bytes:0 (0.0 b)  TX bytes:600 (600.0 b)
          Interrupt:11 Base address:0x7000

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:16436  Metric:1
          RX packets:4 errors:0 dropped:0 overruns:0 frame:0
          TX packets:4 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:336 (336.0 b)  TX bytes:336 (336.0 b)


Как видно из данного листинга, на моей системе запущено два сетевых интерфейса: eth0 и lo, что обозначает сетевую плату ethernet и локальный "петлевой" (loopback) интерфейс. Локальный интерфейс loopback являет собой исключительно программную реализацию и не имеет аппаратного обеспечения. Напротив, интерфейсу eth0, в моем случае, соответствует реальная сетевая плата Realtek 8139. Кроме того, в листинге можно увидеть аппаратный адрес (HWaddr), или MAC-адрес, сетевой платы, IP-адрес (inet addr), адрес для широковещательных посылок Broadcast (Bcast), маску подсети (Mask) и дополнительную статистическую информацию, связанную с передачей данных, которая включает в себя максимальный размер блока данных (MTU), количество принятых пакетов (RX), количество переданных пакетов (TX), количество коллизий и пр.. Команда ifconfig может использоваться также для запуска сетевых интерфейсов, которые не были обнаружены системой на этапе загрузки. Одновременно можно переназначить IP-адрес, как показано ниже.

  ifconfig eht0 192.9.200.1 up

Данная команда назначает плате ethernet IP-адрес 192.9.200.1, сети класса C, и запускает сетевой интерфейс. Кроме того, ifconfig может использоваться для "остановки" активного сетевого интерфейса . Пример команды приводится ниже.

   ifconfig eth0 down

Все вышесказанное вполне применимо и к локальному (loopback) интерфейсу, например:
   ifconfig lo 192.9.200.1 up
        ifconfig lo down

Команда 'ifconfig' имеет множество дополнительных опций, описание которых вы сможете найти в страницах справочного руководства man.

Еще одна утилита, которая нам понадобится -- это netstat. Она выводит сведения о сетевых соединениях, таблицы маршрутизации, статистику по сетевым интерфейсам, маскируемые соединения и т.п.. Исчерпывающее описание дополнительных опций, которые предоставляет данная команда, вы найдете в справочном руководстве man.

Интерфейс ядра

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

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

Каждый интерфейс определяется структурой struct net_device. Так, например, объявление устройства rtl8139 будет выглядеть следующим образом:

   struct net_device rtl8139 = {init: rtl8139_init};

Определение структуры struct net_device находится в заголовочном файле linux/net_device.h. Код, представленный выше, инициализирует только одно поле структуры -- 'init', что соответствует функции инициализации модуля драйвера. Всякий раз, когда происходит регистрация устройства, ядро вызывает эту функцию, а она, в свою очередь, должна инициализировать аппаратную часть устройства и заполнить поля структуры struct net_device. Сама по себе структура struct net_device достаточно велика и содержит ссылки на все функции, необходимые для обеспечения взаимодействия с аппаратурой. Давайте рассмотрим некоторые из них.

name : Первое поле, которое требует пояснений -- это 'name', здесь хранится имя интерфейса (строка, идентифицирующая интерфейс). Очевидно, что в данном конкретном случае, это строка "rtl8139".

int (*open) (struct net_device *dev) : Метод, посредством которого интерфейс запускается всякий раз, когда утилита ifconfig активирует его. Метод open должен попытаться захватить все необходимые системные ресурсы.

int (*stop) (struct net_device *dev) : Этот метод закрывает, или останавливает, интерфейс.

int (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev) : Этот метод инициирует передачу данных через устройство 'dev'. Данные расположены в буфере сокета в структуре skb. К рассмотрению структуры skb мы приступим позднее.

struct net_device * (*get_status) (struct net_device *dev): Этот метод вызывается всякий раз, когда приложение пытается получить статистики интерфейса. Так этот метод вызывается, когда запускается, например, утилита ifconfig или netstat -i.

void *priv :Дополнительный указатель, который может использоваться разработчиком драйвера для своих нужд. Назначение этого поля структуры будет объясняться позднее. Структура включает в себя еще целый ряд методов, которые будут упоминаться ниже, но прежде я предлагаю взглянуть на рабочий код заготовки драйвера, построенный с учетом вышесказанного. Этот код поясняет порядок взаимодействия между описанными элементами.

Листинг 1

        #define MODULE
        #define __KERNEL__

        #include <linux/module.h>
        #include <linux/config.h>

        #include <linux/netdevice.h>

        int rtl8139_open (struct net_device *dev)
        {
                printk("rtl8139_open called\n");
                netif_start_queue (dev);
                return 0;
        }

        int rtl8139_release (struct net_device *dev)
        {
                printk ("rtl8139_release called\n");
                netif_stop_queue(dev);
                return 0;
        }

        static int rtl8139_xmit (struct sk_buff *skb,
                                        struct net_device *dev)
        {
                printk ("dummy xmit function called....\n");
                dev_kfree_skb(skb);
                return 0;
        }

        int rtl8139_init (struct net_device *dev)
        {
                dev->open = rtl8139_open;
                dev->stop = rtl8139_release;
                dev->hard_start_xmit = rtl8139_xmit;
                printk ("8139 device initialized\n");
                return 0;
        }

        struct net_device rtl8139 = {init: rtl8139_init};

        int rtl8139_init_module (void)
        {
                int result;

                strcpy (rtl8139.name, "rtl8139");
                if ((result = register_netdev (&rtl8139))) {
                        printk ("rtl8139: Error %d  initializing card rtl8139 card",result);
                        return result;
                }
        return 0;
        }

        void rtl8139_cleanup (void)
        {
                printk ("<0> Cleaning Up the Module\n");
                unregister_netdev (&rtl8139);
                return;
        }

        module_init (rtl8139_init_module);
        module_exit (rtl8139_cleanup);

Это модуль реализует типичную точку входа -- функцию rtl8139_init_module. Здесь определяется устройство типа net_device, задается его имя "rtl8139", после чего устройство регистрируется в ядре. Другая важная функция rtl8139_init -- вставляет заготовки функций (точнее их адреса, прим. перев.) rtl8139_open, rtl8139_stop, rtl8139_xmit в структуре net_device. Когда вызывается функция rtl8139_open -- она объявляет о готовности драйвера к приему данных вызовом функции netif_start_queue. Аналогично, при остановке интерфейса вызывается функция netif_stop_queue.

Попробуем скомпилировать вышеприведенный модуль и "поиграть" с ним. Для этого достаточно воспользоваться командой 'cc' так, как это показано ниже:

[root@localhost modules]# cc -I/usr/src/linux-2.4/include/ -Wall -c rtl8139.c

А теперь попробуем проверить нашу заготовку. Я на своей системе получил следующее (чтобы получить список загруженных модулей, воспользуйтесь утилитой lsmod):

(Примечание: Чтобы иметь возможность загружать/выгружать модули ядра, вы должны обладать привилегиями суперпользователя.)

[root@localhost modules]# insmod rtl8139.o
Warning: loading test.o will taint the kernel: no license
  See http://www.tux.org/lkml/#export-tainted for information about tainted modules
Module test loaded, with warnings

[root@localhost modules]# lsmod
Module                  Size  Used by    Tainted: P
rtl8139                 2336   0  (unused)
mousedev                5492   1  (autoclean)
input                   5856   0  (autoclean) [mousedev]
i810                   67300   6
agpgart                47776   7  (autoclean)
autofs                 13268   0  (autoclean) (unused)

[root@localhost modules]# ifconfig rtl8139 192.9.200.1 up
[root@localhost modules]# ifconfig

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:16436  Metric:1
          RX packets:4 errors:0 dropped:0 overruns:0 frame:0
          TX packets:4 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:336 (336.0 b)  TX bytes:336 (336.0 b)

rtl8139   Link encap:AMPR NET/ROM  HWaddr
          inet addr:192.9.200.1  Mask:255.255.255.0
          UP RUNNING  MTU:0  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 b)  TX bytes:600 (600.0 b)

(Проверено, "мин" нет. Прим.ред.)

Итак, мы познакомились с основными принципа построения драйвера фиктивного устройства. А теперь перейдем к реализации драйвера для конкретной сетевой платы rtl8139.

Плата PCI и ее инициализация

Хотя сетевой интерфейс и был создан нами, но он пока еще не в состоянии обнаружить и инициализировать сетевую плату. Это станет возможным только после того, как мы найдем соответствующее PCI-устройство. Таким образом, нам совершенно необходимо перейти к рассмотрению интерфейса PCI и набора функций для работы с ним.

Как я уже упоминал ранее, PCI -- это полный набор спецификаций, определяющих порядок взаимодействия между различными частями компьютера. Каждое PCI-устройство идентифицируется по номеру шины, номеру устройства и номеру функции. Спецификация допускает наличие в системе до 256 шин PCI и до 32 устройств на каждой из них.

Встроенное программное обеспечение компьютера (firmware) выполняет инициализацию аппаратуры PCI во время загрузки, отображает диапазон адресов ввода/вывода каждого устройства на различные адресные пространства, доступные в конфигурационном пространстве, которое представляет собой 256-ти байтную область на каждое устройство. Каждое из PCI-устройств идентифицируется тремя регистрами: vendorID, deviceID и class. Иногда, в целях более точной идентификации используются регистры Subsystem vendorID и Subsystem deviceID. Рассмотрим их более подробно.

Полный список устройств PCI, в ОС Linux, может быть получен вызовом команды lspci.

Отталкиваясь от этой новой информации, мы теперь можем попробовать идентифицировать плату rtl8139, в функции rtl8139_init. Измененная версия теперь будет выглядеть так:

Листинг 2

#include <linux/pci.h>

static int rtl8139_probe (struct net_device *dev, struct pci_dev *pdev)
{
        int ret;
        unsigned char pci_rev;

        if (! pci_present ()) {
                printk ("No pci device present\n");
                return -ENODEV;
        }
        else  printk ("<0> pci device were found\n");

        pdev = pci_find_device (PCI_VENDOR_ID_REALTEK,
                        PCI_DEVICE_ID_REALTEK_8139, pdev);

        if (pdev)  printk ("probed for rtl 8139 \n");
        else       printk ("Rtl8193 card not present\n");

        pci_read_config_byte (pdev, PCI_REVISION_ID, &pci_rev);

        if (ret = pci_enable_device (pdev)) {
                printk ("Error enabling the device\n");
                return ret;
        }

        if (pdev->irq < 2) {
                printk ("Invalid irq number\n");
                ret = -EIO;
        }
        else {
                printk ("Irq Obtained is %d",pdev->irq);
                dev->irq = pdev->irq;
        }
        return 0;
}

int rtl8139_init (struct net_device *dev)
{
        int ret;
        struct pci_dev *pdev = NULL;

        if ((ret = rtl8139_probe (dev, pdev)) != 0)
                return ret;

        dev->open = rtl8139_open;
        dev->stop = rtl8139_release;
        dev->hard_start_xmit = rtl8139_xmit;
        printk ("My device initialized\n");
        return 0;
}

Как вы можете видеть, функция probe вызывается из функции rtl8139_init. Если вы были достаточно внимательны, то наверняка заметили, что функции probe передаются два входных аргумента: указатель на структуру struct net_device и указатель на структуру struct pci_dev. Где первая соответствует сетевому устройству, а вторая -- PCI-устройству.

Функция pci_present проверяет наличия поддержки PCI. В случае успеха возвращает значение '0'. Затем, с помощью функции pci_find_device, производится попытка инициализации платы RTL8139. Функция принимает на входе значения vendor_ID, device_ID и указатель 'pdev'. В случае отсутствия ошибок, т.е. когда устройство RTL8139 подключено физически, эта функция возвращает указатель на уже заполненную структуру pdev. Константы PCI_VENDOR_ID_REALTEK, PCI_DEVICE_ID_REALTEK_8139 определяют значения vendorID и device_ID для платы realtek. Они определены в файле linux/pci.h.

Функции pci_read_config_byte/word/dword считывают byte/word/dword из конфигурационной области устройства. Вызовом функции pci_enable производится "включение" устройства rtl8139, эта функция так же выполняет регистрацию номера прерывания интерфейса. Соответственно, если все идет гладко, то ваша плата rtl_8139 будет обнаружена и связана с определенным номером прерывания.

В следующей части мы рассмотрим проблему определения аппаратного адреса устройства rtl8139 и начнем обмен данными.

 


Автор только что окончил обучение в B.Tech Govt. Engg. College в городе Thrissur.


Copyright (C) 2003, Bhaskaran. Copying license http://www.linuxgazette.com/copying.html
Published in Issue 93 of Linux Gazette, August 2003

Вернуться на главную страницу