LXF79:Драйвер своими руками

Материал из Linuxformat.

Версия от 13:25, 27 апреля 2008; Lockal (Обсуждение | вклад)
(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск

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

ЧАСТЬ 2 Зарегистрировать пакет данных – это даже не половина, а четверть дела. Игорь Тимошенко продолжает мастер-класс по написанию сетевых драйверов Linux

В прошлый раз мы написали простейший драйвер, который определялся в системе как сетевой, инициализировался как интерфейс и, пользуясь доверием ОС, принимал направленные на него пакеты. На этом его лояльность сетевой подсистеме Linux заканчивалась, и он не отдавал их обратно в сеть, а сохранял в файле /var/log/messages, где мы и изучали их, используя текстовый редактор и утилиту tail. Такое поведение не даёт нам оснований считать программу законченной, и сейчас мы продолжим работу над ней, чтобы довести её до уровня «взрослых» сетевых драйверов.

Для выполнения поставленной задачи мы должны, прежде всего, дать нашему драйверу возможность переправлять принятые пакеты в буфер сетевой подсистемы. Рассмотрим, как это обычно происходит в Linux.

Все передаваемые и принимаемые пакеты в сетевой подсистеме Linux хранятся и обрабатываются в специальных типовых буферах, называемых структурами буфера сокетов (struct sk_buff). Имеющиеся в памяти буферы организованы в очередь, которая управляется ОС. В случае, если нужно разместить поступивший в сетевую подсистему пакет, а существующие буфера заполнены данными, система выделяет в памяти место под новый буфер. Когда пакет обработан и надобности в нём больше нет, буфер не уничтожается, а помечается как свободный и может быть использован для размещения вновь поступившего пакета. Таким образом, обычно в системе существует некоторое количество свободных буферов, которые могут быть использованы для обмена пакетами с сетевыми интерфейсами.

После получения пакета драйвер должен запросить у ОС указатель на свободный буфер для его размещения. Делается это с помощью функции dev_alloc_skb(), которой в качестве параметра передаётся длина полученного пакета. Функция возвращает указатель на свободный буфер, драйвер должен заполнить его данными. Для копирования данных в буфер удобно использовать функцию memcpy() совместно с skb_put(), которая, при необходимости, расширяет буфер до нужной длины и согласует это с системой. Для того, чтобы ОС могла правильно интерпретировать полученные данные в структуре буфера, нужно определить поле unsigned short protocol, содержащее код MAC-уровня, указывающий на тип инкапсулированного в пакет сетевого протокола. Обычно в сетях Интернет используется IPv4, а соответствующий ему код содержится в макроопределении ETH_P_IP в заголовочном файле linux/if_ether.h, на который ссылается linux/netdevice.h. После того, как структура заполнена, об этом нужно сообщить системе при помощи функции netif_rx(), в качестве аргумента которой передаётся указатель на заполненный буфер. Изложенную последовательность действий удобно объединить в отдельную функцию (мы назовем ее load_pack()), которой передается указатель на буфер с принятым пакетом:

/* Структура буфера данных */
 struct my_buf {
 int tlen;
 unsigned char tail[2];
               union tbuffer{
                             unsigned char tbuff[1024];
                             struct iphdr thdr;
               } tb;
 };
 static void load_pack(struct my_buf *xbf) {
               struct net_device_stats *stats = ssl_dev.priv;
               struct sk_buff *r_skb;
               r_skb = dev_alloc_skb(xbf->tlen);
               if (r_skb == NULL) {
                             printk(KERN_WARNING «ssl: memory squeeze, dropping packet.\n»);
                             stats->rx_dropped++;
                             return;
               }
               r_skb->dev = &ssl_dev;
               memcpy(skb_put(r_skb, xbf->tlen), xbf->tb.tbuff, xbf->tlen);
               r_skb->protocol=htons(ETH_P_IP);
                             netif_rx(r_skb);
               stats->rx_bytes += xbf->tlen;
               stats->rx_packets++;
 }

Следующий шаг – привязка драйвера к аппаратному устройству, управляющему передачей данных на физическом уровне – по проводам в виде электрических сигналов. Обычно этим занимаются телефонные модемы или сетевые карты. В современных компьютерных сетях данные могут передаваться также и по оптическим кабелям, и даже вообще без кабелей – по радиоканалу, но это – темы для отдельного разговора. Мы не будем сейчас разбираться в существующих стандартах передачи данных, а выберем в качестве аппаратного устройства асинхронный адаптер последовательного порта (см. врезку), который представлен в системе файлами устройств /dev/ttyS(0....n) и попытаемся связать два компьютера в сеть по кабелю.

Для дальнейшей работы над драйвером нам потребуется нульмодемный кабель, который можно недорого приобрести (как ни странно) почти в каждом компьютерном магазине. При помощи кабеля нужно соединить порты COM1 и COM2 вашего компьютера (или двух компьютеров). Все действия для разных портов нужно проводить в разных текстовых консолях. Если вы проводите наши эксперименты в графической среде, то сможете разместить консоли на одном экране.

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

stty -F /dev/ttyS0 115200

Порту COM2 будет соответствовать файл устройства /dev/ttyS1.

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

outb_p(0, BASE + ICR_N);                  /* запрет прерываний */
 outb_p(0, BASE + IIDR_N);                 /* запрет буферизации */
 reg_mcr.byte = 0;                         /* Сброс дополнительных сигнальных линий */

где BASE, ICR_N, IIDR_N – определенные нами в исходном коде драйвера директивы препроцессора, упрощающие работу с последовательным портом.

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

static int ssl_xmit (struct sk_buff *skb, struct net_device *dev) {
               if (skb->len > 10243) {
                             printk(KERN_WARNING «ssl: T_buffer is small, dropping packet.\n»);
                             stats->tx_dropped++;
               } else {
                             /* Передача пакета в СОМ-порт */
                             shift = 0;
                             while (shift + 1 <= skb->len) {
                                           while ((inb_p(BASE + LSR_N) & 0x20)==0);
                                                        outb_p(skb->data[shift], BASE);
                                                        shift++;
                                           }
                             dev_kfree_skb(skb);
                             stats->tx_bytes += skb->len;
                             stats->tx_packets++;
               }
 return 0;
 }
Асинхронный адаптер

Асинхронный адаптер в компьютере существует в виде микросхемы (UART 16550A), управляющей передачей данных по кабелю, обычно – в стандарте RS-232. Кабель подключается к разъёмам портов COM1 и COM2, которые находятся на задней стороне системного блока. В каждом из направлений, данные передаются по сигнальному проводу (TX->RX) за счёт изменения электрического напряжения на выходе передатчика относительно уровня «земли». Для управления передачей в кабеле имеется несколько дополнительных линий, из которых для нас интересны RTS->CTS (используются для аппаратного управления потоком данных) и DTR->DSR (сигнал подтверждения связи адаптера с каким либо внешним устройством, например – с модемом). Поскольку передача данных обычно бывает двухсторонней, в кабеле предусмотрен полный набор сигнальных линий для каждого из направлений.

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

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

Процедура программирования асинхронного адаптера подробно описана в статье, которую можно найти по адресу: control-m.maket.net/rs-232/ch2.html.

Чтобы проверить работоспособность получившегося драйвера, нужно соединить кабелем передающий COM-порт вашего компьютера с принимающим СОМ-портом второго компьютера. Для программирования порта на принимающей стороне можно использовать следующий сценарий:

#!/bin/sh
 #Подставить нужный номер
 PORT=/dev/ttyS1
 stty -F $PORT cs8 -isig -icanon -iexten -opost -ixon -icrnl -hupcl -echo
 crtscts -parenb 115200
 stty -a < $PORT

После чего можно отображать на консоль принимаемые данные с помощью команды

cat /dev/ttyS1

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

     !»#$%&'()*+,-./01234567ET
    @ЫKю юЦхs0
     C /D=
     !»#$%&'()*+,-./01234567ET
     @ЫJю ю! s0
     D /DЧ%

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

Системный таймер

Системный таймер – это устройство, генерирующее аппаратное прерывание IRQ 0 с заданной частотой – HZ раз в секунду. Значение HZ зависит от назначения системы (рабочая станция, сервер...), архитектуры (i386, PPC, ...), указывается при конфигурации ядра и затем сохраняется в /usr/include/asm/param.h. Обычно оно равно 1000. Каждый раз, когда ядро вызывает обработчик прерывания IRQ 0, он увеличивает значение глобальной переменной jiffies, которая, таким образом, является точкой отсчета при указании временных интервалов в ядре. Помимо этого, прерывание системного таймера используется Linux для реализации механизма таймеров ядра (kernel timers). Таймеры ядра обеспечивают выполнение некоторых функций (называемых обработчиками таймера – timer handler) по истечении определенного промежутка времени и описываются структурой timer_list. Для управления таймерами ядра служат специальные системные функции. Не путайте функцию-обработчик таймера ядра с функцией-обработчиком прерывания системного таймера – это совершенно разные вещи!

Следующий шаг в развитии драйвера – прием данных из кабеля через адаптер последовательного порта и передача их в сетевую подсистему Linux. Для того, чтобы принять байт данных из адаптера, нужно просматривать содержимое регистра состояния линии, а именно – состояние его младшего бита. Если он установлен в 1, это означает, что очередной байт принят из линии и находится в буферном регистре приёмника. Буферный регистр доступен через регистр данных адаптера. Чтение принятого байта освобождает буфер для приёма следующего. Следовательно, принимающая функция драйвера должна проверять младший бит регистра состояния и всякий раз, когда он установится в 1, считывать принятый байт. Для решения этой задачи нам следовало бы воспользоваться механизмом аппаратных прерываний. Но, поскольку мы сразу договорились, что для упрощения программы не будем использовать прерывания асинхронного адаптера, то воспользуемся (весьма опосредованно) прерыванием системного таймера, а точнее – механизмом таймеров ядра (см. врезку «Системный таймер»).

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

#include <linux/timer.h>
 #define T_TICK HZ/100
 struct timer_list timer;
 spinlock_t timer_lock = SPIN_LOCK_UNLOCKED;
 /* ОБРАБОТЧИК ТАЙМЕРА */
 void ssl_tick (unsigned long data) {
              spin_lock(&timer_lock);
              ...
              mod_timer(&timer, jiffies + T_TICK);
              spin_unlock(&timer_lock);
 }

Переменная timer и есть наш таймер ядра, а вот timer_lock, наверное, требует некоторого пояснения. Мы собираемся организовать прием данных, передаваемых асинхронно, и поэтому нам важно исключить одновременный (конкурентный) доступ к буферу my_buf и другим внутренним переменным. Эту синхронизацию и обеспечивает timer_lock. Мы захватываем блокировку перед входом в функцию-обработчик (вызов spin_lock) и освобождаем при выходе из нее (вызов spin_unlock). Если код, выполняющийся на другом процессоре в SMP-системе, попробует захватить уже взятую нами блокировку, он не сможет двинуться дальше до тех пор, пока мы не освободим ее, вызвав spin_unlock. В конце обработчика вызывается функция mod_timer(), которая обеспечивает повторное выполнение данного кода через T_TICK прерываний системного таймера. Это необходимое действие – таймеры ядра обеспечивают лишь однократное срабатывание.

Для начальной инициализации работы таймера нужно также модифицировать функцию ssl_open():

int ssl_open (struct net_device *dev) {
              printk(KERN_WARNING «ssl: ssl_open called.\n»);
              netif_start_queue (dev);
              /* инициализация COM1 */
              outb_p(0, BASE + IIDR_N);            /* запрет буферизации */
              outb_p(0, BASE + ICR_N);             /* запрет прерываний */
              reg_mcr.byte = 0;                    /* Сброс дополнительных сигнальных линий */
              /* инициализация таймера */
              init_timer(&timer);
              timer.expires = jiffies + HZ;
              timer.function = ssl_tick;
              add_timer(&timer);
              return 0;
 }

Для корректного завершения работы таймера после остановки интерфейса нужно также немного модифицировать функцию int ssl_stop ():

int ssl_stop (struct net_device *dev) {
              del_timer_sync(&timer); /* остановка таймера */
              printk (KERN_WARNING «ssl: ssl_stop called.\n»);
              netif_stop_queue(dev);
              return 0;
 }

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

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

Мы задействуем для этих целей две пары линий: DTR->DSR и RTS->CTS. Первая пара отводится передатчику, вторая – приемнику. Теперь мы готовы к тому, чтобы привести полный текст обработчика таймера.

/* ОБРАБОТЧИК ТАЙМЕРА */
 void ssl_tick (unsigned long data) {
              struct net_device_stats *stats = ssl_dev.priv;
              struct my_buf *qbf = &xbf;
              spin_lock(&timer_lock);
              /*ПРЯМОЕ ЧТЕНИЕ ИЗ ПОРТА ttyS0 УПРАВЛЕНИЕ RTS->DSR<-*/
              reg_msr.byte = inb_p(BASE + MSR_N);
              if (reg_msr.bit_reg.dsr != 0) {
 /*проверяем запрос от передатчика */
                           reg_mcr.bit_reg.rts = 1;
                           outb_p(reg_mcr.byte, BASE + MCR_N);
 /*подтверждаем готовность приёма*/
                           shift = 0;
                           while (reg_msr.bit_reg.dsr == 1) { /*принимаем пока передают*/
                                          /*ждём установки бита готовности приёмника*/
                                          while ((inb_p(BASE + LSR_N) & 1)==0);
                                          /*читаем очередной байт*/
                                          qbf->tb.tbuff[++shift-2] = inb_p(BASE);
                                          if (shift > 1024) {
                                                         printk(KERN_WARNING «ssl: R_buffer is small and overfull, dropping packet.\n»);
                                                         stats->rx_dropped++;
                                                         goto r_out;
                                          }
                                          reg_msr.byte = inb_p(BASE + MSR_N);
                           } /* Пакет принят */
                           if (shift == 0) {
                                          printk(KERN_WARNING «ssl: Packet is 0 byte, dropping packet.\n»);
                                          stats->rx_dropped++;
                                          goto r_out;
                           }
                           qbf->tlen = shift-2;
                            //tpdumpk(&xbf);             /*Дамп принятого в буфер пакета в логе*/
                            load_pack(&xbf);      /*Отправка принятого пакета в SKB*/
             } //else printk(KERN_WARNING «ssl: Line DSR is free.\n»);
 r_out:
             reg_mcr.bit_reg.rts = 0;
             outb_p(reg_mcr.byte, BASE + MCR_N); /*снимаем подтверждение приёма*/
             mod_timer(&timer, jiffies + T_TICK);
             spin_unlock(&timer_lock);
 }
Не делайте этого дома, дети!

Приведенный в статье код драйвера является полностью рабочим. Однако необходимо учесть, что он создавался с учебными целями, а потому умышленно прост и демонстрирует очень фамильярное отношение к ресурсам компьютера и системы. Его нельзя рассматривать как образец правильного дизайна драйвера для Linux. Если вы всерьез интересуетесь разработкой собственных драйверов, рекомендуем вам обратиться к специальной литературе, например, «Linux Device Drivers, 3rd Edition» (издательство O´Reilly) и «Linux: сетевая архитектура. Структура и реализация сетевых протоколов в ядре». (издательство «Кудиц-Образ»).

Отмеченные комментариями вызовы функций удобно использовать при изучении работы драйвера и при отладке. Передача пакета происходит по такому же принципу. Перед началом передачи выставляется сигнал DTR, и передатчик ожидает готовности приёмника в течении времени T_TICK. Если сигнал готовности получен, происходит побайтовая передача данных. Каждый следующий байт передаётся только после установки бита готовности передатчика (out_ready) в регистре состояния линии (LSR). По окончании передачи пакета, передатчик снимает сигнал DTR и приёмник заканчивает цикл приёма пакета. Полный текст программы можно найти на прилагаемом диске.

Следует отметить особенность работы адаптера UART 16550A. При последовательной передаче, два байта «зависают» в его буфере, и их необходимо «проталкивать», записывая «пустые» символы в конец пакета. Эта особенность учтена в приведённой программе. «Пустые» символы помещаются в раздел unsigned char tail[2] буфера приёмника (my_buf).

Мы получили «настоящий» драйвер для передачи IP-пакетов через COM-порт. Для его тестирования потребуется второй компьютер. При использовании драйвера помните, что он расчитан на работу с портом COM1 – для использования COM2 и т.п. Нужно изменить значение BASE и пересобрать модуль. Для тестирования драйвера можно использовать сценарий настройки, приведенный в первой части статьи. Выполните его на обоих компьютерах, предварительно изменив на втором узле IP-адрес на 192.168.0.2. После этого можно проверить связь командой ping.

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

Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию