Журнал LinuxFormat - перейти на главную

LXF78:Драйвер сетевого устройства – своими руками

Материал из Linuxformat
Перейти к: навигация, поиск

Часть 1. Не нашли, как включить нужную функцию в make menuconfig? Не беда – Игорь Тимошенко расскажет, как написать драйвер самостоятельно!

Содержание


Эта статья предназначена для тех, кто желает приобрести начальные знания, позволяющие самостоятельно создавать драйверы сетевых устройств, работающих в среде ОС Linux. Не так давно у меня самого возникла такая задача, и я обнаружил явный недостаток информации по этой теме. В доступной литературе на русском языке подробно описаны все драйвера, кроме сетевых. В англоязычной библии на все времена «Linux Device Drivers» (http://www.oreilly.com/catalog/linuxdrive3) есть всё, но понимания этого «всего» можно достичь, пройдя не самый простой путь. Отчаянные метания по форумам привели меня к неутешительному выводу, что интересующихся этим вопросом несколько больше, чем что-то понимающих в нём. Итак, в результате многочисленных проб и ошибок, я получил некоторое количество полезных знаний и драйвер, который сейчас успешно использую дома для подключения второго компьютера к Интернету. Простота получившейся программы натолкнула меня на мысль, что путь, который я прошёл, мог бы быть гораздо короче и куда менее извилист, если бы в начале мне удалось прочитать статью, которую я сейчас вам и предлагаю.

Не смотря на то, что по ходу изложения, я постараюсь сделать необходимые разъяснения и ссылки, для успешного и правильного понимания материала статьи, читателю понадобятся некоторые начальные познания. Прежде всего – знакомство с языком программирования C, кроме того, я полагаю, что читатель представляет, как работают компьютерные сети, уже работал в ОС Linux и знает, что такое «модуль ядра».

Все примеры, приведённые в статье – рабочие, и представляют собой необходимые начальные этапы того самого пути, который нужно пройти, чтобы научиться создавать драйверы различных сетевых устройств. Я использовал дистрибутив Mandrake 10.1 с ядром 2.6.8.1-12mdk. Для других версий ядра, возможно, понадобится внести некоторые незначительные изменения в исходные тексты.

Чтобы иметь возможность компилировать модули ядра, после обычной установки дистрибутива, нужно дополнительно установить исходные тексты вашей версии ядра в каталог usr/src. В моём дистрибутиве это пришлось сделать вручную, потому, что даже при выборе расширенного варианта установки и указании на необходимость включения в неё исходных текстов, в нужном каталоге у меня оказались только дремучие исходники ядра 2.4. Нужные исходные тексты находятся на третьем диске дистрибутива в папке /media/main3/ в виде rpm-пакета: kernel-source-2.6-2.6.8.1-12mdk.i586.rpm. Их можно установить с помощью команды:

rpm -ivh ./ kernel-source-2.6-2.6.8.1-12mdk.i586.rpm.

Установку следует производить в консольном режиме из каталога, содержащего пакет, в режиме суперпользователя.

Перед началом работы советую создать свой рабочий каталог /home/user/<что-нибудь>, в котором будут находиться ваши исход ные тексты и поместить туда Makefile, взятый мной из примера, предлагаемого средой разработки KDevelop, слегка модифицированный, для обеспечения его работоспособности (ох уж эти особенности свободно распространяемых программ!):

TARGET = myname
OBJS = myname.o
MDIR = drivers/misc
EXTRA_CFLAGS = -DEXPORT_SYMTAB
CURRENT = $(shell uname -r)
KDIR = /lib/modules/$(CURRENT)/build
PWD = $(shell pwd)
DEST = /lib/modules/$(CURRENT)/kernel/$(MDIR)
obj-m := $(TARGET).o
default:
make -C $(KDIR) SUBDIRS=$(PWD) modules
$(TARGET).o: $(OBJS)
$(LD) $(LD_RFLAG) -r -o $@ $(OBJS)
ifneq (,$(findstring 2.4.,$(CURRENT)))
install:
su -c «cp -v $(TARGET).o $(DEST) && /sbin/depmod -a»
else
install:
su -c «cp -v $(TARGET).ko $(DEST) && /sbin/depmod -a»
endif
clean:
-rm -f *.o *.ko .*.cmd .*.flags *.mod.c
-include $(KDIR)/Rules.make

Чтобы приспособить этот файл под свои нужды, замените myname на имя вашего модуля. Сборка осуществляется командой make.

Добро пожаловать в мир хакеров Linux!

Для начала, создадим простейший модуль ядра, который будет регистрироваться в системе и сообщать об этом миру доступными ему средствами: «Hello world!». Это необходимый шаг: надо же уважать традиции! Кроме этого, он позволяет удостовериться в работоспособности инструментов (компилятора, компоновщика, ...), корректности make-файлов и правильности установки исходных текстов Linux. Кстати, корифеи от ядра настоятельно рекомендуют разрабатывать и отлаживать модули в текстовой консоли. Вызов printk (речь о котором пойдет ниже) не работает с графическими терминалами типа xterm, поэтому единственным способом понять, что происходит, остается просмотр файлов журнала, а это не всегда удобно. В общем, вооружайтесь любимым текстовым редактором (от mcedit до joe) и – в путь!

 /************************************************************
 * Igor Timoshenko (timigor@yandex.ru)                      *
 * Simple module                                            *
 ************************************************************/
 #include <linux/module.h>
 #include <linux/kernel.h>
 #include <linux/init.h>
 /*============ИНИЦИАЛИЗАЦИЯ МОДУЛЯ=========*/
 int ssl_init_module (void)
 {
 printk(KERN_WARNING «Hello World!.\n»);
 return 0;
 }
 /*============ВЫГРУЗКА МОДУЛЯ===============*/
 void ssl_cleanup (void)
 {
 printk (KERN_WARNING «Goodbye world.\n»);
 return;
 }
 module_init (ssl_init_module);
 module_exit (ssl_cleanup);

Сохраните этот модуль в каком-нибудь файле (например, helloworld.c), внесите соответствующие изменения в Makefile (см. выше), а затем дайте команду make. На экране должен появиться текст:

make -C /lib/modules/2.6.8.1-12mdk/build SUBDIRS=/home/STATYA
modules
make[1]: Entering directory '/usr/src/linux-2.6.8.1-12mdk'
 CC [M] /home/STATYA/helloworld.o
 Building modules, stage 2.
 MODPOST
 CC /home/STATYA/helloworld.mod.o
 LD [M] /home/STATYA/helloworld.ko
make[1]: Leaving directory '/usr/src/linux-2.6.8.1-12mdk'

После успешной компиляции в вашем каталоге образуется множество файлов, из которых для нас интересен собственно объектный модуль helloworld.ko. Для загрузки получившегося модуля в ядро нужно использовать команду:

insmod ./helloworld.ko

При этом наш модуль будет занесён в список загруженных модулей в файле /proc/modules (в этом можно убедиться, используя команду cat /proc/modules | grep helloworld). Кроме того, будет запущена инициализирующая функция ssl_init_module(void), вызывающая системную функцию printk(), которая предназначена для регистрации событий и предупреждений из режима ядра. Мы не будем сейчас подробно рассматривать работу этой функции, отметим лишь, что переданное ей в качестве аргумента сообщение «Hello World!» после загрузки модуля следует искать в файле /var/log/messages, куда (при соответствующей настройке демона syslog) отправляется большинство сообщений, приходящих из ядра. Для удобства наблюдения за появляющимися новыми сообщениями в этом файле можно воспользоваться командой

tail -f /var/log/messages

Если вы проигнорировали советы бывалых и все-таки разрабатываете модули ядра в графической среде, её удобно запускать в отдельном окне терминала, создав своеобразную «консоль сообщений».

Выгрузить модуль из ядра можно при помощи команды:

rmmod helloworld

При этом в запущенной консоли сообщений можно будет наблюдать, как модуль попрощается с миром все теми же «доступными ему средствами».

Матрица: подключение

Следующим шагом на нашем пути будет создание простейшего модуля сетевого драйвера. Для этого в нашем модуле нужно определить структуру данных сетевого драйвера struct net_device, определение которой находится в заголовочном файле /usr/include/linux/netdevice.h. В структуре представлено множество функций и полей, из которых для нас наиболее важными являются следующие:
char name[IFNAMSIZ] – содержит имя интерфейса, которое будет отображаться при конфигурировании сетевой подсистемы.
int (*open) (struct net_device *dev) – функция, которая запускается всякий раз, когда утилита ifconfig активирует интерфейс.
int (*stop) (struct net_device *dev) – функция, которая запускается при остановке интерфейса.
int (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev) – функция, которая активизируется сетевой подсистемой всякий раз, когда нужно передать пакет данных.
struct net_device_stats* (*get_stats)(struct net_device *dev) – функция, которая вызывается всякий раз, когда какое либо приложение пытается получить статистические данные о работе интерфейса.
void *priv – дополнительный указатель, который можно использовать произвольным образом.

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

Нам нужно заполнить необходимые для работы драйвера поля. Если поле содержит указатель на функцию – необходимо реализовать эту функцию. В нашем драйвере мы определим функции инициализации (open), остановки (stop) и передачи данных (hard_start_xmit), кроме того определим структуру net_device_stats, содержимое которой мы будем получать через дополнительный указатель (priv).

Для того, чтобы система узнала, что загружаемый модуль является драйвером сетевого устройства, при загрузке модуль дожен сообщить ей об этом специальной функцией register_netdev(). О своей выгрузке модуль сообщает функцией unregister_netdev(). В качестве аргумента для этих функций передаётся указатель на структуру данных драйвера.

Для инициализации интерфейса в сетевой подсистеме драйвер должен воспользоваться специальной функцией netif_start_queue(), при остановке – функцией netif_stop_queue(), аргументом которых также является указатель на структуру драйвера.

При необходимости передачи данных через сетевой интерфейс ОС вызывает функцию hard_start_xmit() которой в качестве аргумента передаётся указатель на буфер (sk_buff), содержащий готовую IP-датаграмму. Вызванная функция должна сделать всё необходимое для отправки датаграммы сетевому устройству для передачи на физическом уровне, а затем сообщить системе о том, что пакет передан, вызвав функцию dev_kfree_skb(), аргументом которой служит указатель на буфер, полученный от ОС. На этом цикл передачи для драйвера завершается, система может заполнять освобождённый буфер новыми данными, а драйвер ожидает следующей передачи и указателя на готовый буфер.

Поскольку наш драйвер пока не связан с каким либо устройством, функция передачи будет переносить передаваемые данные в свой собственный буфер (my_buf), а затем регистрировать эти данные в виде сообщений при помощи функции printk(). Для представления регистрируемых данных в текстовом формате мы введём в программу вспомогательные функции tpdumpk() и printAddr().

Приведём текст нашего, пусть простейшего, но уже сетевого драйвера:

 /************************************************************
  * Igor Timoshenko (timigor@yandex.ru)                    *
  * Simple net driver                               *
  ************************************************************/
 #include <linux/module.h>
 #include <linux/kernel.h>
 #include <linux/init.h>
 #include <linux/netdevice.h>
 #include <linux/skbuff.h>
 /*Структура заголовка IP пакета*/
 struct iphdr {
   unsigned int ihl:4;
   unsigned int version:4;
   u_int8_t tos;
   u_int16_t tot_len;
   u_int16_t id;
   u_int16_t frag_off;
   u_int8_t ttl;
   u_int8_t protocol;
   u_int16_t check;
   unsigned char saddr[4]; /* IP address of originator */
   unsigned char daddr[4]; /* IP address of destination */
   /*The options start here. */
  };
 /*Структура буфера данных */
 struct my_buf {
 int tlen;
               union tbuffer{
                            unsigned char tbuff[1024];
                            struct iphdr thdr;
              } tb;
 };
 struct net_device_stats *stats;
 struct my_buf xbf;
 int retval, shift, i;
 int ssl_stop (struct net_device *dev);
 int ssl_open (struct net_device *dev);
 static int ssl_xmit(struct sk_buff *skb, struct net_device *dev);
 void tpdumpk(struct my_buf *txbf);
 /*Структура данных драйвера*/
 struct net_device ssl_dev = {
              .name                                     = «ssl»,
              .open                                     = ssl_open,
              .stop                                     = ssl_stop,
              .hard_start_xmit            = ssl_xmit,
 };
 static struct net_device_stats *ssl_get_stats(struct net_device *dev)
 { return dev->priv; }
 /*============ПЕРЕДАЧА ПАКЕТА============*/
 static int ssl_xmit (struct sk_buff *skb, struct net_device *dev)
 {
              struct my_buf *qbf;
              qbf = &xbf;
              if (skb->len > 1024 – 3) {
                            printk(KERN_WARNING «ssl: T_buffer is small, dropping packet.\n»);
                            stats->tx_dropped++;
              } else {
              xbf.tlen = skb->len;
              shift = 0;
                            while (shift + 1 <= skb->len) {
                            xbf.tb.tbuff[shift] = skb->data[shift];
                            shift++;
                            }
              qbf->tlen = skb->len;
              stats->tx_bytes += skb->len;
              stats->tx_packets++;
              }
              tpdumpk(&xbf); //Дамп переданного пакета из буфера
              dev_kfree_skb(skb);
              stats->tx_bytes += skb->len;
              stats->tx_packets++;
              return 0;
 }
 /*============ИНИЦИАЛИЗАЦИЯ ИНТЕРФЕЙСА ============*/
 int ssl_open (struct net_device *dev)
 {
              printk(KERN_WARNING «ssl: ssl_open called.\n»);
              netif_start_queue (dev);
              return 0;
 }
 /*============ОСТАНОВКА ИНТЕРФЕЙСА ============*/
 int ssl_stop (struct net_device *dev)
 {
              printk (KERN_WARNING «ssl: ssl_stop called.\n»);
              netif_stop_queue(dev);
              return 0;
 }
 /*=============ИНИЦИАЛИЗАЦИЯ МОДУЛЯ============*/
 int ssl_init_module (void)
 {
             stats = kmalloc(sizeof(struct net_device_stats), GFP_ KERNEL);
             if (stats) {
             memset(stats, 0, sizeof(struct net_device_stats));
                             ssl_dev.priv = stats;
                             ssl_dev.get_stats = &ssl_get_stats;
             }
             if ((retval = register_netdev (&ssl_dev))) {
             printk (KERN_WARNING «ssl: Error %d while initializing module.\n»,retval);
                             return retval;
             }
             printk(KERN_WARNING «ssl: initializing the module.\n»);
             return 0;
 }
 /*============ВЫГРУЗКА МОДУЛЯ==============*/
 void ssl_cleanup (void)
 {
             unregister_netdev (&ssl_dev);
             printk (KERN_WARNING «ssl: Cleaning Up the Module.\n»);
             return;
 }
 module_init (ssl_init_module);
 module_exit (ssl_cleanup);
 /*============ ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ============*/
 // Печать IP-адреса в формате «разделенный точками».
 void printAddr(unsigned char *addr)
 {
 int j;
             for ( j = 0; j < 4; j++ )
             {
             printk(«%, addr[j]);
             if ( j < 3 )
                             printk(«.»);
             }
 }
 // Дамп буфера передатчика
 void tpdumpk(struct my_buf *txbf)
{
 int i;
             printk(«\n---TRANSMITING---»);
             for ( i = 0; i < txbf->tlen; i++ )
             {
                             if ( !(i & 15) ) printk(«\n%04X: «, i);
                             printk(«%02X «, ((unsigned char*)txbf->tb.
 tbuff)[i]);
             }
             printk(«\n»);
             printk(«IPv%d: hdr-size=%d pkt-size=%d protocol=%d TTL=%,
                     txbf->tb.thdr.version,         txbf->tb.thdr.ihl*4,
                     ntohs(txbf->tb.thdr.tot_len),
                             txbf->tb.thdr.protocol, txbf->tb.thdr.ttl);
             printk(«\n»);
             printk(«rst= «);
             printAddr(txbf->tb.thdr.saddr);
             printk(« dst= «);
             printAddr(txbf->tb.thdr.daddr);
             printk(«\n»);
 }

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

#!/bin/sh
insmod ./smpldriver.ko
sleep 1
ifconfig ssl 192.168.0.1 up
ifconfig ssl netmask 255.255.255.0
ifconfig

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

ssl Link encap:AMPR NET/ROM HWaddr
    inet addr:192.168.0.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:0 errors:0 dropped:0 overruns:0 carrier:0
    collisions:0 txqueuelen:0
    RX bytes:0 (0.0 b) TX bytes:0 (0.0 b)

Как видно, наш интерфейс запущен, имеет собственный IP-адрес и готов к работе.

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

Для проверки работоспособности нашего интерфейса нужно запустить в отдельной консоли команду tail -f /var/log/messages для отображения текущих сообщений из ядра, а в вашей рабочей консоли воспользоваться командой:

ping 192.168.0.2.

Таким образом, мы пытаемся отправлять ICMP-пакеты на несуществующий интерфейс, который доступен через наш драйвер. Поскольку такого интерфейса, как и функций приема данных, у нашего драйвера пока нет, для ОС все отправленные пакеты будут потеряны. Но не для нас! Все передаваемые пакеты будут документироваться в файле /var/log/messages, и это можно наблюдать в консоли сообщений примерно в таком виде:

Mar 19 09:28:21 eng-fxih kernel: ---TRANSMITTING---
Mar 19 09:28:21 eng-fxih kernel: 0000: 45 00 00 54 00 02 00 00 40 01 F9 53 C0 A8 00 01
Mar 19 09:28:21 eng-fxih kernel: 0010: C0 A8 00 02 08 00 5A 64 3A 45 00 03 05 FA 1C 44
Mar 19 09:28:21 eng-fxih kernel: 0020: 4E 12 08 00 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13
Mar 19 09:28:21 eng-fxih kernel: 0030: 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23
Mar 19 09:28:21 eng-fxih kernel: 0040: 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33
Mar 19 09:28:21 eng-fxih kernel: 0050: 34 35 36 37
Mar 19 09:28:21 eng-fxih kernel: IPv4: hdr-size=20 pkt-size=84 protocol=1  TTL=64
Mar 19 09:28:21 eng-fxih kernel: rst= 192.168.0.1 dst= 192.168.0.2 

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

Наша следующая задача – научить драйвер принимать данные и отправлять их в сетевую подсистему ОС Linux. Для этого мы введём в него необходимые функции, а затем свяжем с аппаратным устройством, управляющим передачей данных по кабелю. В качестве такого устройства мы используем СОМ-порт компьютера. Таким образом мы получим программу, похожую на известный драйвер SLIP. Конечно, наш драйвер не будет поддерживать всех функций своего известного прототипа, но зато он будет намного проще и не будет требовать для работы дополнительных программ-демонов. Однако, все это будет уже в следующей части. Не пропустите!


...Полезные ссылки

http://linuxcenter.ru/lib/books/lkmpg.phtml – перевод «The Linux Kernel Module Programming Guide» на русский язык

Персональные инструменты
купить
подписаться
Яндекс.Метрика