Операционная система ЗОСРВ «Нейтрино» > Руководство разработчика > Основные принципы системной разработки > Разработка драйверов и драйверные библиотеки > Драйверные библиотеки > Библиотеки разработки сетевых драйверов (devnp-*) > Статьи и обзоры > Разработка сетевых драйверов devnp для сетевого менеджера io-pkt-*



Разработка сетевых драйверов devnp для сетевого менеджера io-pkt-*

В статье приведён обзор общего подхода к разработке драйверов сетевой подсистемы

Список подразделов:

Инициализация
Структура сетевого устройства
Точка входа
Реализация device_entry()
Описание dev_attach()
Реализация device_parse_options()
Присоединение устройства device_attach()
Заполнение структур
Маппирование и PCI-attach
Очередь событий
BSD MII media
Подключение и настройка PHY
Завершающий этап device_attach()
Включение устройства device_init()
Включение MAC уровня и RX/TX FIFO
Инициализация колец дескрипторов
Включение callout'ов
Инициализация обработчиков прерываний
Установка флагов, указывающих, что интерфейс готов к приёму/передаче.
IOCTL вызовы device_ioctl()
Реализация device_ioctl()
Отправка фрейма device_start()
Проверки для отправки фрейма
Реализация отправки
Реализация паддинга фрейма
Реализация дефрагментации цепочки mbuf'ов
Примечания по разработке и тестирования отправки фреймов
Получение фрейма. Обработчик прерывания RX.
Реализация обработчика прерывания.
Отключение устройства device_stop()
Реализация остановки устройства
Отсоединение устройства от стека device_detach()
Реализация отсоединения устройства

Инициализация

В данном пункте мы рассмотрим начало работы драйвера, после монтирования его к стеку io-pkt-*. При чтении данного DDK желательно смотреть на исходные коды других драйверов.

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

Для начала необходимо описать базовую структуру сетевого устройства.

typedef struct {
struct device sc_dev; /* common device */
struct ethercom ec; /* common ethernet */
struct _iopkt_self *iopkt;
struct cache_ctrl cachectl; /* для управления кэшем */
struct mii_data bsd_mii; /* для работы с шиной MII */
void *pci_dev_hdl; /* если устройство на шине PCI*/
nic_config_t cfg; /* структура с конфигурацией устройства */
nic_stats_t stats; /* структура со статистикой */
mdi_t *mdi; /* для работы с MDI */
void *sd_hook; /* аварийный callout-вызов */
/* PCI and Irq */
int iid; /* для передачи interrupt id */
int irq[ALL_IRQS_AMOUNT]; /* массив для id прерываний, если их несколько */
struct pci_dev_info pci_info; /* структура PCI устройства */
struct _iopkt_inter inter_misc; /* структура потокового обработчика событий */
struct _iopkt_inter inter_rx;
struct _iopkt_inter inter_tx;
/* Memory */
volatile uint8_t *hw_mem; /* указатель на память устройства */
int32_t bmtrans; /* смещение адресов если это PCI устройство */
/* Rings */
/* Здесь описываются кольца дескрипторов, уже зависит от вашей реализации */
/* Other */
struct callout mii_callout; /* для периодического вызова монитора MII шины */
struct callout reaper_callout; /* для периодического вызова очистки кольца дескрипторов */
uint16_t mii_timeout;
uint16_t reaper_timeout;
} device_dev_t

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

Точка входа

Код инициализации является самой важной частью драйвера. Он разбит на две части: Но перед этим, необходимо зарегистрировать точку входа device_entry() в драйвере. Это делается при помощи макроса IOPKT_DRVR_ENTRY_SYM_INIT(). Также необходимо проинициализировать структуру cfattach С помощью вызова макроса CFATTACH_DECL(). В своём коде слово "device..." меняется на название вашего сетевого адаптера, например eth2500_entry.

struct _iopkt_drvr_entry IOPKT_DRVR_ENTRY_SYM(device) = IOPKT_DRVR_ENTRY_SYM_INIT(device_entry);
CFATTACH_DECL(device,
sizeof(device_dev_t),
NULL,
device_attach,
device_detach,
NULL);

На данном этапе мы зарегистрировали точку входа в наш драйвер device_entry().
Теперь необходимо определить и реализовать данную функцию.

Реализация device_entry()

Прототип определён в struct _iopkt_drvr_entry :: drvr_init().

В общем случае в ней реализуется, как правило, парсинг строки параметров, передаваемых в драйвер и заполнение структуры nic_config_t.

Пример реализации device_entry() для PCI ethernet адаптера:

struct i_attach_args {
struct _iopkt_self *iopkt;
char *options;
unsigned busvendor, busdevice;
int busindex;
uint8_t bus, devfn;
uint32_t my_parsed1_param;
uint32_t my_parsed2_param;
uint8_t fill[2];
};
static unsigned known_device_ids[] = {
PCI_DEVICE_ID_VENDOR_DEVICE0, /* 0x0001 */
PCI_DEVICE_ID_VENDOR_DEVICE1, /* 0x0002 */
PCI_DEVICE_ID_VENDOR_DEVICE2, /* 0x0003 */
0
};
int device_entry (void *dll_hdl, struct _iopkt_self *iopkt, char *options){
nic_config_t *cfg;
int devid, idx;
unsigned bus, dev_func;
int err, single;
int instance;
struct device *dev;
struct i_attach_args iargs;
cfg = calloc(1, sizeof(*cfg));
if (cfg == NULL) {
return ENOMEM;
}
if (options != NULL) {
cfg->vendor_id = 0xffffffff;
cfg->device_id = 0xffffffff;
cfg->device_index = -1;
if ((err = device_parse_options (NULL, options, cfg)) != EOK) {
(free)(cfg);
return (err);
}
if (cfg->device_id != 0xffffffff) {
known_device_ids[0] = cfg->device_id;
known_device_ids[1] = 0;
}
if (cfg->vendor_id == 0xffffffff) {
cfg->vendor_id = PCI_VENDOR_ID_VENDOR;
}
else {
cfg->vendor_id = PCI_VENDOR_ID_VENDOR;
cfg->device_index = -1;
}
memset (&iargs, 0x00, sizeof(iargs));
iargs.iopkt = iopkt;
iargs.options = options;
instance = single = 0;
for (devid = 0; known_device_ids[devid] != 0; devid++) {
idx = ((cfg->device_index == -1) ? 0 : cfg->device_index);
while (1) {
if (pci_find_device (known_device_ids[devid],
cfg->vendor_id, idx, &bus, &dev_func) != PCI_SUCCESS) {
break;
}
iargs.busvendor = cfg->vendor_id;
iargs.busdevice = known_device_ids[devid];
iargs.busindex = idx;
iargs.bus = (uint8_t) bus;
iargs.devfn = (uint8_t) dev_func;
dev = NULL; /* No Parent */
if (dev_attach ("wm", options, &device_pci_ca, &iargs, &single,
&dev, NULL) != EOK) {
goto done;
}
dev->dv_dll_hdl = dll_hdl;
instance++;
if (cfg->device_index != -1)
/* Only looking at a specific index */
break;
idx++;
}
}
done:
(free)(cfg);
if (instance)
return (EOK);
return (ENODEV);
}

Данный код является довольно шаблонным для всех PCI ethernet устройств.
В нём осуществляется парсинг параметров переданных в драйвер с помощью функции device_parse_options(), реализацию которой объясним ниже.
Также заполнение структуры i_attach_args. В ней могут быть заполнены определённые нами параметры для текущего устройства и параметры PCI. Далее данная структура передаётся в device_attach() функцию.
В цикле происходит поиск устройства на PCI шине с помощью функции pci_find_device() по VID:DID. DID берётся из определённого нами массива known_device_ids[]. В случае если драйвер универсален для разных устройств одного вендора.
Если устройство найдено, вызывается dev_attach(), который в свою очередь передаст исполнение в device_attach функцию.

Описание dev_attach()

Прототип dev_attach():

int dev_attach( char *drvr,
char *options,
struct cfattach *ca,
void *cfat_arg,
int *single,
struct device **devnp,
int (*print)( void *, const char * ) );

Параметры:

drvr
Строка, используемая в качестве префикса имени интерфейса. В нашем примере прeфикс равен "wm", интерфейсы будут получать имена "wmX" (X - порядок вызова dev_attach() для конкретного устройства).
options
Строка параметров, переданая драйверу. Она анализируется dev_attach() в поисках параметров name, lan и unit, которые переопределяют имя интерфейса по умолчанию. Параметры lan и unit идентичны по смыслу - они переопределяют число, добавляемое к имени интерфейса. Опция name переопределяет аргумент drvr.
ca
Указатель на структуру cfattach, которая определяет размер структуры устройства, а также detach/attach-функции драйвера. Для инициализации экземпляра этой структуры используется макрос CFATTACH_DECL().
cfat_arg
Аргумент, который передается attach-функции драйвера в качестве третьего аргумента.
single
Если параметр lan или unit находится в строке параметров, то целое число, на которое указывает single, устанавливается равным 1.
devnp
Указатель на структуру struct device. Связано с родительским устройством. Функция dev_attach() через devnp передаёт указатель на структуру, созданную функцией для нового устройства. Этот указатель также передается в качестве второго аргумента attach-функции.
print
NULL или указатель на функцию отладки.

Реализация device_parse_options()

Отдельно рассмотрим реализацию функции device_parse_options(). Она тоже является шаблонной, вызывается несколько раз:

Пример реализации device_parse_options():

static char *device_opts[] = {
"num_receive", // 0
NULL // END
};
int device_parse_options (device_dev_t *device, const char *optstring, nic_config_t *cfg){
char *value, *options, *freeptr, *c;
int opt, invalid, rc = EOK;
int tmp;
if (optstring == NULL)
return 0;
/* getsubopt() is destructive */
options = malloc (strlen (optstring) + 1, M_TEMP, M_NOWAIT);
if (options == NULL)
return ENOMEM;
strcpy (options, optstring);
freeptr = options;
while (options && *options != '\0') {
c = options;
invalid = 0;
if ((opt = getsubopt (&options, device_opts, &value)) != -1) {
if (device == NULL)
continue;
switch (opt) {
case 0:
device->num_receive = strtoul(value, 0, 0);
break;
default:
rc = EINVAL;
invalid = 1;
break;
}
}
else
if (nic_parse_options (cfg, value) != EOK) {
rc = EINVAL;
invalid = 1;
}
if (invalid) {
slogf (_SLOGC_NETWORK, _SLOG_WARNING, "devnp-e1000: unknown option %s", c);
}
}
free (freeptr, M_TEMP);
return rc;
}

В device_opts[] устанавливаются названия параметров, передаваемых в драйвер.
В switch/case заносим код для данного параметра.
После обработки параметров и нахождения устройства вызывается функция dev_attach(), которая впоследствии вызовет функцию device_attach().

Присоединение устройства device_attach()

В функции device_attach() как правило необходимо:

Прототип определён в struct cfattach :: ca_attach().

Заполнение структур

Первое, что необходимо сделать - заполнить nic_config_t значениями по-умолчанию. Также заполнить структуру устройства с уникальными для него параметрами. Затем вызвать функцию device_parse_options(), которая изменит стандартные параметры на переданные в драйвер.
Устанавливаем флаги ifp->if_flags, которые отвечают за отображение режима приёма устройства:

struct ifnet *ifp = NULL;
ifp = &device_dev->ec.ec_if;
ifp->if_softc = device_dev;
ifp->if_flags = IFF_BROADCAST | IFF_SIMPLEX | IFF_MULTICAST;

IFF_UP
Интерфейс поднят
IFF_BROADCAST
Интерфейс принимает broadcast пакеты.
IFF_DEBUG
Включён дебаг.
IFF_LOOPBACK
В режиме loopback.
IFF_POINTOPOINT
В режиме point-to-point.
IFF_NOTRAILERS
Избегает использования прицепов.
IFF_RUNNING
Выделены ресурсы, интерфейс работает.
IFF_NOARP
Не используется ARP протокол.
IFF_PROMISC
Интерфейс принимает все пакеты (неразборчивый режим, promiscuous mode).
IFF_ALLMULTI
Интерфейс принимает все multicast пакеты.
IFF_OACTIVE
Устройство в процессе передачи.
IFF_SIMPLEX
Устройства не принимает собственные передачи.
IFF_MULTICAST
Включена поддержка multicast пакетов.

Далее заполняем поле valid_stats в nic_stats_t и nic_ethernet_stats_t. Исходя из документации на устройство, указываем какая статистика актуальна.

Маппирование и PCI-attach

Пример для PCI устройства:

device_dev->pci_dev_hdl = pci_attach_device( NULL,
PCI_INIT_ALL | PCI_INIT_IRQ | PCI_MASTER_ENABLE,
busindex,
pci_info);
if (device_dev->pci_dev_hdl == NULL) {
err = errno;
return errno;
}
device_dev->hw_mem = mmap_device_memory( NULL,
pci_info->BaseAddressSize[0],
PROT_READ | PROT_WRITE | PROT_NOCACHE, MAP_SHARED,
PCI_MEM_ADDR(pci_info->CpuBaseAddress[0]));
if (device_dev->hw_mem == MAP_FAILED) {
return errno;
}

Затем инициализируем работу с кэшем процессора:

if (cache_init(0, &device_dev->cachectl, NULL) == -1) {
err = errno;
return (err);
}

В дальнейшем это необходимо для управления когерентностью кэша процессора при помощи макросов CACHE_INVAL() и CACHE_FLUSH()

При помощи mmap(), mmap64() выделяем память для колец дескрипторов.


Note: Поскольку mmap(), mmap64() выделяет память страницами по 4КБ, стоит задуматься над тем, чтобы в одно выделение мы поместили и RX и TX кольцо.

device_dev->mem_area = mmap(NULL,
TX_RING_SIZE(device_dev) * sizeof(device_tx_desc_t) +
RX_RING_SIZE(device_dev) * sizeof(device_rx_desc_t) +
NET_CACHELINE_SIZE,
prot_flags,
map_flags,
tmem_fd,
0);

Затем при помощи drvr_mphys() получаем физический адрес памяти нужного кольца и передаём его в сетевое устройство.

Очередь событий

Для обработки событий требуется заполнить структуру struct _iopkt_inter, в которой мы регистрируем 2 callback-функции func(), enable() и поле arg, в которое мы сохраняем указатель на структуру нашего сетевого устройства.
func() - функция обработчик событий в потоке. В ней будет вся тяжеловесная логика обработки события.
enable() - функция включения/размаскирования прерываний. Вызывается сразу после func().

Пример для регистрации события RX в драйвере:

device_dev->inter_rx.func = device_process_interrupt_rx;
device_dev->inter_rx.enable = device_intr_enable_rx;
device_dev->inter_rx.arg = device_dev;
if ((interrupt_entry_init(&device_dev->inter_rx, 0, NULL, device_dev->priority)) != EOK)
return (-1);

Для пополнения очереди событий нужно вызвать interrupt_queue().
Прототип interrupt_queue().

const struct sigevent * interrupt_queue( struct _iopkt_self *,
struct _iopkt_inter * );

BSD MII media

Механизм BSD Media используется для управления сетевыми интерфейсами, а именно скоростью, дуплексным режимом и rx/tx-pause (flow control).
Для упрощения разработки, код bsd_media копируется из драйвера в драйвер. Меняются параметры и указатель на устройство.
Описание полной реализации в будущем будет вынесено в отдельную статью, примеры есть в исходных кодах драйверов.
Ниже описание API:

Подключение и настройка PHY

PHY - это приёмо-передатчик физического интерфейса, реализующий физический уровень.
Связь с MAC уровнем осуществляется по шине MII. Как правило, общение с PHY осуществляется путём записи и чтения регистров MAC уровня (MII_ACCESS, MII_DATA, и т.д.).
Необходимо настроить режим автосогласования PHY и определение соединения (link).
Для этого есть два варианта пути:

Предпочтительнее выбирать API MDI. Поподробнее о нём API MDI

Для работы с API MDI понадобится:

  1. Реализовать функции phy_read(), phy_write(), mdi_callback()
  2. Зарегистрировать их в MDI_Register_Extended()
  3. При необходимости реализовать функцию поиска phy addr, использовать MDI_FindPhy() в цикле. Если заранее известен phy addr, нужно просто передать его в функцию.
  4. Инициализировать PHY с помощью MDI_InitPhy()
  5. Инициировать процесс автосогласования соединения MDI_AutoNegotiate()
  6. Разрешить MDI API проводить мониторинг соединения MDI_EnableMonitor()
  7. Произвести проверку изменения соединения MDI_MonitorPhy()

Реализации phy_read() и phy_write() аппаратно-зависимы, прототипы функций представлены в MDI_Register_Extended().
Пример реализации mdi_callback():

void mdi_callback ( void *hdl,
uint8_t phy_id,
uint8_t link_state )
{
device_dev *dev = (device_dev*)hdl;
int i, mode;
char *s;
struct ifnet *ifp = &dev->ecom.ec_if;
switch (link_state) {
case MDI_LINK_UP:
if ((i = MDI_GetActiveMedia(dev->mdi, dev->cfg.phy_addr, &mode)) != MDI_LINK_UP)
mode = 0;
switch (mode) {
case MDI_10bTFD:
s = "10 BaseT Full Duplex";
dev->cfg.duplex = 1;
dev->cfg.media_rate = 10000L;
break;
case MDI_10bT:
s = "10 BaseT Half Duplex";
dev->cfg.duplex = 0;
dev->cfg.media_rate = 10000L;
break;
case MDI_100bTFD:
s = "100 BaseT Full Duplex";
dev->cfg.duplex = 1;
dev->cfg.media_rate = 100000L;
break;
case MDI_100bT:
s = "100 BaseT Half Duplex";
dev->cfg.duplex = 0;
dev->cfg.media_rate = 100000L;
break;
case MDI_100bT4:
s = "100 BaseT4";
dev->cfg.duplex = 0;
dev->cfg.media_rate = 100000L;
break;
case MDI_1000bT:
s = "1000 BaseT Half Duplex";
dev->cfg.duplex = 0;
dev->cfg.media_rate = 1000000L;
break;
case MDI_1000bTFD:
s = "1000 BaseT Full Duplex";
dev->cfg.duplex = 1;
dev->cfg.media_rate = 1000000L;
break;
default:
s = "Unknown";
dev->cfg.duplex = 0;
dev->cfg.media_rate = 0L;
break;
}
dev->cfg.flags &= ~NIC_FLAG_LINK_DOWN;
if_link_state_change(ifp, LINK_STATE_UP);
break;
case MDI_LINK_DOWN:
dev->cfg.media_rate = dev->cfg.duplex = -1;
MDI_AutoNegotiate (dev->mdi, dev->cfg.phy_addr, NoWait);
dev->cfg.flags |= NIC_FLAG_LINK_DOWN;
if_link_state_change(ifp, LINK_STATE_DOWN);
break;
default:
break;
}
}

Суть данной функции в установке текущего медиастатуса и состояния соединения в интерфейсе. Вызывается она при помощи MDI_MonitorPhy().
При обнаружении MDI_MonitorPhy() изменения в состоянии соединения (link), вызывается mdi_callback().

Есть два варианта реализации мониторинга состояния PHY:

Второй предпочтительнее, но не все контроллеры имеют такую возможность.

Завершающий этап device_attach()

Инициализация callback-функций в структуре сетевого интерфейса "struct ifnet":

ifp->if_ioctl = device_ioctl;
ifp->if_start = device_start;
ifp->if_init = device_init;
ifp->if_stop = device_stop;
IFQ_SET_READY(&ifp->if_snd);
if_attach(ifp);

Установка описания устройства и установка MAC-адреса:

#define DEVICE_DESCRIPTION "Ethernet device description"
strcpy((char *)cfg->device_description, DEVICE_DESCRIPTION);
ether_ifattach(ifp, cfg->current_address);

Функция ether_ifattach() доинициализирует интерфейс как интерфейс медиасреды Ethernet (IEEE 802.3). Существуют другие варианты доинициализации: ieee80211_ifattach(); token_ifattach(); и другие.

Установка callout-вызова, который будет запущен при завершении работы стека:

void device_shutdown(void *arg);
device_dev->sd_hook = shutdownhook_establish(device_shutdown, device_dev);

На этом attach сетевого устройства завершён.

Включение устройства device_init()

Прототип определён в struct ifnet :: if_init().

Данный callback вызывается при ifconfig up интерфейса. Также при смене параметров, например ifconfig mtu 9000.

Задачи данного callback'a:

Включение MAC уровня и RX/TX FIFO

Данный пункт является аппаратно-зависимым. Как правило это запись включения в регистр MAC блока. Тоже самое и с FIFO блоком.

Инициализация колец дескрипторов

Сетевые устройства используют дескрипторы для приёма/передачи фреймов по сети.
Кольцевые RX/TX буферы состоят из данных дескрипторов. Для того, чтобы отправить фрейм в сеть, необходимо заполнить дескриптор и выставить флаг OWN, означающий, что данным дескриптором теперь владеет сетевое устройство (флаг владения дескриптором на некоторых контроллерах может называться по-другому). После передачи дескриптора устройству, начинается процесс отправки буфера (находящегося в дескрипторе) в сеть. Иногда несколько дескрипторов собираются в один фрейм. Это рассматривается ниже в описании процесса отправки. Структуры дескрипторов, как правило, аппаратно-зависимы, их определения чаще всего присутствуют в документации на конкретное сетевое устройство.
В качестве примера ниже приведена структура дескриптора сетевого контроллера ETH2500:

typedef struct {
uint32_t base; /* RBADR [31:0] */
int16_t buf_length; /* BCNT only [13:0] */
int16_t status;
int16_t msg_length; /* MCNT only [13:0] */
uint16_t reserved1;
uint32_t etmr; /* timer count for ieee 1588 */
} __attribute__((packed)) eth2500_rx_desc_t;

В device_init() необходимо инициализировать rx-дескрипторы. Как правило, это подразумевает:

Пример инициализации rx-дескрипторов:

struct mbuf *m;
off64_t phys;
device_tx_desc_t *desc = rx_desc_ring[i];
m = m_getcl_wtp(M_DONTWAIT, MT_DATA, M_PKTHDR, wtp); /* Получаем mbuf от стека */
phys = pool_phys(m->m_data, m->m_ext.ext_page); /* Получаем физический адрес данного mbuf */
CACHE_INVAL(&device_dev->cachectl, m->m_data, phys, m->m_ext.ext_size); /* Инвалидируем кэш процессора */
/* Присваимаем текущему дескриптору физический адрес и размер mbuf */
desc->base = phys;
desc->buf_lenght = m->m_ext.ext_size;
desc->status = RD_OWN; /* Указываем, что данным дескриптором управляет сетевое устрйство */

Всё это делается в цикле для каждого дескриптора.

Включение callout'ов

В сетевом драйвере обычно используются 2 callout'a: mii_callout и reaper_callout.
mii_callout_dev() - Включается для мониторинга состояния соединения (если нет возможности подключить прерывание на изменение состояния соединения)
reaper_callout_dev() - Включается для очистки кольца TX дескрипторов. Для того, чтобы в каждом TX прерывании успешной передачи не очищать mbuf, указатели на них накапливаются в отдельной очереди и затем периодическим вызовом reaper_callout() очищаются накопленные mbuf. Если не хватает дескрипторов, в device_start будет вызываться функция очистки принудительно. Инициализируются они в device_attach(), а включаются в device_init().

Пример инициализации и включения callout'a:

struct callout mii_callout;
int interval = 3 * 1000; /* Задержка, спустя которую будет вызвана переданная функция. 3 секунды */
void mii_callout_dev(void *arg) {
device_dev_t* device_dev = (device_dev_t*)arg;
/* логика проверки состояния соединения */
/* ... */
callout_msec(&mii_callout, device_dev->interval, mii_callout_dev, device_dev); /* Для повторного вызова */
}
callout_init(&mii_callout); /* Инициализация callout'a. В device_attach() */
callout_msec(&mii_callout, interval, mii_callout_dev, device_dev); /* Для запуска callout'a. В device_init() */

Инициализация обработчиков прерываний

Прерывания могут быть трёх типов:

Как правило в SoC и PCI устройствах используются Legacy прерывания. MSI и MSI-X относятся именно к шине PCI, но их активация и поддержка не везде реализована.

В PCI устройствах тип прерываний определяется после pci_attach_device() в структуре struct pci_dev_info в поле msi.
А номер прерывания для регистрации обработчика прерываний в поле Irq.

В SoC устройствах номера прерываний как правило задаются в таблице прерываний устройства в документации. Для некоторых устройств подобная таблица может описывать отдельно SPI (shared peripheral interrupts) поэтому для получения правильного номера прерывания необходимо будет добавить 32.

Обработчик прерываний должен проходить как можно быстрее. Обработка "события-источника прерывания" происходит в потоке. Очередь событий для потока заполняет обработчик прерываний. Обработчик прерываний, как правило, должен: замаскировать прерывания; добавить событие в очередь. Остальные операции в обработчике прерываний зависят от конкретного контроллера.

Заполняем структуры и регистрируем обработчики:

/* Записываем номер прерывания. Данная секция должна находиться в device_attach() */
cfg->num_irqs = ALL_IRQS_AMOUNT;
device_dev->irq[IRQ_RX0] = cfg->irq[0] = pci_info->Irq;
/* Регистрируем обработчик прерывания. Данная секция должна находиться в device_init() */
if ((InterruptAttach_r( device_dev->irq[IRQ_RX0],
device_isr_rx,
device_dev,
sizeof(*device_dev),
_NTO_INTR_FLAGS_TRK_MSK)) < 0)
return -1;
/* Регистрируем потоковый обработчик. Данная секция должна находиться в device_attach()*/
device_dev->inter_rx.func = device_process_interrupt_rx;
device_dev->inter_rx.enable = device_intr_enable_rx;
device_dev->inter_rx.arg = device_dev;
if ((interrupt_entry_init(&device_dev->inter_rx, 0, NULL, device_dev->priority)) != EOK)
return (-1);

Подробнее об InterruptAttach(), InterruptAttach_r()
Подробнее реализация обработчиков прерываний будет рассмотрена ниже.

Установка флагов, указывающих, что интерфейс готов к приёму/передаче.

Данные флаги необходимы для работы логики прёма/передачи:

ifp->if_flags_tx |= IFF_RUNNING;
ifp->if_flags_tx &= ~IFF_OACTIVE;
ifp->if_flags |= IFF_RUNNING;

Флаги описаны выше в пункте Заполнение структур

IOCTL вызовы device_ioctl()

Прототип определён в struct ifnet :: if_ioctl().

IOCTL вызовы необходимы для манипуляции с базовыми параметрами устройств. Копирование статистики nicinfo. Управление интерфейсом с помощью ifconfig

Реализация device_ioctl()

Обычно данная реализация является шаблонной:

int
device_ioctl(struct ifnet * ifp, unsigned long cmd, caddr_t data)
{
int error = 0;
device_dev_t *device = ifp->if_softc;
struct drvcom_config *dcfgp;
struct drvcom_stats *dstp;
struct ifdrv_com *ifdc;
switch (cmd) {
case SIOCGDRVCOM:
ifdc = (struct ifdrv_com *)data;
switch (ifdc->ifdc_cmd) {
case DRVCOM_CONFIG:
dcfgp = (struct drvcom_config *)ifdc;
if (ifdc->ifdc_len != sizeof(nic_config_t)) {
error = EINVAL;
break;
}
memcpy(&dcfgp->dcom_config, &device->cfg, sizeof(device->cfg));
break;
case DRVCOM_STATS:
dstp = (struct drvcom_stats *)ifdc;
if (ifdc->ifdc_len != sizeof(nic_stats_t)) {
error = EINVAL;
break;
}
// update_stats(device);
memcpy(&dstp->dcom_stats, &device->stats, sizeof(device->stats));
break;
default:
error = ENOTTY;
}
break;
case SIOCSIFMEDIA:
case SIOCGIFMEDIA: {
struct ifreq *ifr = (struct ifreq *)data;
error = ifmedia_ioctl(ifp, ifr, &device->bsd_mii.mii_media, cmd);
break;
}
default:
error = ether_ioctl(ifp, cmd, data);
if (error == ENETRESET) {
error = 0;
}
break;
}
return error;
}

Отправка фрейма device_start()

Прототип определён в struct ifnet :: if_start().

Когда необходимо отправить фрейм в сеть, io-pkt-* вызывает функцию device_start().
В данной функции необходимо реализовать:


Note: Функции дефрагментации и паддинга фрейма нужны не всем сетевым устройствам.

Представленный ниже код будет аппаратно-зависимым. Но принципы общие.

Проверки для отправки фрейма

Проверка включённости интерфейса и установка необходимых флагов:

if ((ifp->if_flags_tx & IFF_RUNNING) == 0) {
NW_SIGUNLOCK (&ifp->if_snd_ex, device->iopkt);
return;
}
ifp->if_flags_tx |= IFF_OACTIVE; /* Установка флага, указывающего, что интерфейс занят отправкой. */

Реализация отправки

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

/* Проверка количества свободных дескрипторов и сравнение с пороговым значением */
if (device->tx_free < device->tx_reap)
device_reap(device); /* Запуск процесса очистки дескрипторов перед отправкой */
/* Основной цикл извлечения mbuf'ов из очереди */
for (;;) {
IFQ_POLL(&ifp->if_snd, m0); /* Макрос для просмотра очереди передачи. Не извлекает из очереди mbuf */
if (m0 == NULL)
goto done;
/* Ещё раз убеждаемся, что есть свободные дескрипторы */
if (!device->tx_free) {
device->stats.tx_failed_allocs++;
goto done;
}
/* Функция паддинга фрейма. Заполняем нулями, если размер фрейма слишком мал для данного сетевого устройства. */
if (device_pad(m0)) {
device->stats.tx_failed_allocs++;
goto done;
}
IFQ_DEQUEUE(&ifp->if_snd, m0); /* Макрос для извлечения mbuf из очереди. Если извлекаем, то обязаны отправить фрейм. */
#ifndef CHAIN_SUPPORT /* Если устройство не поддерживает отправку фрейма несколькими дескрипторами запускаем дефрагментацию */
if ((m = device_defrag(m0)) == NULL) {
device->stats.tx_failed_allocs++;
ifp->if_oerrors++;
goto done;
}
m0 = m;
#endif
/* Подсчитываем количество mbuf'ов в цепочке */
for (num_frag=0, m=m0; m; num_frag++) {
m = m->m_next;
}
cur_tx_wptr = device->cur_tx_wptr;
free_wptr = cur_tx_wptr;
for (m=m0; m; m = m->m_next) {
if (!m->m_len) {
num_frag--;
continue;
}
tdesc = &device->tdesc[cur_tx_wptr]; /* Берём свободный дескриптор */
phys = mbuf_phys(m); /* Получаем физический адрес mbuf'a */
CACHE_FLUSH(&device->cachectl, m->m_data, phys, m->m_len); /* Сбрасываем кэш процессора */
/* Заполняем TX дескриптор. У каждого устройства своя реализация дескрипторов. */
tdesc->base = ENDIAN_LE32((uint32_t)(phys + device->bmtrans));
tdesc->buf_length = ENDIAN_LE16(-m->m_len);
tdesc->misc = 0x00000000;
#ifdef CHAIN_SUPPORT /* Если устройство поддерживает отправку фрейма несколькими дескрипторами */
tdesc->status = TX_ST_OWN;
if (m == m0)
tdesc->status |= TX_ST_STP;
if (m->m_next == NULL)
tdesc->status |= TX_ST_ENP;
tdesc->status = ENDIAN_LE16(tdesc->status);
#else
tdesc->status = ENDIAN_LE16(TX_ST_OWN | TX_ST_ENP | TX_ST_STP);
#endif
free_wptr = cur_tx_wptr;
cur_tx_wptr = (cur_tx_wptr + 1) % device->num_transmit;
}
device->reg[E_CSR] = INEA|TDMD; /* Запускаем передачу. У данного устройства необходимо выставить флаг */
/* Сохраняем указатель на mbuf, чтобы очистить его позднее */
device->tx_mbuf[free_wptr] = m0;
device->cur_tx_wptr = cur_tx_wptr;
device->tx_free -= num_frag;
#if NBPFILTER > 0 /* Включаем отправку сырых пакетов всем слушающим сокетам. Например для работы tcpdump */
if (ifp->if_bpf) {
bpf_mtap(ifp->if_bpf, m0);
}
#endif
} // for
done:
device->start_running = 0;
ifp->if_flags_tx &= ~IFF_OACTIVE; /* Выставляем флаг окончания процесса отправки. */
NW_SIGUNLOCK_P(&ifp->if_snd_ex, iopkt_selfp, wtp); /* Освобождаем мьютекс отправки */

Реализация паддинга фрейма

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

static inline int device_pad(struct mbuf *pkt)
{
struct mbuf *last = NULL;
int padlen;
if (likely(pkt->m_pkthdr.len >= ETHER_MIN_NOPAD))
goto done;
padlen = ETHER_MIN_NOPAD - pkt->m_pkthdr.len;
if (pkt->m_pkthdr.len == pkt->m_len &&
M_TRAILINGSPACE(pkt) >= padlen) {
last = pkt;
} else {
for (last = pkt; last->m_next != NULL; last = last->m_next) {
continue;
}
if (M_TRAILINGSPACE(last) < padlen) {
struct mbuf *n;
MGET(n, M_DONTWAIT, MT_DATA);
if (n == NULL)
return ENOBUFS;
n->m_len = 0;
last->m_next = n;
last = n;
}
}
KDASSERT(!M_READONLY(last));
KDASSERT(M_TRAILINGSPACE(last) >= padlen);
memset(mtod(last, caddr_t) + last->m_len, 0, padlen);
last->m_len += padlen;
pkt->m_pkthdr.len += padlen;
done:
return 0;
}

Реализация дефрагментации цепочки mbuf'ов

От стека на отправку приходят пакеты в цепочке mbuf'ов. Связано это с оптимизацией отправки фрейма состоящего из нескольких уровней модели OSI.
Некоторые устройства не могут отправлять цепочку mbuf'ов несколькими дескрипторами. В таком случае необходимо использовать функцию дефрагментации цепочки mbuf'ов.
Данная функция является затратной, поскольку производится копирование данных из цепочки в один буфер.

Реализация данной функции является шаблонной:

struct mbuf *device_defrag(struct mbuf *m)
{
struct mbuf *m2;
if (m->m_pkthdr.len > MCLBYTES) {
m_freem(m);
return NULL;
}
MGET(m2, M_DONTWAIT, MT_DATA);
if (m2 == NULL) {
m_freem(m);
return NULL;
}
M_COPY_PKTHDR(m2, m);
MCLGET(m2, M_DONTWAIT);
if ((m2->m_flags & M_EXT) == 0) {
m_freem(m);
m_freem(m2);
return NULL;
}
m_copydata(m, 0, m->m_pkthdr.len, mtod(m2, caddr_t));
m2->m_pkthdr.len = m2->m_len = m->m_pkthdr.len;
m_freem(m);
return m2;
}

Примечания по разработке и тестирования отправки фреймов

При реализации отправки лучше всего начинать с простого:

  1. Отправка ARP запросов при помощи arping. Отправка данных фреймов не зависит от ответов, создаёт стабильный равномерный (например, 1 фрейм/сек.) поток фреймов, гарантированно доходящих до device_start(), в отличии от ping.
  2. Отправка ICMP запросов при помоги ping. Данными фреймами можно тестировать простую отправку и с дефрагментацией при помощи ping -s 10000. Не путать с jumbo фреймами. Больше mtu по сети не передастся.
  3. Отправка TCP/UDP запросов при помощи iperf3. Данными фреймами тестируется отправка с дефрагментацией или отправкой фрейма несколькими дескрипторами. Приём должен быть реализован.

Получение фрейма. Обработчик прерывания RX.

Как правило приём на сетевых устройствах реализован посредством прерываний. В редких случаях это polling (периодический опрос контроллера).
При завершении получения фрейма контроллер генерирует прерывание. При выходе из обработчика прерывания обычно необходимо маскировать прерывание. При этом в потоковом обработчике событий для минимизации использования прерываний по завершении обработки ранее полученного фрейма желательно производить опрос оборудования на предмет получения нового фрейма.
В самом обработчике есть два варианта реализации работы с прерываниями:

Решение способа обработки прерываний зависит от сетевой карты. В данном контексте мы не рассматриваем MSI, MSI-X прерывания. Только INTX (legacy, одна линия - одно прерывание).

Обработчиков прерываний может быть несколько, поскольку могут быть задействованы несколько линий прерываний для разных типов событий (получение/отправка фрейма, ошибки работы MAC/PHY).
У PCI сетевых карт обычно используется одна линия для всех прерываний и нужное прерывание вычисляется при помощи флагов.
У SoC сетевого устройства обычно несколько линий прерываний.

Реализация обработчика прерывания.

Реализация обработчика прерывания RX и потокового обработчика событий.
Ниже рассмотрена реализация без маскирования прерывания. Сбрасываем все прерывания и регистрируем событие:

/* Обработчик прерывания */
const struct sigevent *
device_isr(void *arg, int iid)
{
device_dev_t *device;
struct _iopkt_inter *ient;
uint16_t csr0;
device = arg;
ient = &device->inter;
csr0 = device->reg[E_CSR]; /* Считываем статус регистр прерываний */
if (ient->on_list == 0 &&
!((device->csr0 = csr0) & INTR)) {
/* Прерывание не принадлежит нашему сетевому устройству. */
return NULL;
}
device->iid = iid;
/* Сбрасываем прерывания. Сообщаем сетевому устройству, что мы их обработали */
csr0 &= (BABL|CERR|MISS|MERR|RINT|TINT);
device->reg[E_CSR] = csr0 | IDON;
device->csr0 = csr0;
return interrupt_queue(device->iopkt, ient); /* Добавляем событие в потоковый обработчик событий */
}
/* Потоковый обработчик событий */
int
device_process_interrupt(void *arg, struct nw_work_thread *wtp)
{
device_dev_t *device_dev = (device_dev_t*)arg;
struct ifnet *ifp = &device_dev->ec.ec_if;
device_rx_desc_t *rdesc;
int cur_rx_rptr;
struct mbuf *m, *rm;
off64_t phys;
nic_stats_t *gstats = &device_dev->stats;
nic_ethernet_stats_t *estats = &gstats->un.estats;
int16_t status = 0;
int16_t pkt_len = 0;
const uint32_t csr0 = readl(device_dev->hw_mem + E_CSR);
/* Различные if(csr0 & ERR) для обработки ошибок */
/* ... */
/* RX прерывание */
if(csr0 & Q0_RX_INT){ /* Q0 RECEIVER INTERRUPT. */
cur_rx_rptr = device_dev->cur_rx_rptr;
rdesc = &device_dev->rdesc[cur_rx_rptr];
while((status = (int16_t)ENDIAN_LE16(rdesc->status)) >= 0){
/* Проверка дескриптора на ошибки */
if(status & RD_ERR){ /* error */
estats->internal_rx_errors++;
goto next_pkt;
}
pkt_len = (ENDIAN_LE16(rdesc->msg_length) & 0xFFF) - ETHER_CRC_LEN; /* Вычитаем длину CRC из пакета. Не для всех устройств необходимо */
m = m_getcl_wtp(M_DONTWAIT, MT_DATA, M_PKTHDR, wtp); /* Получаем mbuf для кольца RX, чтобы заместить который сейчас с данными. */
if(m == NULL){
return -1;
}
rm = device_dev->rx_mbuf[cur_rx_rptr];
device_dev->rx_mbuf[cur_rx_rptr] = m;
phys = pool_phys(m->m_data, m->m_ext.ext_page); /* Получаем физический адрес mbuf */
#ifdef USE_LIBCACHE
CACHE_INVAL(&device_dev->cachectl, m->m_data, phys, m->m_ext.ext_size); /* Инвалидируем кэш для нового mbuf */
#endif // USE_LIBCACHE
/* Подготавливаем новый дескриптор для кольца RX */
rdesc->base = ENDIAN_LE32((uint32_t)phys);
rm->m_pkthdr.rcvif = ifp;
rm->m_len = pkt_len;
rm->m_pkthdr.len = pkt_len;
#if NBPFILTER > 0 /* Включаем отправку сырых пакетов всем слушающим сокетам. Например для работы tcpdump */
if (ifp->if_bpf)
bpf_mtap(ifp->if_bpf, rm);
#endif
/* Обновляем статистику */
gstats->rxed_ok++;
gstats->octets_rxed_ok += pkt_len;
/* Отправляем полученный фрейм в стек */
ifp->if_ipackets++;
(*ifp->if_input)(ifp, rm);
next_pkt:
/* Передаём обновлённый дескриптор с новым mbuf сетевой карте. */
rdesc->buf_length = ENDIAN_LE16(-ETHER_MAX_LEN);
rdesc->status = RD_OWN;
cur_rx_rptr = (cur_rx_rptr + 1) % RX_RING_SIZE(device_dev);
rdesc = &device_dev->rdesc[cur_rx_rptr];
}
device_dev->cur_rx_rptr = cur_rx_rptr;
}
return 1;
}
/* callback включения прерывания. Вызывается после обработки события.*/
int device_intr_enable_rx(void *arg){
device_dev_t *device_dev = (device_dev_t*)arg;
const uint32_t q0_csr = readl(device_dev->hw_mem + E_Q0CSR);
writel(device_dev->hw_mem + E_Q0CSR, q0_csr | Q_RINT | Q_MISS | Q_RINT_EN | Q_MISS_EN); /* Включаем прерывания */
return 1;
}

Отключение устройства device_stop()

Прототип определён в struct ifnet :: if_stop().

Данная callback-функция вызывается при ifconfig down сетевого интерфейса.
Останавливает все операции приёма и передачи, очищает используемые буферы, отключает прерывания. Не освобождает структуры драйвера и аппаратные ресурсы.

Реализация остановки устройства

Реализация аппаратно-зависимая:

void device_stop(struct ifnet *ifp, int disable){
device_dev_t *device_dev = ifp->if_softc;
struct _iopkt_self *iopkt = device_dev->iopkt;
struct nw_work_thread *wtp = WTP;
/* Остановка MAC уровня контроллера */
/* ... */
/* Отключение прерываний */
/* ... */
/* Очистка колец дескрипторов RX/TX */
/* ... */
ifp->if_flags &= ~(IFF_RUNNING | IFF_OACTIVE); /* Установка флагов отключённого устройства. */
}

Отсоединение устройства от стека device_detach()

Прототип определён в struct cfattach :: ca_detach().

Данная callback-функция вызывается при ifconfig destroy сетевого интерфейса.
Сбрасывает сетевое устройство, освобождает все ресурсы. Ожидается, что драйвер в скором времени будет выгружен из сетевого стека, но стек продолжит работу.

Реализация отсоединения устройства

Реализация шаблонная:

int device_detach(struct device *dev, int flags)
{
device_dev_t *device_dev = (device_dev_t*)dev;
struct ifnet *ifp = &device_dev->ec.ec_if;
device_dev->sc_dev.dv_dll_hdl = NULL;
device_stop(&device_dev->ec.ec_if, 0); /* Вызываем device_stop() */
/* Код освобождения ресурсов */
/* ... */
#if 1
ether_ifdetach(ifp); /* Отсоединяем ethernet стек */
#else
ieee80211_ifdetach( &device_dev->sc_ic ); /* Отсоединяем wifi стек */
#endif
if_detach(ifp); /* Отсоединяем интерфейс */
shutdownhook_disestablish( device_dev->sd_hook ); /* Не забывает отсоединить shutdown_hook, если мы его подключили */
return 0;
}




Предыдущий раздел: Библиотека разработки сетевых драйверов